refactor(snap): find both tracking directions in one pass
Perpendicular tracking arrived as a second full scan of the drawing's segments, run next to the edge-direction scan on every acquisition. The two ask different questions of the same segments — does this one end here, does this one pass through here — and the scan is the expensive part, so they now ask together and the drawing is walked once. Two things fall out of sharing the walk. The perpendicular path picks up the non-finite guard the edge path already had, so a NaN vertex can no longer reach the projection. And a perpendicular is now skipped when the edge rays already cover its direction: at a square corner one segment's perpendicular is the other segment's own ray, and two rays down the same line are two identical-looking things for the cursor to choose between. Behaviour is otherwise unchanged — same tolerance, same near-parallel dedup, same cap of six each, same skipping of round wires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2920c4deee
commit
1a753e821c
1 changed files with 60 additions and 117 deletions
169
src/snap.rs
169
src/snap.rs
|
|
@ -114,7 +114,7 @@ pub struct Snapper {
|
||||||
pub osnap_radius_px: f32,
|
pub osnap_radius_px: f32,
|
||||||
/// Object Snap Tracking on/off (F11).
|
/// Object Snap Tracking on/off (F11).
|
||||||
pub otrack_enabled: bool,
|
pub otrack_enabled: bool,
|
||||||
/// Acquired OST points, world space. Tracking is planar: `edge_dirs_at`
|
/// Acquired OST points, world space. Tracking is planar: `tracking_dirs_at`
|
||||||
/// builds every direction from X/Y with Z zeroed, and the rays follow.
|
/// builds every direction from X/Y with Z zeroed, and the rays follow.
|
||||||
pub tracking_points: Vec<DVec3>,
|
pub tracking_points: Vec<DVec3>,
|
||||||
/// Edge directions at each acquired point (parallel to `tracking_points`):
|
/// Edge directions at each acquired point (parallel to `tracking_points`):
|
||||||
|
|
@ -387,8 +387,7 @@ impl Snapper {
|
||||||
// Edge directions double as an endpoint test: a point with no incident
|
// Edge directions double as an endpoint test: a point with no incident
|
||||||
// segment — a midpoint, centre, intersection or extension foot — has
|
// segment — a midpoint, centre, intersection or extension foot — has
|
||||||
// none. Extension-driven acquisition (#262) keeps only endpoints.
|
// none. Extension-driven acquisition (#262) keeps only endpoints.
|
||||||
let dirs = edge_dirs_at(p, wires);
|
let (dirs, perp_dirs) = tracking_dirs_at(p, wires);
|
||||||
let perp_dirs = perpendicular_dirs_at(p, wires);
|
|
||||||
if endpoints_only && dirs.is_empty() {
|
if endpoints_only && dirs.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1736,168 +1735,112 @@ fn snap_priority(t: SnapType) -> u8 {
|
||||||
|
|
||||||
// ── Geometric helpers ─────────────────────────────────────────────────────
|
// ── Geometric helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Line directions of every wire segment that has an endpoint at `p` (an
|
/// Tracking directions through `p`, in one pass: the alignment ray along each
|
||||||
/// acquired corner), deduped by near-parallelism and capped. OTRACK offers an
|
/// segment that ENDS at `p`, and the perpendicular of each segment that PASSES
|
||||||
/// alignment ray along each so the cursor can track a segment's extension, not
|
/// THROUGH it.
|
||||||
/// just the ortho/polar axes (#219). Scanned once, at acquisition — not per
|
///
|
||||||
/// move. Empty when `p` is not a segment endpoint (midpoint / centre / node).
|
/// The two ask different questions of the same segments, and the scan is the
|
||||||
fn edge_dirs_at<W: WireSource + ?Sized>(p: DVec3, wires: &W) -> Vec<DVec3> {
|
/// expensive part — asking separately would walk the whole drawing twice for
|
||||||
|
/// every acquisition. Both are deduped by near-parallelism and capped.
|
||||||
|
///
|
||||||
|
/// Edge rays let the cursor track a segment's extension rather than only the
|
||||||
|
/// ortho/polar axes (#219); their sign is kept, because Extension needs to know
|
||||||
|
/// which way the segment left the corner, so two opposite collinear edges stay
|
||||||
|
/// distinct. Perpendicular rays are OTRACK's alone and describe an infinite
|
||||||
|
/// line, so one direction covers both sides (#695).
|
||||||
|
///
|
||||||
|
/// Scanned once, at acquisition — not per move. The edge list is empty when `p`
|
||||||
|
/// is not a segment endpoint (midpoint / centre / node), which is what makes it
|
||||||
|
/// double as the endpoint test.
|
||||||
|
fn tracking_dirs_at<W: WireSource + ?Sized>(
|
||||||
|
p: DVec3,
|
||||||
|
wires: &W,
|
||||||
|
) -> (Vec<DVec3>, Vec<DVec3>) {
|
||||||
// Acquired points and reconstructed wire vertices are both f64. Keep a
|
// Acquired points and reconstructed wire vertices are both f64. Keep a
|
||||||
// small scale-aware window for double-single reconstruction residuals
|
// small scale-aware window for double-single reconstruction residuals
|
||||||
// without allowing unrelated UTM-scale vertices to match.
|
// without allowing unrelated UTM-scale vertices to match.
|
||||||
let tol = 1e-8_f64.max(2e-12 * p.x.abs().max(p.y.abs()));
|
let tol = 1e-8_f64.max(2e-12 * p.x.abs().max(p.y.abs()));
|
||||||
let tol2 = tol * tol;
|
let tol2 = tol * tol;
|
||||||
let mut dirs: Vec<DVec3> = Vec::new();
|
let mut edges: Vec<DVec3> = Vec::new();
|
||||||
|
let mut perps: Vec<DVec3> = Vec::new();
|
||||||
let mut consider = |a: DVec3, b: DVec3| {
|
let mut consider = |a: DVec3, b: DVec3| {
|
||||||
if !a.x.is_finite() || !b.x.is_finite() {
|
if !a.x.is_finite() || !b.x.is_finite() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let seg_len = {
|
||||||
|
let seg = b - a;
|
||||||
|
(seg.x * seg.x + seg.y * seg.y).sqrt()
|
||||||
|
};
|
||||||
|
if seg_len < 1e-9 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let at_a = (a - p).length_squared() < tol2;
|
let at_a = (a - p).length_squared() < tol2;
|
||||||
let at_b = (b - p).length_squared() < tol2;
|
let at_b = (b - p).length_squared() < tol2;
|
||||||
if !at_a && !at_b {
|
|
||||||
return false;
|
if at_a || at_b {
|
||||||
}
|
// Store the outward ray from the acquired endpoint.
|
||||||
// Store the outward ray from the acquired endpoint. Unlike OTRACK's
|
|
||||||
// infinite alignment line, Extension needs the sign, so opposite
|
|
||||||
// collinear incident edges remain distinct.
|
|
||||||
let seg = if at_a { a - b } else { b - a };
|
let seg = if at_a { a - b } else { b - a };
|
||||||
let l = (seg.x * seg.x + seg.y * seg.y).sqrt();
|
let d = DVec3::new(seg.x / seg_len, seg.y / seg_len, 0.0);
|
||||||
if l < 1e-9 {
|
if !edges.iter().any(|e| e.x * d.x + e.y * d.y > 0.99996) {
|
||||||
return false;
|
edges.push(d);
|
||||||
}
|
}
|
||||||
let d = DVec3::new(seg.x / l, seg.y / l, 0.0);
|
|
||||||
if dirs.iter().any(|e| e.x * d.x + e.y * d.y > 0.99996) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
dirs.push(d);
|
|
||||||
dirs.len() >= 6
|
|
||||||
};
|
|
||||||
let is_round = |wire: &WireModel| {
|
|
||||||
wire.snap_pts
|
|
||||||
.iter()
|
|
||||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant | SnapHint::Center))
|
|
||||||
};
|
|
||||||
if let Some(segments) = indexed_segments(wires) {
|
|
||||||
for segment in segments {
|
|
||||||
if wires.source_wire(segment.wire).is_some_and(&is_round) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if consider(segment.a, segment.b) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
'outer: for wire in wires.iter() {
|
|
||||||
if is_round(wire) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let n = wire.points.len();
|
|
||||||
if n < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for i in 0..n - 1 {
|
|
||||||
let a = wp_f64(wire, i);
|
|
||||||
let b = wp_f64(wire, i + 1);
|
|
||||||
if consider(a, b) {
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perpendicular tracking directions through `p`.
|
|
||||||
///
|
|
||||||
/// Unlike `edge_dirs_at`, this also accepts a point lying anywhere on a
|
|
||||||
/// segment, not only an endpoint. This lets Perpendicular + OTRACK acquire
|
|
||||||
/// a point on an existing line and track away from it at exactly 90°. (#695)
|
|
||||||
fn perpendicular_dirs_at<W: WireSource + ?Sized>(
|
|
||||||
p: DVec3,
|
|
||||||
wires: &W,
|
|
||||||
) -> Vec<DVec3> {
|
|
||||||
let tol = 1e-8_f64.max(2e-12 * p.x.abs().max(p.y.abs()));
|
|
||||||
let tol2 = tol * tol;
|
|
||||||
|
|
||||||
let mut dirs: Vec<DVec3> = Vec::new();
|
|
||||||
|
|
||||||
let mut consider = |a: DVec3, b: DVec3| {
|
|
||||||
// `p` may be an endpoint, midpoint or arbitrary Perpendicular snap
|
|
||||||
// point. Accept the segment when p lies on it within reconstruction
|
|
||||||
// tolerance.
|
|
||||||
let nearest = nearest_on_segment(p, a, b);
|
|
||||||
|
|
||||||
if (nearest - p).length_squared() > tol2 {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A perpendicular needs only that `p` lie on the segment — the whole
|
||||||
|
// point of #695 is tracking away from a point partway along a line, not
|
||||||
|
// just from its ends.
|
||||||
|
if at_a || at_b || (nearest_on_segment(p, a, b) - p).length_squared() <= tol2 {
|
||||||
let seg = b - a;
|
let seg = b - a;
|
||||||
let len = (seg.x * seg.x + seg.y * seg.y).sqrt();
|
let perp = DVec3::new(-seg.y / seg_len, seg.x / seg_len, 0.0);
|
||||||
|
|
||||||
if len < 1e-9 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 90-degree rotation in the drawing XY plane.
|
|
||||||
//
|
|
||||||
// OTRACK treats this as an infinite alignment line, so one normalized
|
|
||||||
// direction is enough; the cursor may track either side of the origin.
|
|
||||||
let perp = DVec3::new(-seg.y / len, seg.x / len, 0.0);
|
|
||||||
|
|
||||||
// Opposite perpendiculars describe the same infinite tracking line.
|
// Opposite perpendiculars describe the same infinite tracking line.
|
||||||
if dirs
|
let known = |list: &[DVec3]| {
|
||||||
.iter()
|
list.iter()
|
||||||
.any(|d| (d.x * perp.x + d.y * perp.y).abs() > 0.99996)
|
.any(|d| (d.x * perp.x + d.y * perp.y).abs() > 0.99996)
|
||||||
{
|
};
|
||||||
return false;
|
// Also skip a perpendicular that lands on a direction the edge rays
|
||||||
|
// already cover: at a square corner one segment's perpendicular is
|
||||||
|
// the other's own ray, and two rays down the same line are two
|
||||||
|
// things for the cursor to choose between that look identical.
|
||||||
|
if !known(&perps) && !known(&edges) {
|
||||||
|
perps.push(perp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dirs.push(perp);
|
edges.len() >= 6 && perps.len() >= 6
|
||||||
dirs.len() >= 6
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_round = |wire: &WireModel| {
|
let is_round = |wire: &WireModel| {
|
||||||
wire.snap_pts
|
wire.snap_pts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant | SnapHint::Center))
|
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant | SnapHint::Center))
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(segments) = indexed_segments(wires) {
|
if let Some(segments) = indexed_segments(wires) {
|
||||||
for segment in segments {
|
for segment in segments {
|
||||||
if wires.source_wire(segment.wire).is_some_and(&is_round) {
|
if wires.source_wire(segment.wire).is_some_and(&is_round) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if consider(segment.a, segment.b) {
|
if consider(segment.a, segment.b) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return (edges, perps);
|
||||||
return dirs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
'outer: for wire in wires.iter() {
|
'outer: for wire in wires.iter() {
|
||||||
if is_round(wire) {
|
if is_round(wire) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let n = wire.points.len();
|
let n = wire.points.len();
|
||||||
|
|
||||||
if n < 2 {
|
if n < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..n - 1 {
|
for i in 0..n - 1 {
|
||||||
let a = wp_f64(wire, i);
|
let a = wp_f64(wire, i);
|
||||||
let b = wp_f64(wire, i + 1);
|
let b = wp_f64(wire, i + 1);
|
||||||
|
|
||||||
if consider(a, b) {
|
if consider(a, b) {
|
||||||
break 'outer;
|
break 'outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(edges, perps)
|
||||||
dirs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue