From c5eec2a50777cfc7ac75a52785c4c63d9beb7ac3 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 17 Sep 2026 16:56:52 +0300 Subject: [PATCH] fix: preserve exact mixed polyline intersections --- src/scene/convert/tessellate.rs | 69 +++++++++++++ src/snap.rs | 167 +++++++++++++++++++------------- 2 files changed, 166 insertions(+), 70 deletions(-) diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 829b20cf1..485b192e9 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -3110,6 +3110,75 @@ mod tests { assert_eq!(line_wire.world_width, 10.0); } + #[test] + fn split_mixed_polyline_keeps_exact_line_intersections() { + let tangents = vec![ + TangentGeom::Line { + p1: [-10.0, 0.0, 0.0], + p2: [0.0, 0.0, 0.0], + }, + TangentGeom::Arc { + center: [0.0, 4.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 4.0, + start_angle: -std::f64::consts::FRAC_PI_2, + end_angle: std::f64::consts::FRAC_PI_2, + }, + TangentGeom::Line { + p1: [0.0, 8.0, 0.0], + p2: [10.0, 8.0, 0.0], + }, + ]; + let vertices = vec![ + [-10.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 8.0, 0.0], + [10.0, 8.0, 0.0], + ]; + let wires = split_mixed_polyline( + &tangents, + &vertices, + "mixed", + [1.0; 4], + false, + 0.0, + [0.0; 8], + 1.0, + Vec::new(), + None, + true, + &[(0.0, 0.0); 3], + 0.0, + Vec::new(), + Vec::new(), + ); + let line_wire = wires + .iter() + .find(|wire| { + wire.tangent_geoms + .iter() + .all(|geom| matches!(geom, TangentGeom::Line { .. })) + }) + .expect("straight-segment wire"); + let circle = WireModel { + tangent_geoms: vec![TangentGeom::PlanarCircle { + center: [5.0, 4.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + }], + ..Default::default() + }; + + let mut points = crate::snap::exact_curve_intersections(line_wire, &circle) + .expect("analytical intersections"); + points.sort_by(|left, right| left.x.total_cmp(&right.x)); + assert_eq!(points.len(), 2, "unexpected intersections: {points:?}"); + assert!((points[0] - glam::DVec3::new(2.0, 8.0, 0.0)).length() < 1.0e-9); + assert!((points[1] - glam::DVec3::new(8.0, 8.0, 0.0)).length() < 1.0e-9); + } + #[test] fn test_split_mixed_polyline_zero_width_segments_after_tapered_segment() { let tg_line = TangentGeom::Line { diff --git a/src/snap.rs b/src/snap.rs index c1cb3a9c4..eea0b02e7 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -2797,27 +2797,56 @@ fn wire_plane(wire: &WireModel) -> Option { None } -fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Vec { +fn tangent_line_endpoints( + wire: &WireModel, + p1: [f32; 3], + p2: [f32; 3], +) -> (DVec3, DVec3) { + if wire.points.len() == 2 && wire.tangent_geoms.len() == 1 { + return (wp_f64(wire, 0), wp_f64(wire, 1)); + } + + let matches = |vertex: [f64; 3], point: [f32; 3]| { + (vertex[0] as f32 - point[0]).abs() < 1e-3 + && (vertex[1] as f32 - point[1]).abs() < 1e-3 + && (vertex[2] as f32 - point[2]).abs() < 1e-3 + }; + let count = wire.key_vertices.len(); + if count >= 2 { + for start in 0..count { + let end = (start + 1) % count; + if matches(wire.key_vertices[start], p1) && matches(wire.key_vertices[end], p2) { + return ( + DVec3::from_array(wire.key_vertices[start]), + DVec3::from_array(wire.key_vertices[end]), + ); + } + } + } + + ( + Vec3::from_array(p1).as_dvec3(), + Vec3::from_array(p2).as_dvec3(), + ) +} + +fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Option> { use cadkernel::geom2d::{Arc as KArc, Circle as KCircle, Ellipse as KEllipse, EllipseArc as KEllipseArc, Line as KLine}; let mut curves = Vec::new(); if wire.tangent_geoms.is_empty() { - if wire.points.len() == 2 { - let (p0, p1) = (wp_f64(wire, 0), wp_f64(wire, 1)); - if frame.contains(p0, tol) && frame.contains(p1, tol) { - curves.push(Curve::Line(KLine { start: frame.to_2d(p0), end: frame.to_2d(p1) })); - } - } else if wire.points.len() > 2 { - for i in 0..wire.points.len().saturating_sub(1) { - let p0 = wp_f64(wire, i); - let p1 = wp_f64(wire, i + 1); - if !p0.is_nan() && !p1.is_nan() && frame.contains(p0, tol) && frame.contains(p1, tol) { - curves.push(Curve::Line(KLine { start: frame.to_2d(p0), end: frame.to_2d(p1) })); - } - } + if wire.points.len() != 2 { + return None; } - return curves; + let (p0, p1) = (wp_f64(wire, 0), wp_f64(wire, 1)); + if !frame.contains(p0, tol) || !frame.contains(p1, tol) { + return None; + } + return Some(vec![Curve::Line(KLine { + start: frame.to_2d(p0), + end: frame.to_2d(p1), + })]); } let angle_in_frame = |world_point: DVec3, centre: DVec3| { @@ -2825,93 +2854,91 @@ fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Vec (p2[1] - c2[1]).atan2(p2[0] - c2[0]) }; - for (geom_idx, geom) in wire.tangent_geoms.iter().enumerate() { + for geom in &wire.tangent_geoms { match geom { TangentGeom::Line { p1, p2 } => { - let (p1, p2) = if wire.key_vertices.len() > geom_idx + 1 { - ( - DVec3::from_array(wire.key_vertices[geom_idx]), - DVec3::from_array(wire.key_vertices[geom_idx + 1]), - ) - } else if wire.points.len() == 2 && wire.tangent_geoms.len() == 1 { - (wp_f64(wire, 0), wp_f64(wire, 1)) - } else { - (Vec3::from_array(*p1).as_dvec3(), Vec3::from_array(*p2).as_dvec3()) - }; - if frame.contains(p1, tol) && frame.contains(p2, tol) { - curves.push(Curve::Line(KLine { start: frame.to_2d(p1), end: frame.to_2d(p2) })); + let (p1, p2) = tangent_line_endpoints(wire, *p1, *p2); + if !frame.contains(p1, tol) || !frame.contains(p2, tol) { + return None; } + curves.push(Curve::Line(KLine { start: frame.to_2d(p1), end: frame.to_2d(p2) })); } TangentGeom::Circle { center, radius } => { let c = DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64); - if DVec3::Z.cross(frame.normal).length() <= tol && frame.contains(c, tol) { - curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius as f64 })); + if DVec3::Z.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; } + curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius as f64 })); } TangentGeom::PlanarCircle { center, axis_x, axis_y, radius } => { let c = DVec3::new(center[0], center[1], center[2]); let n = DVec3::new(axis_x[0], axis_x[1], axis_x[2]).cross(DVec3::new(axis_y[0], axis_y[1], axis_y[2])).normalize(); - if n.cross(frame.normal).length() <= tol && frame.contains(c, tol) { - curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius })); + if n.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; } + curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius })); } TangentGeom::Arc { center, axis_x, axis_y, radius, start_angle, end_angle } => { let c = DVec3::new(center[0], center[1], center[2]); let (ax, ay) = (DVec3::new(axis_x[0], axis_x[1], axis_x[2]), DVec3::new(axis_y[0], axis_y[1], axis_y[2])); let n = ax.cross(ay).normalize(); - if n.cross(frame.normal).length() <= tol && frame.contains(c, tol) { - let arc = KArc { centre: frame.to_2d(c), radius: *radius, start_angle: *start_angle, end_angle: *end_angle }; - let sweep = arc.sweep(); - if (sweep - std::f64::consts::TAU).abs() <= 1e-12 { - curves.push(Curve::Circle(KCircle { centre: arc.centre, radius: *radius })); - } else { - let boundary = if n.dot(frame.normal) < 0.0 { *end_angle } else { *start_angle }; - let start_angle = angle_in_frame(c + *radius * (boundary.cos() * ax + boundary.sin() * ay), c); - curves.push(Curve::Arc(KArc { start_angle, end_angle: start_angle + sweep, ..arc })); - } + if n.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; + } + let arc = KArc { centre: frame.to_2d(c), radius: *radius, start_angle: *start_angle, end_angle: *end_angle }; + let sweep = arc.sweep(); + if (sweep - std::f64::consts::TAU).abs() <= 1e-12 { + curves.push(Curve::Circle(KCircle { centre: arc.centre, radius: *radius })); + } else { + let boundary = if n.dot(frame.normal) < 0.0 { *end_angle } else { *start_angle }; + let start_angle = angle_in_frame(c + *radius * (boundary.cos() * ax + boundary.sin() * ay), c); + curves.push(Curve::Arc(KArc { start_angle, end_angle: start_angle + sweep, ..arc })); } } TangentGeom::PlanarEllipse { center, major_axis, normal, minor_axis_ratio, start_param, end_param } => { let c = DVec3::new(center[0], center[1], center[2]); let ell_normal = DVec3::new(normal[0], normal[1], normal[2]).normalize(); - if ell_normal.cross(frame.normal).length() <= tol && frame.contains(c, tol) { - let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); - let major_radius = major.length(); - if major_radius > 1e-12 { - let minor_radius = major_radius * *minor_axis_ratio; - let dir2d = frame.dir_to_2d(major / major_radius); - let len = (dir2d[0] * dir2d[0] + dir2d[1] * dir2d[1]).sqrt(); - if len > 1e-12 { - let major_axis_2d = [dir2d[0] / len, dir2d[1] / len]; - let (start_parameter, end_parameter) = if ell_normal.dot(frame.normal) < 0.0 { - (-*end_param, -*start_param) - } else { - (*start_param, *end_param) - }; - curves.push(Curve::Ellipse(KEllipseArc { - ellipse: KEllipse { centre: frame.to_2d(c), major_radius, minor_radius, major_axis: major_axis_2d }, - start_parameter, - end_parameter, - })); - } - } + if ell_normal.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; } + let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); + let major_radius = major.length(); + if major_radius <= 1e-12 { + return None; + } + let minor_radius = major_radius * *minor_axis_ratio; + let dir2d = frame.dir_to_2d(major / major_radius); + let len = (dir2d[0] * dir2d[0] + dir2d[1] * dir2d[1]).sqrt(); + if len <= 1e-12 { + return None; + } + let major_axis_2d = [dir2d[0] / len, dir2d[1] / len]; + let (start_parameter, end_parameter) = if ell_normal.dot(frame.normal) < 0.0 { + (-*end_param, -*start_param) + } else { + (*start_param, *end_param) + }; + curves.push(Curve::Ellipse(KEllipseArc { + ellipse: KEllipse { centre: frame.to_2d(c), major_radius, minor_radius, major_axis: major_axis_2d }, + start_parameter, + end_parameter, + })); } } } - curves + Some(curves) } -fn exact_curve_intersections(wire_a: &WireModel, wire_b: &WireModel) -> Option> { +pub(crate) fn exact_curve_intersections( + wire_a: &WireModel, + wire_b: &WireModel, +) -> Option> { let frame = wire_plane(wire_a).or_else(|| wire_plane(wire_b))?; const PLANE_TOL: f64 = 1e-7; - let curves_a = curves_in_frame(wire_a, &frame, PLANE_TOL); - let curves_b = curves_in_frame(wire_b, &frame, PLANE_TOL); - if curves_a.is_empty() || curves_b.is_empty() { - return None; - } + let curves_a = curves_in_frame(wire_a, &frame, PLANE_TOL)?; + let curves_b = curves_in_frame(wire_b, &frame, PLANE_TOL)?; // Two lines have nothing this path can improve on (the segment sweep is // already exact for a straight pair); avoid the extra work.