use bevy::log::warn;
use bevy::math::Vec3;
use std::collections::HashMap;
use crate::soup::{
EPS, LatticeHash, LatticeMap, MIN_CROSS2, Plane, WELD, classify, plane_basis, signed_dist,
};
const RELIEF_STRIDE: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FaceKind {
Supplied,
Cut,
Bore,
Tension,
Compression,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProxyCell {
verts: Vec<Vec3>,
faces: Vec<Vec<u32>>,
face_kind: Vec<FaceKind>,
}
impl ProxyCell {
pub fn new(verts: Vec<Vec3>, faces: Vec<Vec<u32>>) -> Option<Self> {
if faces.len() < 4 {
warn!("carnage: proxy cell has {} faces; a closed polyhedron needs at least 4", faces.len());
return None;
}
for (i, f) in faces.iter().enumerate() {
if f.len() < 3 {
warn!("carnage: proxy cell face {i} has {} vertices, needs at least 3", f.len());
return None;
}
if f.iter().any(|&v| v as usize >= verts.len()) {
warn!("carnage: proxy cell face {i} indexes outside its {} vertices", verts.len());
return None;
}
}
let face_kind = vec![FaceKind::Supplied; faces.len()];
let cell = Self { verts, faces, face_kind };
cell.reject_if_concave()?;
Some(cell)
}
fn reject_if_concave(&self) -> Option<()> {
let (mut mn, mut mx) = (Vec3::splat(f32::INFINITY), Vec3::splat(f32::NEG_INFINITY));
for v in &self.verts {
mn = mn.min(*v);
mx = mx.max(*v);
}
let tol = (mx - mn).length().max(1.0) * 1.0e-4;
for fi in 0..self.faces.len() {
let Some((o, n)) = self.face_plane(fi) else {
warn!("carnage: proxy cell face {fi} is degenerate — it encloses no area");
return None;
};
let worst = self.verts.iter().map(|v| (*v - o).dot(n)).fold(f32::NEG_INFINITY, f32::max);
if worst > tol {
warn!(
"carnage: proxy cell is not convex — a vertex sits {worst} in front of face {fi} \
(tolerance {tol}). Refusing it rather than cutting a shape that is not the one you \
described; every cut face of a concave cell is concave too, and the cap fan over \
one folds."
);
return None;
}
}
Some(())
}
pub fn from_box(center: Vec3, half: Vec3) -> Self {
let s = |x: f32, y: f32, z: f32| center + Vec3::new(x * half.x, y * half.y, z * half.z);
let verts = vec![
s(-1.0, -1.0, -1.0),
s(1.0, -1.0, -1.0),
s(1.0, 1.0, -1.0),
s(-1.0, 1.0, -1.0),
s(-1.0, -1.0, 1.0),
s(1.0, -1.0, 1.0),
s(1.0, 1.0, 1.0),
s(-1.0, 1.0, 1.0),
];
let faces = vec![
vec![0, 3, 2, 1], vec![4, 5, 6, 7], vec![0, 1, 5, 4], vec![2, 3, 7, 6], vec![0, 4, 7, 3], vec![1, 2, 6, 5], ];
Self { face_kind: vec![FaceKind::Supplied; faces.len()], verts, faces }
}
pub fn points(&self) -> &[Vec3] {
&self.verts
}
pub fn faces(&self) -> impl Iterator<Item = &[u32]> {
self.faces.iter().map(|f| f.as_slice())
}
pub fn face_is_cut(&self, fi: usize) -> bool {
self.face_kind.get(fi).is_some_and(|k| *k != FaceKind::Supplied)
}
pub fn volume(&self) -> f32 {
self.signed_volume()
}
pub fn center(&self) -> Vec3 {
self.centroid()
}
pub(crate) fn centroid(&self) -> Vec3 {
if self.verts.is_empty() {
return Vec3::ZERO;
}
self.verts.iter().copied().sum::<Vec3>() / self.verts.len() as f32
}
pub(crate) fn signed_volume(&self) -> f32 {
let mut v6 = 0.0f32;
for f in &self.faces {
for i in 1..f.len() - 1 {
let (a, b, c) = (
self.verts[f[0] as usize],
self.verts[f[i] as usize],
self.verts[f[i + 1] as usize],
);
v6 += a.cross(b).dot(c);
}
}
v6 / 6.0
}
pub(crate) fn span_along(&self, dir: Vec3, from: Vec3) -> (f32, f32) {
let mut lo = f32::INFINITY;
let mut hi = f32::NEG_INFINITY;
for v in &self.verts {
let d = (*v - from).dot(dir);
lo = lo.min(d);
hi = hi.max(d);
}
if self.verts.is_empty() { (0.0, 0.0) } else { (lo, hi) }
}
pub(crate) fn face_count(&self) -> usize {
self.faces.len()
}
pub(crate) fn face_ring(&self, fi: usize) -> Vec<Vec3> {
let Some(f) = self.faces.get(fi) else { return Vec::new() };
f.iter().filter_map(|&i| self.verts.get(i as usize).copied()).collect()
}
pub(crate) fn face_plane(&self, fi: usize) -> Option<(Vec3, Vec3)> {
if fi >= self.faces.len() {
return None;
}
let f = &self.faces[fi];
let mut n = Vec3::ZERO;
for i in 0..f.len() {
let a = self.verts[f[i] as usize];
let b = self.verts[f[(i + 1) % f.len()] as usize];
n += Vec3::new(
(a.y - b.y) * (a.z + b.z),
(a.z - b.z) * (a.x + b.x),
(a.x - b.x) * (a.y + b.y),
);
}
let n = n.normalize_or_zero();
if n == Vec3::ZERO { None } else { Some((self.verts[f[0] as usize], n)) }
}
pub(crate) fn contains(&self, p: Vec3) -> bool {
(0..self.faces.len()).all(|fi| match self.face_plane(fi) {
Some((o, n)) => (p - o).dot(n) <= EPS,
None => true,
})
}
pub(crate) fn clip(&self, plane: &Plane, new_face: FaceKind) -> (Option<ProxyCell>, Option<ProxyCell>) {
let d: Vec<f32> = self.verts.iter().map(|v| signed_dist(*v, plane)).collect();
if d.iter().all(|&s| s >= -EPS) {
return (Some(self.clone()), None);
}
if d.iter().all(|&s| s <= EPS) {
return (None, Some(self.clone()));
}
let mut above = CellBuilder::default();
let mut below = CellBuilder::default();
let mut cut: Vec<Vec3> = Vec::new();
for (fi, f) in self.faces.iter().enumerate() {
let (mut ra, mut rb): (Vec<Vec3>, Vec<Vec3>) = (Vec::new(), Vec::new());
for i in 0..f.len() {
let j = (i + 1) % f.len();
let (pi, pj) = (self.verts[f[i] as usize], self.verts[f[j] as usize]);
let (si, sj) = (d[f[i] as usize], d[f[j] as usize]);
let (ci, cj) = (classify(si), classify(sj));
if ci >= 0 {
ra.push(pi);
}
if ci <= 0 {
rb.push(pi);
}
if ci == 0 {
cut.push(pi);
}
if ci != 0 && cj != 0 && ci != cj {
let x = pi.lerp(pj, si / (si - sj));
ra.push(x);
rb.push(x);
cut.push(x);
}
}
if ra.len() >= 3 {
above.face(&ra, self.face_kind[fi]);
}
if rb.len() >= 3 {
below.face(&rb, self.face_kind[fi]);
}
}
let ring = convex_ring(&cut, plane);
if ring.len() >= 3 {
let mut rev = ring.clone();
rev.reverse();
above.face(&rev, new_face);
below.face(&ring, new_face);
}
(above.build(), below.build())
}
pub(crate) fn append_cut_faces(&self, out: &mut crate::soup::Soup, seam: &[Vec3], relief: f32) {
for (fi, f) in self.faces.iter().enumerate() {
if self.face_kind[fi] == FaceKind::Supplied {
continue;
}
let Some((origin, n)) = self.face_plane(fi) else { continue };
let relief = match self.face_kind[fi] {
FaceKind::Bore => 0.0,
FaceKind::Tension => relief * 0.5,
FaceKind::Compression => relief * 2.0,
FaceKind::Cut | FaceKind::Supplied => relief,
};
let ring: Vec<Vec3> = f.iter().map(|&v| self.verts[v as usize]).collect();
let ring = weave_seam(&ring, n, origin, seam);
let (bu, bv) = plane_basis(n);
let vtx = |p: Vec3| crate::soup::Vtx {
pos: p,
nrm: n,
uv: bevy::math::Vec2::new((p - origin).dot(bu), (p - origin).dot(bv)),
};
let n_ring = ring.len();
if n_ring < 3 {
continue;
}
let centre: Vec3 = ring.iter().copied().sum::<Vec3>() / n_ring as f32;
let radius = ring.iter().map(|p| p.distance(centre)).fold(0.0f32, f32::max);
let lift = |p: Vec3, scale: f32| -> Vec3 {
if relief <= 0.0 || radius <= 0.0 {
return p;
}
let q = |x: f32| (x / WELD).round() as i64 as u32;
let h = crate::soup::hash_f32(q(p.x) ^ q(p.y).wrapping_mul(0x9E37_79B9) ^ q(p.z).wrapping_mul(2_654_435_761));
p + n * ((h - 0.5) * 2.0 * relief * radius * scale)
};
let hub = lift(centre, 1.0);
let mut emit = |a: Vec3, b: Vec3, c: Vec3| {
if (b - a).cross(c - a).length_squared() >= 1.0e-12 {
out.push_tri(vtx(a), vtx(b), vtx(c), true);
}
};
if relief <= 0.0 {
for i in 0..n_ring {
emit(hub, ring[i], ring[(i + 1) % n_ring]);
}
continue;
}
let m = (n_ring / RELIEF_STRIDE).clamp(3, n_ring);
let mid: Vec<Vec3> =
(0..m).map(|a| lift(centre.lerp(ring[a * n_ring / m], 0.5), 0.7)).collect();
for a in 0..m {
let next = (a + 1) % m;
let lo = a * n_ring / m;
let hi = (a + 1) * n_ring / m;
emit(hub, mid[a], mid[next]);
for t in lo..hi {
emit(mid[a], ring[t % n_ring], ring[(t + 1) % n_ring]);
}
emit(mid[a], ring[hi % n_ring], mid[next]);
}
}
}
pub(crate) fn append_all_faces(&self, out: &mut crate::soup::Soup) {
for fi in 0..self.faces.len() {
let f = &self.faces[fi];
let Some((origin, n)) = self.face_plane(fi) else { continue };
let (bu, bv) = plane_basis(n);
let vtx = |p: Vec3| crate::soup::Vtx {
pos: p,
nrm: n,
uv: bevy::math::Vec2::new((p - origin).dot(bu), (p - origin).dot(bv)),
};
for i in 1..f.len() - 1 {
let (a, b, c) = (
self.verts[f[0] as usize],
self.verts[f[i] as usize],
self.verts[f[i + 1] as usize],
);
if (b - a).cross(c - a).length_squared() < 1.0e-12 {
continue;
}
out.push_tri(vtx(a), vtx(b), vtx(c), self.face_kind[fi] != FaceKind::Supplied);
}
}
}
}
#[derive(Default)]
struct CellBuilder {
verts: Vec<Vec3>,
faces: Vec<Vec<u32>>,
face_kind: Vec<FaceKind>,
table: LatticeMap<(i64, i64, i64), u32>,
}
impl CellBuilder {
fn weld(&mut self, p: Vec3) -> u32 {
let q = |x: f32| (x / WELD).round() as i64;
let key = (q(p.x), q(p.y), q(p.z));
if let Some(&id) = self.table.get(&key) {
return id;
}
let id = self.verts.len() as u32;
self.verts.push(p);
self.table.insert(key, id);
id
}
fn face(&mut self, ring: &[Vec3], kind: FaceKind) {
let mut idx: Vec<u32> = Vec::with_capacity(ring.len());
for p in ring {
let id = self.weld(*p);
if idx.last() != Some(&id) {
idx.push(id);
}
}
if idx.len() > 1 && idx.first() == idx.last() {
idx.pop();
}
if idx.len() >= 3 {
self.faces.push(idx);
self.face_kind.push(kind);
}
}
fn collapse_undrawable_faces(&mut self) {
let area2 = |ring: &[u32], verts: &[Vec3]| -> f32 {
let mut n = Vec3::ZERO;
for i in 0..ring.len() {
let a = verts[ring[i] as usize];
let b = verts[ring[(i + 1) % ring.len()] as usize];
n += a.cross(b);
}
n.length_squared()
};
let mut parent: Vec<u32> = (0..self.verts.len() as u32).collect();
fn find(parent: &mut [u32], mut i: u32) -> u32 {
while parent[i as usize] != i {
parent[i as usize] = parent[parent[i as usize] as usize];
i = parent[i as usize];
}
i
}
let mut merged = false;
for f in &self.faces {
if area2(f, &self.verts) >= MIN_CROSS2 {
continue;
}
let root = find(&mut parent, f[0]);
for &v in &f[1..] {
let r = find(&mut parent, v);
if r != root {
parent[r as usize] = root;
merged = true;
}
}
}
if !merged {
return;
}
let mut sum: HashMap<u32, (Vec3, f32)> = HashMap::new();
for v in 0..self.verts.len() as u32 {
let r = find(&mut parent, v);
let e = sum.entry(r).or_insert((Vec3::ZERO, 0.0));
e.0 += self.verts[v as usize];
e.1 += 1.0;
}
for (r, (s, n)) in &sum {
self.verts[*r as usize] = *s / *n;
}
let (faces, face_kind) = (std::mem::take(&mut self.faces), std::mem::take(&mut self.face_kind));
for (f, kind) in faces.into_iter().zip(face_kind) {
let mut idx: Vec<u32> = Vec::with_capacity(f.len());
for v in f {
let r = find(&mut parent, v);
if idx.last() != Some(&r) {
idx.push(r);
}
}
if idx.len() > 1 && idx.first() == idx.last() {
idx.pop();
}
if idx.len() >= 3 && area2(&idx, &self.verts) >= MIN_CROSS2 {
self.faces.push(idx);
self.face_kind.push(kind);
}
}
}
fn build(mut self) -> Option<ProxyCell> {
self.collapse_undrawable_faces();
if self.faces.len() < 4 {
return None;
}
Some(ProxyCell { verts: self.verts, faces: self.faces, face_kind: self.face_kind })
}
}
fn on_ring_boundary(ring: &[Vec3], p: Vec3) -> bool {
for i in 0..ring.len() {
let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
let ab = b - a;
let len2 = ab.length_squared();
if len2 <= 1.0e-12 {
continue;
}
let t = (p - a).dot(ab) / len2;
if (-EPS..=1.0 + EPS).contains(&t) && (a + ab * t.clamp(0.0, 1.0)).distance(p) <= EPS {
return true;
}
}
false
}
fn weave_seam(ring: &[Vec3], n: Vec3, origin: Vec3, seam: &[Vec3]) -> Vec<Vec3> {
if seam.is_empty() {
return ring.to_vec();
}
let mut merged = ring.to_vec();
for p in seam {
if (*p - origin).dot(n).abs() > EPS {
continue; }
if on_ring_boundary(ring, *p) {
merged.push(*p);
}
}
if merged.len() == ring.len() {
return merged;
}
let ordered = convex_ring(&merged, &Plane { point: origin, normal: n });
if ordered.len() < 3 { ring.to_vec() } else { ordered }
}
fn convex_ring(pts: &[Vec3], plane: &Plane) -> Vec<Vec3> {
let q = |x: f32| (x / WELD).round() as i64;
let mut seen: LatticeMap<(i64, i64, i64), ()> =
LatticeMap::with_capacity_and_hasher(pts.len(), LatticeHash);
let mut uniq: Vec<Vec3> = Vec::new();
for p in pts {
if seen.insert((q(p.x), q(p.y), q(p.z)), ()).is_none() {
uniq.push(*p);
}
}
if uniq.len() < 3 {
return uniq;
}
let c: Vec3 = uniq.iter().copied().sum::<Vec3>() / uniq.len() as f32;
let (u, v) = plane_basis(plane.normal);
let mut keyed: Vec<(f32, Vec3)> = uniq
.into_iter()
.map(|p| {
let d = p - c;
(d.dot(v).atan2(d.dot(u)), p)
})
.collect();
keyed.sort_by(|(ka, a), (kb, b)| {
ka.total_cmp(kb)
.then_with(|| a.x.to_bits().cmp(&b.x.to_bits()))
.then_with(|| a.y.to_bits().cmp(&b.y.to_bits()))
.then_with(|| a.z.to_bits().cmp(&b.z.to_bits()))
});
keyed.into_iter().map(|(_, p)| p).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn unit_box() -> ProxyCell {
ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5))
}
#[test]
fn a_concave_cell_is_refused() {
let mut verts = vec![
Vec3::new(-0.5, -0.5, -0.5),
Vec3::new(0.5, -0.5, -0.5),
Vec3::new(0.5, 0.5, -0.5),
Vec3::new(-0.5, 0.5, -0.5),
Vec3::new(-0.5, -0.5, 0.5),
Vec3::new(0.5, -0.5, 0.5),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-0.5, 0.5, 0.5),
];
let faces = vec![
vec![0, 3, 2, 1],
vec![4, 5, 6, 7],
vec![0, 1, 5, 4],
vec![2, 3, 7, 6],
vec![0, 4, 7, 3],
vec![1, 2, 6, 5],
];
assert!(ProxyCell::new(verts.clone(), faces.clone()).is_some(), "a box must be admitted");
verts[6] = Vec3::new(0.1, 0.1, 0.1);
assert!(
ProxyCell::new(verts, faces).is_none(),
"a dented cube is not convex and must be refused, not cut"
);
}
#[test]
fn a_large_convex_cell_is_not_rejected_for_being_large() {
for half in [0.01f32, 1.0, 100.0] {
let c = ProxyCell::from_box(Vec3::ZERO, Vec3::splat(half));
let round_tripped = ProxyCell::new(c.points().to_vec(), c.faces().map(|f| f.to_vec()).collect());
assert!(round_tripped.is_some(), "a {half}-half-extent box was refused as non-convex");
}
}
#[test]
fn from_box_is_wound_outward() {
let c = unit_box();
for fi in 0..c.faces.len() {
let (o, n) = c.face_plane(fi).expect("box face is non-degenerate");
assert!(
(o - c.centroid()).dot(n) > 0.0,
"face {fi} normal {n} points back at the centroid — the ring is wound inward"
);
}
assert!((c.volume() - 1.0).abs() < 1.0e-5, "unit box encloses {}, expected 1.0", c.volume());
}
#[test]
fn contains_agrees_with_the_box_it_was_built_from() {
let c = unit_box();
assert!(c.contains(Vec3::ZERO));
assert!(c.contains(Vec3::new(0.49, 0.49, 0.49)));
assert!(!c.contains(Vec3::new(0.51, 0.0, 0.0)));
assert!(!c.contains(Vec3::new(0.0, -2.0, 0.0)));
assert!(c.contains(Vec3::new(0.5, 0.0, 0.0)));
}
#[test]
fn a_plane_splits_a_cell_into_two_closed_halves() {
let c = unit_box();
let p = Plane { point: Vec3::new(0.1, 0.0, 0.0), normal: Vec3::X };
let (a, b) = c.clip(&p, FaceKind::Cut);
let (a, b) = (a.expect("above half exists"), b.expect("below half exists"));
assert!((a.volume() - 0.4).abs() < 1.0e-4, "above encloses {}, expected 0.4", a.volume());
assert!((b.volume() - 0.6).abs() < 1.0e-4, "below encloses {}, expected 0.6", b.volume());
assert!(
(a.volume() + b.volume() - c.volume()).abs() < 1.0e-4,
"the cut gained or lost volume: {} + {} != {}",
a.volume(),
b.volume(),
c.volume()
);
assert_eq!(
a.face_kind.iter().filter(|k| **k == FaceKind::Cut).count(),
1,
"above should have one cut face"
);
assert_eq!(
b.face_kind.iter().filter(|k| **k == FaceKind::Cut).count(),
1,
"below should have one cut face"
);
}
#[test]
fn an_oblique_cut_still_closes_and_conserves_volume() {
let c = unit_box();
let p = Plane { point: Vec3::ZERO, normal: Vec3::new(1.0, 1.0, 1.0).normalize() };
let (a, b) = c.clip(&p, FaceKind::Cut);
let (a, b) = (a.expect("above"), b.expect("below"));
assert!(
(a.volume() + b.volume() - 1.0).abs() < 1.0e-4,
"oblique cut: {} + {} != 1.0",
a.volume(),
b.volume()
);
assert!(a.volume() > 0.0 && b.volume() > 0.0, "both halves must be positively oriented");
}
#[test]
fn a_plane_outside_the_cell_does_not_split_it() {
let c = unit_box();
let (a, b) = c.clip(&Plane { point: Vec3::new(5.0, 0.0, 0.0), normal: Vec3::X }, FaceKind::Cut);
assert!(a.is_none(), "nothing lies above a plane past the cell");
assert!(b.is_some(), "the whole cell lies below it");
}
#[test]
fn eight_successive_cuts_conserve_volume_and_stay_closed() {
let mut cells = vec![unit_box()];
let planes = [
(Vec3::X, 0.05f32),
(Vec3::Y, -0.1),
(Vec3::Z, 0.15),
(Vec3::new(1.0, 1.0, 0.0).normalize(), 0.0),
];
for (n, off) in planes {
let mut next = Vec::new();
for c in &cells {
let (a, b) = c.clip(&Plane { point: n * off, normal: n }, FaceKind::Cut);
next.extend(a);
next.extend(b);
}
cells = next;
}
let total: f32 = cells.iter().map(|c| c.volume()).sum();
assert!(cells.len() > 4, "four planes should produce more than four cells, got {}", cells.len());
assert!((total - 1.0).abs() < 1.0e-3, "{} cells enclose {total}, expected 1.0", cells.len());
for (i, c) in cells.iter().enumerate() {
assert!(c.volume() > 0.0, "cell {i} came out inside out: {}", c.volume());
}
}
#[test]
fn clipping_is_bit_identical_across_runs() {
let p = Plane { point: Vec3::new(0.03, 0.0, 0.0), normal: Vec3::new(1.0, 2.0, 3.0).normalize() };
let first = unit_box().clip(&p, FaceKind::Cut);
for _ in 0..3 {
assert_eq!(unit_box().clip(&p, FaceKind::Cut), first, "the same cut produced different cells");
}
}
}