Boundary surface preparation (weld, manifold/orientation, exact self-intersection, feature edges) and mesh.surface.check (#15)

* mesh: boundary surface preparation (weld, manifold and orientation checks, exact self-intersection, feature edges)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: mesh.surface.check on an open design's body or an imported surface

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: struct-init the test session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* surface: weld at an absolute distance too; the engine welds CAD tessellations at twice SolidCraft's linear tolerance

Faces of a SolidCraft tessellation carry their own copies of shared boundary points, which agree
only to within the kernel's tolerance (up to ~2e-6 mm on the test corpus).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: surface preparation in the literature survey, architecture and roadmap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* surface: simplify after review

- The weld distance travels with the mesh (SurfaceMesh::weld_distance, set by Shape::tessellate),
  so every consumer welds a tessellation correctly without opting in.
- Exact tests are infallible on predicates::orient2d_projected (shared with generic_orient2d), and
  compute each vertex's plane side once per pair with an early reject.
- One Box3 for the weld, the BVH and shells; a Half struct for edges; the weld's two lookups share
  one loop, with chained grid cells; cached normals for feature edges; shared parameter helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* corpus: boundary surface preparation as a corpus stage

20 of 21 parts are valid boundaries with their expected genus; cross-cylinders' tessellation has
cracks and flipped triangles where its two intersection curves meet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 16:01:42 -04:00
1 parent 94dc50bb0c
commit 02cc9dab57
24 files changed
+1987 -60

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
//! Exact triangle–triangle intersection on the orientation predicates.
//!
//! Every decision is a sign of [`orient3d`] or of [`orient2d_projected`] on a coordinate-plane
//! projection, so the answers are exact for every admissible input (Shewchuk 1997; the
//! orientation-only formulation follows Guigue and Devillers 2003). Triangles are closed sets.
//!
//! Two closed triangles meet exactly when an edge of one meets the other: their intersection is
//! a convex set whose extreme points lie on the boundary of one of them. Two triangles of a
//! surface may meet only in what they share, which [`surface_pair_intersects`] tests:
//! - **No shared vertex:** any contact is a defect ([`triangles_meet`]).
//! - **One shared vertex `v`:** the intersection is a convex set containing `v`; it holds another
//! point exactly when an edge opposite `v` meets the other triangle, or an edge from `v` runs
//! into the other triangle's corner at `v`.
//! - **One shared edge:** the triangles meet beyond it only when they are coplanar and on the
//! same side of it (folded onto each other).
//!
//! Every vertex's side of the other triangle's plane is computed once per pair; a triangle
//! strictly on one side of the other's plane rejects the pair at once.
use crate::predicates::{Axis, Point3, Sign, orient2d_projected, orient3d};
type Tri<'a> = [&'a Point3; 3];
/// A coordinate plane on which triangle `abc` projects to a proper triangle, and the
/// orientation of that projection; `None` when the corners are collinear.
fn plane_axis([a, b, c]: Tri) -> Option<(Axis, Sign)> {
[Axis::Z, Axis::X, Axis::Y].into_iter().find_map(|axis| {
let s = orient2d_projected(a, b, c, axis);
(!s.is_zero()).then_some((axis, s))
})
}
/// Whether the corners are exactly collinear (or coincide).
pub fn is_degenerate(t: Tri) -> bool {
plane_axis(t).is_none()
}
fn edges<'a>([a, b, c]: Tri<'a>) -> [(&'a Point3, &'a Point3); 3] {
[(a, b), (b, c), (c, a)]
}
/// The side of `p` against the plane of `t`.
fn side(t: Tri, p: &Point3) -> Sign {
orient3d(t[0], t[1], t[2], p)
}
/// Closed point-in-triangle test in the plane of `t` (`p` must lie in it).
fn point_in_triangle(p: &Point3, t: Tri, drop: Axis, s: Sign) -> bool {
edges(t).into_iter().all(|(u, v)| orient2d_projected(u, v, p, drop) != -s)
}
/// Whether `r`, collinear with `p` and `q`, lies on the closed segment `pq`.
fn on_segment(p: &Point3, q: &Point3, r: &Point3) -> bool {
let (p, q, r) = (p.coords(), q.coords(), r.coords());
(0..3).all(|k| {
let (lo, hi) = if p[k] <= q[k] { (p[k], q[k]) } else { (q[k], p[k]) };
lo <= r[k] && r[k] <= hi
})
}
/// Closed segment–segment intersection of two coplanar segments, on the projection `drop`
/// (which must be injective on their plane).
fn segments_meet(p: &Point3, q: &Point3, a: &Point3, b: &Point3, drop: Axis) -> bool {
let d1 = orient2d_projected(p, q, a, drop);
let d2 = orient2d_projected(p, q, b, drop);
let d3 = orient2d_projected(a, b, p, drop);
let d4 = orient2d_projected(a, b, q, drop);
(d1 * d2 == Sign::Negative && d3 * d4 == Sign::Negative)
|| (d1.is_zero() && on_segment(p, q, a))
|| (d2.is_zero() && on_segment(p, q, b))
|| (d3.is_zero() && on_segment(a, b, p))
|| (d4.is_zero() && on_segment(a, b, q))
}
/// Whether the closed segment `pq` meets the closed, non-degenerate triangle `t`, given the
/// sides `op`, `oq` of `p` and `q` against `t`'s plane.
fn segment_meets(p: &Point3, q: &Point3, op: Sign, oq: Sign, t: Tri) -> bool {
if op == oq && !op.is_zero() {
return false;
}
if op.is_zero() && oq.is_zero() {
let Some((drop, s)) = plane_axis(t) else { return false };
return point_in_triangle(p, t, drop, s)
|| point_in_triangle(q, t, drop, s)
|| edges(t).into_iter().any(|(u, v)| segments_meet(p, q, u, v, drop));
}
// The segment crosses or touches the plane at one point, which is inside the triangle
// exactly when the line pq passes on no strict outside of its three edges.
let s = edges(t).map(|(u, v)| orient3d(p, q, u, v));
!(s.contains(&Sign::Positive) && s.contains(&Sign::Negative))
}
/// Whether the closed segment `pq` meets the closed, non-degenerate triangle `t`.
pub fn segment_meets_triangle(p: &Point3, q: &Point3, t: Tri) -> bool {
segment_meets(p, q, side(t, p), side(t, q), t)
}
/// Whether two closed, non-degenerate triangles intersect at all.
pub fn triangles_meet(s: Tri, t: Tri) -> bool {
let s_sides = s.map(|p| side(t, p));
let t_sides = t.map(|p| side(s, p));
let one_side = |x: [Sign; 3]| !x[0].is_zero() && x[0] == x[1] && x[1] == x[2];
if one_side(s_sides) || one_side(t_sides) {
return false;
}
let crosses = |a: Tri, o: [Sign; 3], b: Tri| (0..3).any(|k| segment_meets(a[k], a[(k + 1) % 3], o[k], o[(k + 1) % 3], b));
crosses(s, s_sides, t) || crosses(t, t_sides, s)
}
/// For triangles sharing the corner `v`: whether the edge from `v` to `a` (whose side of the
/// plane of triangle `v c d` is `side_a`) contains a point of that triangle other than `v`: it
/// lies in that plane, inside the triangle's corner at `v`.
fn edge_enters_corner(v: &Point3, a: &Point3, side_a: Sign, c: &Point3, d: &Point3) -> bool {
if !side_a.is_zero() {
return false;
}
let Some((drop, s)) = plane_axis([v, c, d]) else { return false };
orient2d_projected(v, c, a, drop) != -s && orient2d_projected(v, a, d, drop) != -s
}
/// Whether two triangles of a surface, given by vertex ids and points, meet anywhere other than
/// in the vertices and edge they share. Both must be non-degenerate.
pub fn surface_pair_intersects(ids: [[u32; 3]; 2], s: Tri, t: Tri) -> bool {
let [si, ti] = ids;
// Positions in s of the corners shared with t, and their positions in t.
let mut shared = [(0usize, 0usize); 3];
let mut n = 0;
for (i, v) in si.iter().enumerate() {
if let (Some(j), Some(slot)) = (ti.iter().position(|w| w == v), shared.get_mut(n)) {
*slot = (i, j);
n += 1;
}
}
match shared.get(..n).unwrap_or(&[]) {
[] => triangles_meet(s, t),
&[(i, j)] => {
let v = s[i];
let (a, b) = (s[(i + 1) % 3], s[(i + 2) % 3]);
let (c, d) = (t[(j + 1) % 3], t[(j + 2) % 3]);
let (oa, ob, oc, od) = (side(t, a), side(t, b), side(s, c), side(s, d));
segment_meets(a, b, oa, ob, t)
|| segment_meets(c, d, oc, od, s)
|| edge_enters_corner(v, a, oa, c, d)
|| edge_enters_corner(v, b, ob, c, d)
|| edge_enters_corner(v, c, oc, a, b)
|| edge_enters_corner(v, d, od, a, b)
}
&[(i0, j0), (i1, j1)] => {
// The corner of each triangle off the shared edge.
let (u, w, a, b) = (s[i0], s[i1], s[3 - i0 - i1], t[3 - j0 - j1]);
if !orient3d(u, w, a, b).is_zero() {
return false;
}
let Some((drop, _)) = plane_axis(s) else { return false };
orient2d_projected(u, w, a, drop) == orient2d_projected(u, w, b, drop)
}
// The same three vertices: a duplicate triangle.
_ => true,
}
}