feat: Object Snap Tracking (OTRACK / F11)

- snap/mod.rs: Snapper gains otrack_enabled, tracking_points, dwell state;
  update_otrack_dwell() acquires a snap point after DWELL_THRESHOLD=4
  consecutive moves near the same position; otrack_snap() returns an
  H/V alignment snap position; clear_tracking() resets state on command end
- overlay.rs: selection_overlay accepts ost_points + cursor_screen;
  draws light-blue dashed H/V lines from acquired points + small cross marker
- statusbar.rs: OTRACK pill (F11) added next to DYN
- view.rs: projects tracking_points to screen, passes to selection_overlay
- update.rs: calls update_otrack_dwell + otrack_snap in ViewportMove;
  overrides effective point with OST alignment when no normal snap is active
- ROADMAP.md: Object Snap Tracking marked ✅

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-08 00:26:54 +03:00
1 parent d2a58bdd08
commit f73d38f0a2
7 files changed
+211 -3

No files matched your search

+46
View File
@@ -190,12 +190,20 @@ pub struct UcsIconParams {
// ── Selection overlay ───────────────────────────────────────────────────
/// An acquired OST tracking point with its screen position.
#[derive(Clone, Debug)]
pub struct OstTrackPoint {
pub screen: Point,
}
pub fn selection_overlay<'a>(
selection: SelectionState,
snap: Option<(Point, SnapType)>,
grips: Vec<GripMarker>,
grid: Option<GridParams>,
ucs_icon: Option<UcsIconParams>,
ost_points: Vec<OstTrackPoint>,
cursor_screen: Point,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -203,6 +211,8 @@ pub fn selection_overlay<'a>(
grips,
grid,
ucs_icon,
ost_points,
cursor_screen,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -215,6 +225,8 @@ struct SelectionCanvas {
grips: Vec<GripMarker>,
grid: Option<GridParams>,
ucs_icon: Option<UcsIconParams>,
ost_points: Vec<OstTrackPoint>,
cursor_screen: Point,
}
impl canvas::Program<Message> for SelectionCanvas {
@@ -588,6 +600,40 @@ impl canvas::Program<Message> for SelectionCanvas {
draw_ucs_icon(&mut frame, ucs.view_proj, ucs.bounds);
}
// ── Object Snap Tracking lines ────────────────────────────────────
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()
.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 { x: tp.x - sz, y: tp.y },
Point { x: tp.x + sz, y: tp.y },
);
let v = canvas::Path::line(
Point { x: tp.x, y: tp.y - sz },
Point { x: tp.x, y: tp.y + sz },
);
frame.stroke(&h, dash_stroke.clone());
frame.stroke(&v, dash_stroke);
}
vec![frame.into_geometry()]
}
}
+5
View File
@@ -29,6 +29,7 @@ impl StatusBar {
polar_increment_deg: f32,
show_grid: bool,
dyn_input: bool,
otrack: bool,
layouts: Vec<String>,
current_layout: String,
// If `Some((original, edit_value))`, the named tab shows a text input.
@@ -87,6 +88,10 @@ impl StatusBar {
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
"Dynamic Input\nF12"
),
tip(
toggle_pill("OTRACK", otrack, Message::ToggleOTrack),
"Object Snap Tracking\nF11"
),
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
status_pill(space_label),
status_pill(scale_label),