Skip to main content

fast_mesh_query/
slice.rs

1//! Cutting a mesh with a plane.
2//!
3//! A vertex is on the plane's `+` side when its signed distance is zero or
4//! more, so no vertex lies on the plane and each triangle with vertices on
5//! both sides gives exactly one segment, between crossings strictly inside two
6//! of its edges. Crossings are named by their edge key `(min id, max id)`.
7
8use fast_mesh_bvh::{Aabb, Bvh, NodeKind};
9use fast_mesh_store::{Mesh, TriangleId, VertexId};
10
11use crate::plane::{Plane, dot};
12use crate::{Appender, QueryError, Result, allocation_allowed, check};
13
14/// The piece of the cut inside one triangle, from the crossing on edge `from`
15/// to the crossing on edge `to`.
16///
17/// It is oriented by the triangle's winding: walking from `from` to `to`, seen
18/// from the plane normal, the triangle's outward side (the one its normal
19/// points to) is on the right. For a closed, outward-wound mesh this makes
20/// outer contours counter-clockwise and holes clockwise in the plane's frame,
21/// and the `to` of one segment is the `from` of the next.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub(crate) struct Segment {
24    pub(crate) tri: TriangleId,
25    pub(crate) from: (VertexId, VertexId),
26    pub(crate) to: (VertexId, VertexId),
27}
28
29fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
30    (a.min(b), a.max(b))
31}
32
33fn position(mesh: &Mesh, v: VertexId) -> [f64; 3] {
34    mesh.positions()[v as usize].map(f64::from)
35}
36
37/// The point where `plane` crosses the edge `key`, computed from the edge's
38/// endpoints in id order so that it is the same whichever triangle asks.
39///
40/// The endpoints must be on opposite sides. For an edge with both endpoints
41/// on the same side the result is meaningless, possibly NaN.
42pub(crate) fn crossing(mesh: &Mesh, plane: &Plane, key: (VertexId, VertexId)) -> [f64; 3] {
43    let (a, b) = (
44        position(mesh, key.0.min(key.1)),
45        position(mesh, key.0.max(key.1)),
46    );
47    let (sa, sb) = (plane.signed_distance(a), plane.signed_distance(b));
48    debug_assert!((sa >= 0.0) != (sb >= 0.0), "edge {key:?} is not crossed");
49    let t = sa / (sa - sb);
50    std::array::from_fn(|k| a[k] + t * (b[k] - a[k]))
51}
52
53/// The segment of triangle `tri`, if the plane crosses it.
54fn segment(mesh: &Mesh, plane: &Plane, tri: TriangleId) -> Option<Segment> {
55    let t = mesh.triangles()[tri as usize];
56    let plus = t.map(|v| plane.signed_distance(position(mesh, v)) >= 0.0);
57    // The lone vertex, whose side differs from the other two.
58    let i = if plus[0] == plus[1] {
59        if plus[1] == plus[2] {
60            return None;
61        }
62        2
63    } else if plus[0] == plus[2] {
64        1
65    } else {
66        0
67    };
68    // In winding order (a, b, c) from the lone vertex, the crossed edges are
69    // (a, b) and (c, a). With `a` on the `+` side the cut runs from (a, b) to
70    // (c, a), keeping the outward side on the right.
71    let (a, b, c) = (t[i], t[(i + 1) % 3], t[(i + 2) % 3]);
72    let (ab, ca) = (key(a, b), key(c, a));
73    let (from, to) = if plus[i] { (ab, ca) } else { (ca, ab) };
74    Some(Segment { tri, from, to })
75}
76
77/// Whether the plane may cross a triangle inside `b`: the signed distances
78/// of the box corners, padded for rounding, take both signs.
79fn straddles(plane: &Plane, b: &Aabb) -> bool {
80    let mut lo = f64::INFINITY;
81    let mut hi = f64::NEG_INFINITY;
82    let mut magnitude = 0.0f64;
83    for i in 0..8 {
84        let p: [f64; 3] =
85            std::array::from_fn(|k| f64::from(if i >> k & 1 == 1 { b.max[k] } else { b.min[k] }));
86        let s = plane.signed_distance(p);
87        lo = lo.min(s);
88        hi = hi.max(s);
89        magnitude = magnitude.max(p.iter().fold(0.0, |m, c| m.max(c.abs())));
90    }
91    let offset = dot(plane.normal(), plane.origin()).abs();
92    let pad = 8.0 * f64::from(f32::EPSILON) * (magnitude + offset);
93    lo <= pad && hi >= -pad
94}
95
96/// Appends to `out` the segment of every alive triangle the plane crosses, in
97/// an unspecified order. Elements already in `out` are kept.
98///
99/// # Errors
100/// [`QueryError::MeshMismatch`](crate::QueryError::MeshMismatch) or
101/// [`QueryError::StaleBvh`](crate::QueryError::StaleBvh) when `bvh` does not
102/// match the current `mesh`, and
103/// [`QueryError::OutOfMemory`](crate::QueryError::OutOfMemory) when `out`
104/// cannot grow. On any error `out` keeps its original length.
105pub(crate) fn segments(
106    mesh: &Mesh,
107    bvh: &Bvh,
108    plane: &Plane,
109    out: &mut Vec<Segment>,
110) -> Result<()> {
111    check(mesh, bvh)?;
112    let mut ap = Appender::new(out);
113    let mut failed = None;
114    bvh.traverse(|_, node| {
115        if failed.is_some() || !straddles(plane, &node.bounds) {
116            return false;
117        }
118        if let NodeKind::Leaf { triangles } = node.kind {
119            for &id in triangles {
120                if let Some(s) = segment(mesh, plane, id)
121                    && let Err(e) = ap.push(s)
122                {
123                    failed = Some(e);
124                    return false;
125                }
126            }
127        }
128        true
129    });
130    failed.map_or(Ok(()), Err)
131}
132
133/// The cut of a mesh by a plane, as contours.
134#[derive(Debug, Clone, Default, PartialEq)]
135pub struct Slice {
136    /// The contours, sorted by their first edge key.
137    pub contours: Vec<Contour>,
138}
139
140/// One chain of the cut: a closed loop or an open polyline.
141#[derive(Debug, Clone, Default, PartialEq)]
142pub struct Contour {
143    /// The crossing point of each edge in `edges`, in the mesh's local
144    /// coordinates (the `f32` positions widened to `f64`, without the mesh
145    /// origin), not in the plane's `(u, v)` frame: see
146    /// [`Plane::to_uv`](crate::Plane::to_uv).
147    pub points: Vec<[f64; 3]>,
148    /// The crossed edge `(min id, max id)` of each point.
149    pub edges: Vec<(VertexId, VertexId)>,
150    /// The triangle of each segment: `triangles[i]` joins point `i` to point
151    /// `i + 1`, and the last one of a closed contour joins the last point to
152    /// the first. One fewer than the points when open, as many when closed.
153    pub triangles: Vec<TriangleId>,
154    /// Whether the last point joins back to the first.
155    pub closed: bool,
156    /// Whether every segment runs the same way along the contour, so that
157    /// the triangles agree on their winding.
158    pub oriented: bool,
159    /// Whether no end of the contour is a split at a non-manifold edge.
160    pub manifold: bool,
161}
162
163/// An end of a segment that no other end joins: the edge is a boundary.
164const BOUNDARY: usize = usize::MAX;
165/// An end of a segment at an edge crossed by more than two triangles.
166const SPLIT: usize = usize::MAX - 1;
167
168/// Cuts `mesh` with `plane` and writes the contours to `out`, replacing what
169/// it held and reusing its allocations. The plane and the points are in the
170/// mesh's stored local coordinates (the `f32` positions widened to `f64`,
171/// without the mesh origin).
172///
173/// The triangles' segments are joined at shared crossed edges:
174/// - An edge crossed by one triangle ends an open contour.
175/// - An edge crossed by more than two triangles splits the cut: every piece
176///   ending there is open, with `manifold == false`.
177/// - Where two triangles disagree on their winding the pieces still join,
178///   and the contour has `oriented == false`.
179///
180/// On a closed, outward-wound mesh the contours are closed, oriented loops,
181/// with the solid's inside on their left seen from the plane normal: outer
182/// loops run counter-clockwise in `(u, v)` and holes clockwise. An oriented
183/// open contour runs the same way. A plane through vertices gives
184/// coincident points joined by zero-length segments; they are kept.
185///
186/// A face lying exactly in the plane is cut or not depending on the sign of
187/// the normal, because its vertices are all on the `+` side (`s ≥ 0`): a
188/// cube cut at its top face with the normal `+z` gives the square of that
189/// face (the side faces cross from `−` to `+` there), while with `−z` all of
190/// the cube is on the `+` side and nothing is cut. Offset the plane slightly
191/// to choose.
192///
193/// A zero-area triangle `[a, a, b]` crossed between `a` and `b` gives a
194/// segment from edge `(a, b)` back to itself. It has no length, so it is
195/// dropped rather than counted as a third triangle at that edge.
196///
197/// The order is deterministic: a closed contour starts at its smallest edge
198/// key, an open one at its first end (its `from` end when oriented), and
199/// the contours are sorted by their first edge key.
200///
201/// # Errors
202/// [`QueryError::MeshMismatch`](crate::QueryError::MeshMismatch) or
203/// [`QueryError::StaleBvh`](crate::QueryError::StaleBvh) when `bvh` does not
204/// match the current `mesh`, and
205/// [`QueryError::OutOfMemory`](crate::QueryError::OutOfMemory) when an
206/// allocation fails. On any error `out` is left empty.
207pub fn slice(mesh: &Mesh, bvh: &Bvh, plane: &Plane, out: &mut Slice) -> Result<()> {
208    let r = chain(mesh, bvh, plane, &mut out.contours);
209    if r.is_err() {
210        out.contours.clear();
211    }
212    r
213}
214
215fn chain(mesh: &Mesh, bvh: &Bvh, plane: &Plane, out: &mut Vec<Contour>) -> Result<()> {
216    let mut segs = Vec::new();
217    segments(mesh, bvh, plane, &mut segs)?;
218    segs.retain(|s| s.from != s.to);
219    // By triangle, so that the result does not depend on the BVH.
220    segs.sort_unstable();
221
222    // The segment ends (see `end_key`). Sorted by edge key, with `from`s
223    // first, the ends at each crossed edge are a run.
224    let ends_len = 2 * segs.len();
225    let key_of = |e| end_key(&segs, e);
226    let mut ends = Vec::new();
227    reserve(&mut ends, ends_len)?;
228    ends.extend(0..ends_len);
229    ends.sort_unstable_by_key(|&e| (key_of(e), e & 1, e));
230
231    // The end each end joins, or why it joins none.
232    let mut link = Vec::new();
233    reserve(&mut link, ends_len)?;
234    link.resize(ends_len, BOUNDARY);
235    for run in ends.chunk_by(|&a, &b| key_of(a) == key_of(b)) {
236        match *run {
237            [_] => {}
238            [a, b] => {
239                link[a] = b;
240                link[b] = a;
241            }
242            _ => run.iter().for_each(|&e| link[e] = SPLIT),
243        }
244    }
245
246    let mut used = Vec::new();
247    reserve(&mut used, segs.len())?;
248    used.resize(segs.len(), false);
249    let mut count = 0;
250    // Open contours first, from their ends in ascending key order, then the
251    // closed loops, each from its smallest key: a loop's segments are all
252    // unused when the scan reaches that key, and its `from` end comes first.
253    for open in [true, false] {
254        for &start in &ends {
255            if used[start / 2] || (open && link[start] < SPLIT) {
256                continue;
257            }
258            if count == out.len() {
259                push(out, Contour::default())?;
260            }
261            let c = &mut out[count];
262            count += 1;
263            c.points.clear();
264            c.edges.clear();
265            c.triangles.clear();
266            walk(&segs, &link, &mut used, start, c)?;
267            reserve(&mut c.points, c.edges.len())?;
268            c.points
269                .extend(c.edges.iter().map(|&e| crossing(mesh, plane, e)));
270        }
271    }
272    out.truncate(count);
273    // A triangle is in one contour only, so the order is total.
274    out.sort_unstable_by_key(|c| (c.edges[0], c.triangles[0]));
275    Ok(())
276}
277
278/// Walks the chain entered at end `start`, filling `c`'s edges, triangles
279/// and flags and marking its segments used.
280fn walk(
281    segs: &[Segment],
282    link: &[usize],
283    used: &mut [bool],
284    start: usize,
285    c: &mut Contour,
286) -> Result<()> {
287    let key_of = |e| end_key(segs, e);
288    c.oriented = true;
289    let mut entry = start;
290    loop {
291        let s = entry / 2;
292        used[s] = true;
293        push(&mut c.edges, key_of(entry))?;
294        push(&mut c.triangles, segs[s].tri)?;
295        // Oriented while every segment is entered by the same kind of end.
296        c.oriented &= entry & 1 == start & 1;
297        let exit = entry ^ 1;
298        match link[exit] {
299            next if next == start => {
300                c.closed = true;
301                c.manifold = true;
302                return Ok(());
303            }
304            next if next >= SPLIT => {
305                push(&mut c.edges, key_of(exit))?;
306                c.closed = false;
307                c.manifold = link[start] != SPLIT && next != SPLIT;
308                if c.oriented && start & 1 == 1 {
309                    // Walked against the segments: turn it to run along them.
310                    c.edges.reverse();
311                    c.triangles.reverse();
312                }
313                return Ok(());
314            }
315            next => entry = next,
316        }
317    }
318}
319
320/// The edge key at end `e`: end `2i` is the `from` of segment `i`, end
321/// `2i + 1` its `to`.
322fn end_key(segs: &[Segment], e: usize) -> (VertexId, VertexId) {
323    let s = &segs[e / 2];
324    if e & 1 == 0 { s.from } else { s.to }
325}
326
327/// Reserves room for `additional` more elements, failing with `OutOfMemory`.
328fn reserve<T>(v: &mut Vec<T>, additional: usize) -> Result<()> {
329    allocation_allowed()?;
330    v.try_reserve(additional)
331        .map_err(|_| QueryError::OutOfMemory)
332}
333
334fn push<T>(v: &mut Vec<T>, value: T) -> Result<()> {
335    reserve(v, 1)?;
336    v.push(value);
337    Ok(())
338}
339
340/// [`segments`] by testing every alive triangle, for the tests.
341#[cfg(test)]
342pub(crate) fn segments_brute(mesh: &Mesh, plane: &Plane, out: &mut Vec<Segment>) {
343    out.extend(
344        (0..mesh.triangle_slots() as TriangleId)
345            .filter(|&id| mesh.is_triangle_alive(id))
346            .filter_map(|id| segment(mesh, plane, id)),
347    );
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::plane::{cross, dot};
354    use crate::test_util::{
355        Lcg, box_mesh, build, fin, random_normal, signed_volume6, torus, uv_sphere, wavy,
356        with_degenerates,
357    };
358    use crate::{QueryError, fail_allocations_after};
359    use fast_mesh_bvh::Quality;
360    use std::collections::HashMap;
361
362    fn sorted(mut s: Vec<Segment>) -> Vec<Segment> {
363        s.sort_unstable();
364        s
365    }
366
367    fn brute(mesh: &Mesh, plane: &Plane) -> Vec<Segment> {
368        let mut out = Vec::new();
369        segments_brute(mesh, plane, &mut out);
370        sorted(out)
371    }
372
373    fn fast(mesh: &Mesh, bvh: &Bvh, plane: &Plane) -> Vec<Segment> {
374        let mut out = Vec::new();
375        segments(mesh, bvh, plane, &mut out).unwrap();
376        sorted(out)
377    }
378
379    /// 200 random planes through the mesh's bounding box, 50 through a random
380    /// vertex and 50 containing a random edge.
381    fn planes(mesh: &Mesh, seed: u64) -> Vec<Plane> {
382        let mut rng = Lcg(seed);
383        let mut lo = [f64::INFINITY; 3];
384        let mut hi = [f64::NEG_INFINITY; 3];
385        for p in mesh.positions() {
386            for k in 0..3 {
387                lo[k] = lo[k].min(f64::from(p[k]));
388                hi[k] = hi[k].max(f64::from(p[k]));
389            }
390        }
391        let mut out = Vec::new();
392        for _ in 0..200 {
393            let o = std::array::from_fn(|k| lo[k] + f64::from(rng.next()) * (hi[k] - lo[k]));
394            out.push(Plane::new(o, random_normal(&mut rng)).unwrap());
395        }
396        let tris = mesh.triangles();
397        let pick = |rng: &mut Lcg| tris[(rng.next() * tris.len() as f32) as usize % tris.len()];
398        for _ in 0..50 {
399            let t = pick(&mut rng);
400            let o = position(mesh, t[(rng.next() * 3.0) as usize % 3]);
401            out.push(Plane::new(o, random_normal(&mut rng)).unwrap());
402        }
403        for _ in 0..50 {
404            let t = pick(&mut rng);
405            let (a, b) = (position(mesh, t[0]), position(mesh, t[1]));
406            let e = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
407            let n = cross(e, random_normal(&mut rng));
408            out.push(Plane::new(a, n).unwrap());
409        }
410        out
411    }
412
413    #[test]
414    fn fixtures_are_closed_and_outward() {
415        for mesh in [
416            uv_sphere(32, 64, 1.0),
417            torus(2.0, 0.5, 48, 24),
418            box_mesh([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]),
419        ] {
420            assert!(signed_volume6(&mesh) > 0.0);
421            // Every edge is used once in each direction.
422            let mut directed = HashMap::new();
423            for t in mesh.triangles() {
424                for i in 0..3 {
425                    *directed.entry((t[i], t[(i + 1) % 3])).or_insert(0) += 1;
426                }
427            }
428            assert!(
429                directed
430                    .iter()
431                    .all(|(&(a, b), &n)| n == 1 && directed.get(&(b, a)) == Some(&1))
432            );
433        }
434        assert_eq!(box_mesh([0.0; 3], [1.0; 3]).triangles().len(), 12);
435        assert_eq!(fin().triangles().len(), 6);
436        let d = with_degenerates(box_mesh([0.0; 3], [1.0; 3]));
437        assert_eq!(d.triangles().len(), 12 + 3 * 3);
438        assert_eq!(
439            signed_volume6(&d),
440            signed_volume6(&box_mesh([0.0; 3], [1.0; 3]))
441        );
442    }
443
444    #[test]
445    fn segments_match_brute_force() {
446        let meshes = [wavy(40), uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)];
447        for (i, mesh) in meshes.iter().enumerate() {
448            for quality in [Quality::Sah, Quality::Fast] {
449                let bvh = build(mesh, quality);
450                let mut nonempty = 0;
451                for plane in planes(mesh, 0x5e9 + i as u64) {
452                    let expected = brute(mesh, &plane);
453                    assert_eq!(fast(mesh, &bvh, &plane), expected, "{plane:?}");
454                    nonempty += usize::from(!expected.is_empty());
455                }
456                assert!(nonempty > 150, "only {nonempty} planes cut mesh {i}");
457            }
458        }
459    }
460
461    #[test]
462    fn segment_orientation_rule() {
463        // Each segment runs along `n × N` (N the triangle's winding normal),
464        // which puts the solid's inside on the left seen from `n`, whichever
465        // way the triangle faces.
466        let mesh = uv_sphere(32, 64, 1.0);
467        let bvh = build(&mesh, Quality::Sah);
468        let cases = [
469            ([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]),
470            ([0.0, 0.0, -0.3], [0.3, -0.2, 0.9]),
471            ([0.0, 0.0, -0.3], [-0.3, 0.2, -0.9]),
472        ];
473        let mut facing_away = 0;
474        for (origin, normal) in cases {
475            let plane = Plane::new(origin, normal).unwrap();
476            let found = fast(&mesh, &bvh, &plane);
477            assert!(found.len() > 100);
478            for s in &found {
479                let from = plane.to_uv(crossing(&mesh, &plane, s.from));
480                let to = plane.to_uv(crossing(&mesh, &plane, s.to));
481                let [a, b, c] = mesh.triangles()[s.tri as usize].map(|v| position(&mesh, v));
482                let tn = cross(
483                    std::array::from_fn(|k| b[k] - a[k]),
484                    std::array::from_fn(|k| c[k] - a[k]),
485                );
486                let n = plane.normal();
487                facing_away += usize::from(dot(n, tn) < 0.0);
488                let w = cross(n, tn);
489                let w = [dot(w, plane.u()), dot(w, plane.v())];
490                let d = [to[0] - from[0], to[1] - from[1]];
491                assert!(d[0] * w[0] + d[1] * w[1] > 0.0, "{plane:?} {s:?}");
492            }
493        }
494        assert!(facing_away > 50, "{facing_away}");
495    }
496
497    #[test]
498    fn outer_contours_wind_counter_clockwise() {
499        // Twice the signed area of the cut, summed over the segments, is
500        // positive from either side of the plane for a closed outward mesh.
501        let meshes = [
502            uv_sphere(32, 64, 1.0),
503            box_mesh([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]),
504            torus(2.0, 0.5, 48, 24),
505        ];
506        let normals = [[0.0, 0.0, 1.0], [0.3, -0.2, 0.9], [1.0, 0.0, 0.0]];
507        for mesh in &meshes {
508            let bvh = build(mesh, Quality::Sah);
509            for z in [-0.3, 0.0, 0.2] {
510                for n in normals {
511                    for n in [n, n.map(|c: f64| -c)] {
512                        let plane = Plane::new([0.0, 0.0, z], n).unwrap();
513                        let area2: f64 = fast(mesh, &bvh, &plane)
514                            .iter()
515                            .map(|s| {
516                                let a = plane.to_uv(crossing(mesh, &plane, s.from));
517                                let b = plane.to_uv(crossing(mesh, &plane, s.to));
518                                a[0] * b[1] - a[1] * b[0]
519                            })
520                            .sum();
521                        assert!(area2 > 0.0, "{plane:?}: {area2}");
522                    }
523                }
524            }
525        }
526    }
527
528    #[test]
529    fn segments_chain_head_to_tail_on_closed_meshes() {
530        // On a closed, consistently wound mesh every crossed edge key ends one
531        // segment and starts another.
532        for mesh in [uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)] {
533            let bvh = build(&mesh, Quality::Sah);
534            for plane in planes(&mesh, 0xc4a1) {
535                let found = fast(&mesh, &bvh, &plane);
536                let mut balance: HashMap<(VertexId, VertexId), i32> = HashMap::new();
537                for s in &found {
538                    *balance.entry(s.from).or_default() += 1;
539                    *balance.entry(s.to).or_default() -= 1;
540                }
541                assert!(balance.values().all(|&b| b == 0), "{plane:?}");
542                assert_eq!(balance.len(), found.len(), "{plane:?}");
543            }
544        }
545    }
546
547    #[test]
548    fn crossing_is_symmetric_in_triangle_order() {
549        for mesh in [uv_sphere(32, 64, 1.0), torus(2.0, 0.5, 48, 24)] {
550            let bvh = build(&mesh, Quality::Sah);
551            for plane in planes(&mesh, 0x7b2) {
552                // The bits of each edge's crossing, and how many segment ends
553                // (one per incident triangle) produced it.
554                let mut seen: HashMap<(VertexId, VertexId), ([u64; 3], u32)> = HashMap::new();
555                for s in fast(&mesh, &bvh, &plane) {
556                    for key in [s.from, s.to] {
557                        assert!(key.0 < key.1);
558                        let p = crossing(&mesh, &plane, key);
559                        let bits = p.map(f64::to_bits);
560                        assert_eq!(
561                            crossing(&mesh, &plane, (key.1, key.0)).map(f64::to_bits),
562                            bits
563                        );
564                        let entry = seen.entry(key).or_insert((bits, 0));
565                        assert_eq!(entry.0, bits, "{key:?}");
566                        entry.1 += 1;
567                        // The point lies on the edge, between its endpoints
568                        // up to rounding.
569                        let (a, b) = (position(&mesh, key.0), position(&mesh, key.1));
570                        let tol = 1e-12;
571                        assert!(
572                            (0..3)
573                                .all(|k| a[k].min(b[k]) - tol <= p[k]
574                                    && p[k] <= a[k].max(b[k]) + tol)
575                        );
576                        assert!(plane.signed_distance(p).abs() < 1e-12);
577                    }
578                }
579                assert!(seen.values().all(|&(_, n)| n == 2), "{plane:?}");
580            }
581        }
582    }
583
584    #[test]
585    fn vertex_on_plane_counts_as_plus() {
586        // The plane z = 0 passes through vertices 0, 1, 2 and 3 of the box
587        // [0, 1]^3, which are all `+`: no segment.
588        let mesh = box_mesh([0.0; 3], [1.0; 3]);
589        let bvh = build(&mesh, Quality::Sah);
590        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
591        assert!(fast(&mesh, &bvh, &plane).is_empty());
592        // Flipped, the bottom corners are still `+` and the top ones `-`.
593        let plane = Plane::new([0.0; 3], [0.0, 0.0, -1.0]).unwrap();
594        let found = fast(&mesh, &bvh, &plane);
595        assert_eq!(found.len(), 8);
596        for s in found {
597            for key in [s.from, s.to] {
598                assert_eq!(crossing(&mesh, &plane, key)[2], 0.0);
599            }
600        }
601    }
602
603    #[test]
604    fn degenerate_and_non_manifold_triangles_do_not_panic() {
605        let plane = Plane::new([0.1, 0.2, 0.5], [0.1, 0.2, 1.0]).unwrap();
606        for mesh in [with_degenerates(uv_sphere(8, 16, 1.0)), fin()] {
607            let bvh = build(&mesh, Quality::Sah);
608            assert_eq!(fast(&mesh, &bvh, &plane), brute(&mesh, &plane));
609        }
610        // A triangle [a, a, b] crossed between a and b gives a segment from
611        // the edge back to itself; [a, a, a] gives none.
612        let p = [0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
613        let mesh = Mesh::from_buffers(&p, &[0, 0, 1, 1, 1, 1]).unwrap();
614        let bvh = build(&mesh, Quality::Sah);
615        let found = fast(
616            &mesh,
617            &bvh,
618            &Plane::new([0.0; 3], [0.0, 0.0, -1.0]).unwrap(),
619        );
620        assert_eq!(
621            found,
622            vec![Segment {
623                tri: 0,
624                from: (0, 1),
625                to: (0, 1)
626            }]
627        );
628        // The fin's shared edge is crossed by all three quads.
629        let mesh = fin();
630        let bvh = build(&mesh, Quality::Sah);
631        let found = fast(
632            &mesh,
633            &bvh,
634            &Plane::new([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]).unwrap(),
635        );
636        assert_eq!(found.len(), 6);
637        let shared = found
638            .iter()
639            .filter(|s| s.from == (0, 1) || s.to == (0, 1))
640            .count();
641        assert_eq!(shared, 3);
642    }
643
644    #[test]
645    fn removed_triangles_are_skipped() {
646        let mut mesh = uv_sphere(8, 16, 1.0);
647        let mut bvh = build(&mesh, Quality::Sah);
648        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
649        let before = fast(&mesh, &bvh, &plane);
650        let gone = before[0].tri;
651        let mut tx = mesh.begin_edit();
652        tx.remove_triangles(&[gone]).unwrap();
653        let p = tx.commit().unwrap();
654        bvh.update(&mut mesh, p.changes()).unwrap();
655        let after = fast(&mesh, &bvh, &plane);
656        assert_eq!(after, brute(&mesh, &plane));
657        assert_eq!(after.len(), before.len() - 1);
658        assert!(after.iter().all(|s| s.tri != gone));
659    }
660
661    #[test]
662    fn stale_mismatch_and_oom() {
663        let mut mesh = uv_sphere(8, 16, 1.0);
664        let other = uv_sphere(8, 16, 1.0);
665        let bvh = build(&mesh, Quality::Sah);
666        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
667        let mut out = vec![Segment {
668            tri: 9,
669            from: (1, 2),
670            to: (3, 4),
671        }];
672        let keep = out.clone();
673        assert_eq!(
674            segments(&other, &bvh, &plane, &mut out),
675            Err(QueryError::MeshMismatch)
676        );
677        fail_allocations_after(Some(3));
678        let r = segments(&mesh, &bvh, &plane, &mut out);
679        fail_allocations_after(None);
680        assert_eq!(r, Err(QueryError::OutOfMemory));
681        assert_eq!(out, keep);
682        // Results are appended after what `out` held.
683        segments(&mesh, &bvh, &plane, &mut out).unwrap();
684        assert_eq!(out[0], keep[0]);
685        assert_eq!(sorted(out[1..].to_vec()), brute(&mesh, &plane));
686        let mut tx = mesh.begin_edit();
687        tx.set_positions(&[0], &[[0.0, 0.0, 2.0]]).unwrap();
688        tx.commit().unwrap();
689        assert_eq!(
690            segments(&mesh, &bvh, &plane, &mut out),
691            Err(QueryError::StaleBvh)
692        );
693    }
694
695    fn cut(mesh: &Mesh, plane: &Plane) -> Slice {
696        let bvh = build(mesh, Quality::Sah);
697        let mut out = Slice::default();
698        slice(mesh, &bvh, plane, &mut out).unwrap();
699        for c in &out.contours {
700            check_contour(mesh, plane, c);
701        }
702        out
703    }
704
705    /// The arrays are parallel and every point is its edge's crossing.
706    fn check_contour(mesh: &Mesh, plane: &Plane, c: &Contour) {
707        assert_eq!(c.points.len(), c.edges.len());
708        let segments = c.points.len() - usize::from(!c.closed);
709        assert_eq!(c.triangles.len(), segments);
710        assert!(!c.triangles.is_empty());
711        for (p, &e) in c.points.iter().zip(&c.edges) {
712            assert!(e.0 < e.1);
713            assert_eq!(
714                p.map(f64::to_bits),
715                crossing(mesh, plane, e).map(f64::to_bits)
716            );
717        }
718        // Each triangle's segment joins its two points; when oriented, it
719        // runs from the first to the second.
720        let n = c.edges.len();
721        for (i, &t) in c.triangles.iter().enumerate() {
722            let s = segment(mesh, plane, t).unwrap();
723            let (a, b) = (c.edges[i], c.edges[(i + 1) % n]);
724            if c.oriented {
725                assert_eq!((s.from, s.to), (a, b), "{c:?}");
726            } else {
727                assert!((s.from, s.to) == (a, b) || (s.from, s.to) == (b, a));
728            }
729        }
730    }
731
732    /// The signed area of a closed contour in the plane's `(u, v)` frame.
733    fn area(plane: &Plane, c: &Contour) -> f64 {
734        let q: Vec<[f64; 2]> = c.points.iter().map(|&p| plane.to_uv(p)).collect();
735        (0..q.len())
736            .map(|i| {
737                let (a, b) = (q[i], q[(i + 1) % q.len()]);
738                a[0] * b[1] - a[1] * b[0]
739            })
740            .sum::<f64>()
741            / 2.0
742    }
743
744    fn close(value: f64, expected: f64, rel: f64) -> bool {
745        (value - expected).abs() <= rel * expected.abs()
746    }
747
748    #[test]
749    fn sphere_cut_is_one_closed_ccw_loop() {
750        use std::f64::consts::PI;
751        let mesh = uv_sphere(64, 128, 1.0);
752        let plane = Plane::new([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]).unwrap();
753        let out = cut(&mesh, &plane);
754        assert_eq!(out.contours.len(), 1);
755        let c = &out.contours[0];
756        assert!(c.closed && c.oriented && c.manifold);
757        let a = area(&plane, c);
758        assert!(a > 0.0 && close(a, PI * (1.0 - 0.09), 0.01), "{a}");
759    }
760
761    #[test]
762    fn torus_horizontal_cut_is_outer_and_hole() {
763        use std::f64::consts::PI;
764        let mesh = torus(2.0, 0.5, 96, 48);
765        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
766        let out = cut(&mesh, &plane);
767        assert_eq!(out.contours.len(), 2);
768        let mut areas: Vec<f64> = out
769            .contours
770            .iter()
771            .map(|c| {
772                assert!(c.closed && c.oriented && c.manifold);
773                area(&plane, c)
774            })
775            .collect();
776        areas.sort_by(f64::total_cmp);
777        assert!(close(areas[0], -PI * 1.5 * 1.5, 0.01), "{areas:?}");
778        assert!(close(areas[1], PI * 2.5 * 2.5, 0.01), "{areas:?}");
779    }
780
781    #[test]
782    fn cube_face_flush_plane_is_one_closed_loop() {
783        let mesh = box_mesh([-1.0; 3], [1.0; 3]);
784        for z in [1.0, -1.0] {
785            for n in [1.0, -1.0] {
786                let out = cut(&mesh, &Plane::new([0.0, 0.0, z], [0.0, 0.0, n]).unwrap());
787                assert!(out.contours.len() <= 1, "z = {z}, n = {n}");
788                assert!(out.contours.iter().all(|c| c.closed && c.manifold));
789            }
790        }
791        let out = cut(&mesh, &Plane::new([0.0; 3], [1.0, 0.0, 0.0]).unwrap());
792        assert_eq!(out.contours.len(), 1);
793        let c = &out.contours[0];
794        assert!(c.closed && c.oriented && c.manifold);
795        assert_eq!(c.points.len(), 8);
796    }
797
798    #[test]
799    fn terrain_grid_row_plane_is_open_chains() {
800        let mesh = wavy(20);
801        let mut incident: HashMap<(VertexId, VertexId), usize> = HashMap::new();
802        for t in mesh.triangles() {
803            for i in 0..3 {
804                *incident.entry(key(t[i], t[(i + 1) % 3])).or_default() += 1;
805            }
806        }
807        // Both normals, so that some chain is walked from its `to` end and
808        // turned around.
809        for normal in [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]] {
810            let plane = Plane::new([0.0, 7.0, 0.0], normal).unwrap();
811            let out = cut(&mesh, &plane);
812            assert!(!out.contours.is_empty());
813            for c in &out.contours {
814                assert!(!c.closed && c.oriented && c.manifold);
815                assert_eq!(incident[&c.edges[0]], 1);
816                assert_eq!(incident[c.edges.last().unwrap()], 1);
817            }
818            // Every segment is used exactly once.
819            let mut used: Vec<TriangleId> = out
820                .contours
821                .iter()
822                .flat_map(|c| c.triangles.iter().copied())
823                .collect();
824            used.sort_unstable();
825            let expected: Vec<TriangleId> = brute(&mesh, &plane).iter().map(|s| s.tri).collect();
826            assert_eq!(used, expected);
827        }
828    }
829
830    #[test]
831    fn nonmanifold_fin_splits_chains() {
832        let out = cut(
833            &fin(),
834            &Plane::new([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]).unwrap(),
835        );
836        assert!(out.contours.len() >= 3);
837        for c in &out.contours {
838            assert!(!c.closed && !c.manifold);
839            // Each piece ends at the shared edge.
840            assert!(c.edges[0] == (0, 1) || *c.edges.last().unwrap() == (0, 1));
841        }
842    }
843
844    #[test]
845    fn degenerate_triangles_do_not_break_chains() {
846        let mesh = with_degenerates(uv_sphere(16, 32, 1.0));
847        let mut rng = Lcg(0xde9e);
848        for _ in 0..50 {
849            let plane = Plane::new([0.0; 3], random_normal(&mut rng)).unwrap();
850            let out = cut(&mesh, &plane);
851            assert_eq!(out.contours.len(), 1, "{plane:?}");
852            let c = &out.contours[0];
853            assert!(c.closed && c.oriented && c.manifold, "{plane:?}");
854        }
855    }
856
857    #[test]
858    fn collinear_sliver_with_distinct_ids_crossing_plane() {
859        // A box plus one zero-area triangle: three distinct, collinear
860        // vertices straddling the plane. The sliver's two crossings are
861        // both where its line meets the plane, so it forms its own open
862        // chain of two near-coincident points, leaving the box loop
863        // exactly as cut without it.
864        let flat = box_mesh([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
865        let n = flat.positions().len() as u32;
866        let positions: Vec<f32> = flat
867            .positions()
868            .iter()
869            .flatten()
870            .copied()
871            .chain([2.0, 0.0, -1.0, 2.0, 0.0, 0.5, 2.0, 0.0, 1.0])
872            .collect();
873        let mut indices: Vec<u32> = flat.triangles().iter().flatten().copied().collect();
874        indices.extend([n, n + 1, n + 2]);
875        let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
876        let plane = Plane::new([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]).unwrap();
877        let out = cut(&mesh, &plane);
878        assert_eq!(out.contours.len(), 2);
879        let bare = cut(&flat, &plane);
880        assert_eq!(bare.contours.len(), 1);
881        let closed = out.contours.iter().find(|c| c.closed).unwrap();
882        assert_eq!(closed, &bare.contours[0]);
883        let sliver = out.contours.iter().find(|c| !c.closed).unwrap();
884        assert_eq!(sliver.triangles, [flat.triangle_slots() as TriangleId]);
885        assert_eq!(sliver.points.len(), 2);
886        for p in &sliver.points {
887            assert!(
888                (p[0] - 2.0).abs() < 1e-9 && p[1].abs() < 1e-9 && p[2].abs() < 1e-9,
889                "{p:?}"
890            );
891        }
892    }
893
894    #[test]
895    fn flipped_patch_reports_unoriented() {
896        let sphere = uv_sphere(16, 32, 1.0);
897        let plane = Plane::new([0.0, 0.0, 0.3], [0.0, 0.0, 1.0]).unwrap();
898        let flipped = brute(&sphere, &plane)[3].tri;
899        let positions: Vec<f32> = sphere.positions().iter().flatten().copied().collect();
900        let mut tris = sphere.triangles().to_vec();
901        tris[flipped as usize].swap(1, 2);
902        let indices: Vec<u32> = tris.iter().flatten().copied().collect();
903        let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
904        let out = cut(&mesh, &plane);
905        assert_eq!(out.contours.len(), 1);
906        let c = &out.contours[0];
907        assert!(c.triangles.contains(&flipped));
908        assert!(c.closed && !c.oriented && c.manifold);
909        // Unflipped, the same cut is oriented.
910        assert!(cut(&sphere, &plane).contours[0].oriented);
911    }
912
913    #[test]
914    fn deterministic_order() {
915        let meshes = [torus(2.0, 0.5, 48, 24), wavy(20), fin()];
916        let mut checked = 0;
917        for (i, mesh) in meshes.iter().enumerate() {
918            let sah = build(mesh, Quality::Sah);
919            let quick = build(mesh, Quality::Fast);
920            for plane in planes(mesh, 0x0bde + i as u64).into_iter().step_by(5) {
921                let mut a = Slice::default();
922                let mut b = Slice::default();
923                slice(mesh, &sah, &plane, &mut a).unwrap();
924                slice(mesh, &sah, &plane, &mut b).unwrap();
925                assert_eq!(a, b, "{plane:?}");
926                // Another BVH, and reusing a filled output, give the same value.
927                slice(mesh, &quick, &plane, &mut b).unwrap();
928                assert_eq!(a, b, "{plane:?}");
929                for c in &a.contours {
930                    check_contour(mesh, &plane, c);
931                    if c.closed {
932                        assert_eq!(c.edges[0], *c.edges.iter().min().unwrap());
933                    }
934                }
935                assert!(
936                    a.contours
937                        .windows(2)
938                        .all(|w| w[0].edges[0] <= w[1].edges[0])
939                );
940                checked += usize::from(a.contours.len() > 1);
941            }
942        }
943        assert!(checked > 5, "{checked}");
944    }
945
946    #[test]
947    fn slice_rejects_stale_bvh() {
948        let mut mesh = uv_sphere(8, 16, 1.0);
949        let bvh = build(&mesh, Quality::Sah);
950        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
951        let mut out = Slice::default();
952        slice(&mesh, &bvh, &plane, &mut out).unwrap();
953        assert!(!out.contours.is_empty());
954        assert_eq!(
955            slice(&uv_sphere(8, 16, 1.0), &bvh, &plane, &mut out),
956            Err(QueryError::MeshMismatch)
957        );
958        let mut tx = mesh.begin_edit();
959        tx.set_positions(&[0], &[[0.0, 0.0, 2.0]]).unwrap();
960        tx.commit().unwrap();
961        assert_eq!(
962            slice(&mesh, &bvh, &plane, &mut out),
963            Err(QueryError::StaleBvh)
964        );
965    }
966
967    #[test]
968    fn slice_out_of_memory_clears_output() {
969        let mesh = torus(2.0, 0.5, 24, 12);
970        let bvh = build(&mesh, Quality::Sah);
971        let plane = Plane::new([0.0; 3], [0.0, 0.0, 1.0]).unwrap();
972        let mut expected = Slice::default();
973        slice(&mesh, &bvh, &plane, &mut expected).unwrap();
974        assert_eq!(expected.contours.len(), 2);
975        let mut failures = 0;
976        for n in 0..400 {
977            let mut out = expected.clone();
978            fail_allocations_after(Some(n));
979            let r = slice(&mesh, &bvh, &plane, &mut out);
980            fail_allocations_after(None);
981            match r {
982                Ok(()) => assert_eq!(out, expected),
983                Err(e) => {
984                    assert_eq!(e, QueryError::OutOfMemory);
985                    assert!(out.contours.is_empty());
986                    failures += 1;
987                }
988            }
989        }
990        assert!(failures > 10, "{failures}");
991    }
992}