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 #[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 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 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 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 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 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 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 #[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 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 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 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 if !self.vertices.contains_key(survivor_id) {
204 return MergeVertices {
205 removed_vertices,
206 removed_halfedges,
207 removed_faces,
208 };
209 }
210
211 self.positions[survivor_id] = avg_pos;
213
214 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 {
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 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 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 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 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 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 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 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 #[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 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 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 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 #[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 #[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 self.halfedges[halfedge_id].end_vertex = opposite_v_id;
446
447 self.halfedges[prev_he_id].end_vertex = twin_opposite_v_id;
449 self.make_twins(prev_he_id, twin_next_twin_he_id);
450 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 self.halfedges[twin_he_id].end_vertex = twin_opposite_v_id;
464
465 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 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 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 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 pub fn make_twins(&mut self, he_id1: HalfedgeId, he_id2: HalfedgeId) {
496 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 #[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 #[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 #[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 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 #[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 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}