Skip to main content

ogeom_mesh/editable/
cut.rs

1//! Cutting an editable mesh by a plane, and capping the cut flat.
2//!
3//! # The cut
4//!
5//! Which side of the plane each vertex lies on is [`section`]'s: decided
6//! once per vertex by the exact orientation predicate, a vertex in the plane
7//! counting as above. A vertex within rounding of the plane, so near that
8//! it and its foot on the plane could take the same coordinates in it, or
9//! one the crossing of an edge from it lands within rounding of, is
10//! snapped onto it: it counts as in the plane, so that no new vertex crowds
11//! it and no sliver is left between it and the plane.
12//!
13//! A face whose corners all lie on the kept side stays, one whose corners
14//! all lie on the other goes, and one whose corners disagree is clipped: its
15//! kept part, a triangle or a quadrilateral, takes its place as new faces.
16//! An edge whose ends disagree is cut at one vertex, shared by every face on
17//! the edge: its upper end where that lies in the plane, as [`section`]
18//! cuts it, and otherwise a new vertex standing where [`section`] puts that
19//! point of its chain, to the bit. A face lying in the plane goes with the
20//! side its material is on, as it does for [`section`]: kept above where
21//! it faces down, its material above it, and not otherwise.
22//!
23//! # The caps
24//!
25//! The rims the cut leaves lie in the plane where both ends of every edge
26//! do: a cut vertex, a vertex in the plane, or one snapped onto it. A rim
27//! lying wholly in the plane is a loop. A rim that leaves the plane and
28//! comes back leaves runs in it, and the end of one run is joined to the
29//! start of another where they are one point, or by a straight segment, a
30//! bridge, where they lie within the cap's `bridge` of each other, nearest
31//! pairs first; runs joined round into a loop make one.
32//!
33//! The loops are capped together, so loops within loops give discs, rings
34//! and discs within rings: each loop, run with the kept side's cap on its
35//! left seen from the side the normal points to, winds once about the
36//! points of the cap and not at all about the rest. The cap is the faces
37//! of the constrained Delaunay triangulation of the loops' points, in the
38//! plane's coordinates, that the loops wind about once, each wound outward
39//! from the kept side. Its corners are the rim vertices themselves, and
40//! every loop edge is an edge of it, so the cap shares each rim edge with
41//! the face beyond it.
42//!
43//! A loop is left open where that cannot hold: where two of its points
44//! land on one point of the plane, where an edge of it crosses another or
45//! runs through a point, or where the cap's side of an edge is not wound
46//! about once and the far side not at all, as where two bodies overlap.
47//! The other loops are then capped again without it.
48//!
49//! [`section`]: crate::section()
50
51use 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/// The side of a plane a cut keeps.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum Side {
61    /// Against the plane's normal.
62    Below,
63    /// The way the plane's normal points, the plane itself included.
64    Above,
65}
66
67/// How a cut is capped.
68#[derive(Debug, Clone, Copy, PartialEq, Default)]
69pub struct Cap {
70    /// The longest straight segment drawn across a gap in the cut's rim to
71    /// close a loop. Zero caps only the loops the rim closes itself.
72    pub bridge: f64,
73}
74
75/// What a cut capped and what it left open.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub struct CutReport {
78    /// The loops capped, each closed by the rim alone or with bridges.
79    pub capped_loops: usize,
80    /// The bridges drawn in the loops capped.
81    pub bridged: usize,
82    /// The stretches of the rim in the plane left open: each loop the rim
83    /// closes that was not capped, and each run of it in the plane not
84    /// closed into a loop that was.
85    pub open: usize,
86}
87
88/// The vertices a cut has made: by the edge cut, its points lower end
89/// first, and by position.
90#[derive(Default)]
91struct Cuts {
92    edges: FastMap<(u32, u32), u32>,
93    at: FastMap<[u64; 3], u32>,
94}
95
96/// A loop to cap: vertices in order, with the cap on the left of each edge
97/// to the next seen from the side the normal points to.
98struct Outline {
99    vertices: Vec<u32>,
100    /// The bridges among its edges.
101    bridges: usize,
102    /// The stretches of rim it joins: one for a loop the rim closes.
103    stretches: usize,
104}
105
106/// The rim in the plane: loops the rim closes, and runs of it with ends.
107/// Each runs with the kept side's cap on its left seen from the side the
108/// normal points to.
109struct Stretches {
110    loops: Vec<Vec<u32>>,
111    runs: Vec<Vec<u32>>,
112}
113
114impl Editable {
115    /// Cut the mesh by `plane`, keeping the faces on side `keep` and the
116    /// kept parts of the faces it crosses, and cap the cut flat when `cap`
117    /// asks. See the [module documentation](self) for how the cut is made
118    /// and capped.
119    ///
120    /// Kept vertices keep their numbers and their [`source_vertex`]; the
121    /// vertices the cut makes, on the edges it crosses, have none. Faces
122    /// removed keep their numbers, unused; the kept parts of faces clipped,
123    /// then the cap's faces, take numbers after every face there is. A mesh
124    /// closed before the cut and with every loop capped is closed after.
125    ///
126    /// [`source_vertex`]: Editable::source_vertex
127    ///
128    /// # Errors
129    ///
130    /// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if the
131    /// plane's origin is not finite or `bridge` is negative or not finite,
132    /// and [`OgeomError::Range`](ogeom_core::OgeomError::Range) if the faces
133    /// and vertices the cut may make would pass `u32` numbering.
134    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        // The cut makes at most a vertex per edge and two faces per face;
144        // the cap at most two faces per vertex. So `F` faces and `V`
145        // vertices become at most `9F + 2V` faces of `27F + 6V` half-edges.
146        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    /// Clip every face to the kept side. Returns, by point, whether the
190    /// point lies on the plane: in it, snapped onto it, or made by the cut.
191    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                    // The shorter diagonal of the convex quadrilateral.
238                    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        // A cut vertex every face about it left degenerate is not used.
273        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    /// Each vertex's place: [`section`]'s exact side, except that a vertex
282    /// the cut snaps onto the plane is in it. That is one within rounding
283    /// of the plane ([`Cutter::near`]), or one from which the crossing of an
284    /// edge lies no farther than the crossing's rounding
285    /// ([`Cutter::crossing_blur`]) and the rounding of the plane's
286    /// coordinates at the two ([`Cutter::blur`]) added: the crossing could
287    /// be the vertex itself, and the crossings of the edges from it could
288    /// land on one point of the plane, or so near one another that rounding
289    /// tangles them, which the cap cannot tell apart. Copies of a point
290    /// share its place.
291    ///
292    /// [`section`]: crate::section()
293    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    /// Whether the face with corners `c`, every one in the plane, faces
337    /// the way the normal points, its material below it.
338    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    /// The vertex the edge from `a` to `b`, its ends on opposite sides, is
345    /// cut at: its upper end where that lies in the plane, and otherwise a
346    /// new vertex at [`Cutter::crossing`], one for the edge and one for
347    /// all the crossings at one position.
348    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    /// A new vertex at `at`, a point of its own with no source.
378    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    /// A new face, joined to nothing until relinked.
390    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    /// The rims' stretches in the plane, `level` marking its points.
399    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        // Rims run with their faces on the left. Seen from the side the
427        // normal points to, the cap of a part above lies on the left of its
428        // rim, and the cap of a part below on the right.
429        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    /// Join the runs' ends into loops: the end of one to the start of
438    /// another where they are one point or lie within `bridge`, nearest
439    /// pairs first and ties to the lower-numbered runs. Returns the loops
440    /// and how many runs no loop takes.
441    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        // Each candidate: its length, whether it is a bridge, and the runs
451        // it joins.
452        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        // Each run has at most one run after it and one before, so the
487        // runs fall into paths and cycles; a cycle is a loop.
488        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                    // The next run starts at this one's end point.
520                    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    /// Cap the outlines together, leaving open each one that cannot be
534    /// capped soundly, and return which were capped.
535    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    /// The cap of the active outlines, each face counter-clockwise seen
580    /// from the side the normal points to, or the outlines to leave open.
581    fn cap_faces(
582        &self,
583        cutter: &Cutter,
584        outlines: &[Outline],
585        active: &[bool],
586    ) -> Result<Vec<[u32; 3]>, Vec<usize>> {
587        // Each point once, with a vertex standing for it.
588        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        // Two points landing on one point of the plane, or on coordinates
610        // the triangulation cannot take, cannot be told apart.
611        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        // Each loop edge as the directed constraint edge it became.
648        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                    // Crossing another loop's edge: nothing was added.
669                    [] => {
670                        bad.push(k);
671                        break;
672                    }
673                    // Through a point, split in the triangulation and no
674                    // longer the rim's edge.
675                    _ => 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        // Counts by face, walked from the unbounded face. The face on an
689        // edge's left counts its weight more than the face on its right.
690        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
746/// Append `v` unless it repeats the last.
747fn 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    /// The area of a regular `n`-gon of circumradius `r`.
777    fn polygon_area(n: u32, r: f64) -> f64 {
778        f64::from(n) / 2.0 * r * r * (TAU / f64::from(n)).sin()
779    }
780
781    /// A closed prism about the `z` axis: a regular `sides`-gon of
782    /// circumradius `radius` from `z = 0` to `z = height`, its wall
783    /// `rings - 1` rows of quads, its ends fans about the axis. Wound
784    /// outward, or inward where not `outward`. Wall row `j`, column `i` is
785    /// faces `2 (j sides + i)` and the one after.
786    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    /// A closed tube about the `z` axis between two radii, two rings high,
818    /// with annular ends.
819    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        // Wall `w` (outer 0, inner 1), end `e` (bottom 0, top 1), column `i`.
830        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    /// A sphere of latitude rings and longitude sides about poles on `z`,
846    /// symmetric across `z = 0` to the bit, with a ring in that plane when
847    /// `bands` is even.
848    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    /// The volume a closed mesh bounds, by the divergence theorem.
882    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    /// The area of the faces lying in `plane`, every corner on it to
894    /// within `1e-12`: cut vertices lie within a few rounding units of it,
895    /// at coordinates of a few units, and every other corner of the meshes
896    /// here at least `1e-3` off it.
897    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    /// Equal to within `1e-12` of the larger: sums of a few hundred terms,
910    /// each rounded by a few units in `2^-53`.
911    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            // The ring in the plane is the rim: nothing new is made.
952            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            // Clear of the plane by far more than rounding: on the kept side.
1025            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        // A prism with one quad of its middle row missing; the cut runs
1091        // through that row, so its rim in the plane stops either side of
1092        // the gap, one side of the polygon long.
1093        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        // What is left open is the gap below the plane, closed by the
1120        // bridge: four edges.
1121        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    /// A solid drum holding a void, holding a solid drum, each clear of
1147    /// the next: radii 3, 2 and 1, from `z = 0`, `0.5` and `1` to `z = 3`,
1148    /// `2.5` and `2`.
1149    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    /// `m` moved by `x` along the `x` axis.
1162    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        // Two drums overlapping, and a third apart from both.
1172        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        // Two square drums whose walls meet along one upright edge, their
1192        // corners on the axes exactly. Welded, the edge is one; apart, the
1193        // two edges are cut at one position, one point.
1194        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        // An octahedron whose waist stands `1e-20` above `z = 0`: every
1220        // edge from a pole to the waist crosses the plane within rounding
1221        // of the waist.
1222        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        // A cylinder's wall with no ends.
1250        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        // The top faces up: below keeps the body and caps it afresh, above
1293        // keeps nothing.
1294        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        // The bottom faces down: above keeps the whole, unchanged, below
1302        // nothing.
1303        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    /// Planes through or a rounding from vertices, each of which once left
1339    /// a part open: through a ring a rounding off an axis plane, through a
1340    /// vertex others round onto, through two whose crossings land within a
1341    /// rounding of them, of the plane's coordinates or of the crossing's
1342    /// own, and along a drum's flat side.
1343    #[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        /// Any plane, through a vertex or not, cuts a closed mesh, a torus,
1378        /// a sphere or drums nested in a drum, into two closed parts, capped
1379        /// without a loop left open, whose volumes add up to the whole.
1380        #[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            // Both parts clip each face at the same vertices and cap the
1414            // same loops, so they split the whole exactly but for rounding.
1415            prop_assert!(close(total, all), "{} against {}", total, all);
1416        }
1417    }
1418}