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 <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>
395 lines
16 KiB
Rust
395 lines
16 KiB
Rust
use std::f64::consts::PI;
|
||
|
||
use super::intersect::{segment_meets_triangle, surface_pair_intersects, triangles_meet};
|
||
use super::*;
|
||
|
||
type Mesh = (Vec<[f64; 3]>, Vec<[u32; 3]>, Vec<u32>);
|
||
|
||
/// An axis-aligned cube, counter-clockwise from outside, one face tag per side.
|
||
fn cube(lo: [f64; 3], size: f64) -> Mesh {
|
||
let p =
|
||
(0..8).map(|i| [lo[0] + size * f64::from(i & 1), lo[1] + size * f64::from((i >> 1) & 1), lo[2] + size * f64::from((i >> 2) & 1)]).collect();
|
||
let t = vec![[0, 2, 3], [0, 3, 1], [4, 5, 7], [4, 7, 6], [0, 1, 5], [0, 5, 4], [2, 6, 7], [2, 7, 3], [0, 4, 6], [0, 6, 2], [1, 3, 7], [1, 7, 5]];
|
||
(p, t, vec![0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5])
|
||
}
|
||
|
||
fn flip(m: &mut Mesh) {
|
||
for t in &mut m.1 {
|
||
t.swap(1, 2);
|
||
}
|
||
}
|
||
|
||
fn merge(a: Mesh, b: Mesh) -> Mesh {
|
||
let (mut p, mut t, mut f) = a;
|
||
let n = p.len() as u32;
|
||
let faces = f.iter().max().map_or(0, |m| m + 1);
|
||
p.extend(b.0);
|
||
t.extend(b.1.iter().map(|t| t.map(|v| v + n)));
|
||
f.extend(b.2.iter().map(|x| x + faces));
|
||
(p, t, f)
|
||
}
|
||
|
||
/// Every triangle with its own three vertices, as SolidCraft's tessellator emits them.
|
||
fn soup(m: &Mesh) -> Mesh {
|
||
let mut p = Vec::new();
|
||
let mut t = Vec::new();
|
||
for tri in &m.1 {
|
||
let base = p.len() as u32;
|
||
p.extend(tri.map(|v| m.0[v as usize]));
|
||
t.push([base, base + 1, base + 2]);
|
||
}
|
||
(p, t, m.2.clone())
|
||
}
|
||
|
||
/// A closed parametric surface on an `n × m` grid, (u, v) ↦ point, periodic in u; `poles` caps
|
||
/// v = 0 and v = 1 with single vertices (a sphere), otherwise v is periodic too (a torus).
|
||
fn grid(n: u32, m: u32, poles: bool, f: impl Fn(f64, f64) -> [f64; 3]) -> Mesh {
|
||
let mut p = Vec::new();
|
||
let rows = if poles { m - 1 } else { m };
|
||
for j in 0..rows {
|
||
for i in 0..n {
|
||
let v = if poles { f64::from(j + 1) / f64::from(m) } else { f64::from(j) / f64::from(m) };
|
||
p.push(f(f64::from(i) / f64::from(n), v));
|
||
}
|
||
}
|
||
let id = |i: u32, j: u32| (j % rows) * n + i % n;
|
||
let mut t = Vec::new();
|
||
let quads = if poles { rows - 1 } else { rows };
|
||
for j in 0..quads {
|
||
for i in 0..n {
|
||
let (a, b, c, d) = (id(i, j), id(i + 1, j), id(i + 1, j + 1), id(i, j + 1));
|
||
t.push([a, b, c]);
|
||
t.push([a, c, d]);
|
||
}
|
||
}
|
||
if poles {
|
||
let (south, north) = (p.len() as u32, p.len() as u32 + 1);
|
||
p.push(f(0.0, 0.0));
|
||
p.push(f(0.0, 1.0));
|
||
for i in 0..n {
|
||
t.push([south, id(i + 1, 0), id(i, 0)]);
|
||
t.push([north, id(i, rows - 1), id(i + 1, rows - 1)]);
|
||
}
|
||
}
|
||
let faces = vec![0; t.len()];
|
||
let mut mesh = (p, t, faces);
|
||
// Orient outwards.
|
||
let six_v: f64 = mesh.1.iter().map(|t| crate::vec3::det(mesh.0[t[0] as usize], mesh.0[t[1] as usize], mesh.0[t[2] as usize])).sum();
|
||
if six_v < 0.0 {
|
||
flip(&mut mesh);
|
||
}
|
||
mesh
|
||
}
|
||
|
||
fn torus(n: u32, m: u32) -> Mesh {
|
||
grid(n, m, false, |u, v| {
|
||
let (a, b) = (2.0 * PI * u, 2.0 * PI * v);
|
||
[(3.0 + b.cos()) * a.cos(), (3.0 + b.cos()) * a.sin(), b.sin()]
|
||
})
|
||
}
|
||
|
||
fn sphere(n: u32, m: u32) -> Mesh {
|
||
grid(n, m, true, |u, v| {
|
||
let (a, b) = (2.0 * PI * u, PI * v);
|
||
[b.sin() * a.cos(), b.sin() * a.sin(), -b.cos()]
|
||
})
|
||
}
|
||
|
||
fn moved(m: Mesh, d: [f64; 3]) -> Mesh {
|
||
(m.0.iter().map(|p| [p[0] + d[0], p[1] + d[1], p[2] + d[2]]).collect(), m.1, m.2)
|
||
}
|
||
|
||
fn prepare(m: &Mesh) -> Result<BoundarySurface> {
|
||
BoundarySurface::new(&m.0, &m.1, &m.2, &SurfaceOptions::default())
|
||
}
|
||
|
||
fn defects(m: &Mesh) -> Defects {
|
||
match prepare(m) {
|
||
Err(Error::Surface(d)) => d,
|
||
other => panic!("expected defects, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
fn kinds(d: &Defects) -> Vec<&'static str> {
|
||
let mut k: Vec<_> = d.listed.iter().map(Defect::kind).collect();
|
||
k.dedup();
|
||
k
|
||
}
|
||
|
||
#[test]
|
||
fn cube_is_a_valid_boundary_with_its_edges_as_features() {
|
||
let s = prepare(&cube([0.0; 3], 2.0)).unwrap();
|
||
assert_eq!((s.points().len(), s.triangles().len()), (8, 12));
|
||
assert_eq!(s.shells().len(), 1);
|
||
let sh = s.shells()[0];
|
||
assert_eq!((sh.genus, sh.triangles, sh.vertices, sh.void), (0, 12, 8, false));
|
||
assert!((s.volume() - 8.0).abs() < 1e-12);
|
||
assert_eq!(s.feature_edges().len(), 12);
|
||
assert!(s.feature_edges().iter().all(|e| e.kind == FeatureKind::FaceBoundary && (e.angle - 90.0).abs() < 1e-9));
|
||
assert_eq!(s.feature_corners(), (0..8).collect::<Vec<u32>>());
|
||
for (t, n) in s.neighbors().iter().enumerate() {
|
||
for (k, &m) in n.iter().enumerate() {
|
||
let (a, b) = (s.triangles()[t][k], s.triangles()[t][(k + 1) % 3]);
|
||
let other = s.triangles()[m as usize];
|
||
assert!((0..3).any(|j| other[j] == b && other[(j + 1) % 3] == a), "triangle {t} edge {k}");
|
||
}
|
||
}
|
||
// Feature edges name first the triangle that traverses them low → high.
|
||
for e in s.feature_edges() {
|
||
let t = s.triangles()[e.triangles[0] as usize];
|
||
assert!((0..3).any(|j| t[j] == e.vertices[0] && t[(j + 1) % 3] == e.vertices[1]));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_triangle_soup_welds_into_the_solid() {
|
||
let c = cube([1.0, -2.0, 0.5], 3.0);
|
||
let s = prepare(&soup(&c)).unwrap();
|
||
assert_eq!((s.points().len(), s.triangles().len()), (8, 12));
|
||
assert_eq!(s.source_vertex().len(), 8);
|
||
assert_eq!(s.source_triangle(), (0..12).collect::<Vec<u32>>());
|
||
assert_eq!(s.tri_face(), c.2.as_slice());
|
||
assert!((s.volume() - 27.0).abs() < 1e-9);
|
||
|
||
// Tessellation noise below the tolerance welds; with exact welding it leaves holes.
|
||
let mut noisy = soup(&c);
|
||
for (i, p) in noisy.0.iter_mut().enumerate() {
|
||
p[0] += 1e-12 * (i % 3) as f64;
|
||
}
|
||
assert_eq!(prepare(&noisy).unwrap().points().len(), 8);
|
||
let exact = SurfaceOptions { weld_tolerance: 0.0, ..SurfaceOptions::default() };
|
||
assert!(matches!(BoundarySurface::new(&noisy.0, &noisy.1, &noisy.2, &exact), Err(Error::Surface(_))));
|
||
// Or an absolute distance (a CAD kernel's tolerance).
|
||
let mut noisy = soup(&c);
|
||
for (i, p) in noisy.0.iter_mut().enumerate() {
|
||
p[1] += 1.5e-6 * (i % 2) as f64;
|
||
}
|
||
assert!(matches!(prepare(&noisy), Err(Error::Surface(_))));
|
||
let kernel = SurfaceOptions { weld_distance: 2e-6, ..SurfaceOptions::default() };
|
||
assert_eq!(BoundarySurface::new(&noisy.0, &noisy.1, &noisy.2, &kernel).unwrap().points().len(), 8);
|
||
}
|
||
|
||
#[test]
|
||
fn untagged_faces_use_the_dihedral_angle() {
|
||
let c = cube([0.0; 3], 1.0);
|
||
let s = BoundarySurface::new(&c.0, &c.1, &[], &SurfaceOptions::default()).unwrap();
|
||
assert!(s.tri_face().iter().all(|&f| f == 0));
|
||
assert_eq!(s.feature_edges().len(), 12);
|
||
assert!(s.feature_edges().iter().all(|e| e.kind == FeatureKind::Sharp));
|
||
let blunt = SurfaceOptions { feature_angle: 90.0, ..SurfaceOptions::default() };
|
||
assert!(BoundarySurface::new(&c.0, &c.1, &[], &blunt).unwrap().feature_edges().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn torus_and_sphere_have_their_genus() {
|
||
let t = prepare(&torus(16, 8)).unwrap();
|
||
assert_eq!((t.shells().len(), t.shells()[0].genus), (1, 1));
|
||
let s = prepare(&sphere(24, 12)).unwrap();
|
||
assert_eq!((s.shells().len(), s.shells()[0].genus), (1, 0));
|
||
// Inscribed in the unit sphere: a little less than its volume.
|
||
let ball = 4.0 / 3.0 * PI;
|
||
assert!(s.volume() < ball && s.volume() > 0.9 * ball, "{}", s.volume());
|
||
// A smooth surface tagged as one face has no features at a fine tessellation.
|
||
assert!(prepare(&torus(64, 32)).unwrap().feature_edges().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn a_void_is_a_negative_shell_inside_another() {
|
||
let mut inner = cube([1.0; 3], 1.0);
|
||
flip(&mut inner);
|
||
let s = prepare(&merge(cube([0.0; 3], 3.0), inner)).unwrap();
|
||
assert_eq!(s.shells().len(), 2);
|
||
assert!(!s.shells()[0].void && s.shells()[1].void);
|
||
assert!((s.volume() - 26.0).abs() < 1e-9);
|
||
assert!(s.tri_shell()[..12].iter().all(|&x| x == 0) && s.tri_shell()[12..].iter().all(|&x| x == 1));
|
||
|
||
// Two separate solids are two outer shells.
|
||
let two = prepare(&merge(cube([0.0; 3], 1.0), cube([5.0, 0.0, 0.0], 1.0))).unwrap();
|
||
assert!(two.shells().iter().all(|s| !s.void && s.volume > 0.0));
|
||
|
||
// A void facing outwards, and a solid facing inwards, are inverted.
|
||
let d = defects(&merge(cube([0.0; 3], 3.0), cube([1.0; 3], 1.0)));
|
||
assert_eq!(d.listed, vec![Defect::Inverted { shell: 1, triangle: 12, volume: 1.0 }]);
|
||
let mut c = cube([0.0; 3], 1.0);
|
||
flip(&mut c);
|
||
assert_eq!(kinds(&defects(&c)), vec!["inverted"]);
|
||
}
|
||
|
||
#[test]
|
||
fn holes_and_bad_edges_are_located() {
|
||
let mut open = cube([0.0; 3], 1.0);
|
||
open.1.remove(3);
|
||
open.2.remove(3);
|
||
let d = defects(&open);
|
||
assert_eq!(d.total, 3);
|
||
assert!(d.listed.iter().all(|x| x.kind() == "open_edge" && x.at().is_some()));
|
||
assert!(d.to_string().starts_with("3 defects; open edge"), "{d}");
|
||
|
||
let mut flipped = cube([0.0; 3], 1.0);
|
||
flipped.1[5].swap(0, 1);
|
||
let d = defects(&flipped);
|
||
assert_eq!(kinds(&d), vec!["flipped"]);
|
||
assert_eq!(d.total, 3);
|
||
assert!(d.listed.iter().all(|x| matches!(x, Defect::Flipped { triangles, .. } if triangles.contains(&5))));
|
||
|
||
// A fin on an edge: three triangles there.
|
||
let mut fin = cube([0.0; 3], 1.0);
|
||
fin.0.push([0.5, -1.0, -1.0]);
|
||
fin.1.push([0, 1, 8]);
|
||
fin.2.push(9);
|
||
assert!(kinds(&defects(&fin)).contains(&"non_manifold_edge"));
|
||
}
|
||
|
||
#[test]
|
||
fn cubes_touching_at_a_corner_are_not_a_manifold() {
|
||
let d = defects(&merge(cube([0.0; 3], 1.0), cube([1.0; 3], 1.0)));
|
||
assert_eq!(d.listed, vec![Defect::NonManifoldVertex { vertex: 7, at: [1.0; 3] }]);
|
||
}
|
||
|
||
#[test]
|
||
fn intersecting_and_touching_solids_are_self_intersections() {
|
||
let d = defects(&merge(cube([0.0; 3], 2.0), cube([1.0; 3], 2.0)));
|
||
assert_eq!(kinds(&d), vec!["self_intersection"]);
|
||
// A smaller block resting on a face: coplanar contact without shared vertices.
|
||
let d = defects(&merge(cube([0.0; 3], 2.0), cube([2.0, 0.5, 0.5], 1.0)));
|
||
assert_eq!(kinds(&d), vec!["self_intersection"]);
|
||
// A void whose corner is pulled onto the outer surface (onto the bottom face's diagonal).
|
||
let mut inner = cube([1.0; 3], 1.0);
|
||
inner.0[0] = [1.0, 1.0, 0.0];
|
||
flip(&mut inner);
|
||
let d = defects(&merge(cube([0.0; 3], 3.0), inner));
|
||
assert_eq!(kinds(&d), vec!["self_intersection"]);
|
||
}
|
||
|
||
#[test]
|
||
fn many_intersections_are_capped() {
|
||
// Small cubes all crossing the top of a large one.
|
||
let mut m = cube([0.0; 3], 100.0);
|
||
for i in 0..60 {
|
||
m = merge(m, cube([1.5 * f64::from(i) + 0.1, 0.1, 99.5], 1.0));
|
||
}
|
||
let d = defects(&m);
|
||
assert_eq!(d.listed.len(), MAX_LISTED_DEFECTS);
|
||
assert!(d.listed.windows(2).all(|w| match (&w[0], &w[1]) {
|
||
(Defect::SelfIntersection { triangles: a, .. }, Defect::SelfIntersection { triangles: b, .. }) => a <= b,
|
||
_ => false,
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_input_is_an_error() {
|
||
let c = cube([0.0; 3], 1.0);
|
||
let mut nan = c.clone();
|
||
nan.0[3][1] = f64::NAN;
|
||
assert_eq!(defects(&nan).listed, vec![Defect::BadCoordinate { vertex: 3 }]);
|
||
let mut far = c.clone();
|
||
far.0[0][0] = 1e10;
|
||
assert_eq!(kinds(&defects(&far)), vec!["bad_coordinate"]);
|
||
let mut bad = c.clone();
|
||
bad.1[2][1] = 99;
|
||
assert_eq!(defects(&bad).listed, vec![Defect::BadIndex { triangle: 2 }]);
|
||
let mut degenerate = c.clone();
|
||
degenerate.0.push([2.0, 0.0, 0.0]);
|
||
degenerate.1.push([0, 1, 8]);
|
||
degenerate.2.push(0);
|
||
assert_eq!(kinds(&defects(°enerate)), vec!["degenerate"]);
|
||
|
||
let o = SurfaceOptions::default();
|
||
assert!(matches!(BoundarySurface::new(&[], &[], &[], &o), Err(Error::Invalid(_))));
|
||
assert!(matches!(BoundarySurface::new(&c.0, &c.1, &[0, 1], &o), Err(Error::Invalid(_))));
|
||
assert!(matches!(BoundarySurface::new(&[[1.0; 3]; 3], &[[0, 1, 2]], &[], &o), Err(Error::Invalid(_))));
|
||
for w in [-1.0, f64::NAN, 1e-20, 0.5, f64::INFINITY] {
|
||
assert!(BoundarySurface::new(&c.0, &c.1, &c.2, &SurfaceOptions { weld_tolerance: w, ..o }).is_err(), "{w}");
|
||
}
|
||
for d in [-1.0, f64::NAN, f64::INFINITY, 0.01] {
|
||
assert!(BoundarySurface::new(&c.0, &c.1, &c.2, &SurfaceOptions { weld_distance: d, ..o }).is_err(), "{d}");
|
||
}
|
||
for a in [0.0, -1.0, 181.0, f64::NAN] {
|
||
assert!(BoundarySurface::new(&c.0, &c.1, &c.2, &SurfaceOptions { feature_angle: a, ..o }).is_err(), "{a}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn tessellation_noise_snaps_into_the_predicates_domain() {
|
||
let mut c = cube([0.0; 3], 1.0);
|
||
c.0[0] = [1e-40, -1e-300, 0.0];
|
||
let s = BoundarySurface::new(&c.0, &c.1, &c.2, &SurfaceOptions { weld_tolerance: 0.0, ..SurfaceOptions::default() }).unwrap();
|
||
assert_eq!(s.points()[0], [0.0; 3]);
|
||
assert!((0..8).all(|v| s.point3(v).is_some()));
|
||
}
|
||
|
||
#[test]
|
||
fn output_is_deterministic() {
|
||
let m = merge(merge(soup(&torus(20, 10)), moved(sphere(16, 8), [0.0, 0.0, 10.0])), cube([20.0; 3], 1.0));
|
||
let a = prepare(&m).unwrap();
|
||
assert_eq!(a, prepare(&m).unwrap());
|
||
assert_eq!(a.shells().iter().map(|s| s.genus).collect::<Vec<_>>(), vec![1, 0, 0]);
|
||
}
|
||
|
||
#[test]
|
||
fn random_soups_never_panic() {
|
||
let mut seed = 0x2545_f491_4f6c_dd1d_u64;
|
||
let mut next = || {
|
||
seed ^= seed << 13;
|
||
seed ^= seed >> 7;
|
||
seed ^= seed << 17;
|
||
seed
|
||
};
|
||
let specials = [0.0, -0.0, 1.0, -1.0, 1e-40, 1e9, f64::NAN, f64::INFINITY, 0.5];
|
||
for _ in 0..2000 {
|
||
let n = (next() % 9 + 1) as usize;
|
||
let p: Vec<[f64; 3]> =
|
||
(0..n).map(|_| [0, 1, 2].map(|_| if next() % 4 == 0 { specials[(next() % 9) as usize] } else { (next() % 5) as f64 })).collect();
|
||
let t: Vec<[u32; 3]> = (0..next() % 12).map(|_| [0, 1, 2].map(|_| (next() % (n as u64 + 1)) as u32)).collect();
|
||
let _ = BoundarySurface::new(&p, &t, &[], &SurfaceOptions::default());
|
||
}
|
||
}
|
||
|
||
fn p3(c: [f64; 3]) -> Point3 {
|
||
Point3::try_from(c).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn exact_pair_tests() {
|
||
let [o, x, y, z] = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]].map(p3);
|
||
let t = [&o, &x, &y];
|
||
// Segments: piercing, touching a vertex, coplanar crossing, coplanar outside, parallel.
|
||
assert!(segment_meets_triangle(&p3([0.2, 0.2, -1.0]), &p3([0.2, 0.2, 1.0]), t));
|
||
assert!(segment_meets_triangle(&p3([1.0, 0.0, 0.0]), &p3([1.0, 0.0, 5.0]), t));
|
||
assert!(segment_meets_triangle(&p3([-1.0, 0.3, 0.0]), &p3([2.0, 0.3, 0.0]), t));
|
||
assert!(!segment_meets_triangle(&p3([0.6, 0.6, 0.0]), &p3([2.0, 2.0, 0.0]), t));
|
||
assert!(!segment_meets_triangle(&p3([0.0, 0.0, 1.0]), &p3([1.0, 0.0, 1.0]), t));
|
||
// On the hypotenuse x + y = 1 of a triangle whose corners are not exact in binary.
|
||
let (a, b) = (p3([0.1, 0.9, 0.0]), p3([0.9, 0.1, 0.0]));
|
||
let mid = p3([0.5, 0.5, 0.0]);
|
||
assert!(
|
||
segment_meets_triangle(&p3([0.5, 0.5, -1.0]), &p3([0.5, 0.5, 1.0]), [&o, &a, &b])
|
||
== (orient2d_sign(&a, &b, &mid) * orient2d_sign(&a, &b, &o) >= 0)
|
||
);
|
||
|
||
assert!(triangles_meet(t, [&p3([0.1, 0.1, -1.0]), &p3([0.1, 0.1, 1.0]), &p3([0.5, 0.1, 0.0])]));
|
||
assert!(!triangles_meet(t, [&p3([0.1, 0.1, 0.5]), &p3([0.6, 0.1, 0.5]), &p3([0.1, 0.6, 0.5])]));
|
||
|
||
// Shared edge o–x: folded flat onto t, flat on the other side, and bent out of plane.
|
||
let ids = |s: [u32; 3], t: [u32; 3]| [s, t];
|
||
let (w, w2, w3) = (p3([0.5, 0.5, 0.0]), p3([0.5, -0.5, 0.0]), p3([0.5, 0.5, 1.0]));
|
||
assert!(surface_pair_intersects(ids([0, 1, 2], [1, 0, 3]), t, [&x, &o, &w]));
|
||
assert!(!surface_pair_intersects(ids([0, 1, 2], [1, 0, 3]), t, [&x, &o, &w2]));
|
||
assert!(!surface_pair_intersects(ids([0, 1, 2], [1, 0, 3]), t, [&x, &o, &w3]));
|
||
|
||
// Shared vertex o: a coplanar triangle inside t's corner, one outside it, one crossing t, one
|
||
// out of plane touching only at o, and one whose edge runs along t's edge o–x.
|
||
let at_o = ids([0, 1, 2], [0, 3, 4]);
|
||
assert!(surface_pair_intersects(at_o, t, [&o, &p3([0.3, 0.1, 0.0]), &p3([0.1, 0.3, 0.0])]));
|
||
assert!(!surface_pair_intersects(at_o, t, [&o, &p3([-0.3, -0.1, 0.0]), &p3([-0.1, -0.3, 0.0])]));
|
||
assert!(surface_pair_intersects(at_o, t, [&o, &p3([0.3, 0.3, -1.0]), &p3([0.3, 0.3, 1.0])]));
|
||
assert!(!surface_pair_intersects(at_o, t, [&o, &z, &p3([-1.0, -1.0, 1.0])]));
|
||
assert!(surface_pair_intersects(at_o, t, [&o, &p3([0.5, 0.0, 0.0]), &p3([0.5, -1.0, 0.0])]));
|
||
}
|
||
|
||
/// The exact orientation of `c` against the directed line `ab` seen from +z, as -1, 0, 1.
|
||
fn orient2d_sign(a: &Point3, b: &Point3, c: &Point3) -> i32 {
|
||
let p = |q: &Point3| crate::predicates::Point2::new(q.coords()[0], q.coords()[1]).unwrap();
|
||
crate::predicates::orient2d(&p(a), &p(b), &p(c)) as i32
|
||
}
|