forked from 1b-agent/opencadstudio-upstream
fix(snap): OSNAP Extension tracks only acquired endpoints
Extension snap extended every segment's line in the whole drawing, so in a larger project an extension foot was live almost everywhere and the cursor snapped to it constantly. Extension now behaves like a temporary tracking point: an endpoint must first be acquired by dwelling on it, and only then does that segment's line extend. Acquisition — and the tracking-point marker — is active whenever OTRACK is on OR the Extension snap is on, so Extension works independently of OTRACK's on/off state (and is not tied to POLAR). Both the per-segment extension and the extended-intersection candidates are gated on an acquired endpoint, and the whole scan is skipped when nothing is acquired (the common case), keeping large drawings responsive. Closes #262 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2bbd3cfcd2
commit
d93bcda889
2 files changed
+111
-24
No files matched your search
+1
-1
@@ -340,7 +340,7 @@ impl OpenCADStudio {
|
||||
// Shared OTRACK projection basis: the active pane's camera + rect
|
||||
// (canvas offset included), matching the grips / UCS icon above.
|
||||
let otrack_proj: Option<(glam::Mat4, glam::DVec3, iced::Rectangle)> =
|
||||
if self.snapper.otrack_enabled {
|
||||
if self.snapper.tracking_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)
|
||||
|
||||
+110
-23
@@ -166,6 +166,34 @@ impl Snapper {
|
||||
self.enabled.contains(&t)
|
||||
}
|
||||
|
||||
/// Whether temporary tracking points are being acquired and drawn: OTRACK
|
||||
/// on, or the Extension object snap on. Extension tracks a segment's line
|
||||
/// only from an acquired endpoint, and works independently of OTRACK's
|
||||
/// on/off state (#262).
|
||||
pub fn tracking_active(&self) -> bool {
|
||||
self.otrack_enabled || (self.snap_enabled && self.is_on(SnapType::Extension))
|
||||
}
|
||||
|
||||
/// True when `p` coincides with one of the acquired temporary tracking
|
||||
/// points. Extension snaps a segment's line only from such acquired
|
||||
/// endpoints (#262), so extensions aren't live for every object in the
|
||||
/// drawing. The tolerance mirrors `edge_dirs_at`: acquired points are f32
|
||||
/// truncations of the true vertices, so the match window scales with
|
||||
/// coordinate magnitude with a tight floor near the origin.
|
||||
fn is_tracked_endpoint(&self, p: glam::DVec3) -> bool {
|
||||
if self.tracking_points.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let pf = p.as_vec3();
|
||||
let tol = 1e-4_f32.max(4e-7 * pf.x.abs().max(pf.y.abs()));
|
||||
let tol2 = tol * tol;
|
||||
self.tracking_points.iter().any(|t| {
|
||||
let dx = t.x - pf.x;
|
||||
let dy = t.y - pf.y;
|
||||
dx * dx + dy * dy < tol2
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_global(&mut self) {
|
||||
self.snap_enabled = !self.snap_enabled;
|
||||
}
|
||||
@@ -215,7 +243,11 @@ impl Snapper {
|
||||
bounds: iced::Rectangle,
|
||||
now: Instant,
|
||||
) {
|
||||
if !self.otrack_enabled {
|
||||
// Temporary tracking points are acquired when OTRACK is on, OR when the
|
||||
// Extension object snap is on: Extension tracks a segment's line only
|
||||
// from an endpoint the user has acquired, independently of OTRACK's
|
||||
// on/off state (#262).
|
||||
if !self.tracking_active() {
|
||||
self.last_snap_world = None;
|
||||
self.dwell_since = None;
|
||||
self.dwell_acquired = false;
|
||||
@@ -861,8 +893,11 @@ impl Snapper {
|
||||
// ── Extension — along the extension of a segment beyond endpoints ──
|
||||
// Every segment's line can be extended past either endpoint, so a
|
||||
// polyline offers an extension off each of its vertices, not just the
|
||||
// first and last (#259).
|
||||
if self.is_on(SnapType::Extension) {
|
||||
// first and last (#259). Extension is live only from endpoints the user
|
||||
// has acquired as temporary tracking points (#262), so with none
|
||||
// acquired there is nothing to extend — skip the whole scan. That is the
|
||||
// common case and keeps a large drawing responsive.
|
||||
if self.is_on(SnapType::Extension) && !self.tracking_points.is_empty() {
|
||||
for wire in wires {
|
||||
let n = wire.points.len();
|
||||
if n < 2 {
|
||||
@@ -875,29 +910,36 @@ impl Snapper {
|
||||
if !a.x.is_finite() || !b.x.is_finite() || (a - b).length_squared() < 1e-18 {
|
||||
continue;
|
||||
}
|
||||
// Only extend from an endpoint the user has acquired as a
|
||||
// temporary tracking point, so the extension isn't live for
|
||||
// every object in the drawing (#262).
|
||||
// Beyond `a`, away from `b`.
|
||||
if let Some(ext) = extension_snap(
|
||||
cursor_world,
|
||||
a,
|
||||
a - b,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
try_pt(ext, SnapType::Extension);
|
||||
if self.is_tracked_endpoint(a) {
|
||||
if let Some(ext) = extension_snap(
|
||||
cursor_world,
|
||||
a,
|
||||
a - b,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
try_pt(ext, SnapType::Extension);
|
||||
}
|
||||
}
|
||||
// Beyond `b`, away from `a`.
|
||||
if let Some(ext) = extension_snap(
|
||||
cursor_world,
|
||||
b,
|
||||
b - a,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
try_pt(ext, SnapType::Extension);
|
||||
if self.is_tracked_endpoint(b) {
|
||||
if let Some(ext) = extension_snap(
|
||||
cursor_world,
|
||||
b,
|
||||
b - a,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
try_pt(ext, SnapType::Extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -917,6 +959,11 @@ impl Snapper {
|
||||
if !a0.x.is_finite() || !a1.x.is_finite() {
|
||||
continue;
|
||||
}
|
||||
// Only lines with an acquired endpoint contribute an extended
|
||||
// crossing, matching the per-segment extension gate (#262).
|
||||
if !self.is_tracked_endpoint(a0) && !self.is_tracked_endpoint(a1) {
|
||||
continue;
|
||||
}
|
||||
let s0 = world_to_screen(a0, view_rot, eye, bounds);
|
||||
let s1 = world_to_screen(a1, view_rot, eye, bounds);
|
||||
let ex = s1.x - s0.x;
|
||||
@@ -1461,3 +1508,43 @@ fn t_on_segment(p: Point, a: Point, b: Point) -> f32 {
|
||||
}
|
||||
(((p.x - a.x) * dx + (p.y - a.y) * dy) / len2).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ext_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tracking_active_covers_otrack_and_extension() {
|
||||
let mut s = Snapper::default();
|
||||
// OTRACK off, Extension not enabled → no acquisition.
|
||||
s.snap_enabled = true;
|
||||
s.otrack_enabled = false;
|
||||
assert!(!s.tracking_active());
|
||||
// Extension on with the snap master on → acquire, independent of OTRACK.
|
||||
s.enabled.insert(SnapType::Extension);
|
||||
assert!(s.tracking_active());
|
||||
// Extension is gated by the snap master.
|
||||
s.snap_enabled = false;
|
||||
assert!(!s.tracking_active());
|
||||
// OTRACK acquires regardless of the object-snap master.
|
||||
s.enabled.remove(&SnapType::Extension);
|
||||
s.otrack_enabled = true;
|
||||
assert!(s.tracking_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_only_tracks_acquired_endpoints() {
|
||||
let mut s = Snapper::default();
|
||||
// Nothing acquired → no endpoint is a live extension source (#262).
|
||||
assert!(!s.is_tracked_endpoint(glam::DVec3::new(10.0, 0.0, 0.0)));
|
||||
// Acquire an endpoint → only that vertex tracks.
|
||||
s.tracking_points.push(Vec3::new(10.0, 0.0, 0.0));
|
||||
assert!(s.is_tracked_endpoint(glam::DVec3::new(10.0, 0.0, 0.0)));
|
||||
assert!(!s.is_tracked_endpoint(glam::DVec3::new(5.0, 0.0, 0.0)));
|
||||
// The match tolerance scales with coordinate magnitude, so an acquired
|
||||
// vertex at UTM scale still matches its f32-truncated tracking point.
|
||||
let big = 1_234_567.0_f64;
|
||||
s.tracking_points.push(Vec3::new(big as f32, 0.0, 0.0));
|
||||
assert!(s.is_tracked_endpoint(glam::DVec3::new(big, 0.0, 0.0)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user