use super::*;
use crate::csg::{ClippingProcessor, Plane};
fn unit_box() -> Mesh {
let c = [
[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0],
];
let tris: [[usize; 3]; 12] = [
[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7],
[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5],
[2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
];
let mut m = Mesh::new();
for t in tris {
let base = (m.positions.len() / 3) as u32;
for &vi in &t {
m.positions.extend_from_slice(&c[vi]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
}
fn open_edges(m: &Mesh) -> usize {
use std::collections::HashMap;
let key = |i: usize| -> (i64, i64, i64) {
(
(m.positions[i * 3] as f64 * 1.0e5).round() as i64,
(m.positions[i * 3 + 1] as f64 * 1.0e5).round() as i64,
(m.positions[i * 3 + 2] as f64 * 1.0e5).round() as i64,
)
};
let mut vid: HashMap<(i64, i64, i64), u32> = HashMap::new();
let mut bal: HashMap<(u32, u32), i32> = HashMap::new();
for tri in m.indices.chunks_exact(3) {
let mut id = [0u32; 3];
for (j, &vi) in tri.iter().enumerate() {
let k = key(vi as usize);
let n = vid.len() as u32;
id[j] = *vid.entry(k).or_insert(n);
}
for (x, y) in [(id[0], id[1]), (id[1], id[2]), (id[2], id[0])] {
let (kk, s) = if x < y { ((x, y), 1) } else { ((y, x), -1) };
*bal.entry(kk).or_insert(0) += s;
}
}
bal.values().filter(|&&v| v != 0).count()
}
fn signed_volume(m: &Mesh) -> f64 {
let p = |i: usize| {
[
m.positions[i * 3] as f64,
m.positions[i * 3 + 1] as f64,
m.positions[i * 3 + 2] as f64,
]
};
let mut vol = 0.0;
for tri in m.indices.chunks_exact(3) {
let (a, b, c) = (p(tri[0] as usize), p(tri[1] as usize), p(tri[2] as usize));
vol += (a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0]))
/ 6.0;
}
vol
}
fn extrude_profile(profile: &[[f32; 2]], z0: f32, z1: f32) -> Mesh {
use crate::triangulation::triangulate_polygon;
use nalgebra::Point2;
let mut m = Mesh::new();
let mut push = |a: [f32; 3], b: [f32; 3], c: [f32; 3]| {
let base = (m.positions.len() / 3) as u32;
for v in [a, b, c] {
m.positions.extend_from_slice(&v);
m.normals.extend_from_slice(&[0.0, 0.0, 0.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
};
let n = profile.len();
for i in 0..n {
let a = profile[i];
let b = profile[(i + 1) % n];
let (a0, b0) = ([a[0], a[1], z0], [b[0], b[1], z0]);
let (b1, a1) = ([b[0], b[1], z1], [a[0], a[1], z1]);
push(a0, b0, b1); push(a0, b1, a1);
}
let pts: Vec<Point2<f64>> = profile
.iter()
.map(|p| Point2::new(p[0] as f64, p[1] as f64))
.collect();
let idx = triangulate_polygon(&pts).expect("earcut profile");
for t in idx.chunks_exact(3) {
let (a, b, c) = (profile[t[0]], profile[t[1]], profile[t[2]]);
push([a[0], a[1], z1], [b[0], b[1], z1], [c[0], c[1], z1]);
push([a[0], a[1], z0], [c[0], c[1], z0], [b[0], b[1], z0]);
}
m
}
#[test]
fn two_pass_layer_clip_on_nonconvex_profile_is_watertight() {
let u = [
[0.0f32, 0.0], [3.0, 0.0], [3.0, 3.0], [2.0, 3.0],
[2.0, 1.0], [1.0, 1.0], [1.0, 3.0], [0.0, 3.0],
];
let host = extrude_profile(&u, 0.0, 2.5);
assert_eq!(open_edges(&host), 0, "fixture U-prism must be watertight");
let clipper = ClippingProcessor::new();
let after_prev = Plane::new(Point3::new(0.0, 1.6, 0.0), Vector3::new(0.0, 1.0, 0.0));
let before_next = Plane::new(Point3::new(0.0, 2.4, 0.0), Vector3::new(0.0, 1.0, 0.0));
let mut slab = clipper.clip_mesh(&host, &after_prev).unwrap();
cap_half_space_clip(&mut slab, after_prev.point, after_prev.normal);
let flipped = Plane::new(before_next.point, -before_next.normal);
let mut slab = clipper.clip_mesh(&slab, &flipped).unwrap();
cap_half_space_clip(&mut slab, flipped.point, flipped.normal);
assert_eq!(
open_edges(&slab), 0,
"two-pass-clipped non-convex inner slab must be watertight after capping"
);
let v = signed_volume(&slab);
assert!(v > 0.0, "slab winding must stay outward (got {v})");
assert!((v - 4.0).abs() < 1.0e-3, "slab volume should be ~4.0, got {v}");
}
#[test]
fn cap_welds_ulp_twin_section_corner() {
let one_ulp = f32::from_bits(1.0f32.to_bits() + 1); let c = [
[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0],
];
let c1_twin = [one_ulp, 0.0, 0.0]; let mut m = Mesh::new();
let mut push = |a: [f32; 3], b: [f32; 3], cc: [f32; 3]| {
let base = (m.positions.len() / 3) as u32;
for v in [a, b, cc] {
m.positions.extend_from_slice(&v);
m.normals.extend_from_slice(&[0.0, 0.0, 0.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
};
push(c[4], c[5], c[6]); push(c[4], c[6], c[7]); push(c[0], c[1], c[5]); push(c[0], c[5], c[4]); push(c1_twin, c[2], c[6]); push(c1_twin, c[6], c[5]); push(c[2], c[3], c[7]); push(c[2], c[7], c[6]); push(c[3], c[0], c[4]); push(c[3], c[4], c[7]);
assert!(open_edges(&m) > 0, "fixture is open at z=0 before capping");
cap_half_space_clip(&mut m, Point3::new(0.5, 0.5, 0.0), Vector3::new(0.0, 0.0, 1.0));
assert_eq!(
open_edges(&m), 0,
"cap must weld the ~1-ULP section twin and close the z=0 face"
);
}
#[test]
fn unbounded_half_space_clip_is_capped_and_watertight() {
let bx = unit_box();
assert_eq!(open_edges(&bx), 0, "fixture box must be watertight");
assert!((signed_volume(&bx) - 1.0).abs() < 1.0e-6);
let clip_normal = Vector3::new(0.0, 0.0, 1.0);
let plane_point = Point3::new(0.5, 0.5, 0.5);
let clipper = ClippingProcessor::new();
let mut clipped = clipper
.clip_mesh(&bx, &Plane::new(plane_point, clip_normal))
.unwrap();
assert!(open_edges(&clipped) > 0, "raw plane clip leaves the section open");
let tris_before = clipped.indices.len() / 3;
cap_half_space_clip(&mut clipped, plane_point, clip_normal);
assert_eq!(open_edges(&clipped), 0, "capped clip must be watertight");
assert!(clipped.indices.len() / 3 > tris_before, "cap must add triangles");
let v = signed_volume(&clipped);
assert!((v - 0.5).abs() < 1.0e-5, "capped half-box volume should be +0.5, got {v}");
}