fix(grips): restore tracking references during grip edits

This commit is contained in:
gianlucafiore committed 2026-09-02 20:19:23 -03:00
1 parent 46190ccba1
commit 96b0587a83
5 files changed
+166 -11

No files matched your search

+1
View File
@@ -232,6 +232,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = dirty_before;
}
self.grip_reference_wires.clear();
self.grip_text_verts.clear();
self.grip_text_slide = false;
self.tabs[i].scene.clear_preview_wire();
+7
View File
@@ -600,6 +600,12 @@ pub(super) struct OpenCADStudio {
)>,
/// Document dirty state before the live grip mutation began.
grip_dirty_before: Option<bool>,
/// Frozen pre-drag wire geometry used only as a visual/reference snapshot.
///
/// It is deliberately NOT added to normal snap candidates: the live entity is
/// hidden while dragging and this copy only preserves its original appearance
/// and provides edge directions when the engaged grip is acquired for OTRACK.
grip_reference_wires: Vec<crate::scene::model::wire_model::WireModel>,
/// Drag-start snapshot of the dragged entity's SDF glyph quads. A whole-
/// entity text move slides these each frame (translating the already-shaped
/// glyphs) instead of re-tessellating the run every cursor move (issue #316).
@@ -3321,6 +3327,7 @@ impl OpenCADStudio {
grip_originals: Vec::new(),
grip_history_originals: Vec::new(),
grip_dirty_before: None,
grip_reference_wires: Vec::new(),
grip_text_verts: Vec::new(),
grip_text_slide: false,
qselect: None,
+130 -10
View File
@@ -1266,6 +1266,19 @@ impl OpenCADStudio {
// the re-wrap is exact).
let snap = self.tabs[i].scene.wire_models_for(&edited_handles);
// Preserve the exact geometry from the instant the grip drag began.
// This snapshot is visual/reference-only: do NOT append it to
// `snap_candidates`, otherwise phantom self-snapping can return.
self.grip_reference_wires = snap.clone();
// The engaged grip is always an intentional OTRACK reference.
self.snapper.acquire_grip_tracking_point(
grip.origin_world,
&self.grip_reference_wires,
);
self.grip_text_verts = snap
.iter()
.flat_map(|w| w.text_verts.iter().copied())
@@ -1320,6 +1333,9 @@ impl OpenCADStudio {
self.active_construction_ray(i, raw, base, view_rot, eye, bounds);
// `raw` is already model space (viewport camera or paper→model),
// and the wires are model space, so the snap result is model.
// Normal snap: only the rest of the drawing participates here.
// The frozen grip-reference geometry deliberately stays out, so it cannot
// pull the edited point back onto its old shape.
let snap_hit = self.snapper.snap(
raw,
p,
@@ -1332,11 +1348,87 @@ impl OpenCADStudio {
construction_ray,
);
self.tabs[i].snap_result = snap_hit;
// The frozen pre-drag geometry gets a SECOND, reference-only snap pass.
//
// Its result is never used to place/move the grip. It exists only so the
// user can hover an endpoint/midpoint/etc. of the original geometry and
// acquire it for OTRACK exactly like ordinary drawing geometry.
let reference_snap_hit = if self.snapper.tracking_active()
&& !self.grip_reference_wires.is_empty()
{
self.snapper
.snap(
raw,
p,
&self.grip_reference_wires,
view_rot,
eye,
bounds,
go,
gr,
None,
)
.filter(|hit| {
matches!(
hit.snap_type,
crate::snap::SnapType::Endpoint
| crate::snap::SnapType::Midpoint
| crate::snap::SnapType::Center
| crate::snap::SnapType::Node
| crate::snap::SnapType::Quadrant
| crate::snap::SnapType::Intersection
| crate::snap::SnapType::Insertion
| crate::snap::SnapType::ApparentIntersection
)
})
} else {
None
};
// The visible marker should normally describe the real snap that is driving
// the cursor. However, when there is no real object snap (or only Grid), show
// the reference snap marker so the user can see what point is being acquired.
let display_snap_hit = match snap_hit {
Some(hit) if hit.snap_type != crate::snap::SnapType::Grid => Some(hit),
_ => reference_snap_hit.or(snap_hit),
};
self.tabs[i].snap_result = display_snap_hit;
// OTRACK acquisition needs access to the original wire geometry so, once a
// reference point has dwelt long enough, it can capture the segment directions
// meeting at that point.
//
// IMPORTANT: this combined set is used ONLY for OTRACK acquisition.
// It is never passed to the normal movement snap above.
let mut tracking_candidates: Vec<_> =
snap_candidates.iter().cloned().collect();
tracking_candidates.extend(
self.grip_reference_wires.iter().cloned()
);
// Prefer a genuine drawing snap when it is an acquisition-capable point.
// Otherwise let the frozen-reference snap drive the dwell acquisition.
let normal_tracking_hit = snap_hit.filter(|hit| {
matches!(
hit.snap_type,
crate::snap::SnapType::Endpoint
| crate::snap::SnapType::Midpoint
| crate::snap::SnapType::Center
| crate::snap::SnapType::Node
| crate::snap::SnapType::Quadrant
| crate::snap::SnapType::Intersection
| crate::snap::SnapType::Insertion
| crate::snap::SnapType::ApparentIntersection
)
});
let dwell_hit = normal_tracking_hit.or(reference_snap_hit);
self.snapper.update_otrack_dwell(
snap_hit,
&snap_candidates,
dwell_hit,
&tracking_candidates,
view_rot,
eye,
bounds,
@@ -1357,8 +1449,16 @@ impl OpenCADStudio {
} else {
None
};
// If OTRACK has no acquired-object hit, expose the current polar/ortho
// construction ray through the same visual guide.
let drafting_guide = construction_ray.and_then(|(base, target)| {
let dir = (target - base).try_normalize()?;
Some((base, dir))
});
self.otrack_active = otrack_hit
.map(|hit| (hit.base, hit.dir))
.or(drafting_guide);
self.otrack_active = otrack_hit.map(|hit| (hit.base, hit.dir));
self.otrack_kind = otrack_hit.map(|hit| hit.kind);
let mut snapped = if let Some(dir) = axis_lock {
@@ -1583,12 +1683,31 @@ impl OpenCADStudio {
self.tabs[i].scene.set_preview_text(slid);
self.tabs[i].scene.set_preview_wires(Vec::new());
} else {
// Wire geometry, or a reshape grip: re-tessellate. The
// preview WireModels carry the entity's glyphs (gathered
// for the preview-text buffer in the render path), so a
// dimension / MTEXT-width drag keeps its text visible too.
let preview = self.tabs[i].scene.wire_models_for(&edited_handles);
self.tabs[i].scene.set_preview_wires(preview);
// Current deformed geometry.
let mut preview =
self.tabs[i].scene.wire_models_for(&edited_handles);
// Also show the drag-start geometry as a faint ghost.
//
// This is display-only. Preview wires never participate in the resident
// hit-test/snap set, so keeping the original shape visible does not
// reintroduce self-snapping.
let mut reference = self.grip_reference_wires.clone();
for wire in &mut reference {
wire.selected = false;
// Preserve the entity colour but strongly fade it.
wire.color[3] *= 0.48;
// The original is a positional reference, not another selected object.
wire.line_weight_px = wire.line_weight_px.min(1.0);
}
// Original first, live/deformed geometry over it.
reference.append(&mut preview);
self.tabs[i].scene.set_preview_wires(reference);
}
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
@@ -3024,6 +3143,7 @@ impl OpenCADStudio {
);
self.tabs[i].dirty = true;
}
self.grip_reference_wires.clear();
self.grip_text_verts = Vec::new();
self.grip_text_slide = false;
for &handle in &handles {
+6 -1
View File
@@ -654,8 +654,13 @@ impl OpenCADStudio {
// viewport). Without the `ob.x/ob.y` offset it silently vanishes.
// Shared OTRACK projection basis: the active pane's camera + rect
// (canvas offset included), matching the grips / UCS icon above.
let drafting_alignment_active =
self.snapper.alignment_active()
|| (tab.active_grip.is_some()
&& (self.polar_mode || self.ortho_mode));
let otrack_proj: Option<(glam::Mat4, glam::DVec3, iced::Rectangle)> =
if self.snapper.alignment_active() {
if drafting_alignment_active {
Some(
if let Some((vp_cam, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
(vp_cam.view_proj_rte(full), vp_cam.eye(), full)
+22
View File
@@ -405,6 +405,28 @@ impl Snapper {
}
}
}
/// Immediately acquire the explicitly engaged grip as a tracking point.
///
/// A grip was deliberately selected by the user, so unlike ordinary cursor
/// acquisition it should not require dwell before OTRACK/Extension can use
/// its original position and incident edge directions.
///
/// `wires` must be the frozen pre-drag geometry. It is used only here to
/// capture directions; it is never added to normal OSNAP candidates.
pub fn acquire_grip_tracking_point<W: WireSource + ?Sized>(
&mut self,
p: DVec3,
wires: &W,
) {
if !self.tracking_active() {
return;
}
// With OTRACK disabled, Extension remains endpoint-only.
let endpoints_only = !self.otrack_enabled;
self.acquire_tracking_point(p, wires, endpoints_only);
}
/// Add `p` as a tracking point (capturing its corner edge directions) unless
/// it is already tracked; drops the oldest when the 4-point cap is reached.
/// Edge directions are scanned once here, at acquisition, so OTRACK can align