Performance: reuse nearby snap geometry for tracking acquisition (#698)

Squash of PR #698 by @cyfung1031.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cyfung1031andClaude Opus 5.5 authored and GitHub committed 2026-10-12 03:35:13 -04:00
1 parent 4bb3acb913
commit 890f7f1c6f
3 files changed
+140 -12

No files matched your search

+40 -5
View File
@@ -154,13 +154,31 @@ pub(crate) fn osnap_with(
base: Option<Vec2>,
deferred: bool,
) -> Option<SnapHit> {
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<std::sync::Arc<crate::spatial::SpatialIndex>>,
cursor: Vec2,
aperture: f64,
osmode: u32,
base: Option<Vec2>,
deferred: bool,
) -> (Option<SnapHit>, Option<Vec<Prim>>) {
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<Bounds2>,
) -> Option<SnapHit> {
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<std::sync::Arc<crate::spatial::SpatialIndex>>,
cursor: Vec2,
aperture: f64,
osmode: u32,
base: Option<Vec2>,
deferred: bool,
frozen: &[String],
clip: Option<Bounds2>,
) -> Option<(Option<SnapHit>, Vec<Prim>)> {
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).
+37 -5
View File
@@ -587,14 +587,42 @@ pub fn candidate(
cfg: &TrackConfig,
base: Option<Vec2>,
) -> Option<Acquired> {
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<Vec2>,
prims: &[Prim],
) -> Option<Acquired> {
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<Vec2>,
prims: &[Prim],
) -> Option<Acquired> {
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;
+63 -2
View File
@@ -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<Vec2>) -> 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<Vec2>) {
s.snap_cursor(p, &query(base, 0.0));