fix: preserve exact mixed polyline intersections

This commit is contained in:
Hakan Seven committed 2026-09-17 16:56:52 +03:00
1 parent fa6bf5e7e4
commit c5eec2a507
2 files changed
+166 -70

No files matched your search

+69
View File
@@ -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 {
+97 -70
View File
@@ -2797,27 +2797,56 @@ fn wire_plane(wire: &WireModel) -> Option<WirePlane> {
None
}
fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Vec<Curve> {
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<Vec<Curve>> {
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<Curve>
(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<Vec<DVec3>> {
pub(crate) fn exact_curve_intersections(
wire_a: &WireModel,
wire_b: &WireModel,
) -> Option<Vec<DVec3>> {
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.