forked from 1b-agent/fieldcraft-upstream
* mesh: boundary surface preparation (weld, manifold and orientation checks, exact self-intersection, feature edges) Co-Authored-By: Claude Opus 5.5 <[email protected]> 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: struct-init the test session Co-Authored-By: Claude Opus 5.5 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
161 lines
7.2 KiB
Rust
161 lines
7.2 KiB
Rust
//! 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,
|
||
}
|
||
}
|