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 {