forked from 1b-agent/opencadstudio-upstream
fix(snap): keep the key-vertex lookup linear and pin the vertex layouts
`tangent_line_endpoints` (c5eec2a5) resolves a Line geom's f32 endpoints
to the wire's f64 key vertices by scanning for a matching consecutive
pair. Starting every scan at slot 0 makes a dense polyline quadratic, and
the pairwise intersection loop runs it against every curved wire in the
aperture on each cursor move. Carry a cursor across the wire's geoms: the
pairs arrive in vertex order, so the next match sits at or just after the
previous one and the scan is amortised linear.
Add regression tests for the vertex layouts the by-value lookup exists
for — a center mark (two vertices per segment plus the centre), a merged
block entry (per-entity vertex runs), and the closing segment of a closed
polyline at UTM scale — plus one that checks the cursor walks the wire
once.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
472bb9883d
commit
5ed26c858d
1 file changed
+138
-2
+138
-2
@@ -2797,10 +2797,20 @@ fn wire_plane(wire: &WireModel) -> Option<WirePlane> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve a `TangentGeom::Line`'s f32 endpoints to the wire's f64
|
||||
/// `key_vertices`. The two lists are not index-aligned in general — a split
|
||||
/// polyline keeps every vertex but only its straight geoms, a center mark
|
||||
/// stores two vertices per segment, a block entry appends per entity — so the
|
||||
/// vertices are found by value. `cursor` carries the slot of the previous
|
||||
/// match: the geoms come in vertex order, so the next pair sits at or just
|
||||
/// after it and the scan is amortised linear over the wire rather than
|
||||
/// quadratic (a dense contour polyline is checked against every curved wire
|
||||
/// in the aperture on each cursor move).
|
||||
fn tangent_line_endpoints(
|
||||
wire: &WireModel,
|
||||
p1: [f32; 3],
|
||||
p2: [f32; 3],
|
||||
cursor: &mut usize,
|
||||
) -> (DVec3, DVec3) {
|
||||
if wire.points.len() == 2 && wire.tangent_geoms.len() == 1 {
|
||||
return (wp_f64(wire, 0), wp_f64(wire, 1));
|
||||
@@ -2813,9 +2823,11 @@ fn tangent_line_endpoints(
|
||||
};
|
||||
let count = wire.key_vertices.len();
|
||||
if count >= 2 {
|
||||
for start in 0..count {
|
||||
for step in 0..count {
|
||||
let start = (*cursor + step) % count;
|
||||
let end = (start + 1) % count;
|
||||
if matches(wire.key_vertices[start], p1) && matches(wire.key_vertices[end], p2) {
|
||||
*cursor = end;
|
||||
return (
|
||||
DVec3::from_array(wire.key_vertices[start]),
|
||||
DVec3::from_array(wire.key_vertices[end]),
|
||||
@@ -2854,10 +2866,11 @@ fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Option<Vec<
|
||||
(p2[1] - c2[1]).atan2(p2[0] - c2[0])
|
||||
};
|
||||
|
||||
let mut vertex_cursor = 0usize;
|
||||
for geom in &wire.tangent_geoms {
|
||||
match geom {
|
||||
TangentGeom::Line { p1, p2 } => {
|
||||
let (p1, p2) = tangent_line_endpoints(wire, *p1, *p2);
|
||||
let (p1, p2) = tangent_line_endpoints(wire, *p1, *p2, &mut vertex_cursor);
|
||||
if !frame.contains(p1, tol) || !frame.contains(p2, tol) {
|
||||
return None;
|
||||
}
|
||||
@@ -4317,4 +4330,127 @@ mod ext_tests {
|
||||
assert!(line_pt.is_some(), "expected line crossing at (-5, 0, 0), got {pts:?}");
|
||||
}
|
||||
|
||||
fn xy_circle(cx: f64, cy: f64, radius: f64) -> WireModel {
|
||||
WireModel {
|
||||
tangent_geoms: vec![TangentGeom::PlanarCircle {
|
||||
center: [cx, cy, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius,
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn line_geom(a: [f64; 3], b: [f64; 3]) -> TangentGeom {
|
||||
TangentGeom::Line {
|
||||
p1: [a[0] as f32, a[1] as f32, a[2] as f32],
|
||||
p2: [b[0] as f32, b[1] as f32, b[2] as f32],
|
||||
}
|
||||
}
|
||||
|
||||
/// A center mark (`entities/line.rs`) stores two `key_vertices` per
|
||||
/// segment plus the centre, so index `i` of `tangent_geoms` does not name
|
||||
/// vertex `i`; the endpoints must be found by value.
|
||||
#[test]
|
||||
fn exact_curve_intersections_handles_center_mark_vertex_layout() {
|
||||
let segments: [([f64; 3], [f64; 3]); 4] = [
|
||||
([-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
|
||||
([0.0, -1.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
([2.0, 0.0, 0.0], [7.0, 0.0, 0.0]),
|
||||
([0.0, 2.0, 0.0], [0.0, 7.0, 0.0]),
|
||||
];
|
||||
let mut mark = WireModel::default();
|
||||
for (a, b) in segments {
|
||||
mark.key_vertices.push(a);
|
||||
mark.key_vertices.push(b);
|
||||
mark.tangent_geoms.push(line_geom(a, b));
|
||||
}
|
||||
mark.key_vertices.push([0.0, 0.0, 0.0]);
|
||||
|
||||
let mut pts = exact_curve_intersections(&mark, &xy_circle(0.0, 0.0, 5.0))
|
||||
.expect("the extension lines cross the circle");
|
||||
pts.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
assert_eq!(pts.len(), 2, "only the two extension lines reach r=5: {pts:?}");
|
||||
assert!((pts[0] - DVec3::new(0.0, 5.0, 0.0)).length() < 1e-9, "{pts:?}");
|
||||
assert!((pts[1] - DVec3::new(5.0, 0.0, 0.0)).length() < 1e-9, "{pts:?}");
|
||||
}
|
||||
|
||||
/// A block entry (`cache/block_cache.rs`) appends each entity's
|
||||
/// `key_vertices` and `tangent_geoms` in turn; the seam between two
|
||||
/// entities must not be read as a segment.
|
||||
#[test]
|
||||
fn exact_curve_intersections_handles_block_entry_vertex_layout() {
|
||||
let (a0, a1) = ([-10.0, 0.0, 0.0], [10.0, 0.0, 0.0]);
|
||||
let (b0, b1) = ([20.0, -10.0, 0.0], [20.0, 10.0, 0.0]);
|
||||
let block = WireModel {
|
||||
key_vertices: vec![a0, a1, b0, b1],
|
||||
tangent_geoms: vec![
|
||||
line_geom(a0, a1),
|
||||
line_geom(b0, b1),
|
||||
TangentGeom::PlanarCircle {
|
||||
center: [30.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 1.0,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut pts = exact_curve_intersections(&block, &xy_circle(0.0, 0.0, 5.0))
|
||||
.expect("the first line crosses the circle");
|
||||
pts.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
assert_eq!(pts.len(), 2, "the seam a1->b0 is not a segment: {pts:?}");
|
||||
assert!((pts[0] - DVec3::new(-5.0, 0.0, 0.0)).length() < 1e-9, "{pts:?}");
|
||||
assert!((pts[1] - DVec3::new(5.0, 0.0, 0.0)).length() < 1e-9, "{pts:?}");
|
||||
}
|
||||
|
||||
/// The closing segment of a closed polyline runs from the last vertex
|
||||
/// back to the first; it must still resolve to f64 vertices rather than
|
||||
/// the f32 geom endpoints, which drift by centimetres at UTM scale.
|
||||
#[test]
|
||||
fn exact_curve_intersections_closing_segment_keeps_f64_vertices() {
|
||||
let (ox, oy) = (500_000.123456, 4_000_000.654321);
|
||||
let v = [
|
||||
[ox + 10.0, oy, 0.0],
|
||||
[ox + 10.0, oy + 10.0, 0.0],
|
||||
[ox, oy + 10.0, 0.0],
|
||||
[ox, oy, 0.0],
|
||||
];
|
||||
let square = WireModel {
|
||||
key_vertices: v.to_vec(),
|
||||
tangent_geoms: (0..4).map(|i| line_geom(v[i], v[(i + 1) % 4])).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Centred on the closing edge x = ox, so both crossings sit on it.
|
||||
let pts = exact_curve_intersections(&square, &xy_circle(ox, oy + 5.0, 1.0))
|
||||
.expect("the circle crosses the closing edge");
|
||||
assert_eq!(pts.len(), 2, "{pts:?}");
|
||||
for p in &pts {
|
||||
assert!((p.x - ox).abs() < 1e-9, "closing edge fell back to f32 endpoints: {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The by-value vertex lookup must stay linear over the wire: each geom's
|
||||
/// pair is expected at the cursor left by the previous match.
|
||||
#[test]
|
||||
fn tangent_line_endpoints_cursor_walks_the_wire_once() {
|
||||
let n = 64;
|
||||
let verts: Vec<[f64; 3]> = (0..=n).map(|i| [i as f64, (i % 3) as f64, 0.0]).collect();
|
||||
let wire = WireModel {
|
||||
key_vertices: verts.clone(),
|
||||
tangent_geoms: (0..n).map(|i| line_geom(verts[i], verts[i + 1])).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut cursor = 0usize;
|
||||
for (i, geom) in wire.tangent_geoms.iter().enumerate() {
|
||||
let TangentGeom::Line { p1, p2 } = geom else { unreachable!() };
|
||||
let (a, b) = tangent_line_endpoints(&wire, *p1, *p2, &mut cursor);
|
||||
assert_eq!(a, DVec3::from_array(verts[i]));
|
||||
assert_eq!(b, DVec3::from_array(verts[i + 1]));
|
||||
assert_eq!(cursor, i + 1, "cursor must land on the segment's end vertex");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user