forked from 1b-agent/opencadstudio-upstream
feat(snap): dashed extension guide line for the Extension osnap (#238)
The Extension object snap worked but drew no tracking path, so it was easy to miss — users had to keep the cursor exactly on the invisible extension line without any visual cue. When an Extension snap is active, draw a dashed guide line from the endpoint it extends from, through the snap point and a little beyond, and orient the three-dot marker along that direction. SnapResult carries the endpoint screen position (filled once the winning snap is known); the overlay renders the guide. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
53a4882c7f
commit
1614fd9ab2
4 files changed
+97
-2
No files matched your search
@@ -1068,6 +1068,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
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));
|
||||
|
||||
@@ -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<crate::ui::overlay::GripMarker> =
|
||||
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,
|
||||
|
||||
+61
@@ -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<TangentObject>,
|
||||
/// 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<Point>,
|
||||
}
|
||||
|
||||
/// 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<Point> {
|
||||
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
|
||||
|
||||
+33
-2
@@ -180,6 +180,7 @@ impl canvas::Program<Message> for GridCanvas {
|
||||
pub fn selection_overlay<'a>(
|
||||
selection: SelectionState,
|
||||
snap: Option<(Point, SnapType)>,
|
||||
snap_ext_base: Option<Point>,
|
||||
grips: Vec<GripMarker>,
|
||||
ucs_icons: Vec<UcsIconParams>,
|
||||
ost_points: Vec<OstTrackPoint>,
|
||||
@@ -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<Point>,
|
||||
grips: Vec<GripMarker>,
|
||||
/// 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<Message> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user