From 0db513e07aaa69fb59d6bab33fd507934fb8c5b2 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 26 Jun 2026 09:47:29 +0300 Subject: [PATCH] fix(hatch): one grip per corner, and live preview while editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups to dropping the hatch boundary outline from the wire set: - `closed_outlines` copied segment-list wire points (LwPolyline emits each segment as a NaN-separated pair, so every shared corner repeats) straight into the hatch boundary, giving two grips at each corner. Collapse the points back into a clean ring — skip NaN separators and any vertex coincident with the previous one, and drop a trailing vertex equal to the first. One grip per corner; point-in-polygon and the BOUNDARY / GRADIENT commands get the clean ring too. - Edit previews (move / copy / array / grip-drag) tessellate the edited entity into preview wires, but a hatch now produces none, so a dragged hatch didn't update until release. Build a live outline from the current HatchModel for the preview only (`apply_grip` keeps it in step), so the shape tracks the cursor again. The normal render still shows just the fill. Co-Authored-By: Claude Opus 4.8 --- src/scene/mod.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6133ae6ec..e343bf502 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1968,7 +1968,39 @@ impl Scene { if (dx * dx + dy * dy).sqrt() > 1e-2 { return None; } - Some(pts.iter().map(|p| [p[0], p[1]]).collect()) + // Segment-list wires (e.g. LwPolyline) store each segment as an + // independent NaN-separated pair, so every shared corner repeats + // (`A B | B C | C D | D A`). Collapse that back into a clean ring: + // skip the NaN separators and any vertex coincident with the + // previous one, so consumers (point-in-polygon, the hatch / + // boundary commands) see one vertex per corner — not the doubled + // ring that otherwise shows two grips at every corner. + let mut ring: Vec<[f32; 2]> = Vec::with_capacity(pts.len()); + for p in &pts { + if !p[0].is_finite() || !p[1].is_finite() { + continue; + } + let q = [p[0], p[1]]; + if let Some(&last) = ring.last() { + if (last[0] - q[0]).abs() < 1e-4 && (last[1] - q[1]).abs() < 1e-4 { + continue; + } + } + ring.push(q); + } + // Drop a trailing vertex equal to the first — the ring is closed + // implicitly, so keeping it would be a duplicate corner. + if ring.len() > 1 { + let first = ring[0]; + let last = *ring.last().unwrap(); + if (first[0] - last[0]).abs() < 1e-4 && (first[1] - last[1]).abs() < 1e-4 { + ring.pop(); + } + } + if ring.len() < 3 { + return None; + } + Some(ring) }) .collect() } @@ -5486,15 +5518,50 @@ impl Scene { handles .iter() .flat_map(|h| { - self.document - .entities() - .find(|e| e.common().handle == *h) - .map(|e| self.tessellate_one(e)) - .unwrap_or_default() + match self.document.entities().find(|e| e.common().handle == *h) { + // Hatches carry no outline in the normal wire set, but an + // edit preview (move / copy / array / grip-drag) needs to + // show the shape following the cursor. Build a live boundary + // from the current HatchModel — `apply_grip` keeps it in + // step, so the preview tracks a dragged grip in real time. + Some(EntityType::Hatch(_)) => { + self.hatch_outline_wire(*h).into_iter().collect() + } + Some(e) => self.tessellate_one(e), + None => Vec::new(), + } }) .collect() } + /// Boundary outline wire for a hatch, reconstructed from its cached + /// `HatchModel` (offsets from `world_origin`). Used only for edit previews — + /// the normal render shows the fill, not this outline. + fn hatch_outline_wire(&self, handle: Handle) -> Option { + let m = self.hatches.get(&handle)?; + let (wx, wy) = (m.world_origin[0], m.world_origin[1]); + let pts: Vec<[f64; 3]> = m + .boundary + .iter() + .map(|&[x, y]| { + if x.is_finite() && y.is_finite() { + [wx + x as f64, wy + y as f64, 0.0] + } else { + [f64::NAN; 3] + } + }) + .collect(); + if pts.len() < 2 { + return None; + } + Some(WireModel::solid_f64( + handle.value().to_string(), + pts, + m.color, + false, + )) + } + /// Build wire models for an arbitrary slice of entities (e.g. clipboard contents). /// Entities need not be in the document — they are tessellated directly. pub fn wires_for_entities(&self, entities: &[acadrust::EntityType]) -> Vec {