use super::*;
use crate::ClippingProcessor;
fn push_quad(m: &mut Mesh, quad: [Point3<f64>; 4], target: Vector3<f64>) {
let n = (quad[1] - quad[0])
.cross(&(quad[2] - quad[0]))
.try_normalize(1e-12)
.expect("degenerate quad in fixture");
let q = if n.dot(&target) > 0.0 {
[quad[0], quad[1], quad[2], quad[3]]
} else {
[quad[3], quad[2], quad[1], quad[0]]
};
let nrm = target.normalize();
let b = m.vertex_count() as u32;
for p in &q {
m.add_vertex(*p, nrm);
}
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
fn pre_cut_wall() -> Mesh {
pre_cut_wall_with_slot(2.0)
}
fn pre_cut_wall_with_slot(w: f64) -> Mesh {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let (a, b) = (3.0 - w * 0.5, 3.0 + w * 0.5);
let mut m = Mesh::with_capacity(96, 144);
for (y, out) in [(0.0, Vector3::new(0.0, -1.0, 0.0)), (0.4, Vector3::new(0.0, 1.0, 0.0))] {
push_quad(&mut m, [p(0.0, y, 0.0), p(6.0, y, 0.0), p(6.0, y, 0.5), p(0.0, y, 0.5)], out);
push_quad(&mut m, [p(0.0, y, 2.5), p(6.0, y, 2.5), p(6.0, y, 3.0), p(0.0, y, 3.0)], out);
push_quad(&mut m, [p(0.0, y, 0.5), p(a, y, 0.5), p(a, y, 2.5), p(0.0, y, 2.5)], out);
push_quad(&mut m, [p(b, y, 0.5), p(6.0, y, 0.5), p(6.0, y, 2.5), p(b, y, 2.5)], out);
}
push_quad(&mut m, [p(0.0, 0.0, 0.0), p(6.0, 0.0, 0.0), p(6.0, 0.4, 0.0), p(0.0, 0.4, 0.0)], Vector3::new(0.0, 0.0, -1.0));
push_quad(&mut m, [p(0.0, 0.0, 3.0), p(6.0, 0.0, 3.0), p(6.0, 0.4, 3.0), p(0.0, 0.4, 3.0)], Vector3::new(0.0, 0.0, 1.0));
push_quad(&mut m, [p(0.0, 0.0, 0.0), p(0.0, 0.4, 0.0), p(0.0, 0.4, 3.0), p(0.0, 0.0, 3.0)], Vector3::new(-1.0, 0.0, 0.0));
push_quad(&mut m, [p(6.0, 0.0, 0.0), p(6.0, 0.4, 0.0), p(6.0, 0.4, 3.0), p(6.0, 0.0, 3.0)], Vector3::new(1.0, 0.0, 0.0));
push_quad(&mut m, [p(a, 0.0, 0.5), p(a, 0.4, 0.5), p(a, 0.4, 2.5), p(a, 0.0, 2.5)], Vector3::new(1.0, 0.0, 0.0));
push_quad(&mut m, [p(b, 0.0, 0.5), p(b, 0.4, 0.5), p(b, 0.4, 2.5), p(b, 0.0, 2.5)], Vector3::new(-1.0, 0.0, 0.0));
push_quad(&mut m, [p(a, 0.0, 0.5), p(b, 0.0, 0.5), p(b, 0.4, 0.5), p(a, 0.4, 0.5)], Vector3::new(0.0, 0.0, 1.0));
push_quad(&mut m, [p(a, 0.0, 2.5), p(b, 0.0, 2.5), p(b, 0.4, 2.5), p(a, 0.4, 2.5)], Vector3::new(0.0, 0.0, -1.0));
let want = 7.2 - 0.8 * w;
let vol = mesh_signed_volume(&m);
assert!(
(vol - want).abs() < 1e-6,
"pre_cut_wall_with_slot({w}) is malformed or inward-wound: \
signed volume {vol:.6}, expected {want:+.6}"
);
m
}
#[test]
fn a_pull_in_never_inverts_a_shallow_cutter() {
for span in [0.002_f64, 0.004] {
let host = pre_cut_wall_with_slot(span);
let cutter = GeometryRouter::make_box_mesh(
Point3::new(3.0 - span * 0.5, -10.0, 0.5),
Point3::new(3.0 + span * 0.5, 10.4, 2.5),
);
let ext = GeometryRouter::extend_opening_mesh_through_host(
&cutter,
&host,
Vector3::new(1.0, 0.0, 0.0),
);
let (mn, mx) = ext.bounds();
let remaining = (mx.x as f64) - (mn.x as f64);
let (want_lo, want_hi) = (3.0 - span * 0.25, 3.0 + span * 0.25);
assert!(
((mn.x as f64) - want_lo).abs() <= 1.0e-6
&& ((mx.x as f64) - want_hi).abs() <= 1.0e-6,
"span {span}: caps at {:.9} .. {:.9}, expected {want_lo:.9} .. \
{want_hi:.9} (width {remaining:.9})",
mn.x,
mx.x
);
let want_vol = span * 0.5 * 20.4 * 2.0;
let vol = mesh_signed_volume(&ext);
assert!(
(vol - want_vol).abs() <= want_vol * 1.0e-3,
"span {span}: cutter volume {vol:.9}, expected {want_vol:.9}; a \
NEGATIVE value means the caps swapped and the box is inside out"
);
}
}
#[test]
fn remote_coplanar_facet_does_not_outvote_a_local_exit_cap() {
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let plate = GeometryRouter::make_box_mesh(Point3::new(5.0, 0.0, 0.4), Point3::new(15.0, 10.0, 0.6));
let mut host = slab.clone();
host.merge(&plate);
let (inner, outer) = (-0.5, 0.4);
let cutter = GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, inner), Point3::new(1.5, 0.75, outer));
let span = outer - inner;
let extended =
GeometryRouter::extend_opening_mesh_through_host(&cutter, &host, Vector3::new(0.0, 0.0, 1.0));
let clearance = extended.bounds().1.z as f64 - outer;
assert!(
clearance > 0.1 * span,
"a remote coplanar facet must not outvote the local exit cap; the cap was \
pushed clear by only {clearance:.4} of a {span:.4} span"
);
}
#[test]
fn flush_cap_is_not_pushed_into_a_pre_cut_jamb() {
let host = pre_cut_wall();
let cutter = GeometryRouter::make_box_mesh(Point3::new(2.0, -10.0, 0.5), Point3::new(4.0, 10.4, 2.5));
let dir = Vector3::new(1.0, 0.0, 0.0);
let extended = GeometryRouter::extend_opening_mesh_through_host(&cutter, &host, dir);
let clipper = ClippingProcessor::new();
let before = mesh_signed_volume(&host).abs();
let cut = clipper
.subtract_mesh(&host, &extended)
.expect("subtract must not error on two closed boxes");
let removed = before - mesh_signed_volume(&cut).abs();
assert!(
removed.abs() < 1.0e-3,
"a cutter that exactly fills a hole the host already carries must remove \
nothing; removed {removed:.4} m3 (the flush-cap pad ate the piers)"
);
}
#[test]
fn flush_cap_on_a_genuine_exit_is_still_pushed_clear() {
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let (inner, outer) = (0.15, 0.4);
let pocket = GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, inner), Point3::new(1.5, 0.75, outer));
let span = outer - inner;
let extended =
GeometryRouter::extend_opening_mesh_through_host(&pocket, &slab, Vector3::new(0.0, 0.0, 1.0));
let (mn, mx) = extended.bounds();
let (new_inner, new_outer) = (mn.z as f64, mx.z as f64);
let clearance = new_outer - outer;
assert!(
clearance > 0.1 * span && clearance < 1.0 * span,
"an exit cap must be pushed clear of the surface by an opening-relative \
margin; clearance {clearance:.4} is outside (0.1, 1.0) x span {span:.4}"
);
assert!(
(new_inner - inner).abs() < 1.0e-6,
"the floating inner cap must not move, or the pocket becomes a through-hole; \
moved from {inner:.4} to {new_inner:.4}"
);
}
#[test]
fn remove_internal_membrane_no_panic_on_non_finite_coords() {
let mut m = Mesh::new();
for t in 0..4u32 {
let base = t * 3;
for k in 0..3u32 {
m.positions
.extend_from_slice(&[f32::INFINITY, t as f32 + k as f32, k as f32]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
let out =
GeometryRouter::remove_internal_membrane(&m, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(out.indices.len() % 3, 0);
}
#[test]
fn remove_internal_membrane_deterministic_on_all_nan_extents() {
let build = || {
let mut m = Mesh::new();
for t in 0..4u32 {
let base = t * 3;
for _ in 0..3u32 {
m.positions.extend_from_slice(&[
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
};
let a = GeometryRouter::remove_internal_membrane(&build(), Vector3::new(0.0, 0.0, 0.0));
let b = GeometryRouter::remove_internal_membrane(&build(), Vector3::new(0.0, 0.0, 0.0));
assert_eq!(a.indices, b.indices, "all-NaN extents must pick a deterministic axis");
assert_eq!(a.positions.len(), b.positions.len());
assert_eq!(a.indices.len() % 3, 0);
}
#[test]
fn axis_pick_total_order_semantics() {
let pick = |ext: [f64; 3]| -> usize {
(0..3).max_by(|&i, &j| ext[i].total_cmp(&ext[j])).unwrap()
};
assert_eq!(pick([-0.0, 0.0, -1.0]), 1, "+0.0 outranks -0.0 in the total order");
assert_eq!(pick([0.0, 0.0, 0.0]), 2, "ties resolve to the last index");
assert_eq!(pick([f64::NAN, f64::NAN, f64::NAN]), 2, "all-NaN ties resolve to the last index");
assert_eq!(pick([f64::NAN, 1.0, 2.0]), 0, "positive NaN outranks finite values");
assert_eq!(pick([1.0, f64::INFINITY, f64::NAN]), 2, "positive NaN outranks +inf");
}
fn flip_winding(m: &Mesh) -> Mesh {
let mut o = m.clone();
for t in o.indices.chunks_exact_mut(3) {
t.swap(1, 2);
}
o
}
#[test]
fn an_inward_wound_host_is_read_the_same_as_an_outward_one() {
let up = Vector3::new(0.0, 0.0, 1.0);
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let pocket =
GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, 0.15), Point3::new(1.5, 0.75, 0.4));
let clearance = |host: &Mesh| {
GeometryRouter::extend_opening_mesh_through_host(&pocket, host, up)
.bounds()
.1
.z as f64
- 0.4
};
let (outward, inward) = (clearance(&slab), clearance(&flip_winding(&slab)));
assert!(
(outward - inward).abs() < 1.0e-9 && inward > 0.0,
"an inward-wound host must get the same exit clearance as an outward one; \
outward {outward:.6}, inward {inward:.6}"
);
let cutter =
GeometryRouter::make_box_mesh(Point3::new(2.0, -10.0, 0.5), Point3::new(4.0, 10.4, 2.5));
let ext = GeometryRouter::extend_opening_mesh_through_host(
&cutter,
&flip_winding(&pre_cut_wall()),
Vector3::new(1.0, 0.0, 0.0),
);
let (mn, mx) = ext.bounds();
let band = 2.0f64 * 1.0e-3; assert!(
(mn.x as f64) >= 2.0 - 1.0e-6 && (mx.x as f64) <= 4.0 + 1.0e-6,
"an inward-wound pre-cut host must not have its jambs pushed into the piers; \
cutter spans {:.3} .. {:.3}, authored 2.000 .. 4.000",
mn.x,
mx.x
);
assert!(
(mn.x as f64) <= 2.0 + band + 1.0e-6 && (mx.x as f64) >= 4.0 - band - 1.0e-6,
"and it must not under-reach by more than the one band it is pulled in by; \
cutter spans {:.3} .. {:.3}",
mn.x,
mx.x
);
}