mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Performance: restrict object-snap intersections to cursor aperture (#701)
* Snap: pin finite and apparent intersection behavior * Snap: skip finite intersections outside the aperture --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2 files changed
+196
-2
No files matched your search
@@ -8,7 +8,7 @@ mod viewport;
|
||||
pub use viewport::ViewportPoint;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Insert, MAX_BLOCK_DEPTH, Prim, Space};
|
||||
use cadcraft_geom::{Bounds2, Circle, Ellipse, Mat3, PI, Segment, Vec2, intersect_ext};
|
||||
use cadcraft_geom::{Bounds2, Circle, EPS, Ellipse, Mat3, PI, Segment, Vec2, intersect_ext};
|
||||
use serde::Serialize;
|
||||
|
||||
/// OSMODE bits.
|
||||
@@ -260,7 +260,14 @@ fn osnap_in_with_prims(
|
||||
// meet (lines extended, arcs as full circles: in 2D, the "apparent" crossing of objects
|
||||
// that don't reach each other).
|
||||
if osmode & (mode::INT | mode::APP) != 0 {
|
||||
let segs: Vec<Vec<Segment>> = near_prims.iter().take(200).map(prim_segments).collect();
|
||||
let mut segs: Vec<Vec<Segment>> = near_prims.iter().take(200).map(prim_segments).collect();
|
||||
let probe = finite_intersection_probe(cursor, aperture);
|
||||
for (i, segments) in segs.iter_mut().enumerate() {
|
||||
let preserves_app = osmode & mode::APP != 0 && near_prims.get(i).is_some_and(extendable);
|
||||
if !preserves_app {
|
||||
segments.retain(|segment| segment_may_intersect_probe(segment, probe.as_ref()));
|
||||
}
|
||||
}
|
||||
let real = if osmode & mode::INT != 0 { mode::INT } else { mode::APP };
|
||||
for i in 0..segs.len() {
|
||||
for j in i + 1..segs.len() {
|
||||
@@ -289,6 +296,40 @@ fn osnap_in_with_prims(
|
||||
Some((hit, near_prims))
|
||||
}
|
||||
|
||||
fn finite_intersection_probe(cursor: Vec2, aperture: f64) -> Option<Bounds2> {
|
||||
if !(cursor.is_finite() && aperture.is_finite() && aperture > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let probe = Bounds2::new(cursor, cursor).expand(aperture);
|
||||
if !finite_bounds(&probe) {
|
||||
return None;
|
||||
}
|
||||
// The finite line intersection accepts a 1e-9 parameter slack; retain that slack in the broad phase.
|
||||
let scale = cursor.x.abs().max(cursor.y.abs()).max(aperture).max(1.0);
|
||||
let padded = probe.expand(EPS * scale);
|
||||
finite_bounds(&padded).then_some(padded)
|
||||
}
|
||||
|
||||
fn segment_may_intersect_probe(segment: &Segment, probe: Option<&Bounds2>) -> bool {
|
||||
let Some(probe) = probe else { return true };
|
||||
let bounds = segment.bounds();
|
||||
if !finite_bounds(&bounds) {
|
||||
return true;
|
||||
}
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
if !(width.is_finite() && height.is_finite()) {
|
||||
return true;
|
||||
}
|
||||
let scale = bounds.min.x.abs().max(bounds.min.y.abs()).max(bounds.max.x.abs()).max(bounds.max.y.abs()).max(width).max(height).max(1.0);
|
||||
let padded = bounds.expand(EPS * scale);
|
||||
!finite_bounds(&padded) || padded.intersects(probe)
|
||||
}
|
||||
|
||||
fn finite_bounds(bounds: &Bounds2) -> bool {
|
||||
!bounds.is_empty() && bounds.min.is_finite() && bounds.max.is_finite()
|
||||
}
|
||||
|
||||
/// Whether a primitive has a meaningful extension (a line, an arc, a circle, an xline or ray).
|
||||
pub(crate) fn extendable(p: &Prim) -> bool {
|
||||
matches!(p, Prim::Seg(_) | Prim::Circle(_) | Prim::Infinite { .. })
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
//! Finite and apparent intersection behavior around the cursor aperture.
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, Ellipse, EntityKind, Line, Space};
|
||||
use cadcraft_engine::snap::{self, SnapHit, mode};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
|
||||
fn add_line(d: &mut Drawing, a: Vec2, b: Vec2) {
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) })).unwrap();
|
||||
}
|
||||
|
||||
fn add_ellipse(d: &mut Drawing, center: Vec2, major: Vec2, ratio: f64) {
|
||||
d.add(
|
||||
&Space::Model,
|
||||
Common::default(),
|
||||
EntityKind::Ellipse(Ellipse { center: center.to3(0.0), major: major.to3(0.0), ratio, start: 0.0, end: std::f64::consts::TAU }),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn hit(d: &Drawing, cursor: Vec2, aperture: f64, osmode: u32) -> Option<SnapHit> {
|
||||
snap::osnap(d, &Space::Model, cursor, aperture, osmode, None, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_intersection_on_the_aperture_boundary_is_included() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_line(&mut d, Vec2::new(-1.0, 0.4), Vec2::new(1.0, 0.4));
|
||||
add_line(&mut d, Vec2::new(0.3, -1.0), Vec2::new(0.3, 1.0));
|
||||
|
||||
let h = hit(&d, Vec2::ZERO, 0.5, mode::INT).expect("intersection on aperture boundary");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::new(0.3, 0.4), 1e-12), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_intersection_just_outside_the_aperture_is_rejected() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let x = 0.300001;
|
||||
add_line(&mut d, Vec2::new(-1.0, 0.4), Vec2::new(1.0, 0.4));
|
||||
add_line(&mut d, Vec2::new(x, -1.0), Vec2::new(x, 1.0));
|
||||
|
||||
assert!(hit(&d, Vec2::ZERO, 0.5, mode::INT).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_line_parameter_tolerance_is_preserved() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let x = 1.0 + 5e-10;
|
||||
add_line(&mut d, Vec2::ZERO, Vec2::new(1.0, 0.0));
|
||||
add_line(&mut d, Vec2::new(x, -1.0), Vec2::new(x, 1.0));
|
||||
|
||||
let h = hit(&d, Vec2::new(x, 0.0), 1e-9, mode::INT).expect("intersection within the existing line parameter tolerance");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::new(x, 0.0), 1e-15), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tessellated_line_parameter_tolerance_is_preserved() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_ellipse(&mut d, Vec2::ZERO, Vec2::new(10.0, 0.0), 0.5);
|
||||
let x = 10.0 + 5e-12;
|
||||
add_line(&mut d, Vec2::new(x, -1.0), Vec2::new(x, 1.0));
|
||||
|
||||
let h = hit(&d, Vec2::new(x, 0.0), 1e-10, mode::INT).expect("finite intersection within a tessellated chord's parameter tolerance");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::new(x, 0.0), 1e-10), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_tessellated_chord_parameter_slack_is_not_pruned() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let radius = 1e12;
|
||||
let step = std::f64::consts::TAU / cadcraft_geom::arc_segments(radius, std::f64::consts::TAU, radius * 1e-3) as f64;
|
||||
let next = Vec2::new(-radius + radius * step.cos(), radius * 0.5 * step.sin());
|
||||
let t = 1.0 / next.x;
|
||||
assert!(t.abs() < 1e-9);
|
||||
let cursor = Vec2::new(1.0, t * next.y);
|
||||
add_ellipse(&mut d, Vec2::new(-radius, 0.0), Vec2::new(radius, 0.0), 0.5);
|
||||
add_line(&mut d, Vec2::new(1.0, cursor.y - 100.0), Vec2::new(1.0, cursor.y + 100.0));
|
||||
|
||||
let h = hit(&d, cursor, 0.5, mode::INT).expect("finite intersection within a long tessellated chord's parameter tolerance");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(cursor, 1e-6), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tessellated_ellipse_intersection_near_cursor_is_kept() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_ellipse(&mut d, Vec2::ZERO, Vec2::new(10.0, 0.0), 0.5);
|
||||
add_line(&mut d, Vec2::new(9.0, 0.0), Vec2::new(11.0, 0.0));
|
||||
|
||||
let h = hit(&d, Vec2::new(10.0, 0.0), 0.05, mode::INT).expect("ellipse and line intersection");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::new(10.0, 0.0), 1e-9), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_bounds_covering_cursor_do_not_create_a_curve_hit() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
for i in 0..12usize {
|
||||
let radius = 50.0 + i as f64 * 10.0;
|
||||
let major = Vec2::from_angle(i as f64 * 0.17) * radius;
|
||||
add_ellipse(&mut d, Vec2::ZERO, major, 0.75 + (i % 5) as f64 * 0.04);
|
||||
}
|
||||
|
||||
assert!(hit(&d, Vec2::ZERO, 0.1, mode::INT).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apparent_intersection_keeps_segments_outside_the_finite_aperture() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_line(&mut d, Vec2::new(0.15, 0.0), Vec2::new(0.18, 0.0));
|
||||
add_line(&mut d, Vec2::new(0.0, 0.15), Vec2::new(0.0, 0.18));
|
||||
|
||||
let h = hit(&d, Vec2::ZERO, 0.1, mode::APP).expect("apparent intersection of extended lines");
|
||||
assert_eq!(h.mode, mode::APP);
|
||||
assert!(h.point.near(Vec2::ZERO, 1e-12), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_intersection_keeps_precedence_when_app_is_also_enabled() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_line(&mut d, Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
|
||||
add_line(&mut d, Vec2::new(0.0, -1.0), Vec2::new(0.0, 1.0));
|
||||
add_line(&mut d, Vec2::new(0.15, 0.04), Vec2::new(0.18, 0.04));
|
||||
add_line(&mut d, Vec2::new(0.04, 0.15), Vec2::new(0.04, 0.18));
|
||||
|
||||
let h = hit(&d, Vec2::ZERO, 0.1, mode::INT | mode::APP).expect("real and apparent intersections");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::ZERO, 1e-12), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_and_circle_intersections_remain_available() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Circle(cadcraft_doc::Circle { center: Vec3::ZERO, radius: 2.0 })).unwrap();
|
||||
add_line(&mut d, Vec2::new(-3.0, 0.0), Vec2::new(3.0, 0.0));
|
||||
add_line(&mut d, Vec2::new(0.0, -3.0), Vec2::new(0.0, 3.0));
|
||||
|
||||
let h = hit(&d, Vec2::ZERO, 0.1, mode::INT).expect("crossing lines inside a circle drawing");
|
||||
assert_eq!(h.mode, mode::INT);
|
||||
assert!(h.point.near(Vec2::ZERO, 1e-12), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_query_inputs_return_no_hit() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
add_line(&mut d, Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
|
||||
add_line(&mut d, Vec2::new(0.0, -1.0), Vec2::new(0.0, 1.0));
|
||||
|
||||
assert!(hit(&d, Vec2::ZERO, f64::NAN, mode::INT).is_none());
|
||||
assert!(hit(&d, Vec2::new(f64::INFINITY, 0.0), 0.1, mode::INT).is_none());
|
||||
}
|
||||
Reference in new issue
Block a user