1use super::{Editable, NONE, Scope, index};
52use crate::section::{Cutter, Place};
53use ogeom_core::{FastMap, OgeomResult, ogeom_bail};
54use ogeom_math::{Plane, Point};
55use spade::handles::{FixedDirectedEdgeHandle, FixedVertexHandle};
56use spade::{ConstrainedDelaunayTriangulation, Triangulation as _};
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum Side {
61 Below,
63 Above,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Default)]
69pub struct Cap {
70 pub bridge: f64,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub struct CutReport {
78 pub capped_loops: usize,
80 pub bridged: usize,
82 pub open: usize,
86}
87
88#[derive(Default)]
91struct Cuts {
92 edges: FastMap<(u32, u32), u32>,
93 at: FastMap<[u64; 3], u32>,
94}
95
96struct Outline {
99 vertices: Vec<u32>,
100 bridges: usize,
102 stretches: usize,
104}
105
106struct Stretches {
110 loops: Vec<Vec<u32>>,
111 runs: Vec<Vec<u32>>,
112}
113
114impl Editable {
115 pub fn cut(&mut self, plane: &Plane, keep: Side, cap: Option<Cap>) -> OgeomResult<CutReport> {
135 if !plane.origin().is_finite() {
136 ogeom_bail!(Numeric, "the cutting plane's origin is not finite");
137 }
138 if let Some(Cap { bridge }) = cap
139 && !(bridge.is_finite() && bridge >= 0.0)
140 {
141 ogeom_bail!(Numeric, "a bridge of {bridge} is not a distance");
142 }
143 let (faces, vertices) = (self.corners.len(), self.positions.len());
147 if faces
148 .saturating_mul(27)
149 .saturating_add(vertices.saturating_mul(6))
150 >= NONE as usize
151 {
152 ogeom_bail!(
153 Range,
154 "a cut of {faces} faces and {vertices} vertices may pass u32 numbering"
155 );
156 }
157
158 let cutter = Cutter::new(plane, 0.0);
159 let level = self.clip(&cutter, keep);
160 let Stretches { loops, runs } = self.stretches(&level, keep);
161 let Some(Cap { bridge }) = cap else {
162 return Ok(CutReport {
163 open: loops.len() + runs.len(),
164 ..CutReport::default()
165 });
166 };
167 let (mut outlines, unjoined) = self.join_runs(runs, bridge);
168 outlines.extend(loops.into_iter().map(|vertices| Outline {
169 vertices,
170 bridges: 0,
171 stretches: 1,
172 }));
173 let capped = self.cap(&cutter, keep, &outlines);
174 let mut report = CutReport {
175 open: unjoined,
176 ..CutReport::default()
177 };
178 for (o, &c) in outlines.iter().zip(&capped) {
179 if c {
180 report.capped_loops += 1;
181 report.bridged += o.bridges;
182 } else {
183 report.open += o.stretches;
184 }
185 }
186 Ok(report)
187 }
188
189 fn clip(&mut self, cutter: &Cutter, keep: Side) -> Vec<bool> {
192 let places = self.places(cutter);
193 let mut level: Vec<bool> = places.iter().map(|&p| p == Place::On).collect();
194 let above = keep == Side::Above;
195 let first_new = self.positions.len();
196 let mut cuts = Cuts::default();
197 let mut removed = Vec::new();
198 let mut made: Vec<[u32; 3]> = Vec::new();
199 let mut polygon: Vec<u32> = Vec::with_capacity(4);
200 for f in 0..self.corners.len() {
201 if !self.live[f] {
202 continue;
203 }
204 let c = self.corners[f];
205 let kept = c.map(|v| places[v as usize].up() == above);
206 if kept == [true; 3] && !(above && self.faces_up(cutter, &places, c)) {
207 continue;
208 }
209 if kept == [false; 3] || kept == [true; 3] {
210 removed.push(index(f));
211 continue;
212 }
213 polygon.clear();
214 for i in 0..3 {
215 let j = (i + 1) % 3;
216 if kept[i] {
217 push_new(&mut polygon, c[i]);
218 }
219 if kept[i] != kept[j] {
220 let x = self.crossing(cutter, &places, &mut cuts, c[i], c[j]);
221 level.resize(self.positions.len(), true);
222 level[self.point[x as usize] as usize] = true;
223 push_new(&mut polygon, x);
224 }
225 }
226 if polygon.len() > 1 && polygon.first() == polygon.last() {
227 polygon.pop();
228 }
229 if polygon.len() == 3 && (0..3).any(|r| polygon.iter().eq(c[r..].iter().chain(&c[..r])))
230 {
231 continue;
232 }
233 removed.push(index(f));
234 match polygon[..] {
235 [a, b, c] => made.push([a, b, c]),
236 [a, b, c, d] => {
237 let at = |v: u32| self.positions[v as usize];
239 if at(a).distance(at(c)) <= at(b).distance(at(d)) {
240 made.extend([[a, b, c], [a, c, d]]);
241 } else {
242 made.extend([[b, c, d], [b, d, a]]);
243 }
244 }
245 _ => {}
246 }
247 }
248 if removed.is_empty() {
249 return level;
250 }
251
252 let mut points: Vec<u32> = removed
253 .iter()
254 .flat_map(|&f| self.corners[f as usize])
255 .map(|v| self.point[v as usize])
256 .collect();
257 points.sort_unstable();
258 points.dedup();
259 let mut faces = self.about(&points);
260 points.extend((first_new..self.positions.len()).map(index));
261 for &f in &removed {
262 self.live[f as usize] = false;
263 for h in 3 * f..3 * f + 3 {
264 self.twin[h as usize] = NONE;
265 }
266 }
267 faces.retain(|f| removed.binary_search(f).is_err());
268 for t in made {
269 faces.push(self.add_face(t));
270 }
271 self.relink(&Scope::Points(points), &faces);
272 for v in first_new..self.positions.len() {
274 if self.out[v] == NONE {
275 self.gone[v] = true;
276 }
277 }
278 level
279 }
280
281 fn places(&self, cutter: &Cutter) -> Vec<Place> {
294 let mut places: Vec<Place> = self
295 .positions
296 .iter()
297 .map(|&p| {
298 let place = cutter.side(p);
299 if place != Place::On && cutter.near(p) {
300 Place::On
301 } else {
302 place
303 }
304 })
305 .collect();
306 let mut snapped = Vec::new();
307 for (c, _) in self.corners.iter().zip(&self.live).filter(|&(_, &l)| l) {
308 for i in 0..3 {
309 let (a, b) = (c[i], c[(i + 1) % 3]);
310 let (low, high) = match (places[a as usize].up(), places[b as usize].up()) {
311 (false, true) => (a, b),
312 (true, false) => (b, a),
313 _ => continue,
314 };
315 if places[high as usize] == Place::On {
316 continue;
317 }
318 let (p, q) = (self.positions[low as usize], self.positions[high as usize]);
319 let x = cutter.crossing(p, q);
320 let blur = cutter.blur(x) + Cutter::crossing_blur(p, q, x);
321 if x.distance(q) <= blur + cutter.blur(q) {
322 snapped.push(self.point[high as usize]);
323 } else if x.distance(p) <= blur + cutter.blur(p) {
324 snapped.push(self.point[low as usize]);
325 }
326 }
327 }
328 for p in snapped {
329 for v in self.ring(p) {
330 places[v as usize] = Place::On;
331 }
332 }
333 places
334 }
335
336 fn faces_up(&self, cutter: &Cutter, places: &[Place], c: [u32; 3]) -> bool {
339 let [a, b, d] = c.map(|v| self.positions[v as usize]);
340 c.iter().all(|&v| places[v as usize] == Place::On)
341 && (b - a).cross(d - a).dot(cutter.normal()) > 0.0
342 }
343
344 fn crossing(
349 &mut self,
350 cutter: &Cutter,
351 places: &[Place],
352 cuts: &mut Cuts,
353 a: u32,
354 b: u32,
355 ) -> u32 {
356 let (low, high) = if places[a as usize].up() {
357 (b, a)
358 } else {
359 (a, b)
360 };
361 if places[high as usize] == Place::On {
362 return high;
363 }
364 let key = (self.point[low as usize], self.point[high as usize]);
365 if let Some(&v) = cuts.edges.get(&key) {
366 return v;
367 }
368 let x = cutter.crossing(self.positions[low as usize], self.positions[high as usize]);
369 let v = *cuts
370 .at
371 .entry([x.x, x.y, x.z].map(f64::to_bits))
372 .or_insert_with(|| self.add_vertex(x));
373 cuts.edges.insert(key, v);
374 v
375 }
376
377 pub(super) fn add_vertex(&mut self, at: Point) -> u32 {
379 let v = index(self.positions.len());
380 self.positions.push(at);
381 self.point.push(v);
382 self.copies.push(v);
383 self.source.push(NONE);
384 self.out.push(NONE);
385 self.gone.push(false);
386 v
387 }
388
389 pub(super) fn add_face(&mut self, corners: [u32; 3]) -> u32 {
391 let f = index(self.corners.len());
392 self.corners.push(corners);
393 self.live.push(true);
394 self.twin.extend([NONE; 3]);
395 f
396 }
397
398 fn stretches(&self, level: &[bool], keep: Side) -> Stretches {
400 let flat = |h: u32| {
401 level[self.point[self.tail(h) as usize] as usize]
402 && level[self.point[self.head(h) as usize] as usize]
403 };
404 let mut loops = Vec::new();
405 let mut runs = Vec::new();
406 for rim in self.rims() {
407 let edges = &rim.edges;
408 let Some(j) = edges.iter().position(|&h| !flat(h)) else {
409 loops.push(edges.iter().map(|&h| self.tail(h)).collect());
410 continue;
411 };
412 let m = edges.len();
413 let mut run: Vec<u32> = Vec::new();
414 for k in 1..=m {
415 let h = edges[(j + k) % m];
416 if flat(h) {
417 if run.is_empty() {
418 run.push(self.tail(h));
419 }
420 run.push(self.head(h));
421 } else if !run.is_empty() {
422 runs.push(core::mem::take(&mut run));
423 }
424 }
425 }
426 if keep == Side::Below {
430 for s in loops.iter_mut().chain(runs.iter_mut()) {
431 s.reverse();
432 }
433 }
434 Stretches { loops, runs }
435 }
436
437 fn join_runs(&self, runs: Vec<Vec<u32>>, bridge: f64) -> (Vec<Outline>, usize) {
442 let at = |v: u32| self.positions[v as usize];
443 let ends: Vec<u32> = runs.iter().map(|r| r[r.len() - 1]).collect();
444 let starts: Vec<u32> = runs.iter().map(|r| r[0]).collect();
445 let mut by_x: Vec<(f64, usize)> = starts.iter().map(|&v| (at(v).x, 0)).collect();
446 for (j, s) in by_x.iter_mut().enumerate() {
447 s.1 = j;
448 }
449 by_x.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
450 let mut candidates: Vec<(f64, bool, usize, usize)> = Vec::new();
453 for (i, &e) in ends.iter().enumerate() {
454 let p = at(e);
455 let from = by_x.partition_point(|s| s.0 < p.x - bridge);
456 for &(x, j) in &by_x[from..] {
457 if x > p.x + bridge {
458 break;
459 }
460 let s = starts[j];
461 if self.point[s as usize] == self.point[e as usize] {
462 candidates.push((0.0, false, i, j));
463 } else {
464 let d = p.distance(at(s));
465 if d <= bridge {
466 candidates.push((d, true, i, j));
467 }
468 }
469 }
470 }
471 candidates.sort_by(|a, b| {
472 a.0.total_cmp(&b.0)
473 .then(a.1.cmp(&b.1))
474 .then(a.2.cmp(&b.2))
475 .then(a.3.cmp(&b.3))
476 });
477 let mut link: Vec<Option<(usize, bool)>> = vec![None; runs.len()];
478 let mut taken = vec![false; runs.len()];
479 for (_, bridged, i, j) in candidates {
480 if link[i].is_none() && !taken[j] {
481 link[i] = Some((j, bridged));
482 taken[j] = true;
483 }
484 }
485
486 let mut seen = vec![false; runs.len()];
489 let mut outlines = Vec::new();
490 let mut joined = 0;
491 for first in 0..runs.len() {
492 if seen[first] {
493 continue;
494 }
495 let mut cycle = vec![first];
496 seen[first] = true;
497 let mut at = first;
498 let closed = loop {
499 match link[at] {
500 Some((j, _)) if j == first => break true,
501 Some((j, _)) if !seen[j] => {
502 seen[j] = true;
503 cycle.push(j);
504 at = j;
505 }
506 _ => break false,
507 }
508 };
509 if !closed {
510 continue;
511 }
512 let mut vertices = Vec::new();
513 let mut bridges = 0;
514 for &r in &cycle {
515 vertices.extend_from_slice(&runs[r]);
516 if link[r].is_some_and(|(_, bridged)| bridged) {
517 bridges += 1;
518 } else {
519 vertices.pop();
521 }
522 }
523 joined += cycle.len();
524 outlines.push(Outline {
525 vertices,
526 bridges,
527 stretches: cycle.len(),
528 });
529 }
530 (outlines, runs.len() - joined)
531 }
532
533 fn cap(&mut self, cutter: &Cutter, keep: Side, outlines: &[Outline]) -> Vec<bool> {
536 let mut active: Vec<bool> = outlines
537 .iter()
538 .map(|o| {
539 let mut points: Vec<u32> =
540 o.vertices.iter().map(|&v| self.point[v as usize]).collect();
541 points.sort_unstable();
542 points.dedup();
543 points.len() >= 3
544 })
545 .collect();
546 let faces = loop {
547 match self.cap_faces(cutter, outlines, &active) {
548 Ok(faces) => break faces,
549 Err(bad) => {
550 for k in bad {
551 active[k] = false;
552 }
553 }
554 }
555 };
556 if faces.is_empty() {
557 return active;
558 }
559 let mut points: Vec<u32> = faces
560 .iter()
561 .flatten()
562 .map(|&v| self.point[v as usize])
563 .collect();
564 points.sort_unstable();
565 points.dedup();
566 let mut around = self.about(&points);
567 for t in faces {
568 let [a, b, c] = t;
569 around.push(self.add_face(if keep == Side::Below {
570 [a, b, c]
571 } else {
572 [a, c, b]
573 }));
574 }
575 self.relink(&Scope::Points(points), &around);
576 active
577 }
578
579 fn cap_faces(
582 &self,
583 cutter: &Cutter,
584 outlines: &[Outline],
585 active: &[bool],
586 ) -> Result<Vec<[u32; 3]>, Vec<usize>> {
587 let mut points: Vec<(u32, u32)> = outlines
589 .iter()
590 .zip(active)
591 .filter(|&(_, &a)| a)
592 .flat_map(|(o, _)| o.vertices.iter().map(|&v| (self.point[v as usize], v)))
593 .collect();
594 points.sort_unstable();
595 points.dedup_by_key(|p| p.0);
596 if points.is_empty() {
597 return Ok(Vec::new());
598 }
599 let slot = |v: u32| {
600 points
601 .binary_search_by_key(&self.point[v as usize], |p| p.0)
602 .unwrap_or(0)
603 };
604 let coordinates: Vec<[f64; 2]> = points
605 .iter()
606 .map(|&(p, _)| cutter.along(self.positions[p as usize]))
607 .collect();
608
609 let mut bad_point = vec![false; points.len()];
612 for (k, c) in coordinates.iter().enumerate() {
613 if c.iter().any(|&x| spade::validate_coordinate(x).is_err()) {
614 bad_point[k] = true;
615 }
616 }
617 let mut order: Vec<usize> = (0..points.len()).collect();
618 order.sort_by(|&a, &b| {
619 let (p, q) = (coordinates[a], coordinates[b]);
620 p[0].total_cmp(&q[0]).then(p[1].total_cmp(&q[1]))
621 });
622 for pair in order.windows(2) {
623 if coordinates[pair[0]] == coordinates[pair[1]] {
624 bad_point[pair[0]] = true;
625 bad_point[pair[1]] = true;
626 }
627 }
628 let active_loops = || (0..outlines.len()).filter(|&k| active[k]);
629 let bad: Vec<usize> = active_loops()
630 .filter(|&k| outlines[k].vertices.iter().any(|&v| bad_point[slot(v)]))
631 .collect();
632 if !bad.is_empty() {
633 return Err(bad);
634 }
635
636 let vertices: Vec<spade::Point2<f64>> = coordinates
637 .iter()
638 .map(|c| spade::Point2::new(c[0], c[1]))
639 .collect();
640 let Ok(mut cdt) = ConstrainedDelaunayTriangulation::<spade::Point2<f64>>::bulk_load_cdt(
641 vertices,
642 Vec::new(),
643 ) else {
644 return Err(active_loops().collect());
645 };
646
647 let mut edges: Vec<(usize, FixedDirectedEdgeHandle)> = Vec::new();
649 let mut bad = Vec::new();
650 for k in active_loops() {
651 let vertices = &outlines[k].vertices;
652 let n = vertices.len();
653 for i in 0..n {
654 let (a, b) = (slot(vertices[i]), slot(vertices[(i + 1) % n]));
655 if a == b {
656 continue;
657 }
658 let (from, to) = (
659 FixedVertexHandle::from_index(a),
660 FixedVertexHandle::from_index(b),
661 );
662 let made = cdt.try_add_constraint(from, to);
663 match made[..] {
664 [e] => {
665 let forward = cdt.directed_edge(e).from().fix() == from;
666 edges.push((k, if forward { e } else { e.rev() }));
667 }
668 [] => {
670 bad.push(k);
671 break;
672 }
673 _ => return Err(vec![k]),
676 }
677 }
678 }
679 if !bad.is_empty() {
680 return Err(bad);
681 }
682
683 let mut weight = vec![0_i64; cdt.num_directed_edges()];
684 for &(_, e) in &edges {
685 weight[e.index()] += 1;
686 weight[e.rev().index()] -= 1;
687 }
688 let mut count: Vec<Option<i64>> = vec![None; cdt.num_all_faces()];
691 let outer = cdt.outer_face().fix().index();
692 count[outer] = Some(0);
693 let mut sides = vec![[None; 3]; cdt.num_all_faces()];
694 for face in cdt.inner_faces() {
695 sides[face.fix().index()] = face.adjacent_edges().map(|e| Some(e.fix()));
696 }
697 let mut stack = vec![outer];
698 let mut around: Vec<FixedDirectedEdgeHandle> = Vec::with_capacity(3);
699 while let Some(f) = stack.pop() {
700 let Some(here) = count[f] else { continue };
701 around.clear();
702 if f == outer {
703 for e in cdt.convex_hull() {
704 around.push(if e.face().is_outer() {
705 e.fix()
706 } else {
707 e.rev().fix()
708 });
709 }
710 } else {
711 around.extend(sides[f].iter().flatten());
712 }
713 for &e in &around {
714 let g = cdt.directed_edge(e).rev().face().fix().index();
715 if count[g].is_none() {
716 count[g] = Some(here - weight[e.index()]);
717 stack.push(g);
718 }
719 }
720 }
721
722 let bad: Vec<usize> = {
723 let mut bad: Vec<usize> = edges
724 .iter()
725 .filter(|&&(_, e)| {
726 let h = cdt.directed_edge(e);
727 count[h.face().fix().index()] != Some(1)
728 || count[h.rev().face().fix().index()] != Some(0)
729 })
730 .map(|&(k, _)| k)
731 .collect();
732 bad.dedup();
733 bad
734 };
735 if !bad.is_empty() {
736 return Err(bad);
737 }
738 Ok(cdt
739 .inner_faces()
740 .filter(|face| count[face.fix().index()] == Some(1))
741 .map(|face| face.vertices().map(|v| points[v.fix().index()].1))
742 .collect())
743 }
744}
745
746fn push_new(polygon: &mut Vec<u32>, v: u32) {
748 if polygon.last() != Some(&v) {
749 polygon.push(v);
750 }
751}
752
753#[cfg(test)]
754#[allow(clippy::unwrap_used, clippy::cast_precision_loss, reason = "test code")]
755mod tests {
756 use super::super::tests::{check, mesh, torus};
757 use super::*;
758 use crate::section::section;
759 use core::f64::consts::{PI, TAU};
760 use ogeom_core::Tolerances;
761 use ogeom_math::{Direction, Vector};
762 use ogeom_topo::Triangulation;
763 use proptest::prelude::*;
764
765 fn plane(origin: Point, normal: Vector) -> Plane {
766 Plane::through(
767 origin,
768 Direction::new(normal, Tolerances::millimetres()).unwrap(),
769 )
770 }
771
772 fn horizontal(z: f64) -> Plane {
773 plane(Point::new(0.0, 0.0, z), Vector::Z)
774 }
775
776 fn polygon_area(n: u32, r: f64) -> f64 {
778 f64::from(n) / 2.0 * r * r * (TAU / f64::from(n)).sin()
779 }
780
781 fn prism(radius: f64, height: f64, sides: u32, rings: u32, outward: bool) -> Triangulation {
787 let mut points = Vec::new();
788 for j in 0..rings {
789 let z = height * f64::from(j) / f64::from(rings - 1);
790 for i in 0..sides {
791 let a = TAU * f64::from(i) / f64::from(sides);
792 points.push(Point::new(radius * a.cos(), radius * a.sin(), z));
793 }
794 }
795 let at = |i: u32, j: u32| j * sides + i % sides;
796 let mut triangles = Vec::new();
797 for j in 0..rings - 1 {
798 for i in 0..sides {
799 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
800 triangles.extend([[a, b, c], [a, c, d]]);
801 }
802 }
803 let (low, high) = (rings * sides, rings * sides + 1);
804 points.extend([Point::new(0.0, 0.0, 0.0), Point::new(0.0, 0.0, height)]);
805 for i in 0..sides {
806 triangles.push([low, at(i + 1, 0), at(i, 0)]);
807 triangles.push([high, at(i, rings - 1), at(i + 1, rings - 1)]);
808 }
809 if !outward {
810 for t in &mut triangles {
811 t.swap(1, 2);
812 }
813 }
814 mesh(points, triangles)
815 }
816
817 fn tube(inner: f64, outer: f64, height: f64, sides: u32) -> Triangulation {
820 let mut points = Vec::new();
821 for r in [outer, inner] {
822 for z in [0.0, height] {
823 for i in 0..sides {
824 let a = TAU * f64::from(i) / f64::from(sides);
825 points.push(Point::new(r * a.cos(), r * a.sin(), z));
826 }
827 }
828 }
829 let at = |w: u32, e: u32, i: u32| (2 * w + e) * sides + i % sides;
831 let mut triangles = Vec::new();
832 for i in 0..sides {
833 let (a, b, c, d) = (at(0, 0, i), at(0, 0, i + 1), at(0, 1, i + 1), at(0, 1, i));
834 triangles.extend([[a, b, c], [a, c, d]]);
835 let (a, b, c, d) = (at(1, 0, i), at(1, 0, i + 1), at(1, 1, i + 1), at(1, 1, i));
836 triangles.extend([[a, c, b], [a, d, c]]);
837 let (o, p, q, r) = (at(0, 0, i), at(0, 0, i + 1), at(1, 0, i + 1), at(1, 0, i));
838 triangles.extend([[o, r, q], [o, q, p]]);
839 let (o, p, q, r) = (at(0, 1, i), at(0, 1, i + 1), at(1, 1, i + 1), at(1, 1, i));
840 triangles.extend([[o, p, q], [o, q, r]]);
841 }
842 mesh(points, triangles)
843 }
844
845 fn sphere(radius: f64, bands: u32, sides: u32) -> Triangulation {
849 let mut points = vec![Point::new(0.0, 0.0, radius)];
850 for j in 1..bands {
851 let mirror = bands - j;
852 let polar = PI * f64::from(j.min(mirror)) / f64::from(bands);
853 let (s, c) = (radius * polar.sin(), radius * polar.cos());
854 let z = match (2 * j).cmp(&bands) {
855 core::cmp::Ordering::Less => c,
856 core::cmp::Ordering::Equal => 0.0,
857 core::cmp::Ordering::Greater => -c,
858 };
859 for i in 0..sides {
860 let a = TAU * f64::from(i) / f64::from(sides);
861 points.push(Point::new(s * a.cos(), s * a.sin(), z));
862 }
863 }
864 let south = 1 + (bands - 1) * sides;
865 points.push(Point::new(0.0, 0.0, -radius));
866 let at = |j: u32, i: u32| 1 + (j - 1) * sides + i % sides;
867 let mut triangles = Vec::new();
868 for i in 0..sides {
869 triangles.push([0, at(1, i), at(1, i + 1)]);
870 triangles.push([south, at(bands - 1, i + 1), at(bands - 1, i)]);
871 }
872 for j in 1..bands - 1 {
873 for i in 0..sides {
874 let (a, b, c, d) = (at(j, i), at(j + 1, i), at(j + 1, i + 1), at(j, i + 1));
875 triangles.extend([[a, b, c], [a, c, d]]);
876 }
877 }
878 mesh(points, triangles)
879 }
880
881 fn volume(e: &Editable) -> f64 {
883 let m = e.to_triangulation();
884 m.triangles
885 .iter()
886 .map(|t| {
887 let [a, b, c] = t.map(|v| m.positions[v as usize] - Point::ORIGIN);
888 a.dot(b.cross(c)) / 6.0
889 })
890 .sum()
891 }
892
893 fn area_in(e: &Editable, plane: &Plane) -> f64 {
898 let n = plane.normal().vector();
899 let height = |p: Point| (p - plane.origin()).dot(n);
900 let m = e.to_triangulation();
901 m.triangles
902 .iter()
903 .map(|t| t.map(|v| m.positions[v as usize]))
904 .filter(|t| t.iter().all(|&p| height(p).abs() <= 1e-12))
905 .map(|[a, b, c]| (b - a).cross(c - a).magnitude() / 2.0)
906 .sum()
907 }
908
909 fn close(a: f64, b: f64) -> bool {
912 (a - b).abs() <= 1e-12 * a.abs().max(b.abs())
913 }
914
915 const CAPPED: Option<Cap> = Some(Cap { bridge: 0.0 });
916
917 fn one_loop() -> CutReport {
918 CutReport {
919 capped_loops: 1,
920 ..CutReport::default()
921 }
922 }
923
924 #[test]
925 fn a_closed_prism_cut_across_is_a_closed_prism_of_the_height_kept() {
926 let (r, n) = (2.0, 24);
927 let whole = Editable::from(&prism(r, 3.0, n, 4, true), 0.0).unwrap();
928 let cut = horizontal(1.3);
929 for (keep, height) in [(Side::Below, 1.3), (Side::Above, 3.0 - 1.3)] {
930 let mut e = whole.clone();
931 assert_eq!(e.cut(&cut, keep, CAPPED).unwrap(), one_loop());
932 check(&e);
933 assert!(e.is_closed(), "{keep:?}");
934 assert!(e.non_manifold().is_empty());
935 assert!(close(volume(&e), polygon_area(n, r) * height), "{keep:?}");
936 assert!(close(area_in(&e, &cut), polygon_area(n, r)));
937 }
938 }
939
940 #[test]
941 fn a_sphere_cut_through_its_equator_ring_is_two_closed_halves() {
942 let (r, n) = (1.5, 24);
943 let m = sphere(r, 16, n);
944 let whole = Editable::from(&m, 0.0).unwrap();
945 let all = volume(&whole);
946 for keep in [Side::Below, Side::Above] {
947 let mut e = whole.clone();
948 assert_eq!(e.cut(&horizontal(0.0), keep, CAPPED).unwrap(), one_loop());
949 check(&e);
950 assert!(e.is_closed());
951 assert_eq!(e.vertex_count(), m.positions.len());
953 assert!(close(volume(&e), all / 2.0), "{keep:?}");
954 assert!(close(area_in(&e, &horizontal(0.0)), polygon_area(n, r)));
955 }
956 }
957
958 #[test]
959 fn a_sphere_cut_aslant_caps_both_halves_with_the_area_of_its_section() {
960 let m = sphere(1.5, 15, 22);
961 let whole = Editable::from(&m, 0.0).unwrap();
962 let cut = plane(Point::new(0.1, -0.2, 0.3), Vector::new(1.0, 2.0, 3.0));
963 let s = section(&m, &cut).unwrap();
964 assert_eq!(s.chains.len(), 1);
965 let n = cut.normal().vector();
966 let points = &s.chains[0].points;
967 let enclosed: f64 = points
968 .windows(2)
969 .skip(1)
970 .map(|w| (w[0] - points[0]).cross(w[1] - points[0]).dot(n))
971 .sum::<f64>()
972 / 2.0;
973 let mut halves = 0.0;
974 for keep in [Side::Below, Side::Above] {
975 let mut e = whole.clone();
976 assert_eq!(e.cut(&cut, keep, CAPPED).unwrap(), one_loop());
977 check(&e);
978 assert!(e.is_closed());
979 assert!(close(area_in(&e, &cut), enclosed.abs()), "{keep:?}");
980 halves += volume(&e);
981 }
982 assert!(close(halves, volume(&whole)));
983 }
984
985 #[test]
986 fn the_cut_rim_is_the_section_to_the_bit() {
987 let m = torus(24, 12);
988 let cut = plane(Point::new(0.3, 0.1, 0.2), Vector::new(0.3, -0.2, 1.0));
989 let bits = |p: Point| [p.x, p.y, p.z].map(f64::to_bits);
990 let chains = section(&m, &cut).unwrap().chains;
991 let mut chain: Vec<[u64; 3]> = chains
992 .iter()
993 .flat_map(|c| c.points.iter().map(|&p| bits(p)))
994 .collect();
995 chain.sort_unstable();
996 chain.dedup();
997 for keep in [Side::Below, Side::Above] {
998 let mut e = Editable::from(&m, 0.0).unwrap();
999 let report = e.cut(&cut, keep, None).unwrap();
1000 assert_eq!(report.open, chains.len());
1001 let mut rim: Vec<[u64; 3]> = e
1002 .rims()
1003 .iter()
1004 .flat_map(|r| r.edges.iter().map(|&h| e.edge(h).unwrap()[0]))
1005 .map(|v| bits(e.position(v).unwrap()))
1006 .collect();
1007 rim.sort_unstable();
1008 rim.dedup();
1009 assert_eq!(rim, chain, "{keep:?}");
1010 }
1011 }
1012
1013 #[test]
1014 fn kept_vertices_keep_their_numbers_and_sources() {
1015 let m = torus(24, 12);
1016 let cut = plane(Point::new(0.3, 0.1, 0.2), Vector::new(0.3, -0.2, 1.0));
1017 let n = cut.normal().vector();
1018 let mut e = Editable::from(&m, 0.0).unwrap();
1019 e.cut(&cut, Side::Above, CAPPED).unwrap();
1020 check(&e);
1021 assert!(e.is_closed());
1022 for (v, &p) in m.positions.iter().enumerate() {
1023 let v = index(v);
1024 if (p - cut.origin()).dot(n) > 1e-9 {
1026 assert_eq!(e.position(v), Some(p));
1027 assert_eq!(e.source_vertex(v), Some(v));
1028 }
1029 }
1030 for v in 0..index(e.vertex_count()) {
1031 let Some(p) = e.position(v) else { continue };
1032 match e.source_vertex(v) {
1033 Some(s) => assert_eq!(p, m.positions[s as usize]),
1034 None => {
1035 assert!(v as usize >= m.positions.len());
1036 assert!((p - cut.origin()).dot(n).abs() < 1e-12);
1037 }
1038 }
1039 }
1040 }
1041
1042 #[test]
1043 fn a_tube_cut_across_is_capped_by_a_ring() {
1044 let (inner, outer, n) = (1.0, 2.5, 20);
1045 let whole = Editable::from(&tube(inner, outer, 2.0, n), 0.0).unwrap();
1046 assert!(whole.is_closed());
1047 let cut = horizontal(0.7);
1048 let mut e = whole;
1049 let report = e.cut(&cut, Side::Below, CAPPED).unwrap();
1050 assert_eq!(
1051 report,
1052 CutReport {
1053 capped_loops: 2,
1054 ..CutReport::default()
1055 }
1056 );
1057 check(&e);
1058 assert!(e.is_closed());
1059 assert_eq!(e.components().len(), 1);
1060 let ring = polygon_area(n, outer) - polygon_area(n, inner);
1061 assert!(close(area_in(&e, &cut), ring));
1062 assert!(close(volume(&e), ring * 0.7));
1063 }
1064
1065 #[test]
1066 fn nested_shells_give_nested_caps() {
1067 let n = 18;
1068 let mut e = Editable::from(&nested(n), 0.0).unwrap();
1069 let cut = horizontal(1.4);
1070 let report = e.cut(&cut, Side::Above, CAPPED).unwrap();
1071 assert_eq!(
1072 report,
1073 CutReport {
1074 capped_loops: 3,
1075 ..CutReport::default()
1076 }
1077 );
1078 check(&e);
1079 assert!(e.is_closed());
1080 assert_eq!(e.components().len(), 2);
1081 let solid = polygon_area(n, 3.0) - polygon_area(n, 2.0) + polygon_area(n, 1.0);
1082 assert!(close(area_in(&e, &cut), solid));
1083 let above =
1084 polygon_area(n, 3.0) * 1.6 - polygon_area(n, 2.0) * 1.1 + polygon_area(n, 1.0) * 0.6;
1085 assert!(close(volume(&e), above));
1086 }
1087
1088 #[test]
1089 fn a_gap_in_the_rim_is_bridged_within_reach_and_left_open_beyond() {
1090 let (r, n) = (2.0, 16);
1094 let mut whole = Editable::from(&prism(r, 3.0, n, 4, true), 0.0).unwrap();
1095 whole.remove_faces(&[2 * n, 2 * n + 1]);
1096 let side = 2.0 * r * (PI / f64::from(n)).sin();
1097 let cut = horizontal(1.5);
1098
1099 let mut e = whole.clone();
1100 let report = e
1101 .cut(
1102 &cut,
1103 Side::Below,
1104 Some(Cap {
1105 bridge: side * 1.01,
1106 }),
1107 )
1108 .unwrap();
1109 assert_eq!(
1110 report,
1111 CutReport {
1112 capped_loops: 1,
1113 bridged: 1,
1114 open: 0
1115 }
1116 );
1117 check(&e);
1118 assert!(close(area_in(&e, &cut), polygon_area(n, r)));
1119 let rims = e.rims();
1122 assert_eq!(rims.len(), 1);
1123 assert_eq!(rims[0].edges.len(), 4);
1124
1125 let mut e = whole;
1126 let report = e
1127 .cut(
1128 &cut,
1129 Side::Below,
1130 Some(Cap {
1131 bridge: side * 0.99,
1132 }),
1133 )
1134 .unwrap();
1135 assert_eq!(
1136 report,
1137 CutReport {
1138 open: 1,
1139 ..CutReport::default()
1140 }
1141 );
1142 check(&e);
1143 assert_eq!(area_in(&e, &cut), 0.0);
1144 }
1145
1146 fn nested(sides: u32) -> Triangulation {
1150 let mut m = prism(3.0, 3.0, sides, 4, true);
1151 for (r, base, outward) in [(2.0, 0.5, false), (1.0, 1.0, true)] {
1152 let mut d = prism(r, 3.0 - 2.0 * base, sides, 4, outward);
1153 for p in &mut d.positions {
1154 *p += Vector::new(0.0, 0.0, base);
1155 }
1156 m.append(&d);
1157 }
1158 m
1159 }
1160
1161 fn shifted(mut m: Triangulation, x: f64) -> Triangulation {
1163 for p in &mut m.positions {
1164 *p += Vector::new(x, 0.0, 0.0);
1165 }
1166 m
1167 }
1168
1169 #[test]
1170 fn of_two_overlapping_bodies_one_is_left_open() {
1171 let mut m = prism(1.0, 2.0, 12, 3, true);
1173 m.append(&shifted(prism(1.0, 2.0, 12, 3, true), 1.2));
1174 m.append(&shifted(prism(1.0, 2.0, 12, 3, true), 5.0));
1175 let mut e = Editable::from(&m, 0.0).unwrap();
1176 let report = e.cut(&horizontal(0.5), Side::Below, CAPPED).unwrap();
1177 assert_eq!(
1178 report,
1179 CutReport {
1180 capped_loops: 2,
1181 open: 1,
1182 ..CutReport::default()
1183 }
1184 );
1185 check(&e);
1186 assert_eq!(e.rims().len(), 1);
1187 }
1188
1189 #[test]
1190 fn bodies_meeting_at_an_edge_are_capped_through_the_point_they_share() {
1191 let mut m = prism(1.0, 2.0, 4, 3, true);
1195 m.append(&shifted(prism(1.0, 2.0, 4, 3, true), 2.0));
1196 for p in &mut m.positions {
1197 *p = Point::new(p.x.round(), p.y.round(), p.z);
1198 }
1199 let cut = horizontal(0.5);
1200 for weld in [0.0, 1e-9] {
1201 let mut e = Editable::from(&m, weld).unwrap();
1202 let report = e.cut(&cut, Side::Below, CAPPED).unwrap();
1203 assert_eq!(
1204 report,
1205 CutReport {
1206 capped_loops: 2,
1207 ..CutReport::default()
1208 },
1209 "{weld}"
1210 );
1211 check(&e);
1212 assert!(e.is_closed());
1213 assert!(close(volume(&e), 2.0 * 2.0 * 0.5));
1214 }
1215 }
1216
1217 #[test]
1218 fn a_vertex_a_rounding_above_the_plane_is_cut_at_not_beside() {
1219 let tiny = 1e-20;
1223 let points = vec![
1224 Point::new(1.0, 0.0, tiny),
1225 Point::new(0.0, 1.0, tiny),
1226 Point::new(-1.0, 0.0, tiny),
1227 Point::new(0.0, -1.0, tiny),
1228 Point::new(0.0, 0.0, 1.0),
1229 Point::new(0.0, 0.0, -1.0),
1230 ];
1231 let mut triangles = Vec::new();
1232 for i in 0..4 {
1233 let j = (i + 1) % 4;
1234 triangles.extend([[i, j, 4], [j, i, 5]]);
1235 }
1236 let m = mesh(points, triangles);
1237 for keep in [Side::Below, Side::Above] {
1238 let mut e = Editable::from(&m, 0.0).unwrap();
1239 assert_eq!(e.cut(&horizontal(0.0), keep, CAPPED).unwrap(), one_loop());
1240 check(&e);
1241 assert!(e.is_closed());
1242 assert_eq!(e.vertex_count(), 6, "{keep:?}");
1243 assert!(close(volume(&e), 2.0 / 3.0), "{keep:?}");
1244 }
1245 }
1246
1247 #[test]
1248 fn an_open_sheet_cut_across_leaves_its_stretch_open() {
1249 let mut e = Editable::from(&prism(1.0, 2.0, 12, 3, true), 0.0).unwrap();
1251 e.remove_faces(&(48..72).collect::<Vec<_>>());
1252 e.remove_faces(&[0, 1]);
1253 let report = e.cut(&horizontal(0.5), Side::Below, CAPPED).unwrap();
1254 assert_eq!(
1255 report,
1256 CutReport {
1257 open: 1,
1258 ..CutReport::default()
1259 }
1260 );
1261 check(&e);
1262 }
1263
1264 #[test]
1265 fn a_plane_missing_the_mesh_keeps_all_or_nothing() {
1266 let m = sphere(1.0, 6, 8);
1267 let mut e = Editable::from(&m, 0.0).unwrap();
1268 assert_eq!(
1269 e.cut(&horizontal(2.0), Side::Below, CAPPED).unwrap(),
1270 CutReport::default()
1271 );
1272 assert_eq!(e.to_triangulation().triangles, m.triangles);
1273 assert_eq!(
1274 e.cut(&horizontal(2.0), Side::Above, CAPPED).unwrap(),
1275 CutReport::default()
1276 );
1277 assert!(e.to_triangulation().triangles.is_empty());
1278 check(&e);
1279 }
1280
1281 #[test]
1282 fn a_plane_through_an_end_keeps_the_end_with_its_material() {
1283 let (r, n, h) = (1.0, 8, 2.0);
1284 let m = prism(r, h, n, 3, true);
1285 let full = polygon_area(n, r) * h;
1286 let cut = |z: f64, keep: Side| {
1287 let mut e = Editable::from(&m, 0.0).unwrap();
1288 let report = e.cut(&horizontal(z), keep, CAPPED).unwrap();
1289 check(&e);
1290 (e, report)
1291 };
1292 let (e, report) = cut(h, Side::Below);
1295 assert_eq!(report, one_loop());
1296 assert!(e.is_closed());
1297 assert!(close(volume(&e), full));
1298 let (e, report) = cut(h, Side::Above);
1299 assert_eq!(report, CutReport::default());
1300 assert!(e.to_triangulation().triangles.is_empty());
1301 let (e, report) = cut(0.0, Side::Above);
1304 assert_eq!(report, CutReport::default());
1305 assert_eq!(e.to_triangulation().triangles, m.triangles);
1306 let (e, report) = cut(0.0, Side::Below);
1307 assert_eq!(report, CutReport::default());
1308 assert!(e.to_triangulation().triangles.is_empty());
1309 }
1310
1311 #[test]
1312 fn the_cut_is_deterministic() {
1313 let m = torus(30, 14);
1314 let cut = plane(Point::new(0.0, 0.5, 0.1), Vector::new(1.0, 1.0, 0.4));
1315 let mut a = Editable::from(&m, 0.0).unwrap();
1316 let mut b = a.clone();
1317 let ra = a.cut(&cut, Side::Below, CAPPED).unwrap();
1318 let rb = b.cut(&cut, Side::Below, CAPPED).unwrap();
1319 assert_eq!(ra, rb);
1320 assert_eq!(a, b);
1321 assert!(a.is_closed());
1322 }
1323
1324 #[test]
1325 fn a_bad_plane_or_bridge_is_refused() {
1326 let mut e = Editable::from(&sphere(1.0, 6, 8), 0.0).unwrap();
1327 let far = plane(Point::new(f64::NAN, 0.0, 0.0), Vector::Z);
1328 assert!(e.cut(&far, Side::Below, None).is_err());
1329 for bridge in [-1.0, f64::INFINITY, f64::NAN] {
1330 assert!(
1331 e.cut(&horizontal(0.0), Side::Below, Some(Cap { bridge }))
1332 .is_err()
1333 );
1334 }
1335 assert!(e.is_closed());
1336 }
1337
1338 #[test]
1344 fn planes_grazing_vertices_close_both_parts() {
1345 let shift = 0.001_455_055_119_429_773_2;
1346 let (past, by_origin) = (-0.020_749_998_000_000_002, -0.002_512_462_814_070_363);
1347 let cases = [
1348 (torus(12, 8), 0, Vector::new(-1.0, -1.0, 1.0), 0.0),
1349 (torus(12, 8), 5, Vector::new(0.0, -1.0, 0.0), 0.0),
1350 (torus(12, 8), 91, Vector::new(-1.0, 0.0, 0.0), 0.0),
1351 (torus(12, 8), 0, Vector::new(-1.0, 3.0, 0.0), shift),
1352 (torus(12, 8), 51, Vector::new(1.0, -3.0, 0.0), past),
1353 (torus(12, 8), 52, Vector::new(1.0, -3.0, 0.0), by_origin),
1354 (nested(12), 5, Vector::new(0.0, -2.0, 0.0), 0.0),
1355 (nested(12), 19, Vector::new(3.0, 3.0, 0.0), 0.0),
1356 ];
1357 for (m, through, normal, shift) in cases {
1358 let origin = m.positions[through] + Vector::new(shift, shift / 3.0, shift / 7.0);
1359 let cut = plane(origin, normal);
1360 let whole = Editable::from(&m, 0.0).unwrap();
1361 let mut total = 0.0;
1362 for keep in [Side::Below, Side::Above] {
1363 let mut e = whole.clone();
1364 let report = e.cut(&cut, keep, CAPPED).unwrap();
1365 check(&e);
1366 assert_eq!(report.open, 0, "{through} {normal:?} {keep:?}");
1367 assert!(e.is_closed() || e.to_triangulation().triangles.is_empty());
1368 total += volume(&e);
1369 }
1370 assert!(close(total, volume(&whole)), "{through} {normal:?}");
1371 }
1372 }
1373
1374 proptest! {
1375 #![proptest_config(ProptestConfig::with_cases(64))]
1376
1377 #[test]
1381 fn a_closed_mesh_cut_anywhere_closes_both_parts(
1382 shape in 0_usize..3,
1383 through in 0_usize..96,
1384 on_vertex in any::<bool>(),
1385 shift in -0.5_f64..0.5,
1386 normal in (-3_i32..=3, -3_i32..=3, -3_i32..=3),
1387 ) {
1388 let (x, y, z) = normal;
1389 prop_assume!((x, y, z) != (0, 0, 0));
1390 let m = match shape {
1391 0 => torus(12, 8),
1392 1 => sphere(1.5, 8, 14),
1393 _ => nested(12),
1394 };
1395 let at = m.positions[through % m.positions.len()];
1396 let origin = if on_vertex {
1397 at
1398 } else {
1399 at + Vector::new(shift, shift / 3.0, shift / 7.0)
1400 };
1401 let cut = plane(origin, Vector::new(f64::from(x), f64::from(y), f64::from(z)));
1402 let whole = Editable::from(&m, 0.0).unwrap();
1403 let mut total = 0.0;
1404 for keep in [Side::Below, Side::Above] {
1405 let mut e = whole.clone();
1406 let report = e.cut(&cut, keep, CAPPED).unwrap();
1407 check(&e);
1408 prop_assert_eq!(report.open, 0);
1409 prop_assert!(e.is_closed() || e.to_triangulation().triangles.is_empty());
1410 total += volume(&e);
1411 }
1412 let all = volume(&whole);
1413 prop_assert!(close(total, all), "{} against {}", total, all);
1416 }
1417 }
1418}