Tessellation: a node lying on another chord is untangled like a crossing

A ring whose inner circle touches the outer one (a boss standing on a disc, tangent inside its
rim) is one loop through the touching vertex, both circles leaving it along one tangent. There
the inner circle's first node falls on the outer circle's last chord whenever the outer chord is
twice the inner (exactly but for the next order: the inner circle at s rises s^2 / 2r, the outer
chord at s/2 of its length rises L^2 / 8R, equal for L = 2l, R = 2r), as the samplings fell
here. No crossing by the untangle test, but no triangulation can keep both as constraints: the
strict fill refused the face, and the lenient one, dropping constraints, filled the inner disc
(314 of the ring's 942 mm^2, the Pinch test's 1047 mm^3 shortfall). A node of one chord lying on
another (inside it, not at its ends, and not two chords along one line, as a seam's two uses a
period apart are) now has that chord halved. Regression test: the ring meshes strictly, to
300 pi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 02:22:56 -04:00
committed by Irakli Kourtis
parent a6054603db
commit fcd5f79975
2 changed files with 54 additions and 0 deletions
+35
View File
@@ -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]);
+19
View File
@@ -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::<Vec<_>>());
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);
}