Skip to main content

mesh_graph/ops/
edit.rs

1use glam::Vec3;
2use hashbrown::{HashMap, HashSet};
3use itertools::Itertools;
4use slotmap::SparseSecondaryMap;
5use tracing::{error, instrument};
6
7use crate::{FaceId, HalfedgeId, MeshGraph, VertexId, error_none, utils::unwrap_or_return};
8
9pub struct MergeVertices {
10    pub removed_vertices: Vec<VertexId>,
11    pub removed_halfedges: Vec<HalfedgeId>,
12    pub removed_faces: Vec<FaceId>,
13}
14
15impl MeshGraph {
16    /// Merges the given vertices into a single vertex, reconnecting halfedges and faces as needed.
17    #[instrument(skip_all)]
18    pub fn merge_vertices(
19        &mut self,
20        vertices: impl IntoIterator<Item = VertexId>,
21    ) -> MergeVertices {
22        #[cfg(feature = "instrumentation")]
23        crate::set_current_op("merge_vertices");
24        #[cfg(feature = "instrumentation")]
25        crate::probe_chain_begin(self);
26        let vertex_ids: Vec<VertexId> = vertices
27            .into_iter()
28            .filter(|v| self.vertices.contains_key(*v))
29            .unique()
30            .collect();
31
32        if vertex_ids.len() < 2 {
33            return MergeVertices {
34                removed_vertices: Vec::new(),
35                removed_halfedges: Vec::new(),
36                removed_faces: Vec::new(),
37            };
38        }
39
40        let vertex_set: HashSet<VertexId> = vertex_ids.iter().copied().collect();
41        let survivor_id = vertex_ids[0];
42
43        // Compute average position
44        let mut avg_pos = Vec3::ZERO;
45        let mut count = 0.0;
46        for &v_id in &vertex_ids {
47            if let Some(&pos) = self.positions.get(v_id) {
48                avg_pos += pos;
49                count += 1.0;
50            }
51        }
52        if count > 0.0 {
53            avg_pos /= count;
54        }
55
56        // Find faces to remove: any face with 2+ vertices in the merge set becomes degenerate
57        let mut faces_to_remove = HashSet::new();
58        for &v_id in &vertex_ids {
59            for face_id in self.vertex_adjacent_faces(v_id) {
60                if faces_to_remove.contains(&face_id) {
61                    continue;
62                }
63                if let Some(face) = self.faces.get(face_id) {
64                    let merged_count = face
65                        .vertices(self)
66                        .filter(|v| vertex_set.contains(v))
67                        .count();
68                    if merged_count >= 2 {
69                        faces_to_remove.insert(face_id);
70                    }
71                }
72            }
73        }
74
75        #[cfg(feature = "instrumentation")]
76        crate::record_op_trace!(
77            "merge_vertices({:?}): faces_to_remove={:?}",
78            vertex_ids,
79            faces_to_remove
80        );
81
82        // Determine which halfedges to remove vs make boundary
83        let mut halfedges_to_remove = HashSet::new();
84        for &face_id in &faces_to_remove {
85            let face_hes: Vec<_> = self
86                .halfedges
87                .iter()
88                .filter_map(|(he_id, he)| {
89                    if he.face == Some(face_id) {
90                        Some((he_id, *he))
91                    } else {
92                        None
93                    }
94                })
95                .collect();
96
97            for (he_id, he) in face_hes {
98                if let Some(twin_id) = he.twin
99                    && let Some(twin) = self.halfedges.get(twin_id)
100                {
101                    if twin.is_boundary() || twin.face.is_some_and(|f| faces_to_remove.contains(&f))
102                    {
103                        halfedges_to_remove.insert(he_id);
104                        halfedges_to_remove.insert(twin_id);
105                    } else {
106                        // detach the surviving halfedge: it keeps its twin but no face
107                        if let Some(he_mut) = self.halfedges.get_mut(he_id) {
108                            he_mut.face = None;
109                            he_mut.next = None;
110                        }
111                    }
112                }
113            }
114        }
115
116        // Remove faces from BVH and faces slotmap
117        for &face_id in &faces_to_remove {
118            if let Some(face) = self.faces.get(face_id) {
119                self.bvh.remove(face.index);
120            }
121            self.faces.remove(face_id);
122        }
123
124        // Collect outgoing halfedge cleanup info before removing halfedges
125        // (start_vertex lookup needs the twin to be present)
126        let mut outgoing_cleanup: Vec<(VertexId, HalfedgeId)> = Vec::new();
127        for &he_id in &halfedges_to_remove {
128            if let Some(he) = self.halfedges.get(he_id)
129                && let Some(start_v_id) = he.start_vertex(self)
130            {
131                outgoing_cleanup.push((start_v_id, he_id));
132            }
133        }
134
135        for (v_id, he_id) in outgoing_cleanup {
136            if let Some(out_hes) = self.outgoing_halfedges.get_mut(v_id) {
137                out_hes.retain(|id| *id != he_id);
138            }
139        }
140
141        // Remove halfedges. The batch is symmetric: every removed halfedge's twin is
142        // also in the batch (collected pairwise above), so no survivor references a
143        // removed id.
144        #[cfg(feature = "instrumentation")]
145        self.probe_live_face_removal(
146            &halfedges_to_remove.iter().copied().collect::<Vec<_>>(),
147            "merge_batch",
148        );
149        for &he_id in &halfedges_to_remove {
150            self.halfedges.remove(he_id);
151        }
152
153        let mut removed_vertices = Vec::with_capacity(vertex_ids.len() - 1);
154
155        // Merge non-survivor vertices into survivor
156        for &v_id in &vertex_ids[1..] {
157            if !self.vertices.contains_key(v_id) {
158                continue;
159            }
160
161            removed_vertices.push(v_id);
162
163            // Get incoming halfedges to v_id (twins of outgoing halfedges from v_id)
164            let incoming_he_ids: Vec<HalfedgeId> = self
165                .outgoing_halfedges
166                .get(v_id)
167                .map(|out_hes| {
168                    out_hes
169                        .iter()
170                        .filter_map(|he_id| self.halfedges.get(*he_id)?.twin)
171                        .collect()
172                })
173                .unwrap_or_default();
174
175            for he_id in incoming_he_ids {
176                if let Some(he) = self.halfedges.get_mut(he_id) {
177                    #[cfg(feature = "instrumentation")]
178                    crate::record_op_trace!(
179                        "merge_vertices: repoint end of {he_id:?} to {survivor_id:?}"
180                    );
181                    he.end_vertex = survivor_id;
182                }
183            }
184
185            // Transfer outgoing halfedges to survivor
186            let outgoing = self
187                .outgoing_halfedges
188                .get(v_id)
189                .cloned()
190                .unwrap_or_default();
191
192            if let Some(entry) = self.outgoing_halfedges.entry(survivor_id) {
193                entry.or_default().extend(outgoing);
194            }
195
196            self.remove_only_vertex(v_id);
197        }
198
199        let mut removed_halfedges: Vec<HalfedgeId> = halfedges_to_remove.into_iter().collect();
200        let removed_faces: Vec<FaceId> = faces_to_remove.into_iter().collect();
201
202        // Check if survivor still exists (could have been removed if all faces were removed)
203        if !self.vertices.contains_key(survivor_id) {
204            return MergeVertices {
205                removed_vertices,
206                removed_halfedges,
207                removed_faces,
208            };
209        }
210
211        // check just above
212        self.positions[survivor_id] = avg_pos;
213
214        // Clean up stale outgoing halfedges for survivor
215        if let Some(out_hes) = self.outgoing_halfedges.get_mut(survivor_id) {
216            out_hes.retain(|he_id| self.halfedges.contains_key(*he_id));
217        }
218
219        // Remove duplicate halfedges that now share the same edge after vertex merge
220        {
221            let out_hes = self
222                .outgoing_halfedges
223                .get(survivor_id)
224                .cloned()
225                .unwrap_or_default();
226
227            let mut by_end: HashMap<VertexId, Vec<HalfedgeId>> = HashMap::new();
228            for &he_id in &out_hes {
229                if let Some(he) = self.halfedges.get(he_id) {
230                    by_end.entry(he.end_vertex).or_default().push(he_id);
231                }
232            }
233
234            let mut affected_vertices = HashSet::new();
235
236            for (end_v, group) in by_end {
237                if group.len() < 2 {
238                    continue;
239                }
240
241                // Pick best forward halfedge (prefer non-boundary)
242                let best_fwd = group
243                    .iter()
244                    .copied()
245                    .max_by_key(|&id| {
246                        self.halfedges
247                            .get(id)
248                            .map_or(0u8, |h| if h.face.is_some() { 1 } else { 0 })
249                    })
250                    .unwrap();
251
252                // Collect valid twins
253                let twins: Vec<HalfedgeId> = group
254                    .iter()
255                    .filter_map(|&id| {
256                        self.halfedges
257                            .get(id)?
258                            .twin
259                            .filter(|t| self.halfedges.contains_key(*t))
260                    })
261                    .collect();
262
263                // Pick best reverse halfedge (prefer non-boundary), but never the
264                // forward halfedge itself: pairing a halfedge with itself writes a
265                // self-twin, which strands it in later collapse re-pairs.
266                let best_rev = twins
267                    .iter()
268                    .copied()
269                    .filter(|&r| r != best_fwd)
270                    .max_by_key(|&id| {
271                        self.halfedges
272                            .get(id)
273                            .map_or(0u8, |h| if h.face.is_some() { 1 } else { 0 })
274                    });
275
276                // Update twin pointers
277                if let Some(rev_id) = best_rev {
278                    if let Some(he) = self.halfedges.get_mut(best_fwd) {
279                        he.twin = Some(rev_id);
280                    }
281                    if let Some(he) = self.halfedges.get_mut(rev_id) {
282                        he.twin = Some(best_fwd);
283                    }
284                }
285
286                // A duplicate halfedge is a chain member of a live face, so removing it
287                // in place would break that face's chain. Remove the affected faces first
288                // (which detaches all their halfedges), then drop the leftover duplicates.
289                //
290                // `best_fwd`/`best_rev` are the survivors of the re-pair and must never
291                // be dropped: after a merge re-points `end_vertex`s, a halfedge and its
292                // own twin can share a group (zero-length edge), which puts the forward
293                // halfedge into the twins list and the reverse halfedge into the group;
294                // dropping either would strand the re-paired survivor twinless.
295                let mut faces_to_drop: Vec<FaceId> = Vec::new();
296                let mut duplicate_ids: Vec<HalfedgeId> = Vec::new();
297                for &he_id in &group {
298                    if he_id != best_fwd && Some(he_id) != best_rev {
299                        duplicate_ids.push(he_id);
300                        removed_halfedges.push(he_id);
301                        if let Some(he) = self.halfedges.get(he_id)
302                            && let Some(face_id) = he.face
303                            && !faces_to_drop.contains(&face_id)
304                        {
305                            faces_to_drop.push(face_id);
306                        }
307                    }
308                }
309
310                // Remove duplicate reverse halfedges
311                for &twin_id in &twins {
312                    if Some(twin_id) != best_rev && twin_id != best_fwd {
313                        duplicate_ids.push(twin_id);
314                        removed_halfedges.push(twin_id);
315                        if let Some(he) = self.halfedges.get(twin_id)
316                            && let Some(face_id) = he.face
317                            && !faces_to_drop.contains(&face_id)
318                        {
319                            faces_to_drop.push(face_id);
320                        }
321                    }
322                }
323
324                for face_id in faces_to_drop {
325                    let (_v_ids, he_ids) = self.remove_face(face_id);
326                    removed_halfedges.extend(he_ids);
327                }
328
329                // Drop any duplicates that survived the face removals (e.g. halfedges a
330                // face removal keeps as detached boundary halves).
331                let remaining: Vec<HalfedgeId> = duplicate_ids
332                    .iter()
333                    .copied()
334                    .filter(|id| self.halfedges.contains_key(*id))
335                    .collect();
336                if !remaining.is_empty() {
337                    // Duplicates whose partners are other duplicates in this same
338                    // batch, or the re-paired `best_fwd`/`best_rev` pair (which point
339                    // at each other) — nothing survives referencing a removed id.
340                    #[cfg(feature = "instrumentation")]
341                    self.probe_live_face_removal(&remaining, "merge_remaining");
342                    for he_id in remaining {
343                        self.halfedges.remove(he_id);
344                    }
345                }
346
347                affected_vertices.insert(end_v);
348            }
349
350            // Clean up outgoing halfedges for survivor
351            if let Some(out_hes) = self.outgoing_halfedges.get_mut(survivor_id) {
352                out_hes.retain(|he_id| self.halfedges.contains_key(*he_id));
353            }
354
355            // Clean up outgoing halfedges for affected adjacent vertices
356            for v in affected_vertices {
357                if let Some(out_hes) = self.outgoing_halfedges.get_mut(v) {
358                    out_hes.retain(|he_id| self.halfedges.contains_key(*he_id));
359                }
360                let new_out = self
361                    .outgoing_halfedges
362                    .get(v)
363                    .and_then(|hes| hes.first().copied());
364                if let Some(vertex) = self.vertices.get_mut(v) {
365                    vertex.outgoing_halfedge = new_out;
366                }
367            }
368        }
369
370        // Update outgoing_halfedge reference
371        let new_outgoing = self
372            .outgoing_halfedges
373            .get(survivor_id)
374            .and_then(|hes| hes.first().copied());
375
376        if let Some(vertex) = self.vertices.get_mut(survivor_id) {
377            vertex.outgoing_halfedge = new_outgoing;
378        }
379
380        self.make_outgoing_halfedge_boundary_if_possible(survivor_id);
381        self.compute_vertex_normal(survivor_id);
382
383        // Note: `merge_vertices` runs inside `merge_vertices_one_rings`; it is
384        // validated but deliberately not pushed as a state-history step, so the
385        // ring entries stay 1:1 with the host's journal steps (one
386        // `merge_vertices_one_rings` = one step) and resume positions map
387        // unambiguously to journal entries.
388        #[cfg(feature = "instrumentation")]
389        self.probe_chain_integrity("merge_vertices");
390
391        MergeVertices {
392            removed_vertices,
393            removed_halfedges,
394            removed_faces,
395        }
396    }
397
398    /// Flips this edge so that it represents the other diagonal described by the quad formed by the two incident triangles.
399    ///
400    /// ```text
401    ///     *                    *
402    ///    / \                  / \
403    ///   /   \                / ‖ \
404    ///  /     \              /  ‖  \
405    /// * ===== *     =>     *   ‖   *
406    ///  \     /              \  ‖  /
407    ///   \   /                \ ‖ /
408    ///    \ /                  \ /
409    ///     *                    *
410    /// ```
411    #[instrument(skip(self))]
412    pub fn flip_edge(&mut self, halfedge_id: HalfedgeId) {
413        #[cfg(feature = "rerun")]
414        self.log_he_rerun("flip", halfedge_id);
415
416        #[cfg(feature = "instrumentation")]
417        crate::record_op_trace!("flip_edge({halfedge_id:?})");
418
419        let he = unwrap_or_return!(self.halfedges.get(halfedge_id), "Halfedge not found");
420
421        let prev_he_id = unwrap_or_return!(he.prev(self), "Prev not found");
422        let prev_he = unwrap_or_return!(self.halfedges.get(prev_he_id), "Prev not found");
423        let start_v_id = prev_he.end_vertex;
424        let prev_twin_he_id = unwrap_or_return!(prev_he.twin, "Prev twin not found");
425
426        let next_he_id = unwrap_or_return!(he.next, "Next not found");
427        let next_he = unwrap_or_return!(self.halfedges.get(next_he_id), "Next not found");
428        let opposite_v_id = next_he.end_vertex;
429        let next_twin_he_id = unwrap_or_return!(next_he.twin, "Next twin not found");
430
431        let twin_he_id = unwrap_or_return!(he.twin, "Twin not found");
432        let twin_he = unwrap_or_return!(self.halfedges.get(twin_he_id), "Twin not found");
433
434        let twin_prev_he_id = unwrap_or_return!(twin_he.prev(self), "Prev not found");
435        let twin_prev_he = unwrap_or_return!(self.halfedges.get(twin_prev_he_id), "Prev not found");
436        let twin_start_v_id = twin_prev_he.end_vertex;
437        let twin_prev_twin_he_id = unwrap_or_return!(twin_prev_he.twin, "Prev twin twin not found");
438
439        let twin_next_he_id = unwrap_or_return!(twin_he.next, "Next not found");
440        let twin_next_he = unwrap_or_return!(self.halfedges.get(twin_next_he_id), "Next not found");
441        let twin_opposite_v_id = twin_next_he.end_vertex;
442        let twin_next_twin_he_id = unwrap_or_return!(twin_next_he.twin, "Next twin twin not found");
443
444        // checked at the start
445        self.halfedges[halfedge_id].end_vertex = opposite_v_id;
446
447        // checked above
448        self.halfedges[prev_he_id].end_vertex = twin_opposite_v_id;
449        self.make_twins(prev_he_id, twin_next_twin_he_id);
450        // checked above
451        self.halfedges[next_he_id].end_vertex = start_v_id;
452        self.make_twins(next_he_id, prev_twin_he_id);
453
454        self.remove_outgoing_halfedge(start_v_id, halfedge_id);
455        self.remove_outgoing_halfedge(start_v_id, twin_next_he_id);
456        self.add_outgoing_halfedge(start_v_id, prev_he_id);
457
458        self.remove_outgoing_halfedge(opposite_v_id, prev_he_id);
459        self.add_outgoing_halfedge(opposite_v_id, next_he_id);
460        self.add_outgoing_halfedge(opposite_v_id, twin_he_id);
461
462        // checked above
463        self.halfedges[twin_he_id].end_vertex = twin_opposite_v_id;
464
465        // checked above
466        self.halfedges[twin_prev_he_id].end_vertex = opposite_v_id;
467        self.make_twins(twin_prev_he_id, next_twin_he_id);
468
469        // checked above
470        self.halfedges[twin_next_he_id].end_vertex = twin_start_v_id;
471        self.make_twins(twin_next_he_id, twin_prev_twin_he_id);
472
473        self.remove_outgoing_halfedge(twin_start_v_id, twin_he_id);
474        self.remove_outgoing_halfedge(twin_start_v_id, next_he_id);
475        self.add_outgoing_halfedge(twin_start_v_id, twin_prev_he_id);
476
477        self.remove_outgoing_halfedge(twin_opposite_v_id, twin_prev_he_id);
478        self.add_outgoing_halfedge(twin_opposite_v_id, twin_next_he_id);
479        self.add_outgoing_halfedge(twin_opposite_v_id, halfedge_id);
480
481        // checked if halfedge exists above
482        let face_id1 = unwrap_or_return!(self.halfedges[halfedge_id].face, "Face not found");
483        let face1 = unwrap_or_return!(self.faces.get(face_id1), "Face not found");
484        self.bvh
485            .insert_or_update_partially(face1.aabb(self), face1.index, 0.0);
486
487        // checked if halfedge exists above
488        let face_id2 = unwrap_or_return!(self.halfedges[twin_he_id].face, "Face not found");
489        let face2 = unwrap_or_return!(self.faces.get(face_id2), "Face not found");
490        self.bvh
491            .insert_or_update_partially(face2.aabb(self), face2.index, 0.0);
492    }
493
494    /// Makes two halfedges twins of each other. Doesn't change anything else
495    pub fn make_twins(&mut self, he_id1: HalfedgeId, he_id2: HalfedgeId) {
496        // Check both halfedges before writing any twin pointer, so a missing id cannot
497        // leave a dangling half-pair behind.
498        if !self.halfedges.contains_key(he_id1) || !self.halfedges.contains_key(he_id2) {
499            error!("Halfedge not found in make_twins");
500            return;
501        }
502        self.halfedges.get_mut(he_id1).unwrap().twin = Some(he_id2);
503        self.halfedges.get_mut(he_id2).unwrap().twin = Some(he_id1);
504    }
505
506    /// Removes the outgoing halfedge from a vertex. Doesn't change anything else.
507    #[instrument(skip(self))]
508    pub fn remove_outgoing_halfedge(&mut self, vertex_id: VertexId, halfedge_id: HalfedgeId) {
509        let outgoing_halfedges = unwrap_or_return!(
510            self.outgoing_halfedges.get_mut(vertex_id),
511            "No outgoing halfedges found"
512        );
513
514        outgoing_halfedges.retain(|he_id| *he_id != halfedge_id);
515    }
516
517    /// Adds the outgoing halfedge to a vertex and overrides the vertex.outgoing_halfedge
518    #[instrument(skip(self))]
519    pub fn add_outgoing_halfedge(&mut self, vertex_id: VertexId, outgoing_halfedge: HalfedgeId) {
520        let vertex = unwrap_or_return!(self.vertices.get_mut(vertex_id), "Vertex not found");
521        vertex.outgoing_halfedge = Some(outgoing_halfedge);
522
523        let v_outgoing_halfedges = unwrap_or_return!(
524            self.outgoing_halfedges.get_mut(vertex_id),
525            "No outgoing halfedges found"
526        );
527        v_outgoing_halfedges.push(outgoing_halfedge);
528    }
529
530    /// Smooths the position of the vertex by computing the average of its own and its neighbors' positions and
531    /// moving it there. Also called Laplacian Smoothing.
532    #[instrument(skip_all)]
533    pub fn smooth_vertices(&mut self, vertices: impl IntoIterator<Item = VertexId>) {
534        let mut new_positions = SparseSecondaryMap::new();
535
536        let mut affected_face_ids = HashSet::new();
537
538        for vertex_id in vertices.into_iter() {
539            let Some(pos) = self.compute_smoothed_vertex_pos(vertex_id) else {
540                continue;
541            };
542
543            new_positions.insert(vertex_id, pos);
544
545            // vertex checked if exists in `compute_smoothed_vertex_pos()`
546            affected_face_ids.extend(self.vertices[vertex_id].faces(self));
547        }
548
549        for (vertex_id, &pos) in &new_positions {
550            self.positions.insert(vertex_id, pos);
551        }
552
553        for vertex_id in new_positions.keys() {
554            self.compute_vertex_normal(vertex_id);
555        }
556
557        for face_id in affected_face_ids {
558            let Some(face) = self.faces.get(face_id) else {
559                error!("Face {:?} does not exist", face_id);
560                continue;
561            };
562
563            self.bvh
564                .insert_or_update_partially(face.aabb(self), face.index, 0.0);
565        }
566    }
567
568    /// Smooth the position of the vertex by computing the average of its own and its neighbors' positions and
569    /// moving it there. Also called Laplacian Smoothing.
570    ///
571    /// > Note: If you want to smooth multiple vertices, use the `smooth_vertices` method instead of
572    /// > calling this method multiple times.
573    #[instrument(skip(self))]
574    pub fn smooth_vertex(&mut self, vertex_id: VertexId) {
575        let pos = unwrap_or_return!(
576            self.compute_smoothed_vertex_pos(vertex_id),
577            "Couldn't compute smoothed position"
578        );
579
580        self.positions.insert(vertex_id, pos);
581        self.compute_vertex_normal(vertex_id);
582
583        // vertex checked if exists in `compute_smoothed_vertex_pos()`
584        for face_id in self.vertices[vertex_id].faces(self).collect_vec() {
585            let Some(face) = self.faces.get(face_id) else {
586                error!("Face {:?} does not exist", face_id);
587                continue;
588            };
589
590            self.bvh
591                .insert_or_update_partially(face.aabb(self), face.index, 0.0);
592        }
593    }
594
595    #[instrument(skip(self))]
596    fn compute_smoothed_vertex_pos(&mut self, vertex_id: VertexId) -> Option<Vec3> {
597        let vertex = self.vertices.get(vertex_id)?;
598
599        let mut pos = *self
600            .positions
601            .get(vertex_id)
602            .or_else(error_none!("Position not found for id {vertex_id:?}"))?;
603
604        let mut count = 1.0;
605        for neighbor_v_id in vertex.neighbours(self) {
606            let neighbor_pos = *self.positions.get(neighbor_v_id).or_else(error_none!(
607                "Neighbor position not found for id {neighbor_v_id:?}"
608            ))?;
609
610            pos += neighbor_pos;
611            count += 1.0;
612        }
613
614        pos /= count;
615
616        Some(pos)
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use glam::Vec3;
623
624    use super::*;
625
626    #[cfg(feature = "gltf")]
627    #[test]
628    fn test_merge_vertices_cube() {
629        use crate::{integrations::gltf, utils::get_tracing_subscriber};
630
631        get_tracing_subscriber();
632        let mut meshgraph = gltf::load("src/ops/glb/cube.glb").unwrap();
633
634        #[cfg(feature = "rerun")]
635        meshgraph.log_rerun();
636
637        let x0_vertices: Vec<VertexId> = meshgraph
638            .positions
639            .iter()
640            .filter_map(|(v_id, pos)| if pos.x == 0.0 { Some(v_id) } else { None })
641            .collect();
642
643        let x0_count = x0_vertices.len();
644        assert!(x0_count >= 2, "Expected at least 2 vertices with x=0");
645
646        let MergeVertices {
647            removed_vertices,
648            removed_halfedges,
649            removed_faces,
650        } = meshgraph.merge_vertices(x0_vertices);
651
652        #[cfg(feature = "rerun")]
653        {
654            meshgraph.log_rerun();
655            crate::RR.flush_blocking().unwrap();
656        }
657
658        let remaining_x0: Vec<_> = meshgraph
659            .positions
660            .iter()
661            .filter(|(_, pos)| pos.x == 0.0)
662            .collect();
663
664        assert_eq!(remaining_x0.len(), 1);
665
666        assert_eq!(meshgraph.vertices.len(), 5);
667        assert_eq!(meshgraph.halfedges.len(), 18);
668        assert_eq!(meshgraph.faces.len(), 6);
669
670        assert_eq!(removed_vertices.len(), 3);
671        assert_eq!(removed_halfedges.len(), 18);
672        assert_eq!(removed_faces.len(), 6);
673    }
674
675    #[cfg(feature = "gltf")]
676    #[test]
677    fn test_merge_vertices_cube_w_missing_triangle() {
678        use crate::{integrations::gltf, utils::get_tracing_subscriber};
679
680        get_tracing_subscriber();
681        let mut meshgraph = gltf::load("src/ops/glb/cube_w_missing_triangle.glb").unwrap();
682
683        #[cfg(feature = "rerun")]
684        meshgraph.log_rerun();
685
686        let x0_vertices: Vec<VertexId> = meshgraph
687            .positions
688            .iter()
689            .filter_map(|(v_id, pos)| if pos.x == 0.0 { Some(v_id) } else { None })
690            .collect();
691
692        let x0_count = x0_vertices.len();
693        assert!(x0_count >= 2, "Expected at least 2 vertices with x=0");
694
695        let MergeVertices {
696            removed_vertices,
697            removed_halfedges,
698            removed_faces,
699        } = meshgraph.merge_vertices(x0_vertices);
700
701        #[cfg(feature = "rerun")]
702        {
703            meshgraph.log_rerun();
704            crate::RR.flush_blocking().unwrap();
705        }
706
707        let remaining_x0: Vec<_> = meshgraph
708            .positions
709            .iter()
710            .filter(|(_, pos)| pos.x == 0.0)
711            .collect();
712
713        assert_eq!(remaining_x0.len(), 1);
714
715        assert_eq!(meshgraph.vertices.len(), 5);
716        assert_eq!(meshgraph.halfedges.len(), 18);
717        assert_eq!(meshgraph.faces.len(), 6);
718
719        assert_eq!(removed_vertices.len(), 3);
720        assert_eq!(removed_halfedges.len(), 18);
721        assert_eq!(removed_faces.len(), 5);
722    }
723
724    #[test]
725    fn test_flip_edge() {
726        let mut mesh_graph = MeshGraph::new();
727
728        let v_id1 = mesh_graph.add_vertex(Vec3::new(-1.0, 1.0, 0.0));
729        let v_id2 = mesh_graph.add_vertex(Vec3::new(-1.0, -1.0, 0.0));
730        let v_id3 = mesh_graph.add_vertex(Vec3::new(1.0, -1.0, 0.0));
731        let v_id4 = mesh_graph.add_vertex(Vec3::new(1.0, 1.0, 0.0));
732
733        mesh_graph.add_face_from_vertices(v_id1, v_id2, v_id3);
734        mesh_graph.add_face_from_vertices(v_id1, v_id3, v_id4);
735
736        #[cfg(feature = "rerun")]
737        mesh_graph.log_rerun();
738
739        assert!(mesh_graph.halfedge_from_to(v_id2, v_id4).is_none());
740
741        assert_eq!(mesh_graph.outgoing_halfedges[v_id1].len(), 3);
742        assert_eq!(mesh_graph.outgoing_halfedges[v_id2].len(), 2);
743        assert_eq!(mesh_graph.outgoing_halfedges[v_id3].len(), 3);
744        assert_eq!(mesh_graph.outgoing_halfedges[v_id4].len(), 2);
745
746        mesh_graph.flip_edge(mesh_graph.halfedge_from_to(v_id1, v_id3).unwrap());
747
748        #[cfg(feature = "rerun")]
749        {
750            mesh_graph.log_rerun();
751            crate::RR.flush_blocking().unwrap();
752        }
753
754        assert!(mesh_graph.halfedge_from_to(v_id1, v_id3).is_none());
755        assert!(mesh_graph.halfedge_from_to(v_id2, v_id4).is_some());
756
757        assert_eq!(mesh_graph.outgoing_halfedges[v_id1].len(), 2);
758        assert_eq!(mesh_graph.outgoing_halfedges[v_id2].len(), 3);
759        assert_eq!(mesh_graph.outgoing_halfedges[v_id3].len(), 2);
760        assert_eq!(mesh_graph.outgoing_halfedges[v_id4].len(), 3);
761    }
762}