Skip to main content

fast_mesh_ops/
cut.rs

1//! Cutting a mesh with a plane into two capped parts.
2//!
3//! [`cut_by_plane`] splits the mesh of a transaction in two along `plane`:
4//! triangles on the `+` side (`signed_distance >= 0`, the same rule as
5//! [`slice`](fast_mesh_query::slice)) stay in the mesh, triangles on the `-`
6//! side move into a removed mesh with the same origin, and triangles crossing
7//! the plane are split along it. With [`CutOptions::cap`] (the default), both
8//! openings are closed with flat, unfaired [`fill_holes`] patches, so cutting
9//! a closed mesh yields two closed meshes.
10//!
11//! Cap choice: the caps are flat triangulations of the cut rim, refined to
12//! the surrounding density but not faired (`fair: false`). They follow the
13//! cut plane exactly instead of continuing the surrounding curvature.
14//!
15//! Large rims: the minimum-weight search is cubic, so it is limited to 200
16//! edges, and the fallback of [`fill_holes`] refuses a loop whose walls project
17//! inside it, which guards a hole in a curved surface against folding. A cut
18//! rim is flat by construction, so walls on either side of it cannot fold over
19//! the cap: what [`fill_holes`] turns down is ear-clipped in the plane instead,
20//! in near-linear time. Such a cap, and any cap of a rim above 2,048 edges, gets
21//! at most 20,000 new vertices, since matching the walls' density over a long
22//! rim takes a number of triangles that grows with the square of its length.
23//!
24//! Only positions, triangles and UVs cross over: the removed mesh takes no
25//! other channels. Crossing vertices interpolate their parents.
26//!
27//! UV seams: on a mesh with UVs a rim is read by position, so a seam that
28//! crosses the plane does not stop it from closing. The rim's vertices stay
29//! the walls': where a seam splits a rim position in two the cap takes one of
30//! the two, and the cap's edge next to it closes against the other side's wall
31//! by position only, like any seam. So the cap's UVs are the wall's along the
32//! rim, one side's at a seam, and the weighted mean of the rim inside: the
33//! patch blends across the seam, and it is no island of its own. A rim that
34//! closes by position but cannot be capped (its walls wound against each
35//! other, two rims or more than two sheets at one position, a rim that crosses
36//! itself in the plane, or a plane within rounding of a wall vertex so that the
37//! flat patch would repeat a wall edge) is an error.
38//! A mesh without UVs has no seams: it is read by vertex index only, and split
39//! vertices on it are a real boundary (weld it first).
40//!
41//! [`fill_holes`]: crate::fill_holes
42
43use std::collections::{BTreeMap, BTreeSet};
44
45use fast_mesh_query::Plane;
46use fast_mesh_store::{Mesh, StoreError, Transaction, TriangleId, VertexId};
47
48use crate::fill::{FillOptions, FillOptionsBuilder, Plan};
49use crate::fill_dp::{triangulate_flat_loop, triangulate_loop};
50use crate::fill_holes;
51use crate::refine::refine;
52use crate::topology::{BoundaryLoop, EdgeMap, LoopId, VertexFans, boundary_loops};
53use crate::weld::weld_roots;
54use crate::{OpsError, Result, filled, push, reserve};
55
56/// Options for [`cut_by_plane`].
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct CutOptions {
59    /// Close both openings with flat, unfaired patches, across UV seams on a
60    /// mesh with UVs. Without it both parts keep the open rim the cut leaves.
61    pub cap: bool,
62}
63
64impl Default for CutOptions {
65    fn default() -> Self {
66        CutOptions { cap: true }
67    }
68}
69
70/// What [`cut_by_plane`] did.
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct CutReport {
73    /// Alive triangles of the kept (`+`) mesh, cap included.
74    pub kept_triangles: usize,
75    /// Alive triangles of the removed (`-`) mesh, cap included.
76    pub removed_triangles: usize,
77    /// Cap triangles added on both sides together.
78    pub cap_triangles: usize,
79}
80
81/// The `-` side of [`cut_by_plane`]: a mesh with the input's origin, ready
82/// to add to the scene with the same placement.
83#[derive(Debug)]
84pub struct CutOutput {
85    /// The removed mesh, capped when [`CutOptions::cap`] is set.
86    pub removed: Mesh,
87    /// What the cut did.
88    pub report: CutReport,
89}
90
91fn oom() -> OpsError {
92    OpsError::Store(StoreError::OutOfMemory)
93}
94
95/// Splits the mesh of `tx` with `plane`: the `+` side stays, the `-` side is
96/// returned as a mesh with the same origin. Triangles crossing the plane are
97/// split along it; with `options.cap` both openings are closed with flat,
98/// unfaired [`fill_holes`](crate::fill_holes) patches over the cut rim.
99///
100/// A rim is capped when it closes: by vertex index, or, on a mesh with UVs,
101/// by exact position across seams (see the module docs for the cap's UVs).
102/// One that runs into a real boundary of an open mesh stays open. On a mesh
103/// without UVs split vertices are such a boundary: weld them first.
104///
105/// # Errors
106/// - [`OpsError::InvalidInput`] when the plane does not cut the mesh (every
107///   triangle on one side), when a cut point leaves `f32` range, or, with
108///   `cap`, when a rim cannot be capped: see the module docs.
109/// - [`OpsError::StaleLoops`] when a rim loop vanished between listing and
110///   filling (defensive: both come from the same mesh state).
111/// - [`OpsError::Store`] when memory runs out. Nothing is written then: the
112///   plan is read from the untouched mesh before the first write.
113pub fn cut_by_plane(
114    tx: &mut Transaction,
115    plane: &Plane,
116    options: &CutOptions,
117) -> Result<CutOutput> {
118    let mesh = tx.mesh();
119    let origin = mesh.origin();
120    let slots = mesh.vertex_slots();
121    let tri_slots = mesh.triangle_slots();
122
123    // Sides of every triangle slot, in slot order, and the crossing points of
124    // every crossed edge: read-only over the untouched mesh.
125    let mut points: Vec<[f64; 3]> = Vec::new();
126    reserve(&mut points, slots)?;
127    for v in 0..slots as VertexId {
128        points.push(mesh.position(v).map_or([f64::NAN; 3], |p| p.map(f64::from)));
129    }
130    let mut side: Vec<[bool; 3]> = Vec::new();
131    reserve(&mut side, tri_slots)?;
132    let mut straddling: Vec<TriangleId> = Vec::new();
133    for t in 0..tri_slots as TriangleId {
134        let Some(tri) = mesh.triangle(t) else {
135            side.push([true; 3]);
136            continue;
137        };
138        let s = tri.map(|v| plane.signed_distance(points[v as usize]) >= 0.0);
139        if s[0] != s[1] || s[1] != s[2] {
140            push(&mut straddling, t)?;
141        }
142        push(&mut side, s)?;
143    }
144    if straddling.is_empty() {
145        return Err(OpsError::InvalidInput("plane does not cut the mesh"));
146    }
147    // One crossing per crossed edge, in id order so that both sides agree.
148    // An endpoint exactly on the plane is the crossing: no new vertex.
149    // On a mesh with UVs, rims close by exact position.
150    let by_position = mesh.uvs().is_some();
151    let mut at: BTreeMap<(VertexId, VertexId), VertexId> = BTreeMap::new();
152    let mut point_of: BTreeMap<(VertexId, VertexId), [f64; 3]> = BTreeMap::new();
153    for &t in &straddling {
154        // `side` holds one entry per triangle slot, in order.
155        let s = side[t as usize];
156        let tri = mesh
157            .triangle(t)
158            .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
159        let (e1, e2) = crossed_edges(tri, lone(s));
160        for e in [e1, e2] {
161            if at.contains_key(&e) {
162                continue;
163            }
164            let (a, b) = e;
165            let (pa, pb) = (points[a as usize], points[b as usize]);
166            let (sa, sb) = (plane.signed_distance(pa), plane.signed_distance(pb));
167            debug_assert!((sa >= 0.0) != (sb >= 0.0), "edge {e:?} is not crossed");
168            if sa == 0.0 {
169                at.insert(e, a);
170            } else if sb == 0.0 {
171                at.insert(e, b);
172            } else {
173                point_of.insert(e, crossing_point(pa, pb, sa, sb, by_position));
174            }
175        }
176    }
177
178    // The write phase: add the new crossing vertices first, interpolating
179    // their parents so channels blend across the cut.
180    let mut added_points: BTreeMap<VertexId, [f64; 3]> = BTreeMap::new();
181    for (&e, &p) in &point_of {
182        let (a, b) = e;
183        let q = p.map(|c| c as f32);
184        if !q.iter().all(|c| c.is_finite()) {
185            return Err(OpsError::InvalidInput("a cut point leaves f32 range"));
186        }
187        let (sa, sb) = (
188            plane.signed_distance(points[a as usize]),
189            plane.signed_distance(points[b as usize]),
190        );
191        let f = (sa / (sa - sb)) as f32;
192        let id = tx.add_vertex_with_parents(q, &[(a, 1.0 - f), (b, f)])?;
193        at.insert(e, id);
194        added_points.insert(id, p);
195    }
196
197    // Split every straddling triangle: the first kept polygon triangle
198    // replaces it, the rest are added; the removed side collects into fresh
199    // buffers in its own indexing.
200    let mut rem_index: BTreeMap<VertexId, u32> = BTreeMap::new();
201    let mut rem_positions: Vec<[f64; 3]> = Vec::new();
202    let mut rem_tris: Vec<[u32; 3]> = Vec::new();
203    let mut kept_cut: BTreeSet<(VertexId, VertexId)> = BTreeSet::new();
204    let mut rem_cut: BTreeSet<(u32, u32)> = BTreeSet::new();
205    let mut removed_tris: Vec<TriangleId> = Vec::new();
206    for &t in &straddling {
207        let tri = tx
208            .mesh()
209            .triangle(t)
210            .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
211        let s = side[t as usize];
212        let i = lone(s);
213        let (e1, e2) = crossed_edges(tri, i);
214        let (x1, x2) = (at[&e1], at[&e2]);
215        if x1 != x2 {
216            kept_cut.insert(key(x1, x2));
217        }
218        let (kept_poly, removed_poly) = if s[i] {
219            (
220                vec![tri[i], x1, x2],
221                vec![x1, tri[(i + 1) % 3], tri[(i + 2) % 3], x2],
222            )
223        } else {
224            (
225                vec![x1, tri[(i + 1) % 3], tri[(i + 2) % 3], x2],
226                vec![tri[i], x1, x2],
227            )
228        };
229        write_poly(tx, t, &kept_poly)?;
230        // The removed-side index of `v`: original vertices reuse their
231        // stored position, new crossing vertices their planned point.
232        fn rem_id(
233            v: VertexId,
234            points: &[[f64; 3]],
235            added_points: &BTreeMap<VertexId, [f64; 3]>,
236            rem_index: &mut BTreeMap<VertexId, u32>,
237            rem_positions: &mut Vec<[f64; 3]>,
238        ) -> Result<u32> {
239            if let Some(&idx) = rem_index.get(&v) {
240                return Ok(idx);
241            }
242            let p = match added_points.get(&v) {
243                Some(&p) => p,
244                None => points[v as usize],
245            };
246            if !p.iter().all(|c| c.is_finite()) {
247                return Err(OpsError::InvalidInput("a removed vertex is not finite"));
248            }
249            let idx = u32::try_from(rem_positions.len()).map_err(|_| oom())?;
250            reserve(rem_positions, 1)?;
251            rem_positions.push(p);
252            rem_index.insert(v, idx);
253            Ok(idx)
254        }
255        let mut quad: Vec<u32> = Vec::with_capacity(removed_poly.len());
256        for v in removed_poly {
257            quad.push(rem_id(
258                v,
259                &points,
260                &added_points,
261                &mut rem_index,
262                &mut rem_positions,
263            )?);
264        }
265        let before = rem_tris.len();
266        emit_fan(&mut rem_tris, quad)?;
267        if rem_tris.len() > before && x1 != x2 {
268            match (rem_index.get(&x1), rem_index.get(&x2)) {
269                (Some(&r1), Some(&r2)) if r1 != r2 => {
270                    rem_cut.insert((r1.min(r2), r1.max(r2)));
271                }
272                _ => {}
273            }
274        }
275    }
276    // Triangles fully on the `-` side leave the kept mesh and join the
277    // removed buffers.
278    for t in 0..tri_slots as TriangleId {
279        if side[t as usize] != [false; 3] {
280            continue;
281        }
282        let tri = tx
283            .mesh()
284            .triangle(t)
285            .ok_or(OpsError::InvalidInput("plane does not cut the mesh"))?;
286        let mut idx = [0; 3];
287        for (k, v) in tri.iter().enumerate() {
288            // Original vertices reuse their stored position.
289            if !rem_index.contains_key(v) {
290                let p = points[*v as usize];
291                let n = u32::try_from(rem_positions.len()).map_err(|_| oom())?;
292                reserve(&mut rem_positions, 1)?;
293                rem_positions.push(p);
294                rem_index.insert(*v, n);
295            }
296            idx[k] = rem_index[v];
297        }
298        push(&mut rem_tris, idx)?;
299        push(&mut removed_tris, t)?;
300    }
301    if !removed_tris.is_empty() {
302        tx.remove_triangles(&removed_tris)?;
303    }
304    // Vertices the cut orphaned (used only by the removed side) go.
305    let mut orphans: Vec<VertexId> = Vec::new();
306    for v in 0..tx.mesh().vertex_slots() as VertexId {
307        if tx.mesh().is_vertex_alive(v) && tx.mesh().corner_count(v) == 0 {
308            push(&mut orphans, v)?;
309        }
310    }
311    if !orphans.is_empty() {
312        tx.remove_vertices(&orphans)?;
313    }
314
315    // Close both openings with flat, unfaired patches over the cut rim.
316    let mut cap_triangles = 0;
317    if options.cap {
318        // Cut edges in the plane, by position, while their vertices are known.
319        let mut seam_twins: BTreeSet<PositionEdge> = BTreeSet::new();
320        if by_position {
321            let original = |v: VertexId| {
322                points
323                    .get(v as usize)
324                    .filter(|&&p| plane.signed_distance(p) == 0.0)
325                    .map(|p| p.map(|c| c as f32))
326            };
327            for &(a, b) in &kept_cut {
328                if let (Some(p), Some(q)) = (original(a), original(b)) {
329                    seam_twins.insert(position_edge(p, q));
330                }
331            }
332        }
333        let rims = find_rims(tx.mesh(), &kept_cut, &seam_twins)?;
334        cap_triangles += cap_rims(tx, &rims, plane.normal())?;
335    }
336    let kept_triangles = tx.mesh().triangle_count();
337
338    // The removed side, with the input's origin so the same placement fits.
339    let mut flat_positions: Vec<f64> = Vec::new();
340    reserve(&mut flat_positions, rem_positions.len() * 3)?;
341    for p in &rem_positions {
342        flat_positions.extend_from_slice(&[origin[0] + p[0], origin[1] + p[1], origin[2] + p[2]]);
343    }
344    let mut flat_tris: Vec<u32> = Vec::new();
345    reserve(&mut flat_tris, rem_tris.len() * 3)?;
346    for t in &rem_tris {
347        flat_tris.extend_from_slice(t);
348    }
349    let mut removed = Mesh::from_buffers_f64_with_origin(&flat_positions, &flat_tris, origin)?;
350    // The removed side keeps its UVs, crossing vertices included: their slots
351    // still hold the interpolated values.
352    if let Some(uvs) = tx.mesh().uvs() {
353        let mut rem_uvs = filled(2 * rem_positions.len(), 0.0f32)?;
354        for (&v, &i) in &rem_index {
355            rem_uvs[2 * i as usize] = uvs[2 * v as usize];
356            rem_uvs[2 * i as usize + 1] = uvs[2 * v as usize + 1];
357        }
358        removed.add_uvs(&rem_uvs)?;
359    }
360    if options.cap {
361        // Every removed triangle on a cut edge straddles the plane, so
362        // `rem_cut` has all of them.
363        let rims = find_rims(&removed, &rem_cut, &BTreeSet::new())?;
364        if !rims.whole.is_empty() || !rims.by_position.is_empty() {
365            let mut rtx = removed.begin_edit();
366            cap_triangles += cap_rims(&mut rtx, &rims, plane.normal())?;
367            rtx.commit()?;
368        }
369    }
370    let removed_triangles = removed.triangle_count();
371
372    Ok(CutOutput {
373        removed,
374        report: CutReport {
375            kept_triangles,
376            removed_triangles,
377            cap_triangles,
378        },
379    })
380}
381
382/// Where the plane crosses the edge `a`-`b`, whose ends have signed distances
383/// `sa` and `sb`. With `by_position`, the result does not depend on which end
384/// comes first: the ends are taken in position order, so the two sides of a
385/// seam, which hold the same edge under different ids, get the same point.
386fn crossing_point(a: [f64; 3], b: [f64; 3], sa: f64, sb: f64, by_position: bool) -> [f64; 3] {
387    let swap = by_position && b < a;
388    let (a, b, sa, sb) = if swap { (b, a, sb, sa) } else { (a, b, sa, sb) };
389    let f = sa / (sa - sb);
390    std::array::from_fn(|k| a[k] + f * (b[k] - a[k]))
391}
392
393/// The index of the lone vertex, whose side differs from the other two.
394fn lone(s: [bool; 3]) -> usize {
395    if s[0] == s[1] {
396        2
397    } else if s[0] == s[2] {
398        1
399    } else {
400        0
401    }
402}
403
404fn key(a: VertexId, b: VertexId) -> (VertexId, VertexId) {
405    (a.min(b), a.max(b))
406}
407
408/// The crossed edges of a straddling triangle with lone vertex `i`: the
409/// edge out of it and the edge into it, in id order.
410fn crossed_edges(tri: [VertexId; 3], i: usize) -> ((VertexId, VertexId), (VertexId, VertexId)) {
411    let (a, b, c) = (tri[i], tri[(i + 1) % 3], tri[(i + 2) % 3]);
412    (key(a, b), key(c, a))
413}
414
415/// Writes a kept-side polygon: dedupe drops the vertices the plane passes
416/// through twice, and a fan covers the quad.
417fn write_poly(tx: &mut Transaction, t: TriangleId, poly: &[VertexId]) -> Result<()> {
418    let mut dedup: Vec<VertexId> = Vec::with_capacity(poly.len());
419    for &v in poly {
420        if dedup.last() != Some(&v) {
421            dedup.push(v);
422        }
423    }
424    while dedup.len() > 1 && dedup[0] == *dedup.last().unwrap() {
425        dedup.pop();
426    }
427    match dedup.len() {
428        0..=2 => tx.remove_triangles(&[t]).map_err(OpsError::from),
429        3 => tx
430            .set_triangles(&[t], &[dedup.as_slice().try_into().unwrap()])
431            .map_err(OpsError::from),
432        _ => {
433            tx.set_triangles(&[t], &[[dedup[0], dedup[1], dedup[2]]])?;
434            tx.add_triangles(&[[dedup[0], dedup[2], dedup[3]]])?;
435            Ok(())
436        }
437    }
438}
439
440/// Emits a removed-side polygon as a fan, dropping degenerate vertices.
441fn emit_fan(tris: &mut Vec<[u32; 3]>, poly: Vec<u32>) -> Result<()> {
442    let mut dedup: Vec<u32> = Vec::with_capacity(poly.len());
443    for v in poly {
444        if dedup.last() != Some(&v) {
445            dedup.push(v);
446        }
447    }
448    while dedup.len() > 1 && dedup[0] == *dedup.last().unwrap() {
449        dedup.pop();
450    }
451    if dedup.len() == 4 {
452        reserve(tris, 2)?;
453        tris.push([dedup[0], dedup[1], dedup[2]]);
454        tris.push([dedup[0], dedup[2], dedup[3]]);
455    } else if dedup.len() == 3 {
456        push(tris, [dedup[0], dedup[1], dedup[2]])?;
457    }
458    Ok(())
459}
460
461/// Whether a boundary loop is the cut rim: every edge is a cut edge.
462fn is_cut_rim(l: &BoundaryLoop, cut: &BTreeSet<(u32, u32)>) -> bool {
463    l.closed && loop_edges(&l.vertices).iter().all(|e| cut.contains(e))
464}
465
466/// The edges of a loop as unordered pairs, closing back to the first vertex.
467fn loop_edges(v: &[VertexId]) -> Vec<(u32, u32)> {
468    let mut out = Vec::with_capacity(v.len());
469    for (k, &a) in v.iter().enumerate() {
470        let b = v[(k + 1) % v.len()];
471        out.push((a.min(b), a.max(b)));
472    }
473    out
474}
475
476/// The openings a cut left, ready to cap.
477struct Rims {
478    /// Boundary loops of nothing but cut edges.
479    whole: Vec<BoundaryLoop>,
480    /// Boundary edges to read by position: on a mesh with UVs those that lie
481    /// along the cut in other loops, and the edges of a too large whole loop.
482    by_position: Vec<(VertexId, VertexId)>,
483}
484
485/// The exact position of a vertex, with `-0.0` read as `0.0`, for comparing.
486fn position_key(p: [f32; 3]) -> [u32; 3] {
487    p.map(|x| (x + 0.0).to_bits())
488}
489
490/// An edge by the positions of its ends, in order.
491fn position_edge(a: [f32; 3], b: [f32; 3]) -> PositionEdge {
492    let (a, b) = (position_key(a), position_key(b));
493    if a <= b { (a, b) } else { (b, a) }
494}
495
496type PositionEdge = ([u32; 3], [u32; 3]);
497
498/// The rims of `mesh` after a cut with cut edges `cut`. A side that keeps an edge
499/// in the plane marks it cut only if its neighbour across the edge straddles
500/// the plane, and the neighbour past a seam is another edge. So on a mesh with
501/// UVs an edge at the same positions as one of `seam_twins`, the cut edges in
502/// the plane, counts too. Any other boundary edge in the plane is a real hole
503/// that merely lies there, and stays open.
504fn find_rims(
505    mesh: &Mesh,
506    cut: &BTreeSet<(VertexId, VertexId)>,
507    seam_twins: &BTreeSet<PositionEdge>,
508) -> Result<Rims> {
509    let loops = boundary_loops(mesh)?;
510    let seamed = mesh.uvs().is_some();
511    let twin = |(a, b): (VertexId, VertexId)| match (mesh.position(a), mesh.position(b)) {
512        (Some(p), Some(q)) => seam_twins.contains(&position_edge(p, q)),
513        _ => false,
514    };
515    let mut rims = Rims {
516        whole: Vec::new(),
517        by_position: Vec::new(),
518    };
519    for l in loops {
520        if is_cut_rim(&l, cut) {
521            if l.vertices.len() > LARGE_RIM {
522                let edges = loop_edges(&l.vertices);
523                reserve(&mut rims.by_position, edges.len())?;
524                rims.by_position.extend(edges);
525            } else {
526                push(&mut rims.whole, l)?;
527            }
528        } else if seamed {
529            for e in loop_edges(&l.vertices) {
530                if cut.contains(&e) || twin(e) {
531                    push(&mut rims.by_position, e)?;
532                }
533            }
534        }
535    }
536    Ok(rims)
537}
538
539/// Caps `rims` with flat, unfaired patches. Returns the triangles added.
540fn cap_rims(tx: &mut Transaction, rims: &Rims, normal: [f64; 3]) -> Result<usize> {
541    let before = tx.mesh().triangle_count();
542    let mut by_position = rims.by_position.clone();
543    if !rims.whole.is_empty() {
544        let mut ids: Vec<LoopId> = Vec::new();
545        reserve(&mut ids, rims.whole.len())?;
546        ids.extend(rims.whole.iter().map(|l| l.id));
547        let report = fill_holes(
548            tx,
549            &FillOptionsBuilder::default()
550                .loops(ids)
551                .fair(false)
552                .allow_unclean(true)
553                .build(),
554        )?;
555        // What the search or its fold check turned down is still a flat rim.
556        let refused = report
557            .failed
558            .iter()
559            .map(|&(id, _)| id)
560            .chain(report.skipped.iter().map(|&(id, _)| id));
561        for id in refused {
562            if let Some(l) = rims.whole.iter().find(|l| l.id == id) {
563                let edges = loop_edges(&l.vertices);
564                reserve(&mut by_position, edges.len())?;
565                by_position.extend(edges);
566            }
567        }
568    }
569    if !by_position.is_empty() {
570        cap_by_position(tx, &by_position, normal)?;
571    }
572    Ok(tx.mesh().triangle_count() - before)
573}
574
575/// Above this many edges a rim skips the search and the density-matched patch
576/// of [`fill_holes`], whose triangle count grows with the square of the rim's
577/// length: it gets the bounded cap of [`cap_by_position`].
578const LARGE_RIM: usize = 2048;
579
580/// The most new vertices a cap of [`cap_by_position`] gets, per rim. A rim up to
581/// about 1,000 edges (at even density) reaches the density of its walls within
582/// it. A longer one is refined only part of the way, since matching a rim of
583/// 50,000 edges would take hundreds of millions of triangles.
584const CAP_VERTEX_BUDGET: usize = 20_000;
585
586/// A rim that closes by position: at each step, the position's weld root and
587/// the vertex whose wall edge leaves it.
588type SeamRim = Vec<(VertexId, VertexId)>;
589
590/// The closed rims among boundary `edges` of `mesh`, reading vertices at one
591/// exact position (`roots`) as one. An edge group with an odd position runs
592/// into a real boundary and has no rim. One with only even positions that
593/// cannot be walked as a single cycle in its triangles' direction is an error.
594/// Two edges at one pair of positions, running opposite ways, already close
595/// against each other, like a seam in the plane: they are no part of a rim.
596fn seam_rims(
597    mesh: &Mesh,
598    edges: &[(VertexId, VertexId)],
599    roots: &[VertexId],
600) -> Result<Vec<SeamRim>> {
601    let map = EdgeMap::build(mesh)?;
602    // The directed edges between positions, each with its `from` vertices.
603    let mut directed: BTreeMap<(VertexId, VertexId), Vec<VertexId>> = BTreeMap::new();
604    for &(a, b) in edges {
605        // Only a true boundary edge has one triangle to take a direction from.
606        let [t] = map.triangles(a, b) else {
607            continue;
608        };
609        let [x, y, z] = mesh.triangles()[*t as usize];
610        let forward = [(x, y), (y, z), (z, x)].contains(&(a, b));
611        let (from, to) = if forward { (a, b) } else { (b, a) };
612        let (ra, rb) = (roots[from as usize], roots[to as usize]);
613        if ra != rb {
614            directed.entry((ra, rb)).or_default().push(from);
615        }
616    }
617    // Per position: the edges leaving it, how many arrive, and its neighbours.
618    let mut leaving: BTreeMap<VertexId, Vec<(VertexId, VertexId)>> = BTreeMap::new();
619    let mut arriving: BTreeMap<VertexId, usize> = BTreeMap::new();
620    let mut neighbours: BTreeMap<VertexId, Vec<VertexId>> = BTreeMap::new();
621    for (&(ra, rb), froms) in &directed {
622        let closed = directed.get(&(rb, ra)).map_or(0, Vec::len);
623        for &from in froms.iter().skip(closed) {
624            leaving.entry(ra).or_default().push((from, rb));
625            *arriving.entry(rb).or_default() += 1;
626            neighbours.entry(ra).or_default().push(rb);
627            neighbours.entry(rb).or_default().push(ra);
628        }
629    }
630    let mut rims: Vec<SeamRim> = Vec::new();
631    let mut seen: BTreeSet<VertexId> = BTreeSet::new();
632    let mut group: Vec<VertexId> = Vec::new();
633    for &start in neighbours.keys() {
634        if !seen.insert(start) {
635            continue;
636        }
637        group.clear();
638        push(&mut group, start)?;
639        let mut next = 0;
640        while let Some(&r) = group.get(next) {
641            next += 1;
642            for &n in &neighbours[&r] {
643                if seen.insert(n) {
644                    push(&mut group, n)?;
645                }
646            }
647        }
648        if group.iter().any(|r| neighbours[r].len() % 2 == 1) {
649            continue;
650        }
651        let single = |r: &VertexId| {
652            leaving.get(r).is_some_and(|l| l.len() == 1) && arriving.get(r) == Some(&1)
653        };
654        if !group.iter().all(single) {
655            return Err(OpsError::InvalidInput("a cut rim cannot be capped"));
656        }
657        let mut rim: SeamRim = Vec::new();
658        reserve(&mut rim, group.len())?;
659        let mut r = start;
660        loop {
661            let (from, to) = leaving[&r][0];
662            rim.push((r, from));
663            r = to;
664            if r == start {
665                break;
666            }
667        }
668        rims.push(rim);
669    }
670    Ok(rims)
671}
672
673/// Caps the rims that close by position among `edges`, which lie in the plane
674/// with `normal`. The triangulation and refinement of [`fill_holes`] read a mesh
675/// whose rim closes by index, so they run on a scratch mesh: the triangles at
676/// the rims, with coincident vertices merged. The patches then go to the real
677/// mesh on the walls' rim vertices.
678///
679/// A rim up to `dp_limit` edges takes the search, and past it, or where the
680/// search finds nothing, is ear-clipped in the plane. Refinement stops at
681/// [`CAP_VERTEX_BUDGET`].
682fn cap_by_position(
683    tx: &mut Transaction,
684    edges: &[(VertexId, VertexId)],
685    normal: [f64; 3],
686) -> Result<()> {
687    let mesh = tx.mesh();
688    let roots = weld_roots(mesh, 0.0)?;
689    let rims = seam_rims(mesh, edges, &roots)?;
690    if rims.is_empty() {
691        return Ok(());
692    }
693    let mut on_rim = filled(mesh.vertex_slots(), false)?;
694    for rim in &rims {
695        for &(r, _) in rim {
696            on_rim[r as usize] = true;
697        }
698    }
699    const NONE: u32 = u32::MAX;
700    let mut local = filled(mesh.vertex_slots(), NONE)?;
701    let mut positions: Vec<f32> = Vec::new();
702    let mut indices: Vec<u32> = Vec::new();
703    for t in 0..mesh.triangle_slots() as TriangleId {
704        let Some(tri) = mesh.triangle(t) else {
705            continue;
706        };
707        let tri = tri.map(|v| roots[v as usize]);
708        let distinct = tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0];
709        if !distinct || !tri.iter().any(|&r| on_rim[r as usize]) {
710            continue;
711        }
712        reserve(&mut indices, 3)?;
713        for r in tri {
714            if local[r as usize] == NONE {
715                local[r as usize] = (positions.len() / 3) as u32;
716                reserve(&mut positions, 3)?;
717                positions.extend(mesh.positions()[r as usize]);
718            }
719            indices.push(local[r as usize]);
720        }
721    }
722    // No origin: the scratch mesh holds the very local positions of `mesh`.
723    let scratch = Mesh::from_buffers(&positions, &indices)?;
724    let map = EdgeMap::build(&scratch)?;
725    let fans = VertexFans::build(&scratch)?;
726    let defaults = FillOptions::default();
727    let mut plan = Plan::default();
728    let mut wall = filled(positions.len() / 3, NONE)?;
729    let mut order: Vec<VertexId> = Vec::new();
730    for rim in &rims {
731        order.clear();
732        reserve(&mut order, rim.len())?;
733        for &(r, from) in rim {
734            order.push(local[r as usize]);
735            wall[local[r as usize] as usize] = from;
736        }
737        let searched = if order.len() <= defaults.dp_limit {
738            triangulate_loop(&scratch, &order, &map, defaults.dp_limit, None)?.ok()
739        } else {
740            None
741        };
742        let tris = match searched {
743            Some(tris) => tris,
744            None => triangulate_flat_loop(&scratch, &order, &map, normal)?
745                .map_err(|_| OpsError::InvalidInput("a cut rim cannot be capped"))?,
746        };
747        let mut patch = refine(
748            &scratch,
749            &fans,
750            &map,
751            &order,
752            &tris,
753            Some(CAP_VERTEX_BUDGET),
754        )?;
755        for v in &mut patch.old {
756            *v = wall[*v as usize];
757        }
758        plan.add(&patch)?;
759    }
760    plan.write(tx)
761}
762
763#[cfg(test)]
764mod tests {
765    use super::*;
766    use crate::test_util::{edit, grid_without, mesh_of};
767    use fast_mesh_store::Mesh;
768
769    /// A closed unit cube, outward wound.
770    fn cube() -> Mesh {
771        Mesh::from_buffers(
772            &[
773                0.0, 0.0, 0.0, //
774                1.0, 0.0, 0.0, //
775                1.0, 1.0, 0.0, //
776                0.0, 1.0, 0.0, //
777                0.0, 0.0, 1.0, //
778                1.0, 0.0, 1.0, //
779                1.0, 1.0, 1.0, //
780                0.0, 1.0, 1.0,
781            ],
782            &[
783                0, 2, 1, 0, 3, 2, // bottom (-z)
784                4, 5, 6, 4, 6, 7, // top (+z)
785                0, 1, 5, 0, 5, 4, // front (-y)
786                2, 3, 7, 2, 7, 6, // back (+y)
787                1, 2, 6, 1, 6, 5, // right (+x)
788                3, 0, 4, 3, 4, 7, // left (-x)
789            ],
790        )
791        .unwrap()
792    }
793
794    /// Six times the signed volume: positive for outward-wound meshes.
795    fn volume6(mesh: &Mesh) -> f64 {
796        let mut v = 0.0;
797        for t in 0..mesh.triangle_slots() as TriangleId {
798            let Some(tri) = mesh.triangle(t) else {
799                continue;
800            };
801            let [a, b, c] = tri.map(|x| mesh.position(x).unwrap().map(f64::from));
802            v += a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
803                + a[2] * (b[0] * c[1] - b[1] * c[0]);
804        }
805        v
806    }
807
808    fn plane(origin: [f64; 3], normal: [f64; 3]) -> Plane {
809        Plane::new(origin, normal).unwrap()
810    }
811
812    fn cut_kept(mesh: &mut Mesh, plane: &Plane, cap: bool) -> CutOutput {
813        let (out, _) = edit(mesh, |tx| cut_by_plane(tx, plane, &CutOptions { cap }));
814        out
815    }
816
817    /// The local x of a kept-mesh position.
818    fn kept_x(mesh: &Mesh, v: VertexId) -> f64 {
819        mesh.position(v).unwrap()[0] as f64 + mesh.origin()[0]
820    }
821
822    #[test]
823    fn cube_cut_in_half_is_two_closed_halves() {
824        let mut mesh = cube();
825        let origin = mesh.origin();
826        let out = cut_kept(&mut mesh, &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]), true);
827        // Closed on both sides: the caps are there.
828        assert!(boundary_loops(&mesh).unwrap().is_empty());
829        assert!(boundary_loops(&out.removed).unwrap().is_empty());
830        // The volumes add up to the cube's, both wound outward.
831        let (vk, vr) = (volume6(&mesh), volume6(&out.removed));
832        assert!(vk > 0.0 && vr > 0.0, "{vk} {vr}");
833        assert!((vk + vr - 6.0).abs() < 1e-9, "{vk} {vr}");
834        assert!((vk - vr).abs() < 1e-6, "{vk} {vr}");
835        // Every vertex stays on its side.
836        assert!((0..mesh.vertex_slots() as VertexId).all(|v| {
837            mesh.position(v)
838                .is_none_or(|_| kept_x(&mesh, v) >= 0.5 - 1e-6)
839        }));
840        assert!(
841            out.removed
842                .positions()
843                .iter()
844                .all(|p| p[0] as f64 + out.removed.origin()[0] <= 0.5 + 1e-6)
845        );
846        assert_eq!(out.report.kept_triangles, mesh.triangle_count());
847        assert_eq!(out.report.removed_triangles, out.removed.triangle_count());
848        assert!(out.report.cap_triangles >= 4, "{:?}", out.report);
849        assert_eq!(out.removed.origin(), origin);
850    }
851
852    #[test]
853    fn cap_off_leaves_both_sides_open() {
854        let mut mesh = cube();
855        let out = cut_kept(&mut mesh, &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]), false);
856        assert!(!boundary_loops(&mesh).unwrap().is_empty());
857        assert!(!boundary_loops(&out.removed).unwrap().is_empty());
858        assert_eq!(out.report.cap_triangles, 0);
859        // The cut rim vertices lie exactly on the plane.
860        assert!(
861            mesh.positions()
862                .iter()
863                .any(|p| (kept_x_of(&mesh, p) - 0.5).abs() < 1e-6)
864        );
865    }
866
867    fn kept_x_of(mesh: &Mesh, p: &[f32; 3]) -> f64 {
868        p[0] as f64 + mesh.origin()[0]
869    }
870
871    #[test]
872    fn plane_missing_the_mesh_is_rejected_and_changes_nothing() {
873        let mut mesh = cube();
874        let mut tx = mesh.begin_edit();
875        let r = cut_by_plane(
876            &mut tx,
877            &plane([5.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
878            &CutOptions::default(),
879        );
880        assert_eq!(
881            r.unwrap_err(),
882            OpsError::InvalidInput("plane does not cut the mesh")
883        );
884        tx.rollback();
885        assert_eq!(mesh.triangle_count(), 12);
886    }
887
888    #[test]
889    fn open_grid_cut_keeps_origin_and_sides() {
890        // An open 8x8 grid cut along its middle without cap.
891        let mut mesh = grid_without(8, &[]);
892        let origin = mesh.origin();
893        let out = cut_kept(&mut mesh, &plane([3.5, 0.0, 0.0], [1.0, 0.0, 0.0]), false);
894        assert_eq!(out.removed.origin(), origin);
895        let alive = |m: &Mesh, v: VertexId| m.is_vertex_alive(v);
896        assert!(
897            (0..mesh.vertex_slots() as VertexId)
898                .filter(|&v| alive(&mesh, v))
899                .all(|v| kept_x(&mesh, v) >= 3.5 - 1e-6)
900        );
901        assert!(
902            out.removed
903                .positions()
904                .iter()
905                .all(|p| p[0] as f64 + out.removed.origin()[0] <= 3.5 + 1e-6)
906        );
907    }
908
909    // ---- caps across UV seams
910
911    /// A tube around `z` of `slices` columns and `rows` rows, wound outward,
912    /// with a UV seam: column `slices` repeats column 0 exactly, `u` runs
913    /// along the columns and `v` up the rows. With `lids` both ends are
914    /// closed by a fan around a centre vertex, so the mesh is closed by
915    /// position, though its boundary loop by index runs along the seam.
916    fn seamed_tube(slices: u32, rows: u32, lids: bool) -> Mesh {
917        let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
918        let id = |i: u32, j: u32| i * (slices + 1) + j;
919        for i in 0..=rows {
920            for j in 0..=slices {
921                let a = std::f32::consts::TAU * (j % slices) as f32 / slices as f32;
922                positions.extend([a.cos(), a.sin(), i as f32]);
923                uvs.extend([j as f32 / slices as f32, i as f32 / rows as f32]);
924            }
925        }
926        for i in 0..rows {
927            for j in 0..slices {
928                let (a, b) = (id(i, j), id(i, j + 1));
929                let (c, d) = (id(i + 1, j + 1), id(i + 1, j));
930                triangles.extend([[a, b, c], [a, c, d]]);
931            }
932        }
933        if lids {
934            let bottom = (positions.len() / 3) as u32;
935            positions.extend([0.0, 0.0, 0.0, 0.0, 0.0, rows as f32]);
936            uvs.extend([0.5, 0.0, 0.5, 1.0]);
937            for j in 0..slices {
938                triangles.push([bottom, id(0, j + 1), id(0, j)]);
939                triangles.push([bottom + 1, id(rows, j), id(rows, j + 1)]);
940            }
941        }
942        let mut mesh = mesh_of(&positions, &triangles);
943        mesh.add_uvs(&uvs).unwrap();
944        mesh
945    }
946
947    /// A unit cube with every triangle its own UV island: 36 vertices, every
948    /// edge a seam.
949    fn cube_of_islands() -> Mesh {
950        let whole = cube();
951        let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
952        for (t, tri) in whole.triangles().iter().enumerate() {
953            let island = t as f32;
954            let base = (positions.len() / 3) as u32;
955            for (k, &v) in tri.iter().enumerate() {
956                positions.extend(whole.position(v).unwrap());
957                uvs.extend([island / 12.0, k as f32 / 3.0]);
958            }
959            triangles.push([base, base + 1, base + 2]);
960        }
961        let mut mesh = mesh_of(&positions, &triangles);
962        mesh.add_uvs(&uvs).unwrap();
963        mesh
964    }
965
966    /// `mesh` with exactly coincident vertices merged and no channels.
967    fn welded_by_position(mesh: &Mesh) -> Mesh {
968        let roots = crate::weld::weld_roots(mesh, 0.0).unwrap();
969        let positions: Vec<f32> = mesh.positions().iter().flatten().copied().collect();
970        let triangles: Vec<[u32; 3]> = (0..mesh.triangle_slots() as TriangleId)
971            .filter_map(|t| mesh.triangle(t))
972            .map(|t| t.map(|v| roots[v as usize]))
973            .collect();
974        mesh_of(&positions, &triangles)
975    }
976
977    /// Every edge, read by position, is used once in each direction.
978    fn assert_closed_by_position(mesh: &Mesh) {
979        let welded = welded_by_position(mesh);
980        let mut uses: BTreeMap<(u32, u32), i32> = BTreeMap::new();
981        for t in 0..welded.triangle_slots() as TriangleId {
982            let Some([a, b, c]) = welded.triangle(t) else {
983                continue;
984            };
985            // A plane within rounding of a vertex can leave a triangle without
986            // area: it spans its two positions both ways, which closes nothing.
987            if a == b || b == c || c == a {
988                continue;
989            }
990            for (x, y) in [(a, b), (b, c), (c, a)] {
991                *uses.entry((x, y)).or_default() += 1;
992            }
993        }
994        for (&(a, b), &n) in &uses {
995            assert_eq!(n, 1, "edge {a}-{b}");
996            assert_eq!(uses.get(&(b, a)), Some(&1), "edge {b}-{a} is open");
997        }
998    }
999
1000    /// Cuts `mesh` with a cap and checks what any capped cut of a mesh that
1001    /// is closed by position owes: both sides closed by position and wound
1002    /// outward, volume kept, an honest report, aligned UVs. Returns the kept
1003    /// side and the output.
1004    fn assert_cut_closes(mut mesh: Mesh, plane: &Plane) -> (Mesh, CutOutput) {
1005        assert_closed_by_position(&mesh);
1006        let whole = volume6(&mesh);
1007        let triangles = mesh.triangle_count();
1008        let out = cut_kept(&mut mesh, plane, true);
1009        assert_closed_by_position(&mesh);
1010        assert_closed_by_position(&out.removed);
1011        let (vk, vr) = (volume6(&mesh), volume6(&out.removed));
1012        assert!(vk > 0.0 && vr > 0.0, "{vk} {vr}");
1013        assert!(
1014            (vk + vr - whole).abs() < 1e-4 * whole,
1015            "{vk} + {vr} != {whole}"
1016        );
1017        assert_eq!(out.report.kept_triangles, mesh.triangle_count());
1018        assert_eq!(out.report.removed_triangles, out.removed.triangle_count());
1019        assert!(out.report.cap_triangles > 0, "{:?}", out.report);
1020        assert!(mesh.triangle_count() + out.removed.triangle_count() > triangles);
1021        for m in [&mesh, &out.removed] {
1022            assert_eq!(m.uvs().unwrap().len(), 2 * m.vertex_slots());
1023            assert!(m.uvs().unwrap().iter().all(|u| u.is_finite()));
1024        }
1025        (mesh, out)
1026    }
1027
1028    #[test]
1029    fn a_seam_across_the_plane_is_capped_on_both_sides() {
1030        let plane = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1031        let (kept, out) = assert_cut_closes(seamed_tube(16, 3, true), &plane);
1032        // The seam stays: its sides are apart by index on both parts, and
1033        // the cap closes against them by position.
1034        assert!(!boundary_loops(&kept).unwrap().is_empty());
1035        assert!(!boundary_loops(&out.removed).unwrap().is_empty());
1036        // Rim vertices keep the wall's `v` (half way up), so every cap vertex
1037        // blends only values with that `v`.
1038        for m in [&kept, &out.removed] {
1039            let uvs = m.uvs().unwrap();
1040            let on_plane = (0..m.vertex_slots() as VertexId)
1041                .filter(|&v| {
1042                    m.position(v)
1043                        .is_some_and(|p| (f64::from(p[2]) + m.origin()[2] - 1.5).abs() < 1e-5)
1044                })
1045                .collect::<Vec<_>>();
1046            assert!(on_plane.len() >= 17, "{}", on_plane.len());
1047            for v in on_plane {
1048                assert!((uvs[2 * v as usize + 1] - 0.5).abs() < 1e-5, "vertex {v}");
1049                assert!((0.0..=1.0).contains(&uvs[2 * v as usize]), "vertex {v}");
1050            }
1051        }
1052    }
1053
1054    #[test]
1055    fn every_edge_a_seam_is_capped_on_both_sides() {
1056        for p in [
1057            plane([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]),
1058            plane([0.25, 0.5, 0.5], [1.0, 0.25, 0.125]),
1059            plane([0.5, 0.5, 0.5], [1.0, 1.0, 1.0]),
1060        ] {
1061            assert_cut_closes(cube_of_islands(), &p);
1062        }
1063    }
1064
1065    #[test]
1066    fn oblique_cuts_across_a_seam_close() {
1067        for p in [
1068            plane([0.0, 0.0, 1.5], [0.3, 0.2, 1.0]),
1069            plane([0.1, 0.0, 1.2], [1.0, 0.1, 1.0]),
1070            // Through the axis and across the seam at an angle.
1071            plane([0.0, 0.0, 1.5], [0.2, 1.0, 0.1]),
1072        ] {
1073            assert_cut_closes(seamed_tube(24, 3, true), &p);
1074        }
1075    }
1076
1077    #[test]
1078    fn a_plane_through_the_seam_vertices_closes() {
1079        // Through a row of vertices, the two sides of the seam included.
1080        assert_cut_closes(
1081            seamed_tube(16, 4, true),
1082            &plane([0.0, 0.0, 2.0], [0.0, 0.0, 1.0]),
1083        );
1084        assert_cut_closes(
1085            seamed_tube(16, 4, true),
1086            &plane([0.0, 0.0, 2.0], [0.0, 0.0, -1.0]),
1087        );
1088    }
1089
1090    #[test]
1091    fn a_plane_along_the_seam_closes() {
1092        // The seam itself lies in the plane: one side keeps its edges and the
1093        // other side's triangles leave.
1094        assert_cut_closes(
1095            seamed_tube(16, 3, true),
1096            &plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
1097        );
1098        assert_cut_closes(
1099            seamed_tube(16, 3, true),
1100            &plane([0.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
1101        );
1102    }
1103
1104    #[test]
1105    fn a_plane_on_a_face_of_islands_closes_like_a_plain_cube() {
1106        // Only that face stays: a flat sheet, closed against its own cap.
1107        let on_face = plane([0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
1108        let mut plain = cube();
1109        let mut islands = cube_of_islands();
1110        let (a, b) = (
1111            cut_kept(&mut plain, &on_face, true),
1112            cut_kept(&mut islands, &on_face, true),
1113        );
1114        assert!(a.report.cap_triangles > 0 && b.report.cap_triangles > 0);
1115        assert_closed_by_position(&islands);
1116        assert_closed_by_position(&b.removed);
1117    }
1118
1119    #[test]
1120    fn an_open_tube_with_a_seam_gets_its_cut_capped_and_its_ends_left() {
1121        let mut mesh = seamed_tube(16, 3, false);
1122        let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), true);
1123        assert!(out.report.cap_triangles > 0);
1124        // Only the far ends stay open by position.
1125        for m in [&mesh, &out.removed] {
1126            let open = boundary_loops(&welded_by_position(m)).unwrap();
1127            assert_eq!(open.len(), 1, "{open:?}");
1128            assert_eq!(open[0].vertices.len(), 16);
1129        }
1130    }
1131
1132    #[test]
1133    fn an_open_sheet_with_a_seam_stays_open_without_an_error() {
1134        // The cut runs from border to border: no rim closes, so no cap is due.
1135        let mut mesh = seamed_tube(16, 3, false);
1136        let half = plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1137        // Half a tube first, without a cap: an open sheet with no seam left
1138        // on one side, cut again across the rows.
1139        let out = cut_kept(&mut mesh, &half, false);
1140        assert_eq!(out.report.cap_triangles, 0);
1141        let across = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1142        let again = cut_kept(&mut mesh, &across, true);
1143        assert_eq!(again.report.cap_triangles, 0);
1144        assert_eq!(boundary_loops(&mesh).unwrap().len(), 1);
1145    }
1146
1147    #[test]
1148    fn a_rim_at_a_seam_wound_against_itself_is_an_error_and_writes_nothing() {
1149        // The cube's islands, with one crossed face flipped: closed by
1150        // position, but no patch can agree with both neighbours.
1151        let whole = cube_of_islands();
1152        let positions: Vec<f32> = whole.positions().iter().flatten().copied().collect();
1153        let mut triangles = whole.triangles().to_vec();
1154        // Triangles 4 and 5 are the front face (-y), crossed by `x = 0.5`.
1155        triangles[4].swap(1, 2);
1156        triangles[5].swap(1, 2);
1157        let mut mesh = mesh_of(&positions, &triangles);
1158        mesh.add_uvs(whole.uvs().unwrap()).unwrap();
1159        let before = crate::test_util::parts(&mesh);
1160        let mut tx = mesh.begin_edit();
1161        let r = cut_by_plane(
1162            &mut tx,
1163            &plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]),
1164            &CutOptions::default(),
1165        );
1166        assert_eq!(
1167            r.unwrap_err(),
1168            OpsError::InvalidInput("a cut rim cannot be capped")
1169        );
1170        tx.rollback();
1171        assert_eq!(crate::test_util::parts(&mesh), before);
1172    }
1173
1174    #[test]
1175    fn overlaid_seamed_sheets_are_an_error_rather_than_a_silent_gap() {
1176        let one = seamed_tube(16, 3, true);
1177        let (p, t) = crate::test_util::parts(&one);
1178        let base = (p.len() / 3) as u32;
1179        let positions: Vec<f32> = p.iter().chain(&p).copied().collect();
1180        let triangles: Vec<[u32; 3]> = t
1181            .iter()
1182            .copied()
1183            .chain(t.iter().map(|x| x.map(|v| v + base)))
1184            .collect();
1185        let uvs: Vec<f32> = one.uvs().unwrap().repeat(2);
1186        let mut mesh = mesh_of(&positions, &triangles);
1187        mesh.add_uvs(&uvs).unwrap();
1188        let mut tx = mesh.begin_edit();
1189        let r = cut_by_plane(
1190            &mut tx,
1191            &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]),
1192            &CutOptions::default(),
1193        );
1194        assert_eq!(
1195            r.unwrap_err(),
1196            OpsError::InvalidInput("a cut rim cannot be capped")
1197        );
1198        tx.rollback();
1199    }
1200
1201    #[test]
1202    fn cap_off_leaves_a_seamed_mesh_open() {
1203        let mut mesh = seamed_tube(16, 3, true);
1204        let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), false);
1205        assert_eq!(out.report.cap_triangles, 0);
1206        assert!(
1207            !boundary_loops(&welded_by_position(&mesh))
1208                .unwrap()
1209                .is_empty()
1210        );
1211    }
1212
1213    #[test]
1214    fn without_uvs_split_vertices_are_read_by_index_as_before() {
1215        // No UV channel, no seams: duplicate vertices are a real boundary, as
1216        // for every other operation that reads such a mesh. Weld it first.
1217        let seamed = seamed_tube(16, 3, true);
1218        let (p, t) = crate::test_util::parts(&seamed);
1219        let mut mesh = mesh_of(&p, &t);
1220        let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]), true);
1221        assert_eq!(out.report.cap_triangles, 0);
1222        assert!(
1223            !boundary_loops(&welded_by_position(&mesh))
1224                .unwrap()
1225                .is_empty()
1226        );
1227    }
1228
1229    #[test]
1230    fn a_seamless_mesh_with_uvs_caps_exactly_as_one_without() {
1231        let p = plane([0.5, 0.0, 0.0], [1.0, 0.0, 0.0]);
1232        let mut plain = cube();
1233        let mut with_uvs = cube();
1234        with_uvs.add_uvs(&[0.25; 16]).unwrap();
1235        let (a, b) = (
1236            cut_kept(&mut plain, &p, true),
1237            cut_kept(&mut with_uvs, &p, true),
1238        );
1239        assert_eq!(a.report, b.report);
1240        assert_eq!(
1241            crate::test_util::parts(&plain),
1242            crate::test_util::parts(&with_uvs)
1243        );
1244        assert_eq!(
1245            crate::test_util::parts(&a.removed),
1246            crate::test_util::parts(&b.removed)
1247        );
1248    }
1249
1250    #[test]
1251    fn a_crossing_is_the_same_point_from_either_end_when_asked() {
1252        let p = plane([0.1, 0.2, 0.3], [0.3, -0.7, 0.9]);
1253        // A fixed walk over pairs on opposite sides, far from round numbers.
1254        let at = |i: u32, s: f64| {
1255            let t = f64::from(i);
1256            [
1257                s * (1.0 + (t * 0.37).sin()) / 3.0,
1258                s * (t * 0.11).cos() / 7.0,
1259                s * (2.0 + (t * 0.73).cos()) / 11.0,
1260            ]
1261        };
1262        let mut differ = 0;
1263        for i in 0..1000 {
1264            let (a, b) = (at(i, 1.0), at(i + 1, -1.0));
1265            let (sa, sb) = (p.signed_distance(a), p.signed_distance(b));
1266            if (sa >= 0.0) == (sb >= 0.0) {
1267                continue;
1268            }
1269            assert_eq!(
1270                crossing_point(a, b, sa, sb, true),
1271                crossing_point(b, a, sb, sa, true)
1272            );
1273            differ += usize::from(
1274                crossing_point(a, b, sa, sb, false) != crossing_point(b, a, sb, sa, false),
1275            );
1276        }
1277        // Otherwise the test shows nothing: id order does move the point.
1278        assert!(differ > 0);
1279    }
1280
1281    /// `mesh` welded by position, with no UV channel: the same solid, seamless.
1282    fn seamless(mesh: &Mesh) -> Mesh {
1283        let welded = welded_by_position(mesh);
1284        let (p, t) = crate::test_util::parts(&welded);
1285        mesh_of(&p, &t)
1286    }
1287
1288    #[test]
1289    fn a_sweep_of_planes_never_leaves_a_seamed_mesh_open() {
1290        let meshes: [fn() -> Mesh; 3] = [
1291            || seamed_tube(12, 3, true),
1292            || seamed_tube(7, 2, true),
1293            cube_of_islands,
1294        ];
1295        // Round values hit rows, columns and the seam; the others miss them.
1296        let origins = [0.0, 0.25, 0.5, 1.0, 1.5, 0.3137];
1297        let normals = [
1298            [1.0, 0.0, 0.0],
1299            [0.0, 1.0, 0.0],
1300            [0.0, 0.0, 1.0],
1301            [0.0, -1.0, 0.0],
1302            [1.0, 1.0, 0.0],
1303            [1.0, 1.0, 1.0],
1304            [0.3, -0.5, 0.8],
1305            [-0.7, 0.2, 0.1],
1306        ];
1307        let (mut cuts, mut refused) = (0, 0);
1308        for make in meshes {
1309            for &o in &origins {
1310                for &n in &normals {
1311                    let p = plane(n.map(|c: f64| c * o), n);
1312                    let mut mesh = make();
1313                    let whole = volume6(&mesh);
1314                    let mut tx = mesh.begin_edit();
1315                    let r = cut_by_plane(&mut tx, &p, &CutOptions::default());
1316                    let Ok(out) = r else {
1317                        // A plane may miss the mesh. It may also pass within
1318                        // rounding of a wall vertex so that the flat patch
1319                        // would repeat a wall edge, as the docs say: rare.
1320                        let err = r.unwrap_err();
1321                        if err == OpsError::InvalidInput("a cut rim cannot be capped") {
1322                            refused += 1;
1323                        } else {
1324                            assert_eq!(
1325                                err,
1326                                OpsError::InvalidInput("plane does not cut the mesh"),
1327                                "{o} {n:?}"
1328                            );
1329                        }
1330                        tx.rollback();
1331                        continue;
1332                    };
1333                    tx.commit().unwrap();
1334                    cuts += 1;
1335                    assert_closed_by_position(&mesh);
1336                    assert_closed_by_position(&out.removed);
1337                    let sum = volume6(&mesh) + volume6(&out.removed);
1338                    assert!(
1339                        (sum - whole).abs() < 1e-4 * whole,
1340                        "{o} {n:?}: {sum} {whole}"
1341                    );
1342                }
1343            }
1344        }
1345        assert!(cuts > 80, "{cuts}");
1346        assert!(refused <= 3, "{refused}");
1347    }
1348
1349    #[test]
1350    fn a_rim_above_the_search_limit_caps_across_a_seam_too() {
1351        // More rim edges than `dp_limit`, on an hourglass cut at its waist,
1352        // whose walls flare away from the plane on both sides.
1353        let slices = FillOptions::default().dp_limit as u32 + 40;
1354        let tube = seamed_tube(slices, 2, true);
1355        let (mut p, t) = crate::test_util::parts(&tube);
1356        for c in p.as_chunks_mut::<3>().0 {
1357            if c[2] != 1.0 {
1358                c[0] *= 2.0;
1359                c[1] *= 2.0;
1360            }
1361        }
1362        let mut hourglass = mesh_of(&p, &t);
1363        hourglass.add_uvs(tube.uvs().unwrap()).unwrap();
1364        let waist = plane([0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
1365        let (kept, out) = assert_cut_closes(hourglass, &waist);
1366        assert!(kept.triangle_count() > 2 * slices as usize);
1367        assert!(out.removed.triangle_count() > 2 * slices as usize);
1368    }
1369
1370    // ---- large rims
1371
1372    /// A sphere of `stacks` bands and `slices` columns, wound outward, with one
1373    /// UV seam from pole to pole: column `slices` repeats column 0 exactly.
1374    fn seamed_sphere(stacks: u32, slices: u32) -> Mesh {
1375        let (mut positions, mut uvs, mut triangles) = (Vec::new(), Vec::new(), Vec::new());
1376        let id = |i: u32, j: u32| 2 + (i - 1) * (slices + 1) + j;
1377        positions.extend([0.0, 0.0, -1.0, 0.0, 0.0, 1.0]);
1378        uvs.extend([0.5, 0.0, 0.5, 1.0]);
1379        for i in 1..stacks {
1380            let v = i as f32 / stacks as f32;
1381            let lat = std::f32::consts::PI * (v - 0.5);
1382            for j in 0..=slices {
1383                let a = std::f32::consts::TAU * (j % slices) as f32 / slices as f32;
1384                positions.extend([lat.cos() * a.cos(), lat.cos() * a.sin(), lat.sin()]);
1385                uvs.extend([j as f32 / slices as f32, v]);
1386            }
1387        }
1388        for j in 0..slices {
1389            triangles.push([0, id(1, j + 1), id(1, j)]);
1390            triangles.push([1, id(stacks - 1, j), id(stacks - 1, j + 1)]);
1391            for i in 1..stacks - 1 {
1392                let (a, b) = (id(i, j), id(i, j + 1));
1393                let (c, d) = (id(i + 1, j + 1), id(i + 1, j));
1394                triangles.extend([[a, b, c], [a, c, d]]);
1395            }
1396        }
1397        let mut mesh = mesh_of(&positions, &triangles);
1398        mesh.add_uvs(&uvs).unwrap();
1399        mesh
1400    }
1401
1402    #[test]
1403    fn seamed_spheres_and_tubes_cap_past_the_search_limit() {
1404        let across = plane([0.0, 0.0, 0.13], [0.0, 0.0, 1.0]);
1405        let tilted = plane([0.0, 0.0, 0.13], [0.2, 0.1, 1.0]);
1406        let along = plane([0.0, 0.0, 0.0], [0.3, 1.0, 0.0]);
1407        // Through the poles, along the seam and across it, and along columns.
1408        let on_seam = plane([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1409        let off_seam = plane([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1410        for slices in [128, 256] {
1411            for p in [&across, &tilted, &along, &on_seam, &off_seam] {
1412                assert_cut_closes(seamed_sphere(32, slices), p);
1413            }
1414            let mid = plane([0.0, 0.0, 1.5], [0.0, 0.0, 1.0]);
1415            let tilted = plane([0.0, 0.0, 1.5], [0.2, 0.1, 1.0]);
1416            for p in [&mid, &tilted] {
1417                assert_cut_closes(seamed_tube(slices, 3, true), p);
1418            }
1419        }
1420    }
1421
1422    #[test]
1423    fn a_plane_through_a_wall_vertex_caps() {
1424        // Rounding puts the crossings next to the vertex, not on it, so a wall
1425        // edge can join two rim vertices that the flat patch would also join.
1426        // Flipping that diagonal away nearly always works; what is left is the
1427        // documented error, never an open part.
1428        let mut refused = 0;
1429        for slices in [12, 64, 256] {
1430            for (i, j) in [
1431                (1, 0),
1432                (1, slices / 4),
1433                (2, slices / 2),
1434                (2, 3 * slices / 4),
1435            ] {
1436                let a = std::f32::consts::TAU * j as f32 / slices as f32;
1437                let at = [f64::from(a.cos()), f64::from(a.sin()), f64::from(i)];
1438                for n in [[0.3, -0.5, 0.8], [-0.7, 0.2, 0.1], [0.1, 0.9, -0.2]] {
1439                    let mut mesh = seamed_tube(slices, 3, true);
1440                    let mut tx = mesh.begin_edit();
1441                    let p = plane(at, n);
1442                    let r = cut_by_plane(&mut tx, &p, &CutOptions::default());
1443                    tx.rollback();
1444                    match r {
1445                        Ok(_) => {
1446                            assert_cut_closes(seamed_tube(slices, 3, true), &p);
1447                        }
1448                        Err(e) => {
1449                            assert_eq!(e, OpsError::InvalidInput("a cut rim cannot be capped"));
1450                            refused += 1;
1451                        }
1452                    }
1453                }
1454            }
1455        }
1456        assert!(refused <= 3, "{refused} of 36");
1457    }
1458
1459    #[test]
1460    fn moving_a_refused_plane_a_little_caps_it() {
1461        // The case the sweep can refuse: within rounding of a wall vertex.
1462        let n = [-0.7, 0.2, 0.1];
1463        let moved = plane(n.map(|c: f64| c * 1e-3), n);
1464        assert_cut_closes(seamed_tube(12, 3, true), &moved);
1465    }
1466
1467    #[test]
1468    fn a_rim_of_tens_of_thousands_of_edges_caps_across_a_seam() {
1469        let (kept, out) = assert_cut_closes(
1470            seamed_tube(12_000, 2, true),
1471            &plane([0.0, 0.0, 0.5], [0.0, 0.0, 1.0]),
1472        );
1473        // A bounded cap: refined, but not to the wall's density all the way in.
1474        for m in [&kept, &out.removed] {
1475            assert!(m.triangle_count() < 1_000_000, "{}", m.triangle_count());
1476        }
1477    }
1478
1479    #[test]
1480    fn a_seamless_rim_past_the_search_limit_caps_too() {
1481        // The same rim without a seam, with or without UVs.
1482        let p = plane([0.0, 0.0, 0.13], [0.0, 0.0, 1.0]);
1483        let mut plain = seamless(&seamed_sphere(32, 256));
1484        let whole = volume6(&plain);
1485        let out = cut_kept(&mut plain, &p, true);
1486        assert!(boundary_loops(&plain).unwrap().is_empty());
1487        assert!(boundary_loops(&out.removed).unwrap().is_empty());
1488        let sum = volume6(&plain) + volume6(&out.removed);
1489        assert!((sum - whole).abs() < 1e-4 * whole, "{sum} {whole}");
1490    }
1491
1492    // ---- real holes in the plane
1493
1494    /// An open tube standing on the plane `z = 0`, its mouth exactly in it, and
1495    /// a separate box crossing the plane.
1496    fn mouth_on_the_plane_and_a_box(uvs: bool) -> Mesh {
1497        let tube = seamless(&seamed_tube(16, 2, false));
1498        let (mut positions, mut triangles) = crate::test_util::parts(&tube);
1499        let base = (positions.len() / 3) as u32;
1500        let (cube_p, cube_t) = crate::test_util::parts(&cube());
1501        for c in cube_p.as_chunks::<3>().0 {
1502            positions.extend([c[0] + 5.0, c[1], c[2] - 0.5]);
1503        }
1504        triangles.extend(cube_t.iter().map(|t| t.map(|v| v + base)));
1505        let mut mesh = mesh_of(&positions, &triangles);
1506        if uvs {
1507            let values = vec![0.25; 2 * mesh.vertex_slots()];
1508            mesh.add_uvs(&values).unwrap();
1509        }
1510        mesh
1511    }
1512
1513    #[test]
1514    fn a_real_hole_in_the_plane_stays_open_with_or_without_uvs() {
1515        let on_mouth = plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1516        let mut plain = mouth_on_the_plane_and_a_box(false);
1517        let mut with_uvs = mouth_on_the_plane_and_a_box(true);
1518        let (a, b) = (
1519            cut_kept(&mut plain, &on_mouth, true),
1520            cut_kept(&mut with_uvs, &on_mouth, true),
1521        );
1522        assert_eq!(a.report, b.report);
1523        // The tube's two mouths stay open; only the box is capped.
1524        assert_eq!(boundary_loops(&with_uvs).unwrap().len(), 2);
1525        assert_eq!(
1526            boundary_loops(&plain).unwrap().len(),
1527            boundary_loops(&with_uvs).unwrap().len()
1528        );
1529    }
1530
1531    #[test]
1532    fn a_real_hole_with_a_seam_in_the_plane_stays_open() {
1533        // The tube's own seam: its mouth is a real hole all the same.
1534        let tube = seamed_tube(16, 2, false);
1535        let (mut positions, mut triangles) = crate::test_util::parts(&tube);
1536        let mut uvs = tube.uvs().unwrap().to_vec();
1537        let base = (positions.len() / 3) as u32;
1538        let (cube_p, cube_t) = crate::test_util::parts(&cube());
1539        for c in cube_p.as_chunks::<3>().0 {
1540            positions.extend([c[0] + 5.0, c[1], c[2] - 0.5]);
1541            uvs.extend([0.25, 0.25]);
1542        }
1543        triangles.extend(cube_t.iter().map(|t| t.map(|v| v + base)));
1544        let mut mesh = mesh_of(&positions, &triangles);
1545        mesh.add_uvs(&uvs).unwrap();
1546        let out = cut_kept(&mut mesh, &plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]), true);
1547        // Only the box is capped: its two sides, as on a plain box.
1548        let mut plain = mouth_on_the_plane_and_a_box(false);
1549        let want = cut_kept(&mut plain, &plane([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]), true);
1550        assert_eq!(out.report.cap_triangles, want.report.cap_triangles);
1551    }
1552}