Skip to main content

mesh_graph/ops/
mod.rs

1mod add;
2mod cleanup;
3mod collapse;
4mod edit;
5mod merge_one_ring;
6mod query;
7mod remove;
8mod subdivide;
9mod transform;
10
11use std::{cmp::Reverse, collections::BinaryHeap};
12
13pub use add::*;
14use hashbrown::HashMap;
15pub use merge_one_ring::*;
16
17use ordered_float::OrderedFloat;
18
19use crate::{HalfedgeId, MeshGraph};
20
21/// The outcome of [`MeshGraph::collapse_until_edges_above_min_length`] and
22/// [`MeshGraph::subdivide_until_edges_below_max_length`].
23///
24/// Both operations are bounded: they will not grind indefinitely on a mesh they
25/// cannot fix. This says which way the operation ended, so a caller that needs a
26/// clean mesh can react instead of guessing an iteration count.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum EdgeLengthCleanup {
29    /// No edge violates the length threshold any more. A caller looping until the
30    /// mesh is clean can stop.
31    Converged,
32
33    /// Edges still violate the threshold and the operation could not reduce them
34    /// further — either the work bound was reached, or the remaining edges cannot
35    /// be changed at all (a collapse that would invert a face, for instance).
36    ///
37    /// Calling again may help in the first case but not the second, and the two
38    /// are not distinguished here. A caller that cannot accept a dirty mesh should
39    /// report a failure rather than spin.
40    Stalled,
41}
42
43impl EdgeLengthCleanup {
44    /// `true` only for [`Self::Converged`].
45    #[inline]
46    pub fn converged(self) -> bool {
47        self == Self::Converged
48    }
49}
50
51/// Which end of the length range [`PendingEdges::pop_live`] yields first.
52#[derive(Clone, Copy, PartialEq, Eq, Debug)]
53pub(crate) enum PendingOrder {
54    /// Shortest edge first, for `collapse_until_edges_above_min_length`.
55    ShortestFirst,
56    /// Longest edge first, for `subdivide_until_edges_below_max_length`.
57    LongestFirst,
58}
59
60/// The set of edges still violating a length threshold, ordered so that the
61/// next edge to operate on can be taken in `O(log n)` instead of a linear scan.
62///
63/// Keys are canonical halfedge ids (`he_id.min(twin_id)`, one entry per
64/// undirected edge, as produced by [`MeshGraph::halfedges_map`]); values are
65/// squared lengths.
66///
67/// The `HashMap` is the authoritative state and the heap is only an index into
68/// it, kept honest by two rules:
69///
70/// 1. [`Self::insert`] is the *only* way to add an entry, and it pushes to the
71///    heap whenever it changes the map. That makes "every live entry has a
72///    matching heap entry" structural rather than something each call site has
73///    to remember.
74/// 2. [`Self::remove`] touches only the map. The orphaned heap entry is
75///    detected and discarded when it surfaces (lazy invalidation), which is far
76///    cheaper than finding and erasing it eagerly.
77///
78/// Ties are broken towards the smallest halfedge id in both orderings, so the
79/// choice is deterministic. (The linear scans this replaced used a strict
80/// comparison and so resolved ties by `hashbrown` iteration order, which
81/// `foldhash`'s randomly-seeded state makes vary between runs.)
82pub(crate) struct PendingEdges {
83    lengths: HashMap<HalfedgeId, f32>,
84    /// Max-heap over the sort key. `ShortestFirst` negates the length so that
85    /// the shortest edge compares greatest; squared lengths are non-negative so
86    /// negation is exact and strictly order-reversing.
87    ///
88    /// `OrderedFloat` supplies the total order. Squared edge lengths are never
89    /// NaN (a threshold comparison against NaN is false in both directions, so
90    /// such an edge is never admitted to the pending set), so the NaN corner of
91    /// that order is unreachable here.
92    heap: BinaryHeap<(OrderedFloat<f32>, Reverse<HalfedgeId>)>,
93    order: PendingOrder,
94}
95
96impl PendingEdges {
97    /// Wraps a pending map from [`MeshGraph::halfedges_map`].
98    pub(crate) fn new(lengths: HashMap<HalfedgeId, f32>, order: PendingOrder) -> Self {
99        // Build the heap in one O(n) heapify rather than n pushes.
100        let heap = BinaryHeap::from(
101            lengths
102                .iter()
103                .map(|(&he_id, &len_sqr)| (Self::key(order, len_sqr), Reverse(he_id)))
104                .collect::<Vec<_>>(),
105        );
106
107        Self {
108            lengths,
109            heap,
110            order,
111        }
112    }
113
114    #[inline]
115    fn key(order: PendingOrder, len_sqr: f32) -> OrderedFloat<f32> {
116        match order {
117            PendingOrder::ShortestFirst => OrderedFloat(-len_sqr),
118            PendingOrder::LongestFirst => OrderedFloat(len_sqr),
119        }
120    }
121
122    #[inline]
123    fn unkey(order: PendingOrder, key: OrderedFloat<f32>) -> f32 {
124        match order {
125            PendingOrder::ShortestFirst => -key.0,
126            PendingOrder::LongestFirst => key.0,
127        }
128    }
129
130    /// Records `he_id` as pending at `len_sqr`, pushing to the heap only when
131    /// this actually changes the map.
132    ///
133    /// Skipping the push for an unchanged value is what keeps the heap from
134    /// growing without bound: both callers re-examine every edge of the faces
135    /// they touched, and most of those edges are already pending at exactly the
136    /// length recorded for them. A re-insert after a [`Self::remove`] does
137    /// change the map, so it is always pushed — which is the one way a single
138    /// map entry can end up with two matching heap entries, see
139    /// [`Self::pop_live`].
140    pub(crate) fn insert(&mut self, he_id: HalfedgeId, len_sqr: f32) {
141        if self.lengths.insert(he_id, len_sqr) != Some(len_sqr) {
142            self.heap
143                .push((Self::key(self.order, len_sqr), Reverse(he_id)));
144        }
145    }
146
147    /// Restores the heap entry for an id that [`Self::pop_live`] returned but the
148    /// caller chose not to act on.
149    ///
150    /// `pop_live` consumes the heap entry without touching the map, so an
151    /// unacted-on id is left in the map with nothing in the heap pointing at it.
152    /// Routing it back through [`Self::insert`] would not help: the map still
153    /// holds the same length, so the change check would skip the push and the
154    /// entry would be unreachable forever. Hence the unconditional push.
155    ///
156    /// A later `insert` or `remove` for the same id simply makes this entry
157    /// stale, and `pop_live` discards it like any other stale entry.
158    pub(crate) fn requeue(&mut self, he_id: HalfedgeId, len_sqr: f32) {
159        self.heap
160            .push((Self::key(self.order, len_sqr), Reverse(he_id)));
161    }
162
163    /// Drops `he_id` from the pending set. Its heap entry is left to be
164    /// discarded by [`Self::pop_live`].
165    pub(crate) fn remove(&mut self, he_id: &HalfedgeId) {
166        self.lengths.remove(he_id);
167    }
168
169    pub(crate) fn is_empty(&self) -> bool {
170        self.lengths.is_empty()
171    }
172
173    pub(crate) fn len(&self) -> usize {
174        self.lengths.len()
175    }
176
177    /// Removes and returns the extreme pending edge as `(he_id, len_sqr)`,
178    /// discarding stale heap entries on the way.
179    ///
180    /// An entry is stale when the map no longer holds that id (it was removed)
181    /// or holds a different length for it (a later `insert` superseded it). The
182    /// comparison is exact `f32` equality, which is right because the value
183    /// being compared is the very same `f32` that was stored, not a
184    /// recomputation of it.
185    ///
186    /// The contract is "yields *a* live entry", not "yields each id once": a
187    /// [`Self::remove`] followed by an `insert` of a bit-identical length leaves
188    /// the pre-`remove` heap entry matching the map again, so the id can come
189    /// back twice. Callers must tolerate that — both re-run their own check on
190    /// the second yield, which is why it stays harmless — and must not derive an
191    /// entry count from how many times `pop_live` returned (the drained-heap
192    /// `debug_assert!` in `collapse_until_edges_above_min_length` allows for the
193    /// surplus). Exact repeats are not exotic: collapsing to edge midpoints on a
194    /// regular grid regenerates lengths like `1.0` and `0.25` bit-for-bit.
195    ///
196    /// Returns `None` once no live entry remains, which — given rule 1 above —
197    /// means the pending set is empty. Both callers `debug_assert!` that.
198    pub(crate) fn pop_live(&mut self) -> Option<(HalfedgeId, f32)> {
199        while let Some((key, Reverse(he_id))) = self.heap.pop() {
200            if self.lengths.get(&he_id) == Some(&Self::unkey(self.order, key)) {
201                return Some((he_id, Self::unkey(self.order, key)));
202            }
203        }
204
205        None
206    }
207}
208
209impl MeshGraph {
210    pub fn halfedges_map(&mut self, predicate: impl Fn(f32) -> bool) -> HashMap<HalfedgeId, f32> {
211        let mut halfedges_map = HashMap::new();
212
213        for (he_id, he) in &self.halfedges {
214            // A twinless halfedge is a broken edge, not a reason to abandon the scan:
215            // returning the partial map here used to silently drop every edge after
216            // the first one, leaving those edges unprocessed by the caller's loop.
217            let Some(twin_id) = he.twin else {
218                tracing::error!("Twin missing for {he_id:?}");
219                continue;
220            };
221
222            let id = he_id.min(twin_id);
223
224            if halfedges_map.contains_key(&id) {
225                continue;
226            }
227            let len_sqr = he.length_squared(self);
228
229            if predicate(len_sqr) {
230                halfedges_map.insert(id, len_sqr);
231            }
232        }
233
234        halfedges_map
235    }
236}
237
238#[cfg(test)]
239mod pending_edges_test {
240    use super::*;
241    use crate::utils::build_grid;
242
243    /// Three distinct, live canonical halfedge ids to key entries with.
244    fn ids() -> Vec<HalfedgeId> {
245        let g = build_grid(2);
246        let mut ids: Vec<HalfedgeId> = g.halfedges.keys().take(3).collect();
247        ids.sort();
248        ids
249    }
250
251    #[test]
252    fn pops_shortest_first() {
253        let id = ids();
254        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::ShortestFirst);
255        p.insert(id[0], 9.0);
256        p.insert(id[1], 1.0);
257        p.insert(id[2], 5.0);
258
259        assert_eq!(p.pop_live(), Some((id[1], 1.0)));
260        p.remove(&id[1]);
261        assert_eq!(p.pop_live(), Some((id[2], 5.0)));
262    }
263
264    #[test]
265    fn pops_longest_first() {
266        let id = ids();
267        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::LongestFirst);
268        p.insert(id[0], 9.0);
269        p.insert(id[1], 1.0);
270        p.insert(id[2], 5.0);
271
272        assert_eq!(p.pop_live(), Some((id[0], 9.0)));
273        p.remove(&id[0]);
274        assert_eq!(p.pop_live(), Some((id[2], 5.0)));
275    }
276
277    #[test]
278    fn removed_entries_are_skipped() {
279        let id = ids();
280        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::LongestFirst);
281        p.insert(id[0], 9.0);
282        p.insert(id[1], 1.0);
283
284        p.remove(&id[0]);
285
286        assert_eq!(p.pop_live(), Some((id[1], 1.0)));
287    }
288
289    #[test]
290    fn superseded_entries_are_skipped() {
291        let id = ids();
292        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::LongestFirst);
293        p.insert(id[0], 9.0);
294        // Same id re-inserted shorter: the stale 9.0 heap entry must not be returned.
295        p.insert(id[0], 2.0);
296
297        assert_eq!(p.pop_live(), Some((id[0], 2.0)));
298        p.remove(&id[0]);
299        assert_eq!(p.pop_live(), None);
300    }
301
302    /// `pop_live` consumes the heap entry but leaves the map entry, so an id the
303    /// caller declines to act on is unreachable until it is requeued. Routing it
304    /// back through `insert` is NOT enough — the map already holds that length, so
305    /// the change check would skip the push and the entry would be lost.
306    #[test]
307    fn requeue_makes_a_declined_entry_reachable_again() {
308        let id = ids();
309        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::ShortestFirst);
310        p.insert(id[0], 1.0);
311
312        let popped = p.pop_live().expect("entry is live");
313        assert_eq!(popped, (id[0], 1.0));
314
315        // Declining to act on it: still pending in the map, but gone from the heap.
316        assert!(!p.is_empty());
317        assert_eq!(p.pop_live(), None);
318
319        // `insert` cannot resurrect it, because the value is unchanged.
320        p.insert(id[0], 1.0);
321        assert_eq!(
322            p.pop_live(),
323            None,
324            "insert must not resurrect a declined id"
325        );
326
327        p.requeue(id[0], 1.0);
328        assert_eq!(p.pop_live(), Some((id[0], 1.0)));
329    }
330
331    /// A requeued entry that is later removed must not come back.
332    #[test]
333    fn requeue_then_remove_stays_gone() {
334        let id = ids();
335        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::ShortestFirst);
336        p.insert(id[0], 1.0);
337        p.pop_live();
338        p.requeue(id[0], 1.0);
339
340        p.remove(&id[0]);
341
342        assert_eq!(p.pop_live(), None);
343        assert!(p.is_empty());
344    }
345
346    /// `remove` leaves the heap entry behind, so re-inserting a *bit-identical*
347    /// length makes that orphan match the map again and the id is yielded twice.
348    /// Documented behaviour rather than a bug: both callers re-run their own check
349    /// on the second yield. It is pinned here because the drained-heap
350    /// `debug_assert!` in `collapse_until_edges_above_min_length` has to allow for
351    /// the resulting surplus - an equality there would panic on valid state.
352    #[test]
353    fn reinsert_after_remove_can_yield_the_same_id_twice() {
354        let id = ids();
355        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::ShortestFirst);
356        p.insert(id[0], 1.0);
357
358        p.remove(&id[0]);
359        // Same bit pattern, so the orphaned heap entry matches the map once more.
360        p.insert(id[0], 1.0);
361
362        assert_eq!(p.pop_live(), Some((id[0], 1.0)));
363        assert_eq!(
364            p.pop_live(),
365            Some((id[0], 1.0)),
366            "the orphaned entry is live again and must surface, not be silently dropped"
367        );
368        assert_eq!(p.len(), 1, "two heap entries, still one pending edge");
369
370        p.remove(&id[0]);
371        assert_eq!(p.pop_live(), None);
372    }
373
374    /// Re-inserting an unchanged value must not push, or the re-check passes would
375    /// grow the heap without bound.
376    #[test]
377    fn unchanged_reinsert_does_not_grow_the_heap() {
378        let id = ids();
379        let mut p = PendingEdges::new(HashMap::new(), PendingOrder::LongestFirst);
380        p.insert(id[0], 4.0);
381        let heap_len = p.heap.len();
382
383        for _ in 0..100 {
384            p.insert(id[0], 4.0);
385        }
386
387        assert_eq!(p.heap.len(), heap_len);
388        assert_eq!(p.len(), 1);
389    }
390
391    #[test]
392    fn seeding_from_a_map_preserves_order() {
393        let id = ids();
394        let mut seed = HashMap::new();
395        seed.insert(id[0], 3.0);
396        seed.insert(id[1], 7.0);
397
398        let mut p = PendingEdges::new(seed, PendingOrder::LongestFirst);
399        assert_eq!(p.len(), 2);
400        assert_eq!(p.pop_live(), Some((id[1], 7.0)));
401    }
402
403    /// Equal lengths must resolve to the smallest halfedge id. The linear scans
404    /// this replaced used a strict comparison, so ties fell to `hashbrown`
405    /// iteration order and varied between runs; every other test here uses
406    /// distinct lengths, which leaves the `Reverse(HalfedgeId)` half of the sort
407    /// key unexercised. Ids go in out of order so a heap that leaked insertion
408    /// order would fail too.
409    fn assert_ties_break_towards_smallest_id(order: PendingOrder) {
410        let id = ids();
411        let mut p = PendingEdges::new(HashMap::new(), order);
412        p.insert(id[2], 4.0);
413        p.insert(id[0], 4.0);
414        p.insert(id[1], 4.0);
415
416        for expected in &id {
417            assert_eq!(
418                p.pop_live(),
419                Some((*expected, 4.0)),
420                "{order:?} did not break the tie towards the smallest id"
421            );
422            p.remove(expected);
423        }
424
425        assert!(p.is_empty());
426    }
427
428    #[test]
429    fn shortest_first_breaks_ties_towards_the_smallest_id() {
430        assert_ties_break_towards_smallest_id(PendingOrder::ShortestFirst);
431    }
432
433    #[test]
434    fn longest_first_breaks_ties_towards_the_smallest_id() {
435        assert_ties_break_towards_smallest_id(PendingOrder::LongestFirst);
436    }
437}
438
439#[cfg(test)]
440mod halfedges_map_test {
441    use crate::utils::build_grid;
442
443    /// A twinless halfedge must not abandon the scan: the old code returned the
444    /// partial map built so far, which silently dropped every edge after it. Breaking
445    /// the *first* halfedge in slotmap order makes that distinction observable — the
446    /// buggy code returns (near-)nothing, while breaking a late halfedge would return
447    /// everything before it and pass on the bug too.
448    #[test]
449    fn test_halfedges_map_skips_twinless_and_keeps_scanning() {
450        let mut mg = build_grid(2);
451        let baseline = mg.halfedges_map(|_| true).len();
452
453        let broken_id = mg.halfedges.keys().next().expect("grid mesh has halfedges");
454        mg.halfedges.get_mut(broken_id).unwrap().twin = None;
455
456        // The broken halfedge is skipped, but its former twin still points back at
457        // it, so the pair is still registered from the other direction.
458        let after = mg.halfedges_map(|_| true).len();
459        assert_eq!(
460            after, baseline,
461            "breaking one halfedge's twin link must not shrink the map beyond that one pair"
462        );
463    }
464}