diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 2d446b2f8..f0fa53697 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1068,6 +1068,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { screen, snap_type: SnapType::ObjectPick, tangent_obj: None, + extension_base: None, }); if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { cmd.set_acquisition_hint(Some(pick.label)); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index e230814c7..6ef439b9f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -193,6 +193,7 @@ impl OpenCADStudio { let selection_overlay = { let sel = tab.scene.selection.borrow().clone(); let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type)); + let snap_ext_base = tab.snap_result.and_then(|s| s.extension_base); let grips: Vec = if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { @@ -397,6 +398,7 @@ impl OpenCADStudio { crate::ui::overlay::selection_overlay( sel, snap_info, + snap_ext_base, grips, ucs_icons, ost_points, diff --git a/src/snap.rs b/src/snap.rs index 4ce9dee83..7bc292640 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -64,6 +64,10 @@ pub struct SnapResult { pub snap_type: SnapType, /// Set when `snap_type == Tangent`; provides entity geometry for TTR/TTT. pub tangent_obj: Option, + /// Screen position of the endpoint an Extension snap extends from, so the + /// overlay can draw the dashed extension guide line back to it. `None` for + /// every other snap type. (#238) + pub extension_base: Option, } /// Object-snap-tracking alignment: the cursor projected onto a ray from an @@ -523,6 +527,7 @@ impl Snapper { screen, snap_type: SnapType::Grid, tangent_obj: None, + extension_base: None, }); best_rank = snap_priority(SnapType::Grid); best_d2 = d2; @@ -596,6 +601,7 @@ impl Snapper { screen, snap_type, tangent_obj: None, + extension_base: None, }); } }; @@ -886,6 +892,7 @@ impl Snapper { screen: screen_pt, snap_type: SnapType::Tangent, tangent_obj: Some(tangent_obj), + extension_base: None, }); } } @@ -936,11 +943,22 @@ impl Snapper { screen, snap_type: SnapType::Center, tangent_obj: None, + extension_base: None, }); } } } + // If an Extension snap won, re-find the endpoint whose ray it lies on + // so the overlay can draw the dashed extension guide line back to it. + // The snapped point is on some endpoint's extension by construction, so + // this just identifies which one. (#238) + if let Some(b) = best.as_mut() { + if b.snap_type == SnapType::Extension { + b.extension_base = extension_base_screen(b.world, wires, view_rot, eye, bounds); + } + } + best } } @@ -1099,6 +1117,49 @@ fn extension_snap( Some(world_pt) } +/// Given a finalized Extension snap point, return the screen position of the +/// endpoint whose extension ray it lies on — for the dashed guide line (#238). +/// Mirrors [`extension_snap`]'s endpoint/direction choice (beyond the first or +/// last vertex); the nearest exactly-on-ray endpoint wins on the rare tie. +fn extension_base_screen( + snapped: glam::DVec3, + wires: &[WireModel], + view_rot: Mat4, + eye: glam::DVec3, + bounds: Rectangle, +) -> Option { + let mut best: Option<(f64, glam::DVec3)> = None; + for wire in wires { + let n = wire.points.len(); + if n < 2 { + continue; + } + for (origin, prev) in [ + (wp_f64(wire, 0), wp_f64(wire, 1)), + (wp_f64(wire, n - 1), wp_f64(wire, n - 2)), + ] { + let dir = origin - prev; + let len2 = dir.x * dir.x + dir.y * dir.y; + if len2 < 1e-12 { + continue; + } + let t = ((snapped.x - origin.x) * dir.x + (snapped.y - origin.y) * dir.y) / len2; + if t < 0.05 { + continue; // must be beyond the endpoint, matching extension_snap + } + // Off-ray distance²: ~0 means the snapped point genuinely lies on + // this endpoint's extension. + let on_x = origin.x + t * dir.x; + let on_y = origin.y + t * dir.y; + let off = (on_x - snapped.x).powi(2) + (on_y - snapped.y).powi(2); + if off < 1e-9 && best.map_or(true, |(bo, _)| off < bo) { + best = Some((off, origin)); + } + } + } + best.map(|(_, origin)| world_to_screen(origin, view_rot, eye, bounds)) +} + // ── Projection helpers ──────────────────────────────────────────────────── /// Project a world point to screen relative-to-eye: subtract the f64 eye first diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 7fcdf0089..eac35fb7f 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -180,6 +180,7 @@ impl canvas::Program for GridCanvas { pub fn selection_overlay<'a>( selection: SelectionState, snap: Option<(Point, SnapType)>, + snap_ext_base: Option, grips: Vec, ucs_icons: Vec, ost_points: Vec, @@ -195,6 +196,7 @@ pub fn selection_overlay<'a>( canvas(SelectionCanvas { selection, snap, + snap_ext_base, grips, ucs_icons, ost_points, @@ -215,6 +217,9 @@ pub fn selection_overlay<'a>( struct SelectionCanvas { selection: SelectionState, snap: Option<(Point, SnapType)>, + /// Screen position of the endpoint an active Extension snap extends from, + /// so the dashed extension guide line can be drawn back to it. (#238) + snap_ext_base: Option, grips: Vec, /// One UCS icon per Model pane (each viewport shows its own at its origin); /// a single entry for paper / floating-viewport. Only the active pane's @@ -788,10 +793,36 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&path, stroke); } SnapType::Extension => { - // Three dots strung along a tracked direction. + // Unit direction from the endpoint the snap extends from + // toward the snap point, so the guide line and the three + // dots follow the actual extension path (#238). + let dir = self.snap_ext_base.and_then(|base| { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = (dx * dx + dy * dy).sqrt(); + (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) + }); + // Dashed guide line from that endpoint, through the snap + // point and a little beyond, so the extension path is + // visible as the cursor tracks along it. + if let Some((base, u)) = dir { + let dash = canvas::Stroke { + line_dash: canvas::LineDash { + segments: &[4.0, 4.0], + offset: 0, + }, + ..canvas::Stroke::default().with_color(marker).with_width(1.0) + }; + let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); + frame.stroke(&canvas::Path::line(base, tip), dash); + } + // Three dots at the snap point, strung along the extension + // direction (horizontal fallback when the base is unknown). + let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); let r = 1.4_f32; for k in [-7.0_f32, 0.0, 7.0] { - let dot = canvas::Path::circle(Point::new(sp.x + k, sp.y), r); + let dot = + canvas::Path::circle(Point::new(sp.x + u.x * k, sp.y + u.y * k), r); frame.fill(&dot, marker); } }