diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 20cbdb597..5aa2108e9 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1525,14 +1525,36 @@ impl OpenCADStudio { // In a viewport, pick the cursor's model point with the // viewport camera directly; otherwise project on the paper // sheet and map to model. Either way `raw` is model space. + // + // A model-space grip moves in the drawing plane through where it + // started (the UCS XY plane at the grip's own elevation). The + // plane facing the screen through the orbit target put the point + // somewhere else in depth in any tilted view, so a circle jumped + // to another radius and a vertex left its plane (#1033). Seen + // edge-on that plane cannot be hit; the screen plane takes over. + let grip_plane = self.tabs[i].editing_model_space().then(|| { + let normal = self.tabs[i] + .active_ucs + .as_ref() + .map_or(glam::DVec3::Z, ucs_z_axis); + (normal, grip.origin_world) + }); + let pick = |cam: &crate::scene::view::camera::Camera| match grip_plane { + Some((normal, origin)) + if (cam.rotation * glam::Vec3::NEG_Z) + .as_dvec3() + .dot(normal) + .abs() + > 0.05 => + { + cam.pick_on_plane(p, bounds, normal.as_vec3(), origin) + } + _ => cam.pick_on_target_plane(p, bounds), + }; let raw = match &edit_cam { - Some(cam) => cam.pick_on_target_plane(p, bounds), + Some(cam) => pick(cam), None => { - let paper = self.tabs[i] - .scene - .camera - .borrow() - .pick_on_target_plane(p, bounds); + let paper = pick(&self.tabs[i].scene.camera.borrow()); self.tabs[i].scene.paper_to_model(paper) } }; @@ -1647,7 +1669,7 @@ impl OpenCADStudio { } else { self.tabs[i].scene.hit_test_wires() }; - let snap_candidates = self.tabs[i].scene.interaction_candidates_near( + let mut snap_candidates = self.tabs[i].scene.interaction_candidates_near( all_wires, raw, view_rot, @@ -1655,6 +1677,16 @@ impl OpenCADStudio { bounds, self.snapper.osnap_radius_px, ); + // A solid stays drawn while its grip moves, so its own edges are + // still in the set: the grip would snap back onto the shape it is + // reshaping (a cone's base radius landed on its own side, #1033). + let edited = &self.grip_preview_handles; + if !edited.is_empty() { + snap_candidates.exclude_wires(|wire| { + crate::scene::Scene::handle_from_wire_name(&wire.name) + .is_some_and(|handle| edited.contains(&handle)) + }); + } // The engaged grip is the rubber-band origin. Perpendicular // snapping must drop its foot from this point, including when a // hot-grip set is moved by the same drag vector. diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index 45769c73d..1cb8049b8 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -1646,6 +1646,39 @@ impl InteractionCandidates { .map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds)) } + /// Leave out every wire `drop` matches, with the segments, points and + /// triangles that belong to it. + pub(crate) fn exclude_wires(&mut self, drop: impl Fn(&WireModel) -> bool) { + let wires = Arc::clone(&self.wires); + let dropped = |wire: u32| wires.get(wire as usize).is_some_and(&drop); + match self.wire_indices.as_mut() { + Some(indices) => indices.retain(|&wire| !dropped(wire)), + None => { + self.wire_indices = Some( + (0..wires.len() as u32) + .filter(|&wire| !dropped(wire)) + .collect(), + ); + } + } + fn keep( + refs: &mut Option>, + wire: impl Fn(&T) -> u32, + dropped: impl Fn(u32) -> bool, + ) { + if let Some(refs) = refs.as_mut() { + refs.retain(|item| !dropped(wire(item))); + } + } + keep(&mut self.segments, |r| r.wire, dropped); + keep(&mut self.snap_points, |r| r.wire, dropped); + keep(&mut self.key_vertices, |r| r.wire, dropped); + keep(&mut self.key_segments, |r| r.wire, dropped); + keep(&mut self.fill_triangles, |r| r.wire, dropped); + keep(&mut self.pick_triangles, |r| r.wire, dropped); + keep(&mut self.glyphs, |r| r.wire, dropped); + } + pub(crate) fn extend_indexed_area(&mut self, other: Self) { debug_assert!(Arc::ptr_eq(&self.wires, &other.wires));