feat(snap): OTRACK ortho lock + edge-extension tracking (#218, #219)

#218: with Ortho on and a base point the axis is now a hard lock — a lone
tracking ray can't pull the cursor off it. Only a tracking ray crossing
the ortho axis locks (the corner-finding case); otherwise the ortho
constraint applies as the fallback, so Ortho reliably locks when OTRACK
is also on.

#219: an acquired corner now contributes an alignment ray along its own
edge direction(s) (captured once at acquisition), so OTRACK follows a
segment's extension, not just the ortho/polar axes. Acquisition is also
captured on leave — a perfectly still cursor emits no move events, so
"pause on a corner, then drag" now reliably acquires it. The acquired
marker projects through the active pane's camera + rect (canvas offset
included) instead of vanishing, and the active alignment path is drawn
as a full-length dashed guide through the corner.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-03 20:34:20 +03:00
1 parent 0322128f48
commit c3e86d08cc
4 files changed
+271 -61

No files matched your search

+47 -27
View File
@@ -185,7 +185,7 @@ pub fn selection_overlay<'a>(
grips: Vec<GripMarker>,
ucs_icons: Vec<UcsIconParams>,
ost_points: Vec<OstTrackPoint>,
cursor_screen: Point,
otrack_line: Option<(Point, Point)>,
show_viewcube: bool,
dividers: Vec<iced::Rectangle>,
pane_move_rect: Option<iced::Rectangle>,
@@ -202,7 +202,7 @@ pub fn selection_overlay<'a>(
grips,
ucs_icons,
ost_points,
cursor_screen,
otrack_line,
show_viewcube,
dividers,
pane_move_rect,
@@ -231,7 +231,11 @@ struct SelectionCanvas {
/// entry carries hover/selected (grips).
ucs_icons: Vec<UcsIconParams>,
ost_points: Vec<OstTrackPoint>,
cursor_screen: Point,
/// Active OTRACK alignment: (acquired tracking point, locked cursor), both
/// in screen space. Drawn as a dashed guide extended a little past the
/// cursor so the extension / tracking line the user snapped to is visible.
/// (#219)
otrack_line: Option<(Point, Point)>,
show_viewcube: bool,
/// Divider bars (pixel rects, canvas-relative) between Model panes — drawn
/// as filled lines and used to suppress the crosshair over a divider.
@@ -1003,32 +1007,48 @@ impl canvas::Program<Message> for SelectionCanvas {
);
}
// ── Object Snap Tracking lines ────────────────────────────────────
// ── Object Snap Tracking ─────────────────────────────────────────────
let track_color = Color {
r: 0.15,
g: 0.85,
b: 0.95,
a: 0.7,
};
// The alignment line the cursor is currently locked to — drawn at its
// real angle from the acquired point through the lock and a little
// beyond, dashed so it reads as a construction guide. This covers the
// ortho (0°/90°), polar, and edge-extension cases uniformly (#219).
if let Some((base, tip)) = self.otrack_line {
let dx = tip.x - base.x;
let dy = tip.y - base.y;
let len = (dx * dx + dy * dy).sqrt();
if len > 1e-3 {
// Extend the alignment path well past both ends along its
// direction (the canvas clips it to the viewport) so it reads
// as a full construction line through the acquired point, not a
// stub between the corner and the cursor. (#219)
let (ux, uy) = (dx / len, dy / len);
const L: f32 = 5000.0;
let p0 = Point::new(base.x - ux * L, base.y - uy * L);
let p1 = Point::new(tip.x + ux * L, tip.y + uy * L);
let dash = canvas::Stroke {
line_dash: canvas::LineDash {
segments: &[6.0, 4.0],
offset: 0,
},
..canvas::Stroke::default()
.with_color(track_color)
.with_width(1.0)
};
frame.stroke(&canvas::Path::line(p0, p1), dash);
}
}
// Small cross at each acquired tracking point.
for ost in &self.ost_points {
let tp = ost.screen;
let cx = self.cursor_screen.x;
let cy = self.cursor_screen.y;
let track_color = Color {
r: 0.15,
g: 0.85,
b: 0.95,
a: 0.7,
};
let dash_stroke = canvas::Stroke::default()
let stroke = canvas::Stroke::default()
.with_color(track_color)
.with_width(1.0);
// Draw horizontal line from tracking point to cursor.
if (cy - tp.y).abs() < 8.0 {
let path = canvas::Path::line(tp, Point { x: cx, y: tp.y });
frame.stroke(&path, dash_stroke.clone());
}
// Draw vertical line.
if (cx - tp.x).abs() < 8.0 {
let path = canvas::Path::line(tp, Point { x: tp.x, y: cy });
frame.stroke(&path, dash_stroke.clone());
}
// Small cross at the tracking point.
let sz = 5.0_f32;
let h = canvas::Path::line(
Point {
@@ -1050,8 +1070,8 @@ impl canvas::Program<Message> for SelectionCanvas {
y: tp.y + sz,
},
);
frame.stroke(&h, dash_stroke.clone());
frame.stroke(&v, dash_stroke);
frame.stroke(&h, stroke.clone());
frame.stroke(&v, stroke);
}
vec![frame.into_geometry()]