From 7e90d4d1a19665738b50a5074011135c32c6f299 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 25 Sep 2026 15:17:53 +0300 Subject: [PATCH] fix(pdf): per-piece snap bounds, linear image layering, snap op checks Review of #1484: - The underlay's snap geometry became one hidden wire per segment, but every piece took the whole page's bounding box, so each cursor move visited every piece. Each piece now gets its own box. - Assigning unlayered images to the last layer compared Debug strings of every layered path and text per drawn item (quadratic, formatted anew each time); the owners are keyed once in a map. - The `snap` automation op used (0, 0, 0) for a missing point; it now answers invalid_point. It also left the live snapper's base point set to the query's; the base point is restored. Co-Authored-By: Claude Opus 5.5 --- src/app/control/mod.rs | 11 +++++------ src/app/update/viewport.rs | 10 +++++++--- src/scene/convert/tess.rs | 4 +++- src/scene/model/pdf_vector.rs | 13 ++++++++----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index a1f65a86f..cd08c9916 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -708,13 +708,12 @@ impl OpenCADStudio { "measure" => self.control_measure(&req), // The object snap a cursor over `point` (world) would get; // `from` is the base for perpendicular and tangent. - "snap" => { + "snap" => 'snap: { let p = &req["point"]; - let world = glam::DVec3::new( - p[0].as_f64().unwrap_or(0.0), - p[1].as_f64().unwrap_or(0.0), - p[2].as_f64().unwrap_or(0.0), - ); + let (Some(x), Some(y)) = (p[0].as_f64(), p[1].as_f64()) else { + break 'snap failure("invalid_point", "point must be [x, y] or [x, y, z]"); + }; + let world = glam::DVec3::new(x, y, p[2].as_f64().unwrap_or(0.0)); let from = req["from"].as_array().map(|f| { glam::DVec3::new( f.first().and_then(|v| v.as_f64()).unwrap_or(0.0), diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index b5cab8276..f3340e923 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2908,11 +2908,15 @@ impl OpenCADStudio { self.snapper.osnap_radius_px, ); // Perpendicular and tangent measure from `from`, else the running - // command's last point. + // command's last point; the live cursor's base point is restored. + let live_from = self.snapper.from_point; self.snapper.from_point = from.or(self.last_point).map(|p| p.as_vec3()); let (go, gr) = self.drafting_grid_basis(i); - self.snapper - .snap(raw, p, &candidates, view_rot, eye, bounds, go, gr, None) + let hit = self + .snapper + .snap(raw, p, &candidates, view_rot, eye, bounds, go, gr, None); + self.snapper.from_point = live_from; + hit } /// Apply one frame of a UCS-icon grip drag: map the cursor onto the active diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 08388d34e..b3ce78917 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1308,7 +1308,9 @@ fn tessellate_entity_inner( crate::scene::model::wire_model::SnapHint::Center, )]; } - set_wire_aabb(&mut wire, aabb); + // Each piece gets its own box (not the page's), so candidate + // gathering only visits the pieces near the cursor. + set_wire_aabb(&mut wire, WireModel::UNBOUNDED_AABB); bases.push(wire); } } diff --git a/src/scene/model/pdf_vector.rs b/src/scene/model/pdf_vector.rs index 8a815be7d..1a918db34 100644 --- a/src/scene/model/pdf_vector.rs +++ b/src/scene/model/pdf_vector.rs @@ -200,11 +200,14 @@ fn images_to_last_layer( return; }; let key = |s: &dyn std::fmt::Debug| format!("{s:?}"); - let owner = |item: String| { - layered.iter().position(|(_, pv)| { - pv.paths.iter().any(|p| key(p) == item) || pv.texts.iter().any(|t| key(t) == item) - }) - }; + // Each layered path and text keyed once; the first layer holding it wins. + let mut owners: std::collections::HashMap = std::collections::HashMap::new(); + for (n, (_, pv)) in layered.iter().enumerate() { + for item in pv.paths.iter().map(|p| key(p)).chain(pv.texts.iter().map(|t| key(t))) { + owners.entry(item).or_insert(n); + } + } + let owner = |item: String| owners.get(&item).copied(); let mut last: Option = None; let mut moves: Vec<(usize, PdfImage)> = Vec::new(); for drawn in &full.order {