fix(grips): drag model grips in the drawing plane, not onto themselves

In a tilted 3D view a grip followed the cursor onto the screen plane
through the orbit target, so a circle or arc jumped to another radius and
a polyline vertex left its plane. A model-space grip now moves in the UCS
XY plane at its own elevation (the screen plane only when seen edge-on).
A solid stays drawn while its grip moves, and its own edges were still
snap candidates: a cone's base radius landed on its own side. The edited
entities are left out of the grip's snap set.

Fixes #1033

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 12:12:27 +03:00
1 parent df94117fe5
commit 9cc890c911
2 files changed
+72 -7

No files matched your search

+39 -7
View File
@@ -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.
+33
View File
@@ -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<T>(
refs: &mut Option<Vec<T>>,
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));