diff --git a/crates/kernel/src/tess/tangle.rs b/crates/kernel/src/tess/tangle.rs index c53a982cef..877b656277 100644 --- a/crates/kernel/src/tess/tangle.rs +++ b/crates/kernel/src/tess/tangle.rs @@ -95,6 +95,19 @@ pub(crate) fn crossing(s: &EvalSurface, reach: f64, loops: &[seam::Loop]) -> Vec out.push(x.at); out.push(y.at); } + // A node of one chord lying on the other (inside it, not at its ends) is no crossing, + // but no triangulation can keep both as constraints either: where two curves leave a + // vertex they touch at along one tangent (a circle touching another from inside), the + // inner one's node can fall on the outer one's chord (with the outer chord twice the + // inner, exactly so but for the next order). The chord the node lies on is halved. + // (Not where the two chords run along one line: a seam's two uses, a period apart, + // lie on each other as copies.) + if !along(x.a, x.b, y.a, y.b) && [y.a, y.b].iter().any(|q| on(x.a, x.b, *q)) { + out.push(x.at); + } + if !along(y.a, y.b, x.a, x.b) && [x.a, x.b].iter().any(|q| on(y.a, y.b, *q)) { + out.push(y.at); + } } } out.sort_unstable(); @@ -102,6 +115,28 @@ pub(crate) fn crossing(s: &EvalSurface, reach: f64, loops: &[seam::Loop]) -> Vec out } +/// Whether `q` lies on the segment `ab` within [`STRADDLE`] (of the segment's squared length, +/// off its line) strictly between its ends (not at either, by the same measure along it). +fn on(a: [f64; 2], b: [f64; 2], q: [f64; 2]) -> bool { + let (dx, dy) = (b[0] - a[0], b[1] - a[1]); + let len2 = dx * dx + dy * dy; + if !(len2 > 0.0) { + return false; + } + let (qx, qy) = (q[0] - a[0], q[1] - a[1]); + let off = (dx * qy - dy * qx).abs(); + let along = (dx * qx + dy * qy) / len2; + off <= 1e3 * STRADDLE * len2 && along > 1e-6 && along < 1.0 - 1e-6 +} + +/// Whether `c` and `d` both lie on the line of `ab` (within [`on`]'s measure). +fn along(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool { + let (dx, dy) = (b[0] - a[0], b[1] - a[1]); + let len2 = dx * dx + dy * dy; + let off = |q: [f64; 2]| (dx * (q[1] - a[1]) - dy * (q[0] - a[0])).abs(); + len2 > 0.0 && off(c) <= 1e3 * STRADDLE * len2 && off(d) <= 1e3 * STRADDLE * len2 +} + /// Whether the segments `ab` and `cd` cross: each one's ends strictly either side of the other. fn cross(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool { let orient = |p: [f64; 2], q: [f64; 2], r: [f64; 2]| (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]); diff --git a/crates/kernel/src/tess/tests.rs b/crates/kernel/src/tess/tests.rs index a98fe87e09..1f4b9ffc92 100644 --- a/crates/kernel/src/tess/tests.rs +++ b/crates/kernel/src/tess/tests.rs @@ -1001,3 +1001,22 @@ fn triangles_standing_across_a_curved_sheet_are_flipped_along_it() { assert!(sides.contains(&(2, 3)) && !sides.contains(&(0, 1)), "{tris:?}"); assert!(after < 0.5 * before, "{before} -> {after}"); } + +/// A disc with a boss standing on it tangent inside its rim: the disc's top is a ring whose inner +/// circle touches the outer one, one loop through the touching vertex. There both circles leave +/// along one tangent, and with the outer chord twice the inner (as their samplings fall) the inner +/// circle's first node lies on the outer's last chord: no triangulation keeps both. That chord is +/// halved, and the ring meshes strictly, to its area (300 pi), not the inner disc's. +#[test] +fn a_ring_touching_itself_at_a_vertex_meshes_strictly() { + use solidcraft_geom::{Loop2, Plane, Region2, Vec2}; + let prism = |c: Vec2, r: f64, z0: f64, z1: f64| { + ok(crate::extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(c, r), holes: vec![] }], z0, z1)).pop().unwrap() + }; + let stepped = + ok(crate::boolean(&prism(Vec2::ZERO, 20.0, 0.0, 5.0), &prism(Vec2::new(10.0, 0.0), 10.0, 5.0, 15.0), crate::BoolOp::Union)).unwrap(); + let (_, stats) = ok(plan(&record(&stepped), 1e-4, None)); + assert!(stats.lenient.is_empty(), "{:?}", stats.lenient.iter().map(|r| &r.reason).collect::>()); + let ring = ok(stepped.faces(1e-4)).into_iter().find(|f| (f.centroid.z - 5.0).abs() < 1e-6).unwrap(); + assert!((ring.area - 300.0 * PI).abs() < 0.05, "ring area {}", ring.area); +}