use std::collections::HashMap;
use bevy::math::{Vec2, Vec3, Vec4};
use super::MeshData;
#[derive(Clone, Debug, Default)]
pub struct CutMesh {
pub kept: MeshData,
pub faces: MeshData,
pub outlines: Vec<Vec<Vec3>>,
}
pub fn cut(mesh: &MeshData, y: f32, keep_above: bool) -> CutMesh {
cut_plane(mesh, &Plane::horizontal(y), keep_above)
}
pub fn cut_plane(mesh: &MeshData, plane: &Plane, keep_above: bool) -> CutMesh {
let (kept, runs) = split(mesh, plane, keep_above);
let outlines = outermost(runs.into_iter().map(|run| run.points).filter(|p| p.len() >= 3).collect(), plane);
let side = if keep_above { 1.0 } else { -1.0 };
let faces = fill(&outlines, plane.normal * -side, plane);
CutMesh { kept, faces, outlines }
}
fn split(mesh: &MeshData, plane: &Plane, keep_above: bool) -> (MeshData, Vec<Run>) {
let side = if keep_above { 1.0 } else { -1.0 };
let distance: Vec<f32> = mesh.positions.iter().map(|p| side * plane.distance(*p)).collect();
let mut builder =
Builder { mesh, kept: MeshData::default(), remap: vec![None; mesh.positions.len()], edges: HashMap::new() };
let mut segments: Vec<(u32, u32)> = Vec::new();
for tri in mesh.indices.as_chunks::<3>().0 {
let inside = [0, 1, 2].map(|k| distance[tri[k] as usize] >= 0.0);
match inside.iter().filter(|&&i| i).count() {
3 => {
for &v in tri {
let v = builder.vertex(v);
builder.kept.indices.push(v);
}
}
0 => {}
1 => {
let r = inside.iter().position(|&i| i).expect("one vertex is inside");
let (a, b, c) = (tri[r], tri[(r + 1) % 3], tri[(r + 2) % 3]);
let ab = builder.edge(a, b, &distance);
let ac = builder.edge(a, c, &distance);
let a = builder.vertex(a);
builder.kept.indices.extend([a, ab, ac]);
segments.push((ac, ab));
}
_ => {
let r = inside.iter().position(|&i| !i).expect("one vertex is outside");
let (a, b, c) = (tri[r], tri[(r + 1) % 3], tri[(r + 2) % 3]);
let ab = builder.edge(a, b, &distance);
let ca = builder.edge(c, a, &distance);
let (b, c) = (builder.vertex(b), builder.vertex(c));
builder.kept.indices.extend([ab, b, c, ab, c, ca]);
segments.push((ab, ca));
}
}
}
let runs = chain(&builder.kept.positions, &segments);
(builder.kept, runs)
}
fn outermost(outlines: Vec<Vec<Vec3>>, plane: &Plane) -> Vec<Vec<Vec3>> {
let (u, v) = plane_axes(plane);
let flat = |p: Vec3| Vec2::new(p.dot(u), p.dot(v));
let area = |o: &[Vec3]| {
let twice: f32 = (0..o.len())
.map(|i| {
let (a, b) = (flat(o[i]), flat(o[(i + 1) % o.len()]));
a.x * b.y - b.x * a.y
})
.sum();
twice.abs()
};
let inside = |p: Vec3, o: &[Vec3]| {
let p = flat(p);
let mut crossings = false;
for i in 0..o.len() {
let (a, b) = (flat(o[i]), flat(o[(i + 1) % o.len()]));
if (a.y > p.y) != (b.y > p.y) && p.x < a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) {
crossings = !crossings;
}
}
crossings
};
let areas: Vec<f32> = outlines.iter().map(|o| area(o)).collect();
let centres: Vec<Vec3> = outlines.iter().map(|o| o.iter().copied().sum::<Vec3>() / o.len() as f32).collect();
(0..outlines.len())
.filter(|&i| !(0..outlines.len()).any(|j| j != i && areas[j] > areas[i] && inside(centres[i], &outlines[j])))
.map(|i| outlines[i].clone())
.collect()
}
struct Builder<'a> {
mesh: &'a MeshData,
kept: MeshData,
remap: Vec<Option<u32>>,
edges: HashMap<(u32, u32), u32>,
}
impl Builder<'_> {
fn vertex(&mut self, v: u32) -> u32 {
if let Some(kept) = self.remap[v as usize] {
return kept;
}
let kept = self.push(v, v, 0.0);
self.remap[v as usize] = Some(kept);
kept
}
fn edge(&mut self, a: u32, b: u32, distance: &[f32]) -> u32 {
let key = (a.min(b), a.max(b));
if let Some(&v) = self.edges.get(&key) {
return v;
}
let (da, db) = (distance[a as usize], distance[b as usize]);
let t = if (da - db).abs() > f32::EPSILON { da / (da - db) } else { 0.0 };
let v = self.push(a, b, t);
self.edges.insert(key, v);
v
}
fn push(&mut self, a: u32, b: u32, t: f32) -> u32 {
let (m, out, a, b) = (self.mesh, &mut self.kept, a as usize, b as usize);
out.positions.push(m.positions[a].lerp(m.positions[b], t));
out.normals.push(m.normals[a].lerp(m.normals[b], t).normalize_or(m.normals[a]));
out.uvs.push(m.uvs[a].lerp(m.uvs[b], t));
if !m.colors.is_empty() {
out.colors.push(m.colors[a].lerp(m.colors[b], t));
}
if !m.uv1.is_empty() {
out.uv1.push(m.uv1[a].lerp(m.uv1[b], t));
}
if !m.sway.is_empty() {
out.sway.push(lerp4(m.sway[a], m.sway[b], t));
}
(out.positions.len() - 1) as u32
}
}
fn lerp4(a: Vec4, b: Vec4, t: f32) -> Vec4 {
a + (b - a) * t
}
struct Run {
points: Vec<Vec3>,
closed: bool,
}
fn chain(positions: &[Vec3], segments: &[(u32, u32)]) -> Vec<Run> {
let key = |v: u32| (positions[v as usize] * 1.0e4).round().as_i64vec3().to_array();
let mut next: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
let mut arrives: HashMap<[i64; 3], usize> = HashMap::new();
for (i, &(from, to)) in segments.iter().enumerate() {
if key(from) != key(to) {
next.entry(key(from)).or_default().push(i);
*arrives.entry(key(to)).or_default() += 1;
}
}
let mut used = vec![false; segments.len()];
let mut runs = Vec::new();
let heads = (0..segments.len()).filter(|&i| !arrives.contains_key(&key(segments[i].0)));
let order: Vec<usize> = heads.chain(0..segments.len()).collect();
for start in order {
if used[start] || key(segments[start].0) == key(segments[start].1) {
continue;
}
let mut points = Vec::new();
let mut current = start;
let closed = loop {
used[current] = true;
let (from, to) = segments[current];
points.push(positions[from as usize]);
let Some(follow) = next.get(&key(to)).and_then(|options| options.iter().copied().find(|&i| !used[i]))
else {
let closed = key(to) == key(segments[start].0);
if !closed {
points.push(positions[to as usize]);
}
break closed;
};
current = follow;
};
if points.len() >= 3 || (!closed && points.len() >= 2) {
runs.push(Run { points, closed });
}
}
runs
}
fn beyond(outline: &[Vec3], plane: &Plane, keep_above: bool) -> Vec<Run> {
let side = if keep_above { 1.0 } else { -1.0 };
let distance: Vec<f32> = outline.iter().map(|p| side * plane.distance(*p)).collect();
let Some(outside) = distance.iter().position(|&d| d < 0.0) else {
return vec![Run { points: outline.to_vec(), closed: true }];
};
let n = outline.len();
let mut runs = Vec::new();
let mut points: Vec<Vec3> = Vec::new();
for k in 0..n {
let (i, j) = ((outside + k) % n, (outside + k + 1) % n);
let (a, b, da, db) = (outline[i], outline[j], distance[i], distance[j]);
if (da >= 0.0) != (db >= 0.0) {
let crossing = a.lerp(b, da / (da - db));
points.push(crossing);
if da >= 0.0 {
runs.push(Run { points: std::mem::take(&mut points), closed: false });
}
}
if db >= 0.0 {
points.push(b);
}
}
runs.into_iter().filter(|r| r.points.len() >= 2).collect()
}
fn close_runs(runs: Vec<Run>, along: Vec3) -> Vec<Vec<Vec3>> {
let mut ends: Vec<(f32, usize, bool)> = runs
.iter()
.enumerate()
.filter(|(_, run)| !run.closed)
.flat_map(|(i, run)| [(run.points[0].dot(along), i, true), (run.points[run.points.len() - 1].dot(along), i, false)])
.collect();
ends.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut next = vec![None; runs.len()];
for &[a, b] in ends.as_chunks::<2>().0 {
match (a, b) {
((_, end, false), (_, start, true)) | ((_, start, true), (_, end, false)) => next[end] = Some(start),
_ => {}
}
}
let mut used = vec![false; runs.len()];
let mut outlines = Vec::new();
for first in 0..runs.len() {
if used[first] {
continue;
}
let mut outline = Vec::new();
let mut current = first;
loop {
used[current] = true;
outline.extend_from_slice(&runs[current].points);
match next[current] {
Some(n) if !used[n] => current = n,
_ => break,
}
}
if outline.len() >= 3 {
outlines.push(outline);
}
}
outlines
}
fn plane_axes(plane: &Plane) -> (Vec3, Vec3) {
if plane.normal.y.abs() > 1.0 - 1.0e-6 {
(Vec3::X, Vec3::Z)
} else {
plane.normal.any_orthonormal_pair()
}
}
pub fn fill(outlines: &[Vec<Vec3>], normal: Vec3, plane: &Plane) -> MeshData {
let (u, v) = plane_axes(plane);
let mut faces = MeshData::default();
for outline in outlines {
let centre = outline.iter().copied().sum::<Vec3>() / outline.len() as f32;
let reach = outline.iter().map(|p| (*p - centre).length()).fold(1.0e-4, f32::max);
let uv = |p: Vec3| Vec2::new(0.5, 0.5) + Vec2::new((p - centre).dot(u), (p - centre).dot(v)) / (2.0 * reach);
let n = outline.len();
let fans = (0..n).all(|i| (outline[i] - centre).cross(outline[(i + 1) % n] - centre).dot(normal) >= 0.0);
let first = faces.positions.len() as u32;
if fans {
for p in std::iter::once(centre).chain(outline.iter().copied()) {
faces.positions.push(p);
faces.normals.push(normal);
faces.uvs.push(uv(p));
}
for i in 0..n as u32 {
faces.indices.extend([first, first + 1 + i, first + 1 + (i + 1) % n as u32]);
}
} else {
for &p in outline {
faces.positions.push(p);
faces.normals.push(normal);
faces.uvs.push(uv(p));
}
for [a, b, c] in ear_clip(outline, normal) {
faces.indices.extend([first + a as u32, first + b as u32, first + c as u32]);
}
}
}
faces
}
fn ear_clip(outline: &[Vec3], normal: Vec3) -> Vec<[usize; 3]> {
let facing = |a: Vec3, b: Vec3, c: Vec3| (b - a).cross(c - a).dot(normal);
let holds = |a: Vec3, b: Vec3, c: Vec3, p: Vec3| {
facing(a, b, p) > 0.0 && facing(b, c, p) > 0.0 && facing(c, a, p) > 0.0
};
let mut left: Vec<usize> = (0..outline.len()).collect();
let mut triangles = Vec::new();
while left.len() > 3 {
let count = left.len();
let ear = (0..count).find(|&i| {
let (a, b, c) = (left[(i + count - 1) % count], left[i], left[(i + 1) % count]);
let (pa, pb, pc) = (outline[a], outline[b], outline[c]);
facing(pa, pb, pc) > 0.0
&& !left.iter().any(|&k| k != a && k != b && k != c && holds(pa, pb, pc, outline[k]))
});
let i = ear.unwrap_or(0);
triangles.push([left[(i + count - 1) % count], left[i], left[(i + 1) % count]]);
left.remove(i);
}
if left.len() == 3 {
triangles.push([left[0], left[1], left[2]]);
}
triangles
}
pub fn clip_outline(outline: &[Vec3], plane: &Plane, keep_above: bool) -> Vec<Vec3> {
let side = if keep_above { 1.0 } else { -1.0 };
let mut kept = Vec::new();
for i in 0..outline.len() {
let (a, b) = (outline[i], outline[(i + 1) % outline.len()]);
let (da, db) = (side * plane.distance(a), side * plane.distance(b));
if da >= 0.0 {
kept.push(a);
}
if (da >= 0.0) != (db >= 0.0) {
kept.push(a.lerp(b, da / (da - db)));
}
}
if kept.len() < 3 { Vec::new() } else { kept }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Notch {
pub floor: f32,
pub mouth: f32,
pub axis: Vec2,
pub toward: Vec2,
pub inner: f32,
pub outer: f32,
pub radius: f32,
pub slope: f32,
}
impl Notch {
pub fn face(&self) -> Plane {
let n = Vec3::new(-self.slope * self.toward.x, 1.0, -self.slope * self.toward.y);
let offset = self.floor - self.slope * (self.inner + self.axis.dot(self.toward));
let length = n.length();
Plane { normal: n / length, offset: offset / length }
}
pub fn back(&self) -> Plane {
self.across(self.inner)
}
fn across(&self, distance: f32) -> Plane {
Plane { normal: Vec3::new(self.toward.x, 0.0, self.toward.y), offset: distance + self.axis.dot(self.toward) }
}
fn lay_grain(&self, faces: &mut MeshData) {
for (p, uv) in faces.positions.iter().zip(&mut faces.uvs) {
*uv = Vec2::splat(0.5) + (Vec2::new(p.x, p.z) - self.axis) / (2.0 * self.radius.max(1.0e-4));
}
}
fn along(&self) -> Vec3 {
Vec3::new(-self.toward.y, 0.0, self.toward.x)
}
}
pub fn carve_notch(mesh: &MeshData, notch: &Notch) -> (MeshData, MeshData) {
let (trunk, rest) = trunk_piece(mesh, notch);
let floor = Plane::horizontal(notch.floor);
let below = cut_plane(&trunk, &floor, false);
let (carved, face) = wedge(&cut_plane(&trunk, &floor, true).kept, notch);
let mut kept = cut_plane(&below.kept, ¬ch.back(), true).kept;
kept.extend(&cut_plane(&below.kept, ¬ch.back(), false).kept);
kept.extend(&carved);
kept.extend(&rest);
let runs = below
.outlines
.iter()
.map(|o| clip_outline(o, ¬ch.across(notch.outer), false))
.filter(|o| !o.is_empty())
.flat_map(|o| beyond(&o, ¬ch.back(), true))
.collect();
let mut faces = fill(&close_runs(runs, notch.along()), Vec3::Y, &floor);
faces.extend(&face);
notch.lay_grain(&mut faces);
(kept, faces)
}
pub fn fell_notched(above: &CutMesh, notch: &Notch) -> (MeshData, MeshData) {
let (trunk, rest) = trunk_piece(&above.kept, notch);
let (mut kept, mut faces) = wedge(&trunk, notch);
kept.extend(&rest);
let largest = (0..above.outlines.len()).max_by_key(|&i| above.outlines[i].len());
let runs = largest.into_iter().flat_map(|i| beyond(&above.outlines[i], ¬ch.back(), false)).collect();
let floor = Plane::horizontal(notch.floor);
faces.extend(&fill(&close_runs(runs, notch.along()), Vec3::NEG_Y, &floor));
notch.lay_grain(&mut faces);
let others: Vec<Vec<Vec3>> =
(0..above.outlines.len()).filter(|&i| Some(i) != largest).map(|i| above.outlines[i].clone()).collect();
faces.extend(&fill(&others, Vec3::NEG_Y, &floor));
(kept, faces)
}
fn trunk_piece(mesh: &MeshData, notch: &Notch) -> (MeshData, MeshData) {
let n = mesh.positions.len();
let mut parent: Vec<usize> = (0..n).collect();
fn root(parent: &mut [usize], mut v: usize) -> usize {
while parent[v] != v {
parent[v] = parent[parent[v]];
v = parent[v];
}
v
}
let join = |parent: &mut Vec<usize>, a: usize, b: usize| {
let (a, b) = (root(parent, a), root(parent, b));
if a != b {
parent[a.max(b)] = a.min(b);
}
};
let mut welded: HashMap<[i64; 3], usize> = HashMap::new();
for (v, p) in mesh.positions.iter().enumerate() {
let key = (*p * 1.0e4).round().as_i64vec3().to_array();
let first = *welded.entry(key).or_insert(v);
join(&mut parent, first, v);
}
for tri in mesh.indices.as_chunks::<3>().0 {
join(&mut parent, tri[0] as usize, tri[1] as usize);
join(&mut parent, tri[0] as usize, tri[2] as usize);
}
let reach = notch.radius * 1.5 + 0.05;
let mut sides: HashMap<usize, (u8, usize)> = HashMap::new();
for tri in mesh.indices.as_chunks::<3>().0 {
let piece = sides.entry(root(&mut parent, tri[0] as usize)).or_default();
piece.1 += 1;
for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
let (a, b) = (mesh.positions[a as usize], mesh.positions[b as usize]);
let (da, db) = (a.y - notch.floor, b.y - notch.floor);
if da * db > 0.0 || da == db {
continue;
}
let crossing = a.lerp(b, da / (da - db));
let offset = Vec2::new(crossing.x, crossing.z) - notch.axis;
if offset.length() <= reach {
piece.0 |= 1 << (usize::from(offset.x >= 0.0) + 2 * usize::from(offset.y >= 0.0));
}
}
}
let trunk = sides
.into_iter()
.filter(|&(_, (quadrants, _))| quadrants == 0b1111)
.max_by_key(|&(_, (_, triangles))| triangles)
.map(|(r, _)| r);
let mut pieces = [
Builder { mesh, kept: MeshData::default(), remap: vec![None; n], edges: HashMap::new() },
Builder { mesh, kept: MeshData::default(), remap: vec![None; n], edges: HashMap::new() },
];
for tri in mesh.indices.as_chunks::<3>().0 {
let builder = &mut pieces[usize::from(Some(root(&mut parent, tri[0] as usize)) != trunk)];
for &v in tri {
let v = builder.vertex(v);
builder.kept.indices.push(v);
}
}
let [trunk, rest] = pieces;
(trunk.kept, rest.kept)
}
fn wedge(above: &MeshData, notch: &Notch) -> (MeshData, MeshData) {
let mouth = Plane::horizontal(notch.mouth);
let outer = notch.across(notch.outer);
let band = cut_plane(above, &mouth, false);
let near = cut_plane(&band.kept, &outer, false);
let face = notch.face();
let (mut kept, runs) = split(&near.kept, &face, true);
let outlines = outermost(close_runs(runs, notch.along()), &face);
kept.extend(&cut_plane(&band.kept, &outer, true).kept);
kept.extend(&cut_plane(above, &mouth, true).kept);
(kept, fill(&outlines, -face.normal, &face))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Plane {
pub normal: Vec3,
pub offset: f32,
}
impl Plane {
pub fn horizontal(y: f32) -> Self {
Self { normal: Vec3::Y, offset: y }
}
pub fn distance(&self, p: Vec3) -> f32 {
self.normal.dot(p) - self.offset
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TreePreset, build_tree};
use bevy::math::Vec3Swizzles;
fn open_edges(mesh: &MeshData) -> usize {
let key = |i: u32| (mesh.positions[i as usize] * 1.0e4).round().as_ivec3().to_array();
let mut uses: HashMap<([i32; 3], [i32; 3]), u32> = HashMap::new();
for tri in mesh.indices.as_chunks::<3>().0 {
for (u, v) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
let (ku, kv) = (key(u), key(v));
if ku != kv {
*uses.entry((ku.min(kv), ku.max(kv))).or_insert(0) += 1;
}
}
}
uses.values().filter(|&&n| n == 1).count()
}
fn winding_normal(mesh: &MeshData) -> Vec3 {
mesh.indices
.as_chunks::<3>()
.0
.iter()
.map(|t| {
let [a, b, c] = [t[0], t[1], t[2]].map(|i| mesh.positions[i as usize]);
(b - a).cross(c - a)
})
.sum::<Vec3>()
}
#[test]
fn a_cut_trunk_is_closed_on_both_sides_and_its_faces_face_out() {
for preset in [TreePreset::Oak, TreePreset::Pine, TreePreset::Baobab, TreePreset::Birch] {
let wood = build_tree(&preset.params(), f32::INFINITY).trunk;
let before = open_edges(&wood);
for (keep_above, outward) in [(true, Vec3::NEG_Y), (false, Vec3::Y)] {
let cut = cut(&wood, 0.6, keep_above);
assert!(!cut.outlines.is_empty(), "{preset:?}: the plane missed the trunk");
let mut closed = cut.kept.clone();
closed.extend(&cut.faces);
assert!(
open_edges(&closed) <= before,
"{preset:?} (keep above: {keep_above}): the cut left {} open edges, the wood had {before}",
open_edges(&closed)
);
assert!(winding_normal(&cut.faces).dot(outward) > 0.0, "{preset:?}: the cut faces face inward");
let wrong = cut.kept.positions.iter().filter(|p| (p.y - 0.6) * if keep_above { 1.0 } else { -1.0 } < -1e-4);
assert_eq!(wrong.count(), 0, "{preset:?}: a vertex is on the wrong side of the cut");
for uv in &cut.faces.uvs {
assert!((uv - Vec2::splat(0.5)).length() <= 0.5 + 1e-4, "{preset:?}: a cut face's uv leaves the disc");
}
}
}
}
fn notch_at(wood: &MeshData, floor: f32, depth: f32) -> Notch {
let level = cut(wood, floor, false);
let centre_of = |o: &[Vec3]| o.iter().copied().sum::<Vec3>() / o.len() as f32;
let outline = level.outlines.iter().min_by(|a, b| centre_of(a).xz().length().total_cmp(¢re_of(b).xz().length()));
let outline = outline.expect("the trunk is cut");
let centre = centre_of(outline);
let front = outline.iter().map(|p| p.x - centre.x).fold(0.0, f32::max);
let back = outline.iter().map(|p| centre.x - p.x).fold(0.0, f32::max);
let inner = front - depth * (front + back);
let rise = front - inner;
Notch {
floor,
mouth: floor + 1.5 * rise + 0.05,
axis: centre.xz(),
toward: Vec2::X,
inner,
outer: front + 0.5 * rise + 0.05,
radius: outline.iter().map(|p| p.distance(centre)).fold(0.0, f32::max),
slope: 1.0,
}
}
#[test]
fn a_notch_takes_its_wedge_out_of_the_trunk_and_closes_it() {
for preset in [TreePreset::Oak, TreePreset::Pine, TreePreset::Baobab, TreePreset::Birch] {
let wood = build_tree(&preset.params(), f32::INFINITY).trunk;
let before = open_edges(&wood);
let notch = notch_at(&wood, 0.5, 0.4);
let (kept, faces) = carve_notch(&wood, ¬ch);
let mut closed = kept.clone();
closed.extend(&faces);
assert!(open_edges(&closed) <= before, "{preset:?}: the notch left {} open edges, the wood had {before}", open_edges(&closed));
let face = notch.face();
let inside = kept.positions.iter().filter(|p| p.y > 0.5 + 1e-3 && p.y < notch.mouth - 1e-3 && face.distance(**p) < -1e-3);
assert_eq!(inside.count(), 0, "{preset:?}: wood left in the notch");
let mut up = 0;
let mut out = 0;
for t in faces.indices.as_chunks::<3>().0 {
let [a, b, c] = [t[0], t[1], t[2]].map(|i| faces.positions[i as usize]);
let winding = (b - a).cross(c - a);
let normal = faces.normals[t[0] as usize];
assert!(winding.length() < 1e-9 || winding.dot(normal) > 0.0, "{preset:?}: a notch face is wound inward");
if normal.y > 0.99 {
up += 1;
} else if normal.x > 0.5 && normal.y < -0.5 {
out += 1;
}
}
assert!(up > 0 && out > 0, "{preset:?}: the notch has {up} floor and {out} face triangles");
for uv in &faces.uvs {
assert!((uv - Vec2::splat(0.5)).length() <= 0.5 + 1e-3, "{preset:?}: a notch face's uv {uv} leaves the disc");
}
}
}
#[test]
fn a_notch_cuts_the_trunk_and_leaves_its_limbs_whole() {
for preset in [TreePreset::Spruce, TreePreset::Willow] {
let wood = build_tree(&preset.params(), f32::INFINITY).trunk;
let before = open_edges(&wood);
let notch = notch_at(&wood, 1.0, 0.5);
let (trunk, limbs) = trunk_piece(&wood, ¬ch);
assert!(!trunk.indices.is_empty() && !limbs.indices.is_empty(), "{preset:?}: no trunk or no limbs");
let base = trunk.positions.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
assert!(base < 0.01, "{preset:?}: the piece taken for the trunk starts {base} m up");
let (kept, faces) = carve_notch(&wood, ¬ch);
let mut closed = kept.clone();
closed.extend(&faces);
assert!(open_edges(&closed) <= before, "{preset:?}: the notch left {} open edges, the wood had {before}", open_edges(&closed));
let key = |p: &Vec3| (*p * 1.0e4).round().as_ivec3();
let carved: std::collections::HashSet<_> = kept.positions.iter().map(key).collect();
let cut_away = limbs.positions.iter().filter(|p| !carved.contains(&key(p)));
assert_eq!(cut_away.count(), 0, "{preset:?}: the notch cut into a limb");
}
}
}