1use fast_mesh_bvh::{Aabb, Bvh, NodeKind};
9use fast_mesh_store::{Mesh, TriangleId, VertexId};
10
11use crate::plane::{Plane, dot};
12use crate::{Appender, QueryError, Result, allocation_allowed, check};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub(crate) struct Segment {
24 pub(crate) tri: TriangleId,
25 pub(crate) from: (VertexId, VertexId),
26 pub(crate) to: (VertexId, VertexId),
27}
28
29fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
30 (a.min(b), a.max(b))
31}
32
33fn position(mesh: &Mesh, v: VertexId) -> [f64; 3] {
34 mesh.positions()[v as usize].map(f64::from)
35}
36
37pub(crate) fn crossing(mesh: &Mesh, plane: &Plane, key: (VertexId, VertexId)) -> [f64; 3] {
43 let (a, b) = (
44 position(mesh, key.0.min(key.1)),
45 position(mesh, key.0.max(key.1)),
46 );
47 let (sa, sb) = (plane.signed_distance(a), plane.signed_distance(b));
48 debug_assert!((sa >= 0.0) != (sb >= 0.0), "edge {key:?} is not crossed");
49 let t = sa / (sa - sb);
50 std::array::from_fn(|k| a[k] + t * (b[k] - a[k]))
51}
52
53fn segment(mesh: &Mesh, plane: &Plane, tri: TriangleId) -> Option<Segment> {
55 let t = mesh.triangles()[tri as usize];
56 let plus = t.map(|v| plane.signed_distance(position(mesh, v)) >= 0.0);
57 let i = if plus[0] == plus[1] {
59 if plus[1] == plus[2] {
60 return None;
61 }
62 2
63 } else if plus[0] == plus[2] {
64 1
65 } else {
66 0
67 };
68 let (a, b, c) = (t[i], t[(i + 1) % 3], t[(i + 2) % 3]);
72 let (ab, ca) = (key(a, b), key(c, a));
73 let (from, to) = if plus[i] { (ab, ca) } else { (ca, ab) };
74 Some(Segment { tri, from, to })
75}
76
77fn straddles(plane: &Plane, b: &Aabb) -> bool {
80 let mut lo = f64::INFINITY;
81 let mut hi = f64::NEG_INFINITY;
82 let mut magnitude = 0.0f64;
83 for i in 0..8 {
84 let p: [f64; 3] =
85 std::array::from_fn(|k| f64::from(if i >> k & 1 == 1 { b.max[k] } else { b.min[k] }));
86 let s = plane.signed_distance(p);
87 lo = lo.min(s);
88 hi = hi.max(s);
89 magnitude = magnitude.max(p.iter().fold(0.0, |m, c| m.max(c.abs())));
90 }
91 let offset = dot(plane.normal(), plane.origin()).abs();
92 let pad = 8.0 * f64::from(f32::EPSILON) * (magnitude + offset);
93 lo <= pad && hi >= -pad
94}
95
96pub(crate) fn segments(
106 mesh: &Mesh,
107 bvh: &Bvh,
108 plane: &Plane,
109 out: &mut Vec<Segment>,
110) -> Result<()> {
111 check(mesh, bvh)?;
112 let mut ap = Appender::new(out);
113 let mut failed = None;
114 bvh.traverse(|_, node| {
115 if failed.is_some() || !straddles(plane, &node.bounds) {
116 return false;
117 }
118 if let NodeKind::Leaf { triangles } = node.kind {
119 for &id in triangles {
120 if let Some(s) = segment(mesh, plane, id)
121 && let Err(e) = ap.push(s)
122 {
123 failed = Some(e);
124 return false;
125 }
126 }
127 }
128 true
129 });
130 failed.map_or(Ok(()), Err)
131}
132
133#[derive(Debug, Clone, Default, PartialEq)]
135pub struct Slice {
136 pub contours: Vec<Contour>,
138}
139
140#[derive(Debug, Clone, Default, PartialEq)]
142pub struct Contour {
143 pub points: Vec<[f64; 3]>,
148 pub edges: Vec<(VertexId, VertexId)>,
150 pub triangles: Vec<TriangleId>,
154 pub closed: bool,
156 pub oriented: bool,
159 pub manifold: bool,
161}
162
163const BOUNDARY: usize = usize::MAX;
165const SPLIT: usize = usize::MAX - 1;
167
168pub fn slice(mesh: &Mesh, bvh: &Bvh, plane: &Plane, out: &mut Slice) -> Result<()> {
208 let r = chain(mesh, bvh, plane, &mut out.contours);
209 if r.is_err() {
210 out.contours.clear();
211 }
212 r
213}
214
215fn chain(mesh: &Mesh, bvh: &Bvh, plane: &Plane, out: &mut Vec<Contour>) -> Result<()> {
216 let mut segs = Vec::new();
217 segments(mesh, bvh, plane, &mut segs)?;
218 segs.retain(|s| s.from != s.to);
219 segs.sort_unstable();
221
222 let ends_len = 2 * segs.len();
225 let key_of = |e| end_key(&segs, e);
226 let mut ends = Vec::new();
227 reserve(&mut ends, ends_len)?;
228 ends.extend(0..ends_len);
229 ends.sort_unstable_by_key(|&e| (key_of(e), e & 1, e));
230
231 let mut link = Vec::new();
233 reserve(&mut link, ends_len)?;
234 link.resize(ends_len, BOUNDARY);
235 for run in ends.chunk_by(|&a, &b| key_of(a) == key_of(b)) {
236 match *run {
237 [_] => {}
238 [a, b] => {
239 link[a] = b;
240 link[b] = a;
241 }
242 _ => run.iter().for_each(|&e| link[e] = SPLIT),
243 }
244 }
245
246 let mut used = Vec::new();
247 reserve(&mut used, segs.len())?;
248 used.resize(segs.len(), false);
249 let mut count = 0;
250 for open in [true, false] {
254 for &start in &ends {
255 if used[start / 2] || (open && link[start] < SPLIT) {
256 continue;
257 }
258 if count == out.len() {
259 push(out, Contour::default())?;
260 }
261 let c = &mut out[count];
262 count += 1;
263 c.points.clear();
264 c.edges.clear();
265 c.triangles.clear();
266 walk(&segs, &link, &mut used, start, c)?;
267 reserve(&mut c.points, c.edges.len())?;
268 c.points
269 .extend(c.edges.iter().map(|&e| crossing(mesh, plane, e)));
270 }
271 }
272 out.truncate(count);
273 out.sort_unstable_by_key(|c| (c.edges[0], c.triangles[0]));
275 Ok(())
276}
277
278fn walk(
281 segs: &[Segment],
282 link: &[usize],
283 used: &mut [bool],
284 start: usize,
285 c: &mut Contour,
286) -> Result<()> {
287 let key_of = |e| end_key(segs, e);
288 c.oriented = true;
289 let mut entry = start;
290 loop {
291 let s = entry / 2;
292 used[s] = true;
293 push(&mut c.edges, key_of(entry))?;
294 push(&mut c.triangles, segs[s].tri)?;
295 c.oriented &= entry & 1 == start & 1;
297 let exit = entry ^ 1;
298 match link[exit] {
299 next if next == start => {
300 c.closed = true;
301 c.manifold = true;
302 return Ok(());
303 }
304 next if next >= SPLIT => {
305 push(&mut c.edges, key_of(exit))?;
306 c.closed = false;
307 c.manifold = link[start] != SPLIT && next != SPLIT;
308 if c.oriented && start & 1 == 1 {
309 c.edges.reverse();
311 c.triangles.reverse();
312 }
313 return Ok(());
314 }
315 next => entry = next,
316 }
317 }
318}
319
320fn end_key(segs: &[Segment], e: usize) -> (VertexId, VertexId) {
323 let s = &segs[e / 2];
324 if e & 1 == 0 { s.from } else { s.to }
325}
326
327fn reserve<T>(v: &mut Vec<T>, additional: usize) -> Result<()> {
329 allocation_allowed()?;
330 v.try_reserve(additional)
331 .map_err(|_| QueryError::OutOfMemory)
332}
333
334fn push<T>(v: &mut Vec<T>, value: T) -> Result<()> {
335 reserve(v, 1)?;
336 v.push(value);
337 Ok(())
338}
339
340#[cfg(test)]
342pub(crate) fn segments_brute(mesh: &Mesh, plane: &Plane, out: &mut Vec<Segment>) {
343 out.extend(
344 (0..mesh.triangle_slots() as TriangleId)
345 .filter(|&id| mesh.is_triangle_alive(id))
346 .filter_map(|id| segment(mesh, plane, id)),
347 );
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::plane::{cross, dot};
354 use crate::test_util::{
355 Lcg, box_mesh, build, fin, random_normal, signed_volume6, torus, uv_sphere, wavy,
356 with_degenerates,
357 };
358 use crate::{QueryError, fail_allocations_after};
359 use fast_mesh_bvh::Quality;
360 use std::collections::HashMap;
361
362 fn sorted(mut s: Vec<Segment>) -> Vec<Segment> {
363 s.sort_unstable();
364 s
365 }
366
367 fn brute(mesh: &Mesh, plane: &Plane) -> Vec<Segment> {
368 let mut out = Vec::new();
369 segments_brute(mesh, plane, &mut out);
370 sorted(out)
371 }
372
373 fn fast(mesh: &Mesh, bvh: &Bvh, plane: &Plane) -> Vec<Segment> {
374 let mut out = Vec::new();
375 segments(mesh, bvh, plane, &mut out).unwrap();
376 sorted(out)
377 }
378
379 fn planes(mesh: &Mesh, seed: u64) -> Vec<Plane> {
382 let mut rng = Lcg(seed);
383 let mut lo = [f64::INFINITY; 3];
384 let mut hi = [f64::NEG_INFINITY; 3];
385 for p in mesh.positions() {
386 for k in 0..3 {
387 lo[k] = lo[k].min(f64::from(p[k]));
388 hi[k] = hi[k].max(f64::from(p[k]));
389 }
390 }
391 let mut out = Vec::new();
392 for _ in 0..200 {
393 let o = std::array::from_fn(|k| lo[k] + f64::from(rng.next()) * (hi[k] - lo[k]));
394 out.push(Plane::new(o, random_normal(&mut rng)).unwrap());
395 }
396 let tris = mesh.triangles();
397 let pick = |rng: &mut Lcg| tris[(rng.next() * tris.len() as f32) as usize % tris.len()];
398 for _ in 0..50 {
399 let t = pick(&mut rng);
400 let o = position(mesh, t[(rng.next() * 3.0) as usize % 3]);
401 out.push(Plane::new(o, random_normal(&mut rng)).unwrap());
402 }
403 for _ in 0..50 {
404 let t = pick(&mut rng);
405 let (a, b) = (position(mesh, t[0]), position(mesh, t[1]));
406 let e = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
407 let n = cross(e, random_normal(&mut rng));
408 out.push(Plane::new(a, n).unwrap());
409 }
410 out
411 }
412
413 #[test]
414 fn fixtures_are_closed_and_outward() {
415 for mesh in [
416 uv_sphere(32, 64, 1.0),
417 torus(2.0, 0.5, 48, 24),
418 box_mesh([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]),
419 ] {
420 assert!(signed_volume6(&mesh) > 0.0);
421 let mut directed = HashMap::new();
423 for t in mesh.triangles() {
424 for i in 0..3 {
425 *directed.entry((t[i], t[(i + 1) % 3])).or_insert(0) += 1;
426 }
427 }
428 assert!(
429 directed
430 .iter()
431 .all(|(&(a, b), &n)| n == 1 && directed.get(&(b, a)) == Some(&1))
432 );
433 }
434 assert_eq!(box_mesh([0.0; 3], [1.0; 3]).triangles().len(), 12);
435 assert_eq!(fin().triangles().len(), 6);
436 let d = with_degenerates(box_mesh([0.0; 3], [1.0; 3]));
437 assert_eq!(d.triangles().len(), 12 + 3 * 3);
438 assert_eq!(
439 signed_volume6(&d),
440 signed_volume6(&box_mesh([0.0; 3], [1.0; 3]))
441 );
442 }
443
444 #[test]
445 fn segments_match_brute_force() {
446 let meshes = [wavy(40), uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)];
447 for (i, mesh) in meshes.iter().enumerate() {
448 for quality in [Quality::Sah, Quality::Fast] {
449 let bvh = build(mesh, quality);
450 let mut nonempty = 0;
451 for plane in planes(mesh, 0x5e9 + i as u64) {
452 let expected = brute(mesh, &plane);
453 assert_eq!(fast(mesh, &bvh, &plane), expected, "{plane:?}");
454 nonempty += usize::from(!expected.is_empty());
455 }
456 assert!(nonempty > 150, "only {nonempty} planes cut mesh {i}");
457 }
458 }
459 }
460
461 #[test]
462 fn segment_orientation_rule() {
463 let mesh = uv_sphere(32, 64, 1.0);
467 let bvh = build(&mesh, Quality::Sah);
468 let cases = [
469 ([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]),
470 ([0.0, 0.0, -0.3], [0.3, -0.2, 0.9]),
471 ([0.0, 0.0, -0.3], [-0.3, 0.2, -0.9]),
472 ];
473 let mut facing_away = 0;
474 for (origin, normal) in cases {
475 let plane = Plane::new(origin, normal).unwrap();
476 let found = fast(&mesh, &bvh, &plane);
477 assert!(found.len() > 100);
478 for s in &found {
479 let from = plane.to_uv(crossing(&mesh, &plane, s.from));
480 let to = plane.to_uv(crossing(&mesh, &plane, s.to));
481 let [a, b, c] = mesh.triangles()[s.tri as usize].map(|v| position(&mesh, v));
482 let tn = cross(
483 std::array::from_fn(|k| b[k] - a[k]),
484 std::array::from_fn(|k| c[k] - a[k]),
485 );
486 let n = plane.normal();
487 facing_away += usize::from(dot(n, tn) < 0.0);
488 let w = cross(n, tn);
489 let w = [dot(w, plane.u()), dot(w, plane.v())];
490 let d = [to[0] - from[0], to[1] - from[1]];
491 assert!(d[0] * w[0] + d[1] * w[1] > 0.0, "{plane:?} {s:?}");
492 }
493 }
494 assert!(facing_away > 50, "{facing_away}");
495 }
496
497 #[test]
498 fn outer_contours_wind_counter_clockwise() {
499 let meshes = [
502 uv_sphere(32, 64, 1.0),
503 box_mesh([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]),
504 torus(2.0, 0.5, 48, 24),
505 ];
506 let normals = [[0.0, 0.0, 1.0], [0.3, -0.2, 0.9], [1.0, 0.0, 0.0]];
507 for mesh in &meshes {
508 let bvh = build(mesh, Quality::Sah);
509 for z in [-0.3, 0.0, 0.2] {
510 for n in normals {
511 for n in [n, n.map(|c: f64| -c)] {
512 let plane = Plane::new([0.0, 0.0, z], n).unwrap();
513 let area2: f64 = fast(mesh, &bvh, &plane)
514 .iter()
515 .map(|s| {
516 let a = plane.to_uv(crossing(mesh, &plane, s.from));
517 let b = plane.to_uv(crossing(mesh, &plane, s.to));
518 a[0] * b[1] - a[1] * b[0]
519 })
520 .sum();
521 assert!(area2 > 0.0, "{plane:?}: {area2}");
522 }
523 }
524 }
525 }
526 }
527
528 #[test]
529 fn segments_chain_head_to_tail_on_closed_meshes() {
530 for mesh in [uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)] {
533 let bvh = build(&mesh, Quality::Sah);
534 for plane in planes(&mesh, 0xc4a1) {
535 let found = fast(&mesh, &bvh, &plane);
536 let mut balance: HashMap<(VertexId, VertexId), i32> = HashMap::new();
537 for s in &found {
538 *balance.entry(s.from).or_default() += 1;
539 *balance.entry(s.to).or_default() -= 1;
540 }
541 assert!(balance.values().all(|&b| b == 0), "{plane:?}");
542 assert_eq!(balance.len(), found.len(), "{plane:?}");
543 }
544 }
545 }
546
547 #[test]
548 fn crossing_is_symmetric_in_triangle_order() {
549 for mesh in [uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)] {
550 let bvh = build(&mesh, Quality::Sah);
551 for plane in planes(&mesh, 0x7b2) {
552 let mut seen: HashMap<(VertexId, VertexId), ([u64; 3], u32)> = HashMap::new();
555 for s in fast(&mesh, &bvh, &plane) {
556 for key in [s.from, s.to] {
557 assert!(key.0 < key.1);
558 let p = crossing(&mesh, &plane, key);
559 let bits = p.map(f64::to_bits);
560 assert_eq!(
561 crossing(&mesh, &plane, (key.1, key.0)).map(f64::to_bits),
562 bits
563 );
564 let entry = seen.entry(key).or_insert((bits, 0));
565 assert_eq!(entry.0, bits, "{key:?}");
566 entry.1 += 1;
567 let (a, b) = (position(&mesh, key.0), position(&mesh, key.1));
570 let tol = 1e-12;
571 assert!(
572 (0..3)
573 .all(|k| a[k].min(b[k]) - tol <= p[k]
574 && p[k] <= a[k].max(b[k]) + tol)
575 );
576 assert!(plane.signed_distance(p).abs() < 1e-12);
577 }
578 }
579 assert!(seen.values().all(|&(_, n)| n == 2), "{plane:?}");
580 }
581 }
582 }
583
584 #[test]
585 fn vertex_on_plane_counts_as_plus() {
586 let mesh = box_mesh([0.0; 3], [1.0; 3]);
589 let bvh = build(&mesh, Quality::Sah);
590 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
591 assert!(fast(&mesh, &bvh, &plane).is_empty());
592 let plane = Plane::new([0.0; 3], [0.0, 0.0, -1.0]).unwrap();
594 let found = fast(&mesh, &bvh, &plane);
595 assert_eq!(found.len(), 8);
596 for s in found {
597 for key in [s.from, s.to] {
598 assert_eq!(crossing(&mesh, &plane, key)[2], 0.0);
599 }
600 }
601 }
602
603 #[test]
604 fn degenerate_and_non_manifold_triangles_do_not_panic() {
605 let plane = Plane::new([0.1, 0.2, 0.5], [0.1, 0.2, 1.0]).unwrap();
606 for mesh in [with_degenerates(uv_sphere(8, 16, 1.0)), fin()] {
607 let bvh = build(&mesh, Quality::Sah);
608 assert_eq!(fast(&mesh, &bvh, &plane), brute(&mesh, &plane));
609 }
610 let p = [0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
613 let mesh = Mesh::from_buffers(&p, &[0, 0, 1, 1, 1, 1]).unwrap();
614 let bvh = build(&mesh, Quality::Sah);
615 let found = fast(
616 &mesh,
617 &bvh,
618 &Plane::new([0.0; 3], [0.0, 0.0, -1.0]).unwrap(),
619 );
620 assert_eq!(
621 found,
622 vec![Segment {
623 tri: 0,
624 from: (0, 1),
625 to: (0, 1)
626 }]
627 );
628 let mesh = fin();
630 let bvh = build(&mesh, Quality::Sah);
631 let found = fast(
632 &mesh,
633 &bvh,
634 &Plane::new([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]).unwrap(),
635 );
636 assert_eq!(found.len(), 6);
637 let shared = found
638 .iter()
639 .filter(|s| s.from == (0, 1) || s.to == (0, 1))
640 .count();
641 assert_eq!(shared, 3);
642 }
643
644 #[test]
645 fn removed_triangles_are_skipped() {
646 let mut mesh = uv_sphere(8, 16, 1.0);
647 let mut bvh = build(&mesh, Quality::Sah);
648 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
649 let before = fast(&mesh, &bvh, &plane);
650 let gone = before[0].tri;
651 let mut tx = mesh.begin_edit();
652 tx.remove_triangles(&[gone]).unwrap();
653 let p = tx.commit().unwrap();
654 bvh.update(&mut mesh, p.changes()).unwrap();
655 let after = fast(&mesh, &bvh, &plane);
656 assert_eq!(after, brute(&mesh, &plane));
657 assert_eq!(after.len(), before.len() - 1);
658 assert!(after.iter().all(|s| s.tri != gone));
659 }
660
661 #[test]
662 fn stale_mismatch_and_oom() {
663 let mut mesh = uv_sphere(8, 16, 1.0);
664 let other = uv_sphere(8, 16, 1.0);
665 let bvh = build(&mesh, Quality::Sah);
666 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
667 let mut out = vec![Segment {
668 tri: 9,
669 from: (1, 2),
670 to: (3, 4),
671 }];
672 let keep = out.clone();
673 assert_eq!(
674 segments(&other, &bvh, &plane, &mut out),
675 Err(QueryError::MeshMismatch)
676 );
677 fail_allocations_after(Some(3));
678 let r = segments(&mesh, &bvh, &plane, &mut out);
679 fail_allocations_after(None);
680 assert_eq!(r, Err(QueryError::OutOfMemory));
681 assert_eq!(out, keep);
682 segments(&mesh, &bvh, &plane, &mut out).unwrap();
684 assert_eq!(out[0], keep[0]);
685 assert_eq!(sorted(out[1..].to_vec()), brute(&mesh, &plane));
686 let mut tx = mesh.begin_edit();
687 tx.set_positions(&[0], &[[0.0, 0.0, 2.0]]).unwrap();
688 tx.commit().unwrap();
689 assert_eq!(
690 segments(&mesh, &bvh, &plane, &mut out),
691 Err(QueryError::StaleBvh)
692 );
693 }
694
695 fn cut(mesh: &Mesh, plane: &Plane) -> Slice {
696 let bvh = build(mesh, Quality::Sah);
697 let mut out = Slice::default();
698 slice(mesh, &bvh, plane, &mut out).unwrap();
699 for c in &out.contours {
700 check_contour(mesh, plane, c);
701 }
702 out
703 }
704
705 fn check_contour(mesh: &Mesh, plane: &Plane, c: &Contour) {
707 assert_eq!(c.points.len(), c.edges.len());
708 let segments = c.points.len() - usize::from(!c.closed);
709 assert_eq!(c.triangles.len(), segments);
710 assert!(!c.triangles.is_empty());
711 for (p, &e) in c.points.iter().zip(&c.edges) {
712 assert!(e.0 < e.1);
713 assert_eq!(
714 p.map(f64::to_bits),
715 crossing(mesh, plane, e).map(f64::to_bits)
716 );
717 }
718 let n = c.edges.len();
721 for (i, &t) in c.triangles.iter().enumerate() {
722 let s = segment(mesh, plane, t).unwrap();
723 let (a, b) = (c.edges[i], c.edges[(i + 1) % n]);
724 if c.oriented {
725 assert_eq!((s.from, s.to), (a, b), "{c:?}");
726 } else {
727 assert!((s.from, s.to) == (a, b) || (s.from, s.to) == (b, a));
728 }
729 }
730 }
731
732 fn area(plane: &Plane, c: &Contour) -> f64 {
734 let q: Vec<[f64; 2]> = c.points.iter().map(|&p| plane.to_uv(p)).collect();
735 (0..q.len())
736 .map(|i| {
737 let (a, b) = (q[i], q[(i + 1) % q.len()]);
738 a[0] * b[1] - a[1] * b[0]
739 })
740 .sum::<f64>()
741 / 2.0
742 }
743
744 fn close(value: f64, expected: f64, rel: f64) -> bool {
745 (value - expected).abs() <= rel * expected.abs()
746 }
747
748 #[test]
749 fn sphere_cut_is_one_closed_ccw_loop() {
750 use std::f64::consts::PI;
751 let mesh = uv_sphere(64, 128, 1.0);
752 let plane = Plane::new([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]).unwrap();
753 let out = cut(&mesh, &plane);
754 assert_eq!(out.contours.len(), 1);
755 let c = &out.contours[0];
756 assert!(c.closed && c.oriented && c.manifold);
757 let a = area(&plane, c);
758 assert!(a > 0.0 && close(a, PI * (1.0 - 0.09), 0.01), "{a}");
759 }
760
761 #[test]
762 fn torus_horizontal_cut_is_outer_and_hole() {
763 use std::f64::consts::PI;
764 let mesh = torus(2.0, 0.5, 96, 48);
765 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
766 let out = cut(&mesh, &plane);
767 assert_eq!(out.contours.len(), 2);
768 let mut areas: Vec<f64> = out
769 .contours
770 .iter()
771 .map(|c| {
772 assert!(c.closed && c.oriented && c.manifold);
773 area(&plane, c)
774 })
775 .collect();
776 areas.sort_by(f64::total_cmp);
777 assert!(close(areas[0], -PI * 1.5 * 1.5, 0.01), "{areas:?}");
778 assert!(close(areas[1], PI * 2.5 * 2.5, 0.01), "{areas:?}");
779 }
780
781 #[test]
782 fn cube_face_flush_plane_is_one_closed_loop() {
783 let mesh = box_mesh([-1.0; 3], [1.0; 3]);
784 for z in [1.0, -1.0] {
785 for n in [1.0, -1.0] {
786 let out = cut(&mesh, &Plane::new([0.0, 0.0, z], [0.0, 0.0, n]).unwrap());
787 assert!(out.contours.len() <= 1, "z = {z}, n = {n}");
788 assert!(out.contours.iter().all(|c| c.closed && c.manifold));
789 }
790 }
791 let out = cut(&mesh, &Plane::new([0.0; 3], [1.0, 0.0, 0.0]).unwrap());
792 assert_eq!(out.contours.len(), 1);
793 let c = &out.contours[0];
794 assert!(c.closed && c.oriented && c.manifold);
795 assert_eq!(c.points.len(), 8);
796 }
797
798 #[test]
799 fn terrain_grid_row_plane_is_open_chains() {
800 let mesh = wavy(20);
801 let mut incident: HashMap<(VertexId, VertexId), usize> = HashMap::new();
802 for t in mesh.triangles() {
803 for i in 0..3 {
804 *incident.entry(key(t[i], t[(i + 1) % 3])).or_default() += 1;
805 }
806 }
807 for normal in [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]] {
810 let plane = Plane::new([0.0, 7.0, 0.0], normal).unwrap();
811 let out = cut(&mesh, &plane);
812 assert!(!out.contours.is_empty());
813 for c in &out.contours {
814 assert!(!c.closed && c.oriented && c.manifold);
815 assert_eq!(incident[&c.edges[0]], 1);
816 assert_eq!(incident[c.edges.last().unwrap()], 1);
817 }
818 let mut used: Vec<TriangleId> = out
820 .contours
821 .iter()
822 .flat_map(|c| c.triangles.iter().copied())
823 .collect();
824 used.sort_unstable();
825 let expected: Vec<TriangleId> = brute(&mesh, &plane).iter().map(|s| s.tri).collect();
826 assert_eq!(used, expected);
827 }
828 }
829
830 #[test]
831 fn nonmanifold_fin_splits_chains() {
832 let out = cut(
833 &fin(),
834 &Plane::new([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]).unwrap(),
835 );
836 assert!(out.contours.len() >= 3);
837 for c in &out.contours {
838 assert!(!c.closed && !c.manifold);
839 assert!(c.edges[0] == (0, 1) || *c.edges.last().unwrap() == (0, 1));
841 }
842 }
843
844 #[test]
845 fn degenerate_triangles_do_not_break_chains() {
846 let mesh = with_degenerates(uv_sphere(16, 32, 1.0));
847 let mut rng = Lcg(0xde9e);
848 for _ in 0..50 {
849 let plane = Plane::new([0.0; 3], random_normal(&mut rng)).unwrap();
850 let out = cut(&mesh, &plane);
851 assert_eq!(out.contours.len(), 1, "{plane:?}");
852 let c = &out.contours[0];
853 assert!(c.closed && c.oriented && c.manifold, "{plane:?}");
854 }
855 }
856
857 #[test]
858 fn collinear_sliver_with_distinct_ids_crossing_plane() {
859 let flat = box_mesh([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
865 let n = flat.positions().len() as u32;
866 let positions: Vec<f32> = flat
867 .positions()
868 .iter()
869 .flatten()
870 .copied()
871 .chain([2.0, 0.0, -1.0, 2.0, 0.0, 0.5, 2.0, 0.0, 1.0])
872 .collect();
873 let mut indices: Vec<u32> = flat.triangles().iter().flatten().copied().collect();
874 indices.extend([n, n + 1, n + 2]);
875 let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
876 let plane = Plane::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]).unwrap();
877 let out = cut(&mesh, &plane);
878 assert_eq!(out.contours.len(), 2);
879 let bare = cut(&flat, &plane);
880 assert_eq!(bare.contours.len(), 1);
881 let closed = out.contours.iter().find(|c| c.closed).unwrap();
882 assert_eq!(closed, &bare.contours[0]);
883 let sliver = out.contours.iter().find(|c| !c.closed).unwrap();
884 assert_eq!(sliver.triangles, [flat.triangle_slots() as TriangleId]);
885 assert_eq!(sliver.points.len(), 2);
886 for p in &sliver.points {
887 assert!(
888 (p[0] - 2.0).abs() < 1e-9 && p[1].abs() < 1e-9 && p[2].abs() < 1e-9,
889 "{p:?}"
890 );
891 }
892 }
893
894 #[test]
895 fn flipped_patch_reports_unoriented() {
896 let sphere = uv_sphere(16, 32, 1.0);
897 let plane = Plane::new([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]).unwrap();
898 let flipped = brute(&sphere, &plane)[3].tri;
899 let positions: Vec<f32> = sphere.positions().iter().flatten().copied().collect();
900 let mut tris = sphere.triangles().to_vec();
901 tris[flipped as usize].swap(1, 2);
902 let indices: Vec<u32> = tris.iter().flatten().copied().collect();
903 let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
904 let out = cut(&mesh, &plane);
905 assert_eq!(out.contours.len(), 1);
906 let c = &out.contours[0];
907 assert!(c.triangles.contains(&flipped));
908 assert!(c.closed && !c.oriented && c.manifold);
909 assert!(cut(&sphere, &plane).contours[0].oriented);
911 }
912
913 #[test]
914 fn deterministic_order() {
915 let meshes = [torus(2.0, 0.5, 48, 24), wavy(20), fin()];
916 let mut checked = 0;
917 for (i, mesh) in meshes.iter().enumerate() {
918 let sah = build(mesh, Quality::Sah);
919 let quick = build(mesh, Quality::Fast);
920 for plane in planes(mesh, 0x0bde + i as u64).into_iter().step_by(5) {
921 let mut a = Slice::default();
922 let mut b = Slice::default();
923 slice(mesh, &sah, &plane, &mut a).unwrap();
924 slice(mesh, &sah, &plane, &mut b).unwrap();
925 assert_eq!(a, b, "{plane:?}");
926 slice(mesh, &quick, &plane, &mut b).unwrap();
928 assert_eq!(a, b, "{plane:?}");
929 for c in &a.contours {
930 check_contour(mesh, &plane, c);
931 if c.closed {
932 assert_eq!(c.edges[0], *c.edges.iter().min().unwrap());
933 }
934 }
935 assert!(
936 a.contours
937 .windows(2)
938 .all(|w| w[0].edges[0] <= w[1].edges[0])
939 );
940 checked += usize::from(a.contours.len() > 1);
941 }
942 }
943 assert!(checked > 5, "{checked}");
944 }
945
946 #[test]
947 fn slice_rejects_stale_bvh() {
948 let mut mesh = uv_sphere(8, 16, 1.0);
949 let bvh = build(&mesh, Quality::Sah);
950 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
951 let mut out = Slice::default();
952 slice(&mesh, &bvh, &plane, &mut out).unwrap();
953 assert!(!out.contours.is_empty());
954 assert_eq!(
955 slice(&uv_sphere(8, 16, 1.0), &bvh, &plane, &mut out),
956 Err(QueryError::MeshMismatch)
957 );
958 let mut tx = mesh.begin_edit();
959 tx.set_positions(&[0], &[[0.0, 0.0, 2.0]]).unwrap();
960 tx.commit().unwrap();
961 assert_eq!(
962 slice(&mesh, &bvh, &plane, &mut out),
963 Err(QueryError::StaleBvh)
964 );
965 }
966
967 #[test]
968 fn slice_out_of_memory_clears_output() {
969 let mesh = torus(2.0, 0.5, 24, 12);
970 let bvh = build(&mesh, Quality::Sah);
971 let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
972 let mut expected = Slice::default();
973 slice(&mesh, &bvh, &plane, &mut expected).unwrap();
974 assert_eq!(expected.contours.len(), 2);
975 let mut failures = 0;
976 for n in 0..400 {
977 let mut out = expected.clone();
978 fail_allocations_after(Some(n));
979 let r = slice(&mesh, &bvh, &plane, &mut out);
980 fail_allocations_after(None);
981 match r {
982 Ok(()) => assert_eq!(out, expected),
983 Err(e) => {
984 assert_eq!(e, QueryError::OutOfMemory);
985 assert!(out.contours.is_empty());
986 failures += 1;
987 }
988 }
989 }
990 assert!(failures > 10, "{failures}");
991 }
992}