Skip to main content

mesh_graph/ops/
subdivide.rs

1use hashbrown::HashSet;
2use tracing::instrument;
3
4use crate::{
5    HalfedgeId, MeshGraph, Selection, SelectionOps, VertexId, error_none,
6    ops::{EdgeLengthCleanup, PendingEdges, PendingOrder},
7    utils::unwrap_or_return,
8};
9
10impl MeshGraph {
11    /// Subdivide all edges until all of them are <= max_length.
12    /// Please note that you have to provide the squared value of max_length.
13    ///
14    /// Returns whether every edge ended up below the threshold, or the operation
15    /// ran out of its work bound first — see [`EdgeLengthCleanup`].
16    ///
17    /// This will schedule necessary updates to the QBVH but you have to call
18    /// `refit_bvh()` after the operation.
19    #[instrument(skip(self))]
20    pub fn subdivide_until_edges_below_max_length(
21        &mut self,
22        max_length_squared: f32,
23        marked_halfedge_ids: &mut HashSet<HalfedgeId>,
24        marked_vertex_ids: &mut HashSet<VertexId>,
25    ) -> EdgeLengthCleanup {
26        #[cfg(feature = "instrumentation")]
27        crate::set_current_op("subdivide");
28        #[cfg(feature = "instrumentation")]
29        crate::probe_chain_begin(self);
30        let pending = self.halfedges_map(|len_sqr| len_sqr > max_length_squared);
31
32        // Bound the work by the initial problem size, not the mesh size: in stretched
33        // regions (e.g. the punch band) splitting the longest edge of a triangle can
34        // re-create a fan edge above the threshold, so the set can fail to drain and the
35        // old `halfedges.len()` bound let the op burn the whole halfedge count per call
36        // while growing the mesh ~6x per iteration. Healthy meshes drain at ~1.2x the
37        // initial set size, so 2x leaves ample headroom and caps pathological blowups.
38        //
39        // The additive term scales with how *deep* the refinement has to go, because an
40        // edge `r` times too long becomes `r` pieces and each split also splits the two
41        // adjacent faces - so the work per over-long edge grows with `r`, not with the
42        // number of over-long edges. A flat constant ignored that and made the bound bite
43        // on small meshes needing deep refinement: a 6-edge tetrahedron taken to a
44        // target of 0.2 needs ~200 splits but got a budget of 112, stopping less than
45        // halfway through. On production meshes `r` is barely above 1, so this is within
46        // a few hundred of the old value and the blowup guard is unchanged.
47        let depth = if max_length_squared > 0.0 {
48            let longest = pending.values().copied().fold(0.0_f32, f32::max);
49            if longest.is_finite() {
50                ((longest / max_length_squared).sqrt().ceil() as usize).clamp(1, 64)
51            } else {
52                1
53            }
54        } else {
55            1
56        };
57
58        let budget = pending.len() * 2 + 100 * depth;
59
60        let mut halfedges_to_subdivide = PendingEdges::new(pending, PendingOrder::LongestFirst);
61
62        for _ in 0..budget {
63            if halfedges_to_subdivide.is_empty() {
64                break;
65            }
66
67            // #[cfg(feature = "rerun")]
68            // self.log_hes_rerun(
69            //     "subdivide/selection",
70            //     &halfedges_to_subdivide
71            //         .iter()
72            //         .map(|(he, _)| *he)
73            //         .collect::<Vec<_>>(),
74            // );
75
76            let Some((max_he_id, _)) = halfedges_to_subdivide.pop_live() else {
77                // `PendingEdges::insert` pushes whenever it changes the map, so
78                // running out of live heap entries means the set really is empty.
79                debug_assert!(
80                    halfedges_to_subdivide.is_empty(),
81                    "heap drained but {} pending edges remain - a push site is missing",
82                    halfedges_to_subdivide.len()
83                );
84                break;
85            };
86
87            halfedges_to_subdivide.remove(&max_he_id);
88
89            let mut affected_faces = Selection::default();
90
91            // already checked
92            let max_he = self.halfedges[max_he_id];
93            if let Some(face_id) = max_he.face {
94                affected_faces.insert(face_id);
95            }
96
97            if let Some(twin_id) = max_he.twin.or_else(error_none!("Twin missing"))
98                && let Some(twin_he) = self
99                    .halfedges
100                    .get(twin_id)
101                    .or_else(error_none!("Halfedge not found"))
102                && let Some(twin_face_id) = twin_he.face
103            {
104                affected_faces.insert(twin_face_id);
105            }
106
107            let subdivide_edge_result = unwrap_or_return!(
108                self.subdivide_edge(max_he_id),
109                "Couldn't subdivide edge",
110                EdgeLengthCleanup::Stalled
111            );
112
113            if marked_halfedge_ids.contains(&max_he_id) {
114                marked_halfedge_ids.extend(subdivide_edge_result.added_halfedges.iter().copied());
115                marked_vertex_ids.insert(subdivide_edge_result.added_vertex);
116            }
117
118            // #[cfg(feature = "rerun")]
119            // {
120            //     crate::RR
121            //         .log("meshgraph/subdivide", &rerun::Clear::recursive())
122            //         .unwrap();
123            //     crate::RR
124            //         .log("meshgraph/halfedge/subdivide", &rerun::Clear::recursive())
125            //         .unwrap();
126            //     crate::RR
127            //         .log("meshgraph/face/subdivide", &rerun::Clear::recursive())
128            //         .unwrap();
129            //     self.log_rerun();
130            // }
131
132            for new_he_id in subdivide_edge_result.added_halfedges {
133                // newly inserted in `self.subdivide_edge`
134                let new_he = self.halfedges[new_he_id];
135
136                if let Some(face_id) = new_he.face {
137                    affected_faces.insert(face_id);
138                }
139
140                let new_twin_id =
141                    unwrap_or_return!(new_he.twin, "New twin missing", EdgeLengthCleanup::Stalled);
142                let new_twin = unwrap_or_return!(
143                    self.halfedges.get(new_twin_id),
144                    "New twin not found",
145                    EdgeLengthCleanup::Stalled
146                );
147
148                if let Some(face_id) = new_twin.face {
149                    affected_faces.insert(face_id);
150                    #[cfg(feature = "rerun")]
151                    self.log_face_rerun("subdivide/selected_new", face_id);
152                }
153            }
154
155            let mut new_hes_to_check = HashSet::new();
156
157            for he_id in affected_faces.resolve_to_halfedges(self) {
158                let he = unwrap_or_return!(
159                    self.halfedges.get(he_id),
160                    "Halfedge not found",
161                    EdgeLengthCleanup::Stalled
162                );
163                let twin_id =
164                    unwrap_or_return!(he.twin, "Twin missing", EdgeLengthCleanup::Stalled);
165
166                new_hes_to_check.insert(he_id.min(twin_id));
167            }
168
169            // Only ever inserts, never removes, which relies on `subdivide_edge` moving
170            // no existing vertex (pinned by
171            // `test_subdivide_adds_geometry_and_moves_no_existing_vertex`): every edge
172            // here either kept both endpoints and so kept its length, or is new. The one
173            // edge that does get shorter is the subdivided one, and it was dropped from
174            // the pending set above before the split. Were vertices ever moved, an edge
175            // that fell below the threshold would have to be removed here or it would
176            // sit pending forever and eventually be split again below `max_length`.
177            for he_id in new_hes_to_check {
178                let he = self.halfedges[he_id]; // checked above
179
180                let len_sqr = he.length_squared(self);
181
182                if len_sqr > max_length_squared {
183                    #[cfg(feature = "rerun")]
184                    self.log_he_rerun("/subdivide/new_edge", he_id);
185
186                    halfedges_to_subdivide.insert(he_id, len_sqr);
187                }
188            }
189        }
190
191        // Subdivision itself only adds geometry, but a marked set is long-lived and may
192        // still carry ids invalidated by an earlier operation. Both ops hand back only
193        // live keys.
194        marked_vertex_ids.retain(|v_id| self.vertices.contains_key(*v_id));
195        marked_halfedge_ids.retain(|he_id| self.halfedges.contains_key(*he_id));
196
197        #[cfg(feature = "instrumentation")]
198        if self.probe_chain_integrity("subdivide_until_edges_below_max_length") {
199            crate::state_history_push(self, "subdivide_until_edges_below_max_length");
200        }
201
202        #[cfg(feature = "rerun")]
203        self.log_rerun();
204
205        // The loop only exits early once the pending set drains, so anything left in
206        // it means the budget ran out with work outstanding.
207        if halfedges_to_subdivide.is_empty() {
208            EdgeLengthCleanup::Converged
209        } else {
210            EdgeLengthCleanup::Stalled
211        }
212    }
213
214    /// Subdivides an edge by computing it's center vertex. This also subdivides any adjacent triangles and
215    /// makes sure everything is properly reconnected. Works only on triangle meshes.
216    ///
217    /// Returns the id of the new halfedge which goes from the center vertex to the original edge's end vertex.
218    /// And also return the halfedges that are created by subdividing the adjacent faces. Only one of the two twin
219    /// halfedges per face subdivision is returned. In total the number `n` of halfedges returned is `1 <= n <= 3`.
220    /// (The one from dividing the halfedge and at most 2 from dividing the two adjacent faces).
221    ///
222    /// Also returns the created vertex id.
223    #[instrument(skip(self))]
224    pub fn subdivide_edge(&mut self, halfedge_id: HalfedgeId) -> Option<SubdivideEdge> {
225        let mut added_halfedges = Vec::with_capacity(3);
226
227        let he = self
228            .halfedges
229            .get(halfedge_id)
230            .or_else(error_none!("Halfedge not found"))?;
231        let twin_id = he.twin.or_else(error_none!("Twin halfedge not found"))?;
232
233        let start_v = he
234            .start_vertex(self)
235            .or_else(error_none!("Start vertex not found"))?;
236        let end_v = he.end_vertex;
237
238        let start_pos = self
239            .positions
240            .get(start_v)
241            .or_else(error_none!("Start position not found"))?;
242        let end_pos = self
243            .positions
244            .get(end_v)
245            .or_else(error_none!("End position not found"))?;
246
247        let center_pos = (start_pos + end_pos) * 0.5;
248
249        // #[cfg(feature = "rerun")]
250        // {
251        //     crate::RR
252        //         .log(
253        //             "meshgraph/subdivide/edge",
254        //             &rerun::Arrows3D::from_vectors([vec3_array(end_pos - start_pos)])
255        //                 .with_origins([vec3_array(start_pos)]),
256        //         )
257        //         .unwrap();
258
259        //     crate::RR
260        //         .log(
261        //             "meshgraph/subdivide/center",
262        //             &rerun::Points3D::new([vec3_array(center_pos)]),
263        //         )
264        //         .unwrap();
265        // }
266
267        let center_v = self.add_vertex(center_pos);
268        if let Some(normals) = &mut self.vertex_normals {
269            let start_normal = normals
270                .get(start_v)
271                .or_else(error_none!("Start normal not found"))?;
272            let end_normal = normals
273                .get(end_v)
274                .or_else(error_none!("End normal not found"))?;
275            normals.insert(center_v, (start_normal + end_normal).normalize());
276        }
277
278        let new_he = self.add_halfedge(center_v, end_v)?;
279        // inserted just above
280        self.vertices[center_v].outgoing_halfedge = Some(new_he);
281
282        added_halfedges.push(new_he);
283
284        if let Some(new_face_he) = self.subdivide_face(halfedge_id, new_he, center_v) {
285            added_halfedges.push(new_face_he);
286        } else {
287            // The face side of the subdivided edge is boundary: `subdivide_face` early-returns
288            // without re-pointing `halfedge_id` at the center vertex. Do it here so the twin
289            // re-pairing below yields a consistent pair (the boundary halfedge of the
290            // subdivided edge) instead of pairing `new_he` with a halfedge that still ends at
291            // the old start vertex (which misattributes `new_he` and breaks the boundary).
292            self.halfedges[halfedge_id].end_vertex = center_v;
293        }
294
295        let new_twin = self.add_halfedge(center_v, start_v)?;
296
297        if let Some(new_face_he) = self.subdivide_face(twin_id, new_twin, center_v) {
298            added_halfedges.push(new_face_he);
299        } else {
300            // Same as above for the twin side (the twin of the subdivided edge is a boundary
301            // halfedge without a face).
302            self.halfedges[twin_id].end_vertex = center_v;
303        }
304
305        // inserted above
306        self.halfedges[new_he].twin = Some(twin_id);
307        self.halfedges
308            .get_mut(twin_id)
309            .or_else(error_none!("Twin halfedge not found"))?
310            .twin = Some(new_he);
311
312        // checked in the beginning of the function
313        self.halfedges[halfedge_id].twin = Some(new_twin);
314        // inserted above
315        self.halfedges[new_twin].twin = Some(halfedge_id);
316
317        // self.vertices[end_v].outgoing_halfedge = Some(new_twin);
318        // self.vertices[start_v].outgoing_halfedge = Some(new_he);
319
320        Some(SubdivideEdge {
321            added_halfedges,
322            added_vertex: center_v,
323        })
324    }
325
326    /// Subdivides a triangle into two halves. Used in [Self::subdivide_edge].
327    #[instrument(skip(self))]
328    fn subdivide_face(
329        &mut self,
330        existing_halfedge_id: HalfedgeId,
331        new_halfedge_id: HalfedgeId,
332        center_v: VertexId,
333    ) -> Option<HalfedgeId> {
334        let he = self
335            .halfedges
336            .get(existing_halfedge_id)
337            .or_else(error_none!("Halfedge not found"))?;
338
339        let face_id = he.face?;
340        self.faces
341            .get_mut(face_id)
342            .or_else(error_none!("Facee not found"))?
343            .halfedge = existing_halfedge_id;
344
345        // checked above
346        let next_he = self.halfedges[existing_halfedge_id]
347            .next
348            .or_else(error_none!("Next halfedge missing"))?;
349        let last_he = self
350            .halfedges
351            .get(next_he)
352            .or_else(error_none!("Next halfedge not found"))?
353            .next
354            .or_else(error_none!("Last halfedge not found"))?;
355
356        // Validate everything that can fail *before* mutating. The re-wiring below
357        // splices a new halfedge into the face chain and creates `new_he`;
358        // aborting in between (e.g. `add_halfedge(next_he.end, ...)` needs
359        // `next_he.end` to be a live vertex) would leave the chain spliced with an
360        // unpaired halfedge and the old `next_he` orphaned — the layer-4 corruption.
361        if !self
362            .vertices
363            .contains_key(self.halfedges[next_he].end_vertex)
364        {
365            tracing::error!("subdivide_face: next halfedge {next_he:?} ends at a missing vertex");
366            return None;
367        }
368        if !self.vertices.contains_key(center_v) {
369            tracing::error!("subdivide_face: center vertex {center_v:?} is missing");
370            return None;
371        }
372
373        // rewire existing face
374        let new_he = self.add_halfedge(center_v, self.halfedges[next_he].end_vertex)?; // checked above
375
376        self.halfedges[existing_halfedge_id].next = Some(new_he); // checked above
377        self.halfedges[new_he].next = Some(last_he); // inserted above
378        self.halfedges[new_he].face = Some(face_id); // inserted above
379
380        let new_twin = self.add_halfedge(self.halfedges[next_he].end_vertex, center_v)?; // checked above
381
382        // insert new face
383        let new_face_id = self.add_face(new_halfedge_id, next_he, new_twin);
384
385        self.halfedges[new_twin].twin = Some(new_he); // inserted above
386        self.halfedges[new_he].twin = Some(new_twin); // inserted above
387
388        self.halfedges[existing_halfedge_id].end_vertex = center_v; // checked above
389
390        let face = self.faces[face_id]; // checked above
391        let new_face = self.faces[new_face_id]; // inserted above
392        self.bvh
393            .insert_or_update_partially(face.aabb(self), face.index, 0.0);
394        self.bvh
395            .insert_or_update_partially(new_face.aabb(self), new_face.index, 0.0);
396
397        // #[cfg(feature = "rerun")]
398        // {
399        //     self.log_he_rerun("subdivide/new_he", new_he);
400        //     self.log_he_rerun("subdivide/new_twin", new_twin);
401        // }
402
403        Some(new_he)
404    }
405}
406
407pub struct SubdivideEdge {
408    /// All halfedges created by the subdivision.
409    added_halfedges: Vec<HalfedgeId>,
410    /// This is the center vertex of the subdivided edge that was created.
411    added_vertex: VertexId,
412}
413
414#[cfg(test)]
415mod test {
416    use super::*;
417    use crate::ops::EdgeLengthCleanup;
418    use crate::utils::{build_grid, mesh_invariant_violations};
419    use glam::Vec3;
420    use hashbrown::HashSet;
421
422    /// `build_grid` uses unit cells, so canonical edge lengths squared are
423    /// exactly 1.0 (axis-aligned) or 2.0 (diagonal).
424    fn max_len_sqr(mg: &MeshGraph) -> f32 {
425        mg.halfedges
426            .values()
427            .map(|he| he.length_squared(mg))
428            .fold(0.0, f32::max)
429    }
430
431    fn count_above(mg: &MeshGraph, max_length_squared: f32) -> usize {
432        mg.halfedges
433            .values()
434            .filter(|he| he.length_squared(mg) > max_length_squared)
435            .count()
436    }
437
438    fn subdivide(mg: &mut MeshGraph, max_length_squared: f32) {
439        mg.subdivide_until_edges_below_max_length(
440            max_length_squared,
441            &mut HashSet::new(),
442            &mut HashSet::new(),
443        );
444    }
445
446    /// The small-mesh case the old flat `+ 100` work bound could not reach.
447    ///
448    /// A unit tetrahedron taken to a target edge of 0.2 needs ~200 splits from only 6
449    /// over-long edges. Under the old bound of `2 * 6 + 100` it stopped at 112 splits
450    /// with 60 edges still over-long, so callers had to call again in a loop.
451    #[test]
452    fn test_subdivide_converges_on_a_small_mesh_needing_deep_refinement() {
453        use glam::Vec3;
454
455        let positions = vec![
456            Vec3::new(0.0, 0.0, 0.0),
457            Vec3::new(1.0, 0.0, 0.0),
458            Vec3::new(0.0, 1.0, 0.0),
459            Vec3::new(0.0, 0.0, 1.0),
460        ];
461        let indices: Vec<usize> = vec![0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3];
462        let mut mg = MeshGraph::indexed_triangles(&positions, &indices);
463
464        let target: f32 = 0.2;
465        let outcome = mg.subdivide_until_edges_below_max_length(
466            target * target,
467            &mut HashSet::new(),
468            &mut HashSet::new(),
469        );
470
471        assert_eq!(
472            outcome,
473            EdgeLengthCleanup::Converged,
474            "one call left {} edges over-long",
475            count_above(&mg, target * target)
476        );
477        assert_eq!(count_above(&mg, target * target), 0);
478        assert!(mesh_invariant_violations(&mg).is_empty());
479    }
480
481    #[test]
482    fn test_subdivide_reports_converged_when_it_drains() {
483        let mut mg = build_grid(4);
484
485        let outcome = mg.subdivide_until_edges_below_max_length(
486            0.9,
487            &mut HashSet::new(),
488            &mut HashSet::new(),
489        );
490
491        assert_eq!(outcome, EdgeLengthCleanup::Converged);
492        assert_eq!(count_above(&mg, 0.9), 0);
493    }
494
495    /// An already-clean mesh converges without doing anything.
496    #[test]
497    fn test_subdivide_reports_converged_on_a_clean_mesh() {
498        let mut mg = build_grid(3);
499        let faces = mg.faces.len();
500
501        // Every edge is already below this.
502        let outcome = mg.subdivide_until_edges_below_max_length(
503            100.0,
504            &mut HashSet::new(),
505            &mut HashSet::new(),
506        );
507
508        assert_eq!(outcome, EdgeLengthCleanup::Converged);
509        assert_eq!(mg.faces.len(), faces);
510    }
511
512    #[test]
513    fn test_subdivide_until_max_length_holds_invariants() {
514        let mut mg = build_grid(6);
515        assert!(
516            mesh_invariant_violations(&mg).is_empty(),
517            "fixture is already broken"
518        );
519
520        let before_above = count_above(&mg, 0.5);
521        let before_max = max_len_sqr(&mg);
522
523        subdivide(&mut mg, 0.5);
524
525        assert!(
526            mesh_invariant_violations(&mg).is_empty(),
527            "invariants violated: {:?}",
528            mesh_invariant_violations(&mg)
529        );
530        // Deliberately weaker than "all edges below the threshold": the budget can
531        // stop the op with work remaining, and that behaviour is unchanged.
532        assert!(count_above(&mg, 0.5) < before_above);
533        assert!(max_len_sqr(&mg) < before_max);
534    }
535
536    /// The postcondition the doc comment actually promises. A small grid and a
537    /// threshold it can reach within budget, so a `pop_live` that silently drains
538    /// the heap early fails here instead of passing quietly.
539    #[test]
540    fn test_subdivide_until_max_length_drains_completely() {
541        let mut mg = build_grid(4);
542
543        subdivide(&mut mg, 0.9);
544
545        assert!(mesh_invariant_violations(&mg).is_empty());
546        assert_eq!(
547            count_above(&mg, 0.9),
548            0,
549            "subdivision left {} edges above the threshold",
550            count_above(&mg, 0.9)
551        );
552    }
553
554    /// Subdivision must only ever add geometry, never move an existing vertex.
555    /// The insert-only re-check pass in `subdivide_until_edges_below_max_length`
556    /// is unsound without this, so it is pinned rather than assumed.
557    #[test]
558    fn test_subdivide_adds_geometry_and_moves_no_existing_vertex() {
559        let mut mg = build_grid(4);
560        let before: Vec<(crate::VertexId, Vec3)> =
561            mg.positions.iter().map(|(v, &p)| (v, p)).collect();
562        let faces_before = mg.faces.len();
563
564        subdivide(&mut mg, 0.9);
565
566        assert!(mg.faces.len() > faces_before);
567        for (v_id, pos) in before {
568            assert_eq!(
569                mg.positions.get(v_id),
570                Some(&pos),
571                "vertex {v_id:?} moved during subdivision"
572            );
573        }
574    }
575
576    #[test]
577    fn test_subdivide_is_idempotent_once_converged() {
578        let mut mg = build_grid(4);
579        subdivide(&mut mg, 0.9);
580
581        let verts = mg.vertices.len();
582        let faces = mg.faces.len();
583        let halfedges = mg.halfedges.len();
584
585        subdivide(&mut mg, 0.9);
586
587        assert_eq!(mg.vertices.len(), verts);
588        assert_eq!(mg.faces.len(), faces);
589        assert_eq!(mg.halfedges.len(), halfedges);
590    }
591
592    /// Pins that the *longest* edge is the one chosen, not merely some edge above
593    /// the threshold. The grid's diagonals (len_sqr 2.0) are strictly longer than
594    /// its axis edges (1.0), so a threshold between them admits only diagonals;
595    /// every split must therefore land on a diagonal midpoint, which has exactly
596    /// one half-integer coordinate pair.
597    #[test]
598    fn test_subdivide_picks_longest_edges_first() {
599        let mut mg = build_grid(3);
600        let before: HashSet<crate::VertexId> = mg.positions.keys().collect();
601
602        // 1.0 < threshold < 2.0: only the diagonals qualify.
603        subdivide(&mut mg, 1.5);
604
605        let added: Vec<Vec3> = mg
606            .positions
607            .iter()
608            .filter(|(v, _)| !before.contains(v))
609            .map(|(_, &p)| p)
610            .collect();
611
612        assert!(!added.is_empty(), "expected at least one diagonal split");
613        for p in added {
614            let frac_x = (p.x - p.x.floor() - 0.5).abs();
615            let frac_y = (p.y - p.y.floor() - 0.5).abs();
616            assert!(
617                frac_x < 1e-6 && frac_y < 1e-6,
618                "split at {p:?} is not a diagonal midpoint, so a shorter edge was chosen"
619            );
620        }
621    }
622
623    /// Subdivision only adds geometry, so it cannot invalidate its own marked ids -
624    /// but a marked set is long-lived and can arrive already carrying ones an earlier
625    /// operation killed. Both ops hand back only live keys, so those have to be
626    /// pruned here too.
627    #[test]
628    fn test_subdivide_purges_dead_ids_from_marked_sets() {
629        let mut mg = build_grid(4);
630
631        // Kill some ids first, so the marked sets carry stale entries into the call.
632        let mut marked_vertices: HashSet<crate::VertexId> = mg.vertices.keys().collect();
633        let mut marked_halfedges: HashSet<HalfedgeId> = mg.halfedges.keys().collect();
634        mg.collapse_until_edges_above_min_length(1.5, &mut HashSet::new());
635        assert!(
636            marked_vertices
637                .iter()
638                .any(|v| !mg.vertices.contains_key(*v)),
639            "fixture failed to invalidate anything - test is vacuous"
640        );
641
642        mg.subdivide_until_edges_below_max_length(0.5, &mut marked_halfedges, &mut marked_vertices);
643
644        for v_id in &marked_vertices {
645            assert!(
646                mg.vertices.contains_key(*v_id),
647                "marked vertex {v_id:?} is dead"
648            );
649        }
650        for he_id in &marked_halfedges {
651            assert!(
652                mg.halfedges.contains_key(*he_id),
653                "marked halfedge {he_id:?} is dead"
654            );
655        }
656    }
657}