From 890f7f1c6fefdc6bd3b42fc60c3d6a4dd4a583d1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:35:13 +0900 Subject: [PATCH] Performance: reuse nearby snap geometry for tracking acquisition (#698) Squash of PR #698 by @cyfung1031. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/snap.rs | 45 ++++++++++++++++--- crates/engine/src/snap/tracking.rs | 42 +++++++++++++++--- crates/engine/tests/snap_tracking.rs | 65 +++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/crates/engine/src/snap.rs b/crates/engine/src/snap.rs index f39cb21..e2a8488 100644 --- a/crates/engine/src/snap.rs +++ b/crates/engine/src/snap.rs @@ -154,13 +154,31 @@ pub(crate) fn osnap_with( base: Option, deferred: bool, ) -> Option { + osnap_with_neighborhood(d, space, ix, cursor, aperture, osmode, base, deferred).0 +} + +/// Find the best object snap and retain the local-space primitives gathered for that query. +#[allow(clippy::too_many_arguments)] +pub(crate) fn osnap_with_neighborhood( + d: &Drawing, + space: &Space, + ix: &Option>, + cursor: Vec2, + aperture: f64, + osmode: u32, + base: Option, + deferred: bool, +) -> (Option, Option>) { if osmode == 0 || osmode & mode::OFF != 0 || !cursor.is_finite() || !(aperture.is_finite() && aperture > 0.0) { - return None; + return (None, None); } - let mut best = osnap_in(d, space, ix, cursor, aperture, osmode, base, deferred, &[], None); + let (mut best, near_prims) = match osnap_in_with_prims(d, space, ix, cursor, aperture, osmode, base, deferred, &[], None) { + Some((hit, prims)) => (hit, Some(prims)), + None => (None, None), + }; let views = viewport::near(d, space, cursor, aperture); if views.is_empty() { - return best; + return (best, near_prims); } let model = Space::Model; let model_ix = crate::spatial::index(d, &model); @@ -190,7 +208,7 @@ pub(crate) fn osnap_with( best = Some(hit); } } - best + (best, near_prims) } #[allow(clippy::too_many_arguments)] @@ -206,6 +224,22 @@ fn osnap_in( frozen: &[String], clip: Option, ) -> Option { + osnap_in_with_prims(d, space, ix, cursor, aperture, osmode, base, deferred, frozen, clip).and_then(|(hit, _)| hit) +} + +#[allow(clippy::too_many_arguments)] +fn osnap_in_with_prims( + d: &Drawing, + space: &Space, + ix: &Option>, + cursor: Vec2, + aperture: f64, + osmode: u32, + base: Option, + deferred: bool, + frozen: &[String], + clip: Option, +) -> Option<(Option, Vec)> { let (mut cands, near_prims) = gather_in(d, space, ix, cursor, aperture, osmode, frozen)?; // Intersections between nearby primitives; Apparent Intersection also where their extensions // meet (lines extended, arcs as full circles: in 2D, the "apparent" crossing of objects @@ -236,7 +270,8 @@ fn osnap_in( } } } - finish(cands, &near_prims, cursor, aperture, osmode, base, deferred, clip) + let hit = finish(cands, &near_prims, cursor, aperture, osmode, base, deferred, clip); + Some((hit, near_prims)) } /// Whether a primitive has a meaningful extension (a line, an arc, a circle, an xline or ray). diff --git a/crates/engine/src/snap/tracking.rs b/crates/engine/src/snap/tracking.rs index fcae0f1..7757501 100644 --- a/crates/engine/src/snap/tracking.rs +++ b/crates/engine/src/snap/tracking.rs @@ -587,14 +587,42 @@ pub fn candidate( cfg: &TrackConfig, base: Option, ) -> Option { - let nothing_to_acquire = !cfg.otrack && !cfg.running(mode::EXT | mode::PAR | mode::APP); - if nothing_to_acquire || !(cursor.is_finite() && aperture.is_finite() && aperture > 0.0) { + if !can_acquire(cursor, aperture, cfg) { return None; } let ix = crate::spatial::index(d, space); let prims = super::gather(d, space, &ix, cursor, aperture, mode::NEA).map(|(_, p)| p).unwrap_or_default(); + candidate_from_prims(cursor, hit, aperture, cfg, base, &prims) +} + +pub(crate) fn candidate_with_prims( + cursor: Vec2, + hit: Option<&SnapHit>, + aperture: f64, + cfg: &TrackConfig, + base: Option, + prims: &[Prim], +) -> Option { + if !can_acquire(cursor, aperture, cfg) { + return None; + } + candidate_from_prims(cursor, hit, aperture, cfg, base, prims) +} + +fn can_acquire(cursor: Vec2, aperture: f64, cfg: &TrackConfig) -> bool { + (cfg.otrack || cfg.running(mode::EXT | mode::PAR | mode::APP)) && cursor.is_finite() && aperture.is_finite() && aperture > 0.0 +} + +fn candidate_from_prims( + cursor: Vec2, + hit: Option<&SnapHit>, + aperture: f64, + cfg: &TrackConfig, + base: Option, + prims: &[Prim], +) -> Option { let hit = hit.filter(|h| h.deferred.is_none() && h.mode != mode::NEA && h.point.is_finite()); - let ext_end = if cfg.running(mode::EXT) { extension_near(&prims, hit.map_or(cursor, |h| h.point), aperture) } else { None }; + let ext_end = if cfg.running(mode::EXT) { extension_near(prims, hit.map_or(cursor, |h| h.point), aperture) } else { None }; if let Some((at, ext)) = ext_end && hit.is_none_or(|h| h.point.dist(at) <= cadcraft_geom::EPS * (1.0 + at.len())) { @@ -761,8 +789,12 @@ impl crate::Session { let last = self.last_segment.filter(|(_, b)| q.base.is_some_and(|base| base.near(*b, 1e-9 * (1.0 + b.len())))).map(|(a, b)| a.angle_to(b)); let cfg = TrackConfig::relative_to(&s, angbase, last); let osmode = s.osmode | if s.osnaphatch { mode::HATCH } else { 0 }; - let hit = super::osnap(&doc, &space, raw, q.aperture, osmode, q.base, q.deferred); - let cand = candidate(&doc, &space, raw, hit.as_ref(), q.aperture, &cfg, q.base); + let ix = crate::spatial::index(&doc, &space); + let (hit, near_prims) = super::osnap_with_neighborhood(&doc, &space, &ix, raw, q.aperture, osmode, q.base, q.deferred); + let cand = match (osmode & mode::HATCH, near_prims.as_deref()) { + (0, Some(prims)) => candidate_with_prims(raw, hit.as_ref(), q.aperture, &cfg, q.base, prims), + _ => candidate(&doc, &space, raw, hit.as_ref(), q.aperture, &cfg, q.base), + }; let wait = self.tracking.hover(cand, q.now, cfg.shift_acquire, q.shift); if let Some(h) = hit { self.tracking.last = None; diff --git a/crates/engine/tests/snap_tracking.rs b/crates/engine/tests/snap_tracking.rs index a181af7..4aca052 100644 --- a/crates/engine/tests/snap_tracking.rs +++ b/crates/engine/tests/snap_tracking.rs @@ -1,9 +1,9 @@ //! Object snap tracking (OTRACK, F11) and the Extension, Parallel and Apparent Intersection //! snaps, driven through `Session::snap_cursor` as the UI does, without the UI. -use cadcraft_doc::EntityKind; +use cadcraft_doc::{Common, EntityKind, Insert, Line, Space}; use cadcraft_engine::Session; -use cadcraft_engine::geom::Vec2; +use cadcraft_engine::geom::{Vec2, Vec3}; use cadcraft_engine::snap::tracking::{Acquired, CursorQuery, CursorSnap, DWELL, MAX_ACQUIRED, PathKind, polar_angles}; use cadcraft_engine::snap::{self, mode}; use serde_json::json; @@ -25,6 +25,28 @@ fn line(s: &mut Session, a: (f64, f64), b: (f64, f64)) { s.execute("line", &json!({ "points": [[a.0, a.1], [b.0, b.1]] })).unwrap(); } +fn insert_block(s: &mut Session, name: &str, at: Vec2) -> String { + s.doc_mut() + .unwrap() + .add( + &Space::Model, + Common::default(), + EntityKind::Insert(Insert { + block: name.into(), + insert: Vec3::new(at.x, at.y, 0.0), + scale: Vec3::new(1.0, 1.0, 1.0), + rotation: 0.0, + attribs: Vec::new(), + cols: 1, + rows: 1, + col_spacing: 0.0, + row_spacing: 0.0, + }), + ) + .unwrap() + .hex() +} + fn setvar(s: &mut Session, name: &str, value: serde_json::Value) { s.execute("setvar", &json!({ "name": name, "value": value })).unwrap(); } @@ -37,6 +59,45 @@ fn at(s: &mut Session, cursor: Vec2, base: Option) -> CursorSnap { s.snap_cursor(cursor, &query(base, 100.0)) } +#[test] +fn default_cursor_tracking_acquires_nested_block_geometry() { + let mut s = session(mode::END | mode::CEN | mode::INT | mode::EXT); + assert!(s.settings.otrack); + let line = s + .doc_mut() + .unwrap() + .add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(10.0, 0.0, 0.0) })) + .unwrap() + .hex(); + s.execute("block", &json!({ "name": "INNER", "base": [0, 0], "handles": [line], "keep": "delete" })).unwrap(); + let inner = insert_block(&mut s, "INNER", Vec2::ZERO); + s.execute("block", &json!({ "name": "OUTER", "base": [0, 0], "handles": [inner], "keep": "delete" })).unwrap(); + insert_block(&mut s, "OUTER", Vec2::ZERO); + + rest(&mut s, v(10.02, 0.02), None); + + assert_eq!(s.tracking.acquired.len(), 1); + assert!(matches!( + s.tracking.acquired.first(), + Some(Acquired::Point { at, name, ext: Some(_) }) if at.near(v(10.0, 0.0), 1e-9) && *name == "Endpoint" + )); +} + +#[test] +fn osnaphatch_geometry_is_not_used_for_tracking_extension() { + let mut s = session(mode::END | mode::EXT); + s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 5] })).unwrap(); + let boundary = s.doc().unwrap().model.iter().last().unwrap().handle.hex(); + s.execute("hatch", &json!({ "handles": [boundary] })).unwrap(); + s.execute("erase", &json!({ "handles": [boundary] })).unwrap(); + setvar(&mut s, "OSNAPHATCH", json!(1)); + + rest(&mut s, v(10.02, 4.98), None); + + assert_eq!(s.tracking.acquired.len(), 1); + assert!(matches!(s.tracking.acquired.first(), Some(Acquired::Point { ext: None, .. }))); +} + /// Rest the cursor at `p` long enough to acquire (or release) what is there, then move off. fn rest(s: &mut Session, p: Vec2, base: Option) { s.snap_cursor(p, &query(base, 0.0));