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}