Skip to main content

fast_mesh_ops/
subdivide.rs

1//! Mesh subdivision by Loop's scheme.
2//!
3//! [`subdivide`] refines every triangle into four, [`SubdivideOptions::levels`]
4//! times, smoothing the surface toward the limit: each level moves the old
5//! vertices with Loop's vertex stencil and inserts one vertex per edge with
6//! the edge stencil, then splits every triangle at its three edge vertices.
7//! The whole plan is computed on working copies first and committed through
8//! one [`Transaction`](fast_mesh_store::Transaction), which keeps the call
9//! atomic under sweep: either every level lands or nothing does.
10//!
11//! # Boundary and crease rule
12//!
13//! Standard Loop with boundary handling (no crease tags):
14//!
15//! - An edge two triangles share is interior: its new vertex is
16//!   `3/8 (a + b) + 1/8 (c + d)` from its endpoints `a`, `b` and the two
17//!   opposite vertices `c`, `d`. Any other edge (one owner, or three or more
18//!   on a non-manifold rim) is a crease: its new vertex is the midpoint.
19//! - A vertex on no boundary edge is interior: Loop's vertex stencil over
20//!   all its neighbours, with the exact weight
21//!   `beta = (5/8 - (3/8 + cos(2 pi / n) / 4)^2) / n` for valence `n`.
22//! - A vertex on boundary edges with exactly two boundary neighbours is a
23//!   smooth boundary vertex: `3/4 v + 1/8 (b0 + b1)`, so rims relax along
24//!   their own curve and sharp corners round off.
25//! - A vertex on any other number of boundary edges (one, or three or more
26//!   at a pinch or non-manifold corner) is pinned: it stays where it is.
27//! - Isolated vertices, and triangles with a repeated vertex id, pass
28//!   through untouched.
29//!
30//! UVs ([`Mesh::uvs`](fast_mesh_store::Mesh::uvs)) stay linear: an old vertex
31//! keeps its UV even though it moves, and a new one takes its edge's midpoint.
32//! The two sides of a seam are boundaries, so they split in step.
33//!
34//! The stencils are convex combinations, so finite input stays finite, and
35//! every list is sorted explicitly, so results do not depend on hash
36//! iteration order.
37
38use fast_mesh_store::{Transaction, UV_CHANNEL, VertexId};
39
40use crate::{OpsError, Result, push, reserve};
41
42/// Which subdivision scheme to run. Only Loop's scheme exists so far.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum SubdivideMethod {
45    /// Loop's scheme for triangle meshes, with the boundary rule from the
46    /// module docs.
47    #[default]
48    Loop,
49}
50
51/// Options for [`subdivide`].
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct SubdivideOptions {
54    /// Which scheme to run.
55    pub method: SubdivideMethod,
56    /// How many times to split every triangle. Must be at least 1; each
57    /// level quadruples the triangle count.
58    pub levels: u32,
59}
60
61impl Default for SubdivideOptions {
62    fn default() -> Self {
63        SubdivideOptions {
64            method: SubdivideMethod::Loop,
65            levels: 1,
66        }
67    }
68}
69
70impl SubdivideOptions {
71    /// A builder for [`SubdivideOptions`].
72    pub fn builder() -> SubdivideOptionsBuilder {
73        SubdivideOptionsBuilder::default()
74    }
75}
76
77/// Builder for [`SubdivideOptions`].
78#[derive(Debug, Clone, Default)]
79pub struct SubdivideOptionsBuilder {
80    options: SubdivideOptions,
81}
82
83impl SubdivideOptionsBuilder {
84    /// Which scheme to run.
85    pub fn method(mut self, method: SubdivideMethod) -> Self {
86        self.options.method = method;
87        self
88    }
89
90    /// How many times to split every triangle.
91    pub fn levels(mut self, levels: u32) -> Self {
92        self.options.levels = levels;
93        self
94    }
95
96    /// The options as set.
97    pub fn build(self) -> SubdivideOptions {
98        self.options
99    }
100}
101
102/// Outcome of [`subdivide`].
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
104pub struct SubdivideReport {
105    /// Vertices added, one per subdivided edge per level.
106    pub vertices_added: usize,
107    /// Triangles added: three per subdivided triangle per level.
108    pub triangles_added: usize,
109}
110
111/// A working triangle: `slot` is its store slot when it reuses one, `None`
112/// for an added child. Degenerate input triangles are not clean and ride
113/// through untouched.
114struct WorkTri {
115    slot: Option<u32>,
116    corners: [u32; 3],
117    clean: bool,
118}
119
120fn f64v(p: &[f32; 3]) -> [f64; 3] {
121    [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]
122}
123
124/// The undirected key of the edge `a`-`b`.
125fn key(a: u32, b: u32) -> (u32, u32) {
126    if a < b { (a, b) } else { (b, a) }
127}
128
129/// Loop's vertex weight for valence `n`.
130fn loop_beta(n: usize) -> f64 {
131    use std::f64::consts::PI;
132    let n = n as f64;
133    (5.0 / 8.0 - (3.0 / 8.0 + 0.25 * (2.0 * PI / n).cos()).powi(2)) / n
134}
135
136/// One Loop level over the working mesh: smooths every live vertex and
137/// splits every clean triangle into four, appending the new vertices' UVs to
138/// `uvs` when given. Returns whether any triangle was subdivided.
139fn step(
140    pos: &mut Vec<[f32; 3]>,
141    vlive: &mut Vec<bool>,
142    tris: &mut Vec<WorkTri>,
143    uvs: Option<&mut Vec<f32>>,
144) -> Result<bool> {
145    let clean = tris.iter().filter(|t| t.clean).count();
146    if clean == 0 {
147        return Ok(false);
148    }
149    let slots = pos.len();
150
151    // Edge entries `(edge, triangle, apex)`, sorted by edge so each group is
152    // one unique edge with its owners.
153    let mut entries: Vec<((u32, u32), u32, u32)> = Vec::new();
154    reserve(
155        &mut entries,
156        clean.checked_mul(3).ok_or(crate::OpsError::Store(
157            fast_mesh_store::StoreError::OutOfMemory,
158        ))?,
159    )?;
160    for (i, tri) in tris.iter().enumerate() {
161        if !tri.clean {
162            continue;
163        }
164        let [a, b, c] = tri.corners;
165        push(&mut entries, (key(a, b), i as u32, c))?;
166        push(&mut entries, (key(b, c), i as u32, a))?;
167        push(&mut entries, (key(c, a), i as u32, b))?;
168    }
169    entries.sort_unstable();
170
171    // Unique edges in order, with their owner count and apexes.
172    let mut edges: Vec<(u32, u32)> = Vec::new();
173    reserve(&mut edges, entries.len())?;
174    let mut owners: Vec<usize> = Vec::new();
175    reserve(&mut owners, entries.len())?;
176    let mut apexes: Vec<[u32; 2]> = Vec::new();
177    reserve(&mut apexes, entries.len())?;
178    {
179        let mut start = 0;
180        while start < entries.len() {
181            let mut end = start + 1;
182            while end < entries.len() && entries[end].0 == entries[start].0 {
183                end += 1;
184            }
185            let (a, b) = entries[start].0;
186            push(&mut edges, (a, b))?;
187            push(&mut owners, end - start)?;
188            let mut pair = [entries[start].2, entries[start].2];
189            if end - start == 2 {
190                pair[1] = entries[start + 1].2;
191            }
192            push(&mut apexes, pair)?;
193            start = end;
194        }
195    }
196
197    // Neighbours from clean triangles, and boundary neighbours from edges
198    // one clean triangle uses.
199    let mut nbs: Vec<Vec<u32>> = Vec::new();
200    reserve(&mut nbs, slots)?;
201    nbs.resize_with(slots, Vec::new);
202    let mut bnd: Vec<Vec<u32>> = Vec::new();
203    reserve(&mut bnd, slots)?;
204    bnd.resize_with(slots, Vec::new);
205    for tri in tris.iter() {
206        if !tri.clean {
207            continue;
208        }
209        let [a, b, c] = tri.corners;
210        for (v, w) in [(a, b), (a, c), (b, a), (b, c), (c, a), (c, b)] {
211            push(&mut nbs[v as usize], w)?;
212        }
213    }
214    for (k, &(a, b)) in edges.iter().enumerate() {
215        if owners[k] == 1 {
216            push(&mut bnd[a as usize], b)?;
217            push(&mut bnd[b as usize], a)?;
218        }
219    }
220    for v in 0..slots {
221        if vlive[v] {
222            nbs[v].sort_unstable();
223            nbs[v].dedup();
224            bnd[v].sort_unstable();
225            bnd[v].dedup();
226        }
227    }
228
229    // Smoothed positions for the old vertices.
230    let mut upd: Vec<[f32; 3]> = Vec::new();
231    reserve(&mut upd, slots)?;
232    upd.extend_from_slice(pos);
233    for v in 0..slots as u32 {
234        if !vlive[v as usize] {
235            continue;
236        }
237        let p = f64v(&pos[v as usize]);
238        let b = &bnd[v as usize];
239        if !b.is_empty() {
240            // Smooth boundary vertices relax along their curve; corners
241            // with any other boundary-neighbour count stay pinned.
242            if b.len() == 2 {
243                let q0 = f64v(&pos[b[0] as usize]);
244                let q1 = f64v(&pos[b[1] as usize]);
245                let q = [0, 1, 2].map(|k| 0.75 * p[k] + 0.125 * (q0[k] + q1[k]));
246                upd[v as usize] = [q[0] as f32, q[1] as f32, q[2] as f32];
247            }
248            continue;
249        }
250        let n = nbs[v as usize].len();
251        if n == 0 {
252            continue;
253        }
254        let beta = loop_beta(n);
255        let mut q = [0, 1, 2].map(|k| (1.0 - n as f64 * beta) * p[k]);
256        for &w in &nbs[v as usize] {
257            let r = f64v(&pos[w as usize]);
258            for k in 0..3 {
259                q[k] += beta * r[k];
260            }
261        }
262        upd[v as usize] = [q[0] as f32, q[1] as f32, q[2] as f32];
263    }
264
265    // One new vertex per unique edge, in edge order.
266    let mut edge_point: Vec<[f32; 3]> = Vec::new();
267    reserve(&mut edge_point, edges.len())?;
268    for (k, &(a, b)) in edges.iter().enumerate() {
269        let pa = f64v(&pos[a as usize]);
270        let pb = f64v(&pos[b as usize]);
271        let q = if owners[k] == 2 {
272            let pc = f64v(&pos[apexes[k][0] as usize]);
273            let pd = f64v(&pos[apexes[k][1] as usize]);
274            [0, 1, 2].map(|i| 0.375 * (pa[i] + pb[i]) + 0.125 * (pc[i] + pd[i]))
275        } else {
276            [0, 1, 2].map(|i| 0.5 * (pa[i] + pb[i]))
277        };
278        push(&mut edge_point, [q[0] as f32, q[1] as f32, q[2] as f32])?;
279    }
280
281    // Split every clean triangle into four; the first child reuses its slot.
282    let degenerate = tris.len() - clean;
283    let mut next: Vec<WorkTri> = Vec::new();
284    reserve(
285        &mut next,
286        clean
287            .checked_mul(4)
288            .and_then(|n| n.checked_add(degenerate))
289            .ok_or(crate::OpsError::Store(
290                fast_mesh_store::StoreError::OutOfMemory,
291            ))?,
292    )?;
293    // `edges` is sorted: the new id of `(a, b)` via binary search.
294    let edge_id = |a: u32, b: u32| -> u32 {
295        let k = edges.binary_search(&key(a, b)).expect("edge must exist");
296        slots as u32 + k as u32
297    };
298    for tri in tris.iter() {
299        if !tri.clean {
300            push(
301                &mut next,
302                WorkTri {
303                    slot: tri.slot,
304                    corners: tri.corners,
305                    clean: false,
306                },
307            )?;
308            continue;
309        }
310        let [a, b, c] = tri.corners;
311        let (ab, bc, ca) = (edge_id(a, b), edge_id(b, c), edge_id(c, a));
312        let children = [[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]];
313        for (j, corners) in children.into_iter().enumerate() {
314            push(
315                &mut next,
316                WorkTri {
317                    slot: if j == 0 { tri.slot } else { None },
318                    corners,
319                    clean: true,
320                },
321            )?;
322        }
323    }
324
325    if let Some(uv) = uvs {
326        reserve(uv, 2 * edges.len())?;
327        for &(a, b) in &edges {
328            let (ia, ib) = (2 * a as usize, 2 * b as usize);
329            let mid = [0, 1].map(|c| 0.5 * (uv[ia + c] + uv[ib + c]));
330            uv.extend_from_slice(&mid);
331        }
332    }
333    *pos = upd;
334    reserve(pos, edge_point.len())?;
335    pos.extend_from_slice(&edge_point);
336    reserve(vlive, edge_point.len())?;
337    vlive.resize(vlive.len() + edge_point.len(), true);
338    *tris = next;
339    Ok(true)
340}
341
342/// Subdivides the mesh [`SubdivideOptions::levels`] times by Loop's scheme
343/// (see the module docs). Each level quadruples the clean triangle count:
344/// every triangle becomes four, with one new vertex per subdivided edge.
345/// Degenerate triangles and isolated vertices pass through untouched.
346///
347/// A mesh with no clean triangle succeeds without writing. Otherwise the
348/// patch is non-empty and the report counts what the levels added.
349///
350/// # Errors
351/// [`OpsError::InvalidInput`] for zero levels; [`OpsError::Store`] when
352/// memory runs out.
353pub fn subdivide(tx: &mut Transaction, options: &SubdivideOptions) -> Result<SubdivideReport> {
354    if options.levels == 0 {
355        return Err(OpsError::InvalidInput(
356            "subdivide levels must be at least 1",
357        ));
358    }
359    let _ = options.method;
360    let mesh = tx.mesh();
361    let slots = mesh.vertex_slots();
362    let mut pos: Vec<[f32; 3]> = Vec::new();
363    reserve(&mut pos, slots)?;
364    pos.extend_from_slice(mesh.positions());
365    let mut vlive: Vec<bool> = Vec::new();
366    reserve(&mut vlive, slots)?;
367    vlive.extend((0..slots as u32).map(|v| mesh.is_vertex_alive(v as VertexId)));
368    let mut tris: Vec<WorkTri> = Vec::new();
369    reserve(&mut tris, mesh.triangle_slots())?;
370    for t in 0..mesh.triangle_slots() as u32 {
371        let Some(corners) = mesh.triangle(t) else {
372            continue;
373        };
374        push(
375            &mut tris,
376            WorkTri {
377                slot: Some(t),
378                corners,
379                clean: corners[0] != corners[1]
380                    && corners[1] != corners[2]
381                    && corners[2] != corners[0],
382            },
383        )?;
384    }
385
386    // The working UVs, on a mesh with them.
387    let mut uvs: Option<Vec<f32>> = None;
388    if let Some(from) = mesh.uvs() {
389        let mut copy = Vec::new();
390        reserve(&mut copy, from.len())?;
391        copy.extend_from_slice(from);
392        uvs = Some(copy);
393    }
394    let mut subdivided = false;
395    for _ in 0..options.levels {
396        if !step(&mut pos, &mut vlive, &mut tris, uvs.as_mut())? {
397            break;
398        }
399        subdivided = true;
400    }
401    if !subdivided {
402        return Ok(SubdivideReport::default());
403    }
404
405    // Plan the writes: changed originals, added vertices, reused slots, and
406    // added triangles with synthetic ids past `slots`.
407    let base = mesh.positions();
408    let mut set_p: Vec<VertexId> = Vec::new();
409    let mut set_p_to: Vec<[f32; 3]> = Vec::new();
410    let mut new_p: Vec<[f32; 3]> = Vec::new();
411    let mut set_t: Vec<u32> = Vec::new();
412    let mut set_t_to: Vec<[u32; 3]> = Vec::new();
413    let mut add_t: Vec<[u32; 3]> = Vec::new();
414    let mut n_moved = 0;
415    for v in 0..slots as u32 {
416        if vlive[v as usize] && (v as usize) < pos.len() && pos[v as usize] != base[v as usize] {
417            n_moved += 1;
418        }
419    }
420    for v in slots..pos.len() {
421        if vlive[v] {
422            reserve(&mut new_p, 1)?;
423            push(&mut new_p, pos[v])?;
424        }
425    }
426    reserve(&mut set_p, n_moved)?;
427    reserve(&mut set_p_to, n_moved)?;
428    let mut n_set_t = 0;
429    let mut n_add_t = 0;
430    for tri in &tris {
431        if tri.slot.is_some() {
432            n_set_t += 1;
433        } else {
434            n_add_t += 1;
435        }
436    }
437    reserve(&mut set_t, n_set_t)?;
438    reserve(&mut set_t_to, n_set_t)?;
439    reserve(&mut add_t, n_add_t)?;
440    for v in 0..slots as u32 {
441        if vlive[v as usize] && pos[v as usize] != base[v as usize] {
442            set_p.push(v as VertexId);
443            set_p_to.push(pos[v as usize]);
444        }
445    }
446    for tri in &tris {
447        if let Some(slot) = tri.slot {
448            set_t.push(slot);
449            set_t_to.push(tri.corners);
450        } else {
451            add_t.push(tri.corners);
452        }
453    }
454    // Every reservation the commit needs is made before the first write, so
455    // a later failure cannot leave a partial patch behind.
456    let mut real: Vec<u32> = Vec::new();
457    reserve(&mut real, pos.len())?;
458    let mut set_mapped: Vec<[VertexId; 3]> = Vec::new();
459    reserve(&mut set_mapped, set_t_to.len())?;
460    let mut add_mapped: Vec<[VertexId; 3]> = Vec::new();
461    reserve(&mut add_mapped, add_t.len())?;
462    tx.set_positions(&set_p, &set_p_to)?;
463    // Added vertices take the next free slots, which need not follow the
464    // synthetic order when the mesh has dead slots: map them over. Every
465    // vector below already holds its capacity, so filling them allocates
466    // nothing.
467    let added = tx.add_vertices(&new_p)?;
468    real.extend(0..pos.len() as u32);
469    {
470        let mut k = 0;
471        for v in slots..pos.len() {
472            if vlive[v] {
473                real[v] = added[k];
474                k += 1;
475            }
476        }
477    }
478    let map = |c: [u32; 3]| c.map(|v| real[v as usize] as VertexId);
479    set_mapped.extend(set_t_to.iter().map(|&c| map(c)));
480    add_mapped.extend(add_t.iter().map(|&c| map(c)));
481    if let Some(uv) = &uvs {
482        tx.write_channel(UV_CHANNEL, &added, &uv[2 * slots..])?;
483    }
484    tx.set_triangles(&set_t, &set_mapped)?;
485    tx.add_triangles(&add_mapped)?;
486    Ok(SubdivideReport {
487        vertices_added: added.len(),
488        triangles_added: add_mapped.len(),
489    })
490}
491
492#[cfg(test)]
493mod tests {
494    use fast_mesh_store::{Mesh, Transaction, VertexId};
495
496    use super::*;
497    use crate::test_util::{box_mesh, edit, grid, mesh_of, parts, sweep};
498
499    fn live_count(mesh: &Mesh) -> usize {
500        (0..mesh.triangle_slots() as u32)
501            .filter(|&t| mesh.triangle(t).is_some())
502            .count()
503    }
504
505    fn live_vertices(mesh: &Mesh) -> usize {
506        (0..mesh.vertex_slots() as u32)
507            .filter(|&v| mesh.is_vertex_alive(v as VertexId))
508            .count()
509    }
510
511    /// Unique-edge and boundary-edge counts over the live triangles.
512    fn edge_counts(mesh: &Mesh) -> (usize, usize) {
513        let mut pairs = Vec::new();
514        for t in 0..mesh.triangle_slots() as u32 {
515            if let Some(c) = mesh.triangle(t) {
516                if c[0] == c[1] || c[1] == c[2] || c[2] == c[0] {
517                    continue;
518                }
519                for k in 0..3 {
520                    pairs.push(key(c[k], c[(k + 1) % 3]));
521                }
522            }
523        }
524        pairs.sort_unstable();
525        let mut unique = 0;
526        let mut boundary = 0;
527        let mut start = 0;
528        while start < pairs.len() {
529            let mut end = start + 1;
530            while end < pairs.len() && pairs[end] == pairs[start] {
531                end += 1;
532            }
533            unique += 1;
534            if end - start == 1 {
535                boundary += 1;
536            }
537            start = end;
538        }
539        (unique, boundary)
540    }
541
542    fn assert_no_degenerate(mesh: &Mesh) {
543        for t in 0..mesh.triangle_slots() as u32 {
544            if let Some(c) = mesh.triangle(t) {
545                assert!(
546                    c[0] != c[1] && c[1] != c[2] && c[2] != c[0],
547                    "degenerate: {c:?}"
548                );
549                for &v in &c {
550                    assert!(mesh.is_vertex_alive(v as VertexId));
551                }
552            }
553        }
554    }
555
556    #[test]
557    fn cube_quadruples_closed() {
558        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
559        assert_eq!(live_count(&mesh), 12);
560        let (unique, boundary) = edge_counts(&mesh);
561        assert_eq!(boundary, 0);
562        let options = SubdivideOptions::builder().levels(1).build();
563        let (report, empty) = edit(&mut mesh, |tx| subdivide(tx, &options));
564        assert!(!empty);
565        assert_eq!(live_count(&mesh), 48);
566        assert_eq!(live_vertices(&mesh), 8 + unique);
567        assert_eq!(report.vertices_added, unique);
568        assert_eq!(report.triangles_added, 36);
569        assert_no_degenerate(&mesh);
570    }
571
572    #[test]
573    fn plane_quadruples_and_stays_planar() {
574        // A 2x2 grid: 8 triangles, 8 boundary edges. Loop is affine
575        // invariant, so the flat sheet stays at z = 0; each boundary edge
576        // splits in two.
577        let mut mesh = grid(2);
578        assert_eq!(live_count(&mesh), 8);
579        let (unique, boundary) = edge_counts(&mesh);
580        assert_eq!(boundary, 8);
581        let options = SubdivideOptions::builder().levels(1).build();
582        let (report, empty) = edit(&mut mesh, |tx| subdivide(tx, &options));
583        assert!(!empty);
584        assert_eq!(live_count(&mesh), 32);
585        assert_eq!(live_vertices(&mesh), 9 + unique);
586        assert_eq!(report.vertices_added, unique);
587        assert_eq!(report.triangles_added, 24);
588        for v in 0..mesh.vertex_slots() as u32 {
589            if mesh.is_vertex_alive(v as VertexId) {
590                assert_eq!(mesh.positions()[v as usize][2], 0.0);
591            }
592        }
593        assert_eq!(edge_counts(&mesh).1, 16);
594        assert_no_degenerate(&mesh);
595    }
596
597    #[test]
598    fn two_levels_multiply_by_sixteen() {
599        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
600        let options = SubdivideOptions::builder().levels(2).build();
601        let (report, empty) = edit(&mut mesh, |tx| subdivide(tx, &options));
602        assert!(!empty);
603        assert_eq!(live_count(&mesh), 12 * 16);
604        assert_eq!(live_vertices(&mesh), 8 + report.vertices_added);
605        assert_eq!(report.triangles_added, 12 * 16 - 12);
606        assert_no_degenerate(&mesh);
607    }
608
609    #[test]
610    fn invalid_levels_and_noop_mesh() {
611        let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
612        {
613            let mut tx = mesh.begin_edit();
614            assert_eq!(
615                subdivide(&mut tx, &SubdivideOptions::builder().levels(0).build()),
616                Err(OpsError::InvalidInput(
617                    "subdivide levels must be at least 1"
618                ))
619            );
620        }
621        // An empty mesh succeeds without writing.
622        let mut empty_mesh = mesh_of(&[], &[]);
623        let before = parts(&empty_mesh);
624        let (report, empty) = edit(&mut empty_mesh, |tx| {
625            subdivide(tx, &SubdivideOptions::builder().levels(1).build())
626        });
627        assert_eq!(
628            (report.vertices_added, report.triangles_added, empty),
629            (0, 0, true)
630        );
631        assert_eq!(parts(&empty_mesh), before);
632        // Degenerate triangles ride through untouched.
633        let mut degenerate = mesh_of(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[[0, 0, 1]]);
634        let before = parts(&degenerate);
635        let (report, empty) = edit(&mut degenerate, |tx| {
636            subdivide(tx, &SubdivideOptions::builder().levels(1).build())
637        });
638        assert_eq!(
639            (report.vertices_added, report.triangles_added, empty),
640            (0, 0, true)
641        );
642        assert_eq!(parts(&degenerate), before);
643    }
644
645    #[test]
646    fn subdivide_sweep_keeps_atomicity() {
647        let n = sweep(grid(2), |tx: &mut Transaction| {
648            subdivide(tx, &SubdivideOptions::builder().levels(1).build())?;
649            Ok(())
650        });
651        assert!(n > 0);
652    }
653}