1mod access;
71mod elements;
72pub mod integrations;
73mod iter;
74mod ops;
75mod plane_slice;
76pub mod primitives;
77#[cfg(feature = "rerun")]
78mod rerun_impl;
79mod selection;
80#[cfg(feature = "serde")]
81mod serialize;
82pub mod utils;
83
84pub use elements::*;
85pub use iter::*;
86pub use ops::*;
87pub use plane_slice::*;
88pub use selection::*;
89
90use hashbrown::HashMap;
91use parry3d::partitioning::{Bvh, BvhWorkspace};
92
93use glam::Vec3;
94use slotmap::{SecondaryMap, SlotMap};
95
96use tracing::{error, instrument};
97
98use crate::utils::unwrap_or_return;
99
100#[cfg(feature = "instrumentation")]
101thread_local! {
106 static CURRENT_OP: std::cell::Cell<&'static str> = const { std::cell::Cell::new("unknown") };
107}
108
109#[cfg(feature = "instrumentation")]
111#[inline]
112pub(crate) fn set_current_op(op: &'static str) {
113 CURRENT_OP.with(|cell| cell.set(op));
114}
115
116#[cfg(feature = "instrumentation")]
119static FACE_DEATH_LEDGER: std::sync::Mutex<std::collections::VecDeque<(FaceId, &'static str)>> =
120 std::sync::Mutex::new(std::collections::VecDeque::new());
121
122#[cfg(feature = "instrumentation")]
123const FACE_DEATH_LEDGER_CAP: usize = 64;
124
125#[cfg(feature = "instrumentation")]
128#[inline]
129pub(crate) fn record_face_death(face_id: FaceId) {
130 let op = CURRENT_OP.with(|cell| cell.get());
131 if let Ok(mut ledger) = FACE_DEATH_LEDGER.lock() {
132 ledger.push_back((face_id, op));
133 if ledger.len() > FACE_DEATH_LEDGER_CAP {
134 ledger.pop_front();
135 }
136 }
137}
138
139#[cfg(feature = "instrumentation")]
147thread_local! {
148 static OP_BOUNDARY: std::cell::RefCell<Option<hashbrown::HashSet<(HalfedgeId, HalfedgeId)>>> =
149 const { std::cell::RefCell::new(None) };
150}
151
152#[cfg(feature = "instrumentation")]
155pub(crate) fn hole_check_enabled() -> bool {
156 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
157 *ENABLED.get_or_init(|| std::env::var_os("MESH_GRAPH_HOLE_CHECK").is_some())
158}
159
160#[cfg(feature = "instrumentation")]
166#[inline]
167pub(crate) fn probe_chain_begin(mesh: &MeshGraph) {
168 if !hole_check_enabled() {
169 return;
170 }
171 let boundary = mesh.boundary_edge_set();
172 OP_BOUNDARY.with(|b| *b.borrow_mut() = Some(boundary));
173}
174
175#[cfg(feature = "instrumentation")]
178fn dump_boundary_delta(added: &[(HalfedgeId, HalfedgeId)], removed: &[(HalfedgeId, HalfedgeId)]) {
179 if !added.is_empty() {
180 eprintln!(" added {}:", added.len());
181 for (a, b) in added.iter().take(8) {
182 eprintln!(" edge ({a:?}, {b:?})");
183 }
184 }
185 if !removed.is_empty() {
186 eprintln!(" removed {}:", removed.len());
187 for (a, b) in removed.iter().take(8) {
188 eprintln!(" edge ({a:?}, {b:?})");
189 }
190 }
191}
192
193#[cfg(feature = "instrumentation")]
195pub(crate) fn dump_face_death_ledger() {
196 if let Ok(ledger) = FACE_DEATH_LEDGER.lock() {
197 for (face_id, op) in ledger.iter() {
198 eprintln!(" face {face_id:?} removed by '{op}'");
199 }
200 }
201}
202
203#[cfg(feature = "instrumentation")]
208#[inline]
209pub(crate) fn report_dead_halfedge_in_collapse_check(mesh_graph: &MeshGraph, dead_id: HalfedgeId) {
210 static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
211 if REPORTED.set(()).is_err() {
212 return;
213 }
214 mark_integrity_violation();
215 eprintln!("DEAD ID IN COLLAPSE CHECK: {dead_id:?} was inserted via a live halfedge's twin");
216 for (he_id, he) in &mesh_graph.halfedges {
217 if he.twin == Some(dead_id) {
218 let face_alive = he.face.is_some_and(|f| mesh_graph.faces.contains_key(f));
219 eprintln!(
220 " violator {he_id:?}: face={:?} (alive={face_alive}) next={:?} twin={:?} end={:?}",
221 he.face, he.next, he.twin, he.end_vertex
222 );
223 }
224 }
225 eprintln!("{}", std::backtrace::Backtrace::force_capture());
226 eprintln!("recent face deaths (oldest first):");
227 dump_face_death_ledger();
228 state_history_dump(
229 "dead_id_in_collapse_check",
230 Some(mesh_graph),
231 Some(&dead_id),
232 );
233}
234
235#[cfg(feature = "instrumentation")]
274pub(crate) static OP_TRACE: std::sync::Mutex<std::collections::VecDeque<String>> =
275 std::sync::Mutex::new(std::collections::VecDeque::new());
276
277#[cfg(feature = "instrumentation")]
278pub(crate) const OP_TRACE_CAP: usize = 2000;
279
280#[cfg(feature = "instrumentation")]
283pub(crate) fn op_trace_enabled() -> bool {
284 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
285 *ENABLED.get_or_init(|| std::env::var_os("MESH_GRAPH_TRACE").is_some())
286}
287
288#[cfg(feature = "instrumentation")]
291#[doc(hidden)]
292#[macro_export]
293macro_rules! record_op_trace {
294 ($($arg:tt)*) => {
295 if $crate::op_trace_enabled() {
296 $crate::record_op_trace_impl(format_args!($($arg)*).to_string());
297 }
298 };
299}
300
301#[cfg(feature = "instrumentation")]
302#[inline]
303pub(crate) fn record_op_trace_impl(event: String) {
304 if let Ok(mut trace) = OP_TRACE.lock() {
305 trace.push_back(event);
306 while trace.len() > OP_TRACE_CAP {
307 trace.pop_front();
308 }
309 }
310}
311
312#[cfg(feature = "instrumentation")]
316static REPLAY_POSITION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
317
318#[cfg(feature = "instrumentation")]
323pub fn set_replay_position(pos: u64) {
324 REPLAY_POSITION.store(pos, std::sync::atomic::Ordering::Relaxed);
325}
326
327#[cfg(feature = "instrumentation")]
328fn replay_position() -> u64 {
329 REPLAY_POSITION.load(std::sync::atomic::Ordering::Relaxed)
330}
331
332#[cfg(feature = "instrumentation")]
333thread_local! {
334 static INTEGRITY_VIOLATION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
343}
344
345#[cfg(feature = "instrumentation")]
347pub(crate) fn mark_integrity_violation() {
348 INTEGRITY_VIOLATION.with(|cell| cell.set(true));
349}
350
351#[cfg(feature = "instrumentation")]
354pub fn integrity_violation_reported() -> bool {
355 INTEGRITY_VIOLATION.with(|cell| cell.get())
356}
357
358#[cfg(feature = "instrumentation")]
361pub fn reset_integrity_violation() {
362 INTEGRITY_VIOLATION.with(|cell| cell.set(false));
363}
364
365#[cfg(feature = "instrumentation")]
367struct StateSnapshot {
368 pos: u64,
369 op: &'static str,
370 mesh: MeshGraph,
371}
372
373#[cfg(feature = "instrumentation")]
375pub(crate) static STATE_RING: std::sync::Mutex<std::collections::VecDeque<StateSnapshot>> =
376 std::sync::Mutex::new(std::collections::VecDeque::new());
377
378#[cfg(feature = "instrumentation")]
379thread_local! {
383 static STATE_RING_CAP: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
384}
385
386#[cfg(feature = "instrumentation")]
389static STATE_DUMP_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
390
391#[cfg(feature = "instrumentation")]
395pub fn state_dump_dir() -> Option<std::path::PathBuf> {
396 STATE_DUMP_DIR.get().cloned()
397}
398
399#[cfg(feature = "instrumentation")]
404fn state_ring_cap() -> usize {
405 STATE_RING_CAP.with(|cap| match cap.get() {
406 Some(cap) => cap,
407 None => {
408 let from_env = std::env::var_os("MESH_GRAPH_STATE_HISTORY_LEN")
409 .and_then(|s| s.to_str().and_then(|s| s.parse::<usize>().ok()))
410 .unwrap_or(0);
411 cap.set(Some(from_env));
412 from_env
413 }
414 })
415}
416
417#[cfg(feature = "instrumentation")]
421pub fn set_state_history_len(len: usize) {
422 STATE_RING_CAP.with(|cap| cap.set(Some(len)));
423}
424
425#[cfg(feature = "instrumentation")]
427fn state_dump_at_pos() -> Option<u64> {
428 static AT_POS: std::sync::OnceLock<Option<u64>> = std::sync::OnceLock::new();
429 *AT_POS.get_or_init(|| {
430 std::env::var_os("MESH_GRAPH_STATE_DUMP_AT_POS")
431 .and_then(|s| s.to_str().and_then(|s| s.parse::<u64>().ok()))
432 })
433}
434
435#[cfg(feature = "instrumentation")]
440#[inline]
441pub(crate) fn state_history_push(mesh: &MeshGraph, op: &'static str) {
442 let cap = state_ring_cap();
443 if cap == 0 {
444 return;
445 }
446
447 let snapshot = StateSnapshot {
448 pos: replay_position(),
449 op,
450 mesh: mesh.clone(),
451 };
452 if let Ok(mut ring) = STATE_RING.lock() {
453 ring.push_back(snapshot);
454 while ring.len() > cap {
455 ring.pop_front();
456 }
457 }
458
459 if let Some(target) = state_dump_at_pos()
461 && replay_position() >= target
462 {
463 state_history_dump("at_position", Some(mesh), None);
464 }
465}
466
467#[cfg(feature = "instrumentation")]
471pub(crate) fn state_history_dump(
472 reason: &str,
473 current: Option<&MeshGraph>,
474 context: Option<&dyn std::fmt::Debug>,
475) {
476 static DUMPED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
477 if DUMPED.set(()).is_err() {
478 return;
479 }
480
481 let dir = std::env::var_os("MESH_GRAPH_STATE_DUMP_DIR")
482 .map(std::path::PathBuf::from)
483 .unwrap_or_else(|| {
484 std::path::PathBuf::from(format!("mesh_graph_state_dump_{}", std::process::id()))
485 });
486 if let Err(e) = std::fs::create_dir_all(&dir) {
487 eprintln!("state dump: could not create {}: {e:?}", dir.display());
488 return;
489 }
490 let _ = STATE_DUMP_DIR.set(dir.clone());
493
494 let mut meta = String::new();
495 meta.push_str(&format!(
496 "reason: {reason}\ncurrent replay position: {}\n",
497 replay_position()
498 ));
499 if let Some(context) = context {
500 meta.push_str(&format!("context: {context:?}\n"));
501 }
502
503 if let Ok(ring) = STATE_RING.lock() {
504 meta.push_str("ring entries (oldest first):\n");
505 for (i, snap) in ring.iter().enumerate() {
506 meta.push_str(&format!(
507 " state_{i:02}: pos={} op={}\n",
508 snap.pos, snap.op
509 ));
510 }
511 }
512
513 if let Some(current) = current {
514 let path = dir.join("current.json");
515 if let Err(e) = current.save_state(&path) {
516 eprintln!("state dump: could not write {}: {e:?}", path.display());
517 }
518 meta.push_str(&format!(
519 "current.json: current broken state (pos {})\n",
520 replay_position()
521 ));
522 }
523
524 if let Ok(ring) = STATE_RING.lock() {
525 for (i, snap) in ring.iter().enumerate() {
526 let path = dir.join(format!("state_{i:02}_pos_{:06}_{}.json", snap.pos, snap.op));
527 if let Err(e) = snap.mesh.save_state(&path) {
528 eprintln!("state dump: could not write {}: {e:?}", path.display());
529 }
530 }
531 }
532
533 if let Err(e) = std::fs::write(dir.join("meta.txt"), meta) {
534 eprintln!("state dump: could not write meta.txt: {e:?}");
535 }
536 eprintln!("state history dumped to {}", dir.display());
537}
538
539#[cfg(feature = "rerun")]
540lazy_static::lazy_static! {
541 pub static ref RR: rerun::RecordingStream = rerun::RecordingStreamBuilder::new("mesh_graph").spawn().unwrap();
542}
543
544#[derive(Clone, Default)]
548#[cfg_attr(feature = "bevy", derive(bevy::prelude::Component))]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550#[cfg_attr(
551 feature = "serde",
552 serde(from = "crate::serialize::MeshGraphIntermediate")
553)]
554pub struct MeshGraph {
555 #[cfg_attr(feature = "serde", serde(skip))]
557 pub bvh: Bvh,
558 #[cfg_attr(feature = "serde", serde(skip))]
560 pub bvh_workspace: BvhWorkspace,
561 #[cfg_attr(feature = "serde", serde(skip))]
563 pub index_to_face_id: HashMap<u32, FaceId>,
564 #[cfg_attr(feature = "serde", serde(skip))]
566 pub next_index: u32,
567
568 pub vertices: SlotMap<VertexId, Vertex>,
570 pub halfedges: SlotMap<HalfedgeId, Halfedge>,
572 pub faces: SlotMap<FaceId, Face>,
574
575 pub positions: SecondaryMap<VertexId, Vec3>,
577 pub vertex_normals: Option<SecondaryMap<VertexId, Vec3>>,
579
580 #[cfg_attr(feature = "serde", serde(skip))]
582 pub outgoing_halfedges: SecondaryMap<VertexId, Vec<HalfedgeId>>,
583}
584
585impl MeshGraph {
586 #[inline]
588 pub fn new() -> Self {
589 Self::default()
590 }
591
592 pub fn triangles(vertex_positions: &[Vec3]) -> Option<Self> {
597 if !vertex_positions.len().is_multiple_of(3) {
598 return None;
599 }
600
601 let mut unique_positions: Vec<Vec3> = Vec::with_capacity(vertex_positions.len() / 3);
603 let mut face_indices = Vec::with_capacity(vertex_positions.len());
604
605 for vertex_pos in vertex_positions {
606 let mut idx = None;
608 for (j, pos) in unique_positions.iter().enumerate() {
609 const EPSILON: f32 = 1e-5;
610
611 if pos.distance_squared(*vertex_pos) < EPSILON {
612 idx = Some(j);
613 break;
614 }
615 }
616
617 let vertex_idx = if let Some(idx) = idx {
619 idx
620 } else {
621 let new_idx = unique_positions.len();
622 unique_positions.push(*vertex_pos);
623
624 #[cfg(feature = "rerun")]
625 RR.log(
626 "meshgraph/construct/vertices",
627 &rerun::Points3D::new(unique_positions.iter().map(crate::utils::vec3_array)),
628 )
629 .unwrap();
630
631 new_idx
632 };
633
634 face_indices.push(vertex_idx);
636 }
637
638 Some(Self::indexed_triangles(&unique_positions, &face_indices))
640 }
641
642 #[instrument]
645 pub fn indexed_triangles_with_custom_attribute<T>(
646 vertex_positions: &[Vec3],
647 face_indices: &[usize],
648 custom_attribute: &[T],
649 ) -> (Self, SecondaryMap<VertexId, T>)
650 where
651 T: Clone + std::fmt::Debug,
652 {
653 let (mesh_graph, vertex_ids) =
654 Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices);
655
656 let mut custom_attribute_map = SecondaryMap::with_capacity(custom_attribute.len());
657 for (attr, vertex_id) in custom_attribute.iter().zip(vertex_ids) {
658 custom_attribute_map.insert(vertex_id, attr.clone());
659 }
660
661 (mesh_graph, custom_attribute_map)
662 }
663
664 #[inline]
667 pub fn indexed_triangles(vertex_positions: &[Vec3], face_indices: &[usize]) -> Self {
668 Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices).0
669 }
670
671 #[instrument]
674 pub fn indexed_triangles_and_vertex_ids(
675 vertex_positions: &[Vec3],
676 face_indices: &[usize],
677 ) -> (Self, Vec<VertexId>) {
678 let mut mesh_graph = Self {
679 bvh: Bvh::new(),
680 bvh_workspace: BvhWorkspace::default(),
681 index_to_face_id: HashMap::with_capacity(face_indices.len() / 3),
682 next_index: 0,
683
684 vertices: SlotMap::with_capacity_and_key(vertex_positions.len()),
685 halfedges: SlotMap::with_capacity_and_key(face_indices.len()),
686 faces: SlotMap::with_capacity_and_key(face_indices.len() / 3),
687
688 positions: SecondaryMap::with_capacity(vertex_positions.len()),
689 vertex_normals: None,
690 outgoing_halfedges: SecondaryMap::with_capacity(vertex_positions.len()),
691 };
692
693 let mut vertex_ids = Vec::with_capacity(vertex_positions.len());
694
695 for pos in vertex_positions {
696 vertex_ids.push(mesh_graph.add_vertex(*pos));
697 }
698
699 for chunk in face_indices.as_chunks::<3>().0 {
700 let a = vertex_ids[chunk[0]];
701 let b = vertex_ids[chunk[1]];
702 let c = vertex_ids[chunk[2]];
703
704 if a == b || b == c || c == a {
705 #[cfg(feature = "rerun")]
706 RR.log(
707 "meshgraph/construct/zero_face",
708 &rerun::Points3D::new(
709 [
710 mesh_graph.positions[a],
711 mesh_graph.positions[b],
712 mesh_graph.positions[c],
713 ]
714 .iter()
715 .map(crate::utils::vec3_array),
716 ),
717 )
718 .unwrap();
719
720 continue;
721 }
722
723 let he_a_id = mesh_graph.add_or_get_edge(a, b).unwrap().start_to_end_he_id;
725 let he_b_id = mesh_graph.add_or_get_edge(b, c).unwrap().start_to_end_he_id;
726 let he_c_id = mesh_graph.add_or_get_edge(c, a).unwrap().start_to_end_he_id;
727
728 let _face_id = mesh_graph.add_face(he_a_id, he_b_id, he_c_id);
729 }
730
731 mesh_graph.make_all_outgoing_halfedges_boundary_if_possible();
732 mesh_graph.rebuild_bvh();
733
734 (mesh_graph, vertex_ids)
735 }
736
737 #[instrument(skip(self))]
747 fn pair_with_fresh_boundary_half(
748 &mut self,
749 survivor_id: HalfedgeId,
750 survivor_start_v: VertexId,
751 ) -> Option<HalfedgeId> {
752 let survivor = self
753 .halfedges
754 .get(survivor_id)
755 .or_else(error_none!("survivor he not found"))?;
756 let survivor_end = survivor.end_vertex;
757 let boundary_id = self.add_halfedge(survivor_end, survivor_start_v)?;
760 self.halfedges[survivor_id].twin = Some(boundary_id);
762 self.halfedges[boundary_id].twin = Some(survivor_id);
764
765 #[cfg(feature = "instrumentation")]
766 crate::record_op_trace!(
767 "fresh boundary {boundary_id:?} ({survivor_end:?}->{survivor_start_v:?}) paired with survivor {survivor_id:?}"
768 );
769
770 Some(boundary_id)
771 }
772
773 fn reseed_outgoing_if_dead(&mut self, vertex_id: VertexId) {
779 let Some(vertex) = self.vertices.get(vertex_id) else {
780 return;
781 };
782 if vertex
783 .outgoing_halfedge
784 .is_some_and(|he| self.halfedges.contains_key(he))
785 {
786 return;
787 }
788 let new_seed = self.outgoing_halfedges.get(vertex_id).and_then(|list| {
789 list.iter()
790 .copied()
791 .find(|he| self.halfedges.contains_key(*he))
792 });
793 if let Some(v) = self.vertices.get_mut(vertex_id) {
794 v.outgoing_halfedge = new_seed;
795 }
796 }
797
798 #[cfg(feature = "instrumentation")]
811 pub(crate) fn probe_live_face_removal(&self, removed_ids: &[HalfedgeId], op: &str) {
812 if removed_ids.is_empty() {
813 return;
814 }
815
816 static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
817 if !matches!(op, "remove_face_tail" | "remove_halfedge_face") {
818 for id in removed_ids {
819 if let Some(he) = self.halfedges.get(*id)
820 && let Some(face_id) = he.face
821 && self.faces.contains_key(face_id)
822 {
823 let other_members: Vec<HalfedgeId> = self
824 .halfedges
825 .iter()
826 .filter(|(h_id, h)| {
827 h.face == Some(face_id) && *h_id != *id && !removed_ids.contains(h_id)
828 })
829 .map(|(h_id, _)| h_id)
830 .take(4)
831 .collect();
832 if !other_members.is_empty() && REPORTED.set(()).is_ok() {
833 mark_integrity_violation();
834 eprintln!(
835 "REMOVING LIVE-FACE MEMBER {id:?} of face {face_id:?} (surviving members {other_members:?})"
836 );
837 eprintln!("{}", std::backtrace::Backtrace::force_capture());
838 state_history_dump("live_face_member_removal", Some(self), Some(&id));
839 }
840 }
841 }
842 }
843 }
844
845 #[cfg(feature = "instrumentation")]
872 pub(crate) fn probe_chain_integrity(&self, op: &str) -> bool {
873 static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
874
875 let mut violations: Vec<(HalfedgeId, &'static str)> = Vec::new();
876 let mut dead_face_siblings: Vec<FaceId> = Vec::new();
877 for (he_id, he) in &self.halfedges {
878 let Some(face_id) = he.face else {
879 continue;
880 };
881
882 let verdict = if !self.faces.contains_key(face_id) {
883 if !dead_face_siblings.contains(&face_id) {
884 dead_face_siblings.push(face_id);
885 }
886 Some("member of a removed face")
887 } else {
888 match he.next {
889 None => Some("member of a live face with next=None"),
890 Some(next_id) => match self.halfedges.get(next_id) {
891 None => Some("next references a removed halfedge"),
892 Some(next_he) if next_he.face != Some(face_id) => {
893 Some("next references a halfedge of another/no face")
894 }
895 Some(_) => None,
896 },
897 }
898 };
899
900 if let Some(reason) = verdict {
901 violations.push((he_id, reason));
902 if violations.len() >= 12 {
903 break;
904 }
905 }
906 }
907
908 let mut fork: Option<(FaceId, Vec<HalfedgeId>, Vec<HalfedgeId>)> = None;
914 let mut family_by_face: hashbrown::HashMap<FaceId, Vec<HalfedgeId>> =
915 hashbrown::HashMap::new();
916 for (h_id, h) in &self.halfedges {
917 if let Some(f) = h.face {
918 family_by_face.entry(f).or_default().push(h_id);
919 }
920 }
921 for (face_id, face) in &self.faces {
922 let Some(family) = family_by_face.get(&face_id) else {
923 continue;
924 };
925 if family.len() < 3 {
926 continue;
928 }
929 let mut chain = Vec::with_capacity(family.len());
930 let mut cur = Some(face.halfedge);
931 let mut steps = 0;
932 while let Some(he_id) = cur {
933 if !self.halfedges.contains_key(he_id) || chain.len() > family.len() + 2 {
934 break;
935 }
936 if chain.contains(&he_id) {
937 break;
938 }
939 chain.push(he_id);
940 cur = self.halfedges[he_id].next;
941 steps += 1;
942 if steps > 16 {
943 break;
944 }
945 }
946 if chain.len() != family.len() || family.iter().any(|h_id| !chain.contains(h_id)) {
947 fork = Some((face_id, family.clone(), chain));
948 break;
949 }
950 }
951
952 let fmt_ids = |ids: &[HalfedgeId]| -> String {
960 if ids.len() <= 8 {
961 format!("{ids:?}")
962 } else {
963 format!("{:?}... ({} ids)", &ids[..8], ids.len())
964 }
965 };
966 let mut twin_problems: Vec<String> = Vec::new();
967 let mut membership_problems: Vec<String> = Vec::new();
968 let mut order_deviation_count: usize = 0;
971 let mut first_order_sample: Option<String> = None;
972 struct OutScratch {
976 expected: hashbrown::HashMap<VertexId, Vec<HalfedgeId>>,
977 counts: hashbrown::HashMap<HalfedgeId, usize>,
978 }
979 static OUT_SCRATCH: std::sync::Mutex<Option<OutScratch>> = std::sync::Mutex::new(None);
980 let mut scratch = OUT_SCRATCH.lock().unwrap();
981 let scratch = scratch.get_or_insert_with(|| OutScratch {
982 expected: hashbrown::HashMap::new(),
983 counts: hashbrown::HashMap::new(),
984 });
985 for list in scratch.expected.values_mut() {
986 list.clear();
987 }
988 scratch.expected.clear();
989 scratch.counts.clear();
990
991 for (he_id, he) in &self.halfedges {
992 match he.twin {
993 None => twin_problems.push(format!("halfedge {he_id:?} has twin=None")),
994 Some(twin_id) => {
995 if !self.halfedges.contains_key(twin_id) {
996 twin_problems.push(format!(
997 "halfedge {he_id:?} has twin {twin_id:?} which is removed"
998 ));
999 } else if self.halfedges[twin_id].twin != Some(he_id) {
1000 twin_problems.push(format!(
1001 "halfedge {he_id:?} has twin {twin_id:?} which does not point back"
1002 ));
1003 }
1004 }
1005 }
1006 if let Some(twin_id) = he.twin {
1007 scratch
1008 .expected
1009 .entry(he.end_vertex)
1010 .or_default()
1011 .push(twin_id);
1012 }
1013 }
1014
1015 for (v_id, expected) in scratch.expected.iter() {
1016 let actual: &[HalfedgeId] = self
1017 .outgoing_halfedges
1018 .get(*v_id)
1019 .map(Vec::as_slice)
1020 .unwrap_or(&[]);
1021 scratch.counts.clear();
1023 for &a in actual {
1024 *scratch.counts.entry(a).or_default() += 1;
1025 }
1026 let mut missing: Vec<HalfedgeId> = Vec::new();
1027 for &exp in expected {
1028 match scratch.counts.get_mut(&exp) {
1029 Some(c) if *c > 0 => *c -= 1,
1030 _ => missing.push(exp),
1031 }
1032 }
1033 let mut extra: Vec<HalfedgeId> = Vec::new();
1034 for &a in actual {
1035 if scratch.counts.get(&a) != Some(&0) {
1036 extra.push(a);
1037 }
1038 }
1039 if !missing.is_empty() || !extra.is_empty() {
1040 let mut msg = format!("vertex {v_id:?}: outgoing deviates from ground truth");
1041 if !missing.is_empty() {
1042 msg += &format!(", missing {}", fmt_ids(&missing));
1043 }
1044 if !extra.is_empty() {
1045 msg += &format!(", extra {}", fmt_ids(&extra));
1046 }
1047 membership_problems.push(msg);
1048 } else if actual != expected.as_slice() {
1049 order_deviation_count += 1;
1050 if first_order_sample.is_none() {
1051 first_order_sample = Some(format!(
1052 "vertex {v_id:?}: outgoing order differs from rebuild order"
1053 ));
1054 }
1055 }
1056 }
1057 for (v_id, actual) in &self.outgoing_halfedges {
1059 if !scratch.expected.contains_key(&v_id) && !actual.is_empty() {
1060 membership_problems.push(format!(
1061 "vertex {v_id:?}: outgoing {} but no live halfedge ends at it",
1062 fmt_ids(actual)
1063 ));
1064 }
1065 }
1066
1067 let mut seed_deviations: Vec<String> = Vec::new();
1070 for (v_id, vertex) in &self.vertices {
1071 let stored = vertex.outgoing_halfedge;
1072 let rebuilt_seed = stored
1073 .filter(|he| self.halfedges.contains_key(*he))
1074 .or_else(|| scratch.expected.get(&v_id).and_then(|l| l.first().copied()));
1075 if stored != rebuilt_seed {
1076 seed_deviations.push(format!(
1077 "vertex {v_id:?}: seed {stored:?} != rebuilt {rebuilt_seed:?}"
1078 ));
1079 }
1080 }
1081
1082 let clean =
1083 violations.is_empty() && twin_problems.is_empty() && membership_problems.is_empty();
1084
1085 if !clean && REPORTED.set(()).is_ok() {
1086 mark_integrity_violation();
1087 eprintln!(
1088 "CHAIN CORRUPTION detected at end of op '{op}' ({} violations shown):",
1089 violations.len()
1090 );
1091 for (he_id, reason) in violations {
1092 let detail = self.halfedges.get(he_id).map(|he| {
1093 format!(
1094 "face={:?} next={:?} twin={:?} end={:?}",
1095 he.face, he.next, he.twin, he.end_vertex
1096 )
1097 });
1098 eprintln!(" halfedge {he_id:?}: {reason}; {detail:?}");
1099 if let Some(he) = self.halfedges.get(he_id) {
1103 if let Some(next_id) = he.next
1104 && let Some(next_he) = self.halfedges.get(next_id)
1105 {
1106 eprintln!(
1107 " next {next_id:?}: face={:?} next={:?} twin={:?} end={:?}",
1108 next_he.face, next_he.next, next_he.twin, next_he.end_vertex
1109 );
1110 }
1111 if let Some(twin_id) = he.twin
1112 && let Some(twin_he) = self.halfedges.get(twin_id)
1113 {
1114 eprintln!(
1115 " twin {twin_id:?}: face={:?} next={:?} twin={:?} end={:?}",
1116 twin_he.face, twin_he.next, twin_he.twin, twin_he.end_vertex
1117 );
1118 }
1119 if let Some(start_v) = he.start_vertex(self) {
1120 let out: Vec<HalfedgeId> = self
1121 .outgoing_halfedges
1122 .get(start_v)
1123 .map(|l| l.iter().copied().take(6).collect())
1124 .unwrap_or_default();
1125 let out_desc: Vec<String> = out
1126 .iter()
1127 .filter_map(|id| {
1128 self.halfedges
1129 .get(*id)
1130 .map(|h| format!("{id:?}(face={:?},next={:?})", h.face, h.next))
1131 })
1132 .collect();
1133 eprintln!(" start vertex {start_v:?} outgoing: {out_desc:?}");
1134 }
1135 }
1136 }
1137 for dead_face_id in &dead_face_siblings {
1140 let family: Vec<HalfedgeId> = self
1141 .halfedges
1142 .iter()
1143 .filter(|(_, h)| h.face == Some(*dead_face_id))
1144 .map(|(h_id, _)| h_id)
1145 .collect();
1146 eprintln!(" halfedges claiming removed face {dead_face_id:?}: {family:?}");
1147 }
1148 eprintln!("{}", std::backtrace::Backtrace::force_capture());
1149 eprintln!("recent face deaths (oldest first):");
1150 dump_face_death_ledger();
1151 if let Some((fork_face, fork_family, fork_chain)) = fork {
1152 eprintln!("family-vs-chain for face {fork_face:?}:");
1153 eprintln!(" chain (walked): {fork_chain:?}");
1154 eprintln!(" family (face field): {fork_family:?}");
1155 }
1156 for problem in &twin_problems {
1157 eprintln!("TWIN: {problem}");
1158 }
1159 for problem in membership_problems.iter().take(12) {
1160 eprintln!("OUTGOING: {problem}");
1161 }
1162 if let Ok(trace) = OP_TRACE.lock() {
1163 eprintln!("op trace (oldest first):");
1164 for event in trace.iter() {
1165 eprintln!(" {event}");
1166 }
1167 }
1168 state_history_dump("chain_integrity", Some(self), Some(&op));
1169 }
1170
1171 if hole_check_enabled() {
1188 static HOLE_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
1189 static RIM_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
1190 let current = self.boundary_edge_set();
1191 let begin = OP_BOUNDARY.with(|b| b.borrow().clone());
1192 if let Some(begin) = begin {
1193 let added: Vec<(HalfedgeId, HalfedgeId)> =
1194 current.difference(&begin).copied().collect();
1195 let removed: Vec<(HalfedgeId, HalfedgeId)> =
1196 begin.difference(¤t).copied().collect();
1197 if !added.is_empty() || !removed.is_empty() {
1198 let boundary_count =
1199 self.halfedges.values().filter(|h| h.face.is_none()).count();
1200 if begin.is_empty() {
1201 if HOLE_REPORTED.set(()).is_ok() {
1204 mark_integrity_violation();
1205 eprintln!(
1206 "HOLE DELTA: op '{op}' changed the boundary edge set of a closed region (now {boundary_count} boundary halfedges):"
1207 );
1208 dump_boundary_delta(&added, &removed);
1209 eprintln!("{}", std::backtrace::Backtrace::force_capture());
1210 if let Ok(trace) = OP_TRACE.lock() {
1211 eprintln!("op trace (oldest first):");
1212 for event in trace.iter() {
1213 eprintln!(" {event}");
1214 }
1215 }
1216 state_history_dump("hole", Some(self), Some(&op));
1217 }
1218 } else if RIM_REPORTED.set(()).is_ok() {
1219 eprintln!(
1222 "RIM DELTA: op '{op}' changed the boundary edge set of an open region (now {boundary_count} boundary halfedges) — expected during punch cleanup:"
1223 );
1224 dump_boundary_delta(&added, &removed);
1225 }
1226 }
1227 }
1228 }
1229
1230 if order_deviation_count > 0 {
1234 static ORDER_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
1235 if ORDER_REPORTED.set(()).is_ok() {
1236 eprintln!(
1237 "OUTGOING ORDER deviates from rebuild order at {order_deviation_count} vertices (first: {})",
1238 first_order_sample.as_deref().unwrap_or("")
1239 );
1240 }
1241 }
1242 if !seed_deviations.is_empty() {
1243 static SEED_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
1244 if SEED_REPORTED.set(()).is_ok() {
1245 eprintln!(
1246 "SEED deviates from rebuild at op '{op}' at {} vertices (first: {})",
1247 seed_deviations.len(),
1248 seed_deviations[0]
1249 );
1250 }
1251 }
1252
1253 clean
1254 }
1255
1256 #[cfg(feature = "instrumentation")]
1262 fn boundary_edge_set(&self) -> hashbrown::HashSet<(HalfedgeId, HalfedgeId)> {
1263 let mut edges = hashbrown::HashSet::new();
1264 for (he_id, he) in &self.halfedges {
1265 if he.face.is_none()
1266 && let Some(twin_id) = he.twin
1267 {
1268 edges.insert(if twin_id < he_id {
1269 (twin_id, he_id)
1270 } else {
1271 (he_id, twin_id)
1272 });
1273 }
1274 }
1275 edges
1276 }
1277
1278 pub fn rebuild_vertex_outgoing_list(&mut self, vertex_id: VertexId) {
1288 let mut list: Vec<HalfedgeId> = Vec::new();
1289 for (_, he) in &self.halfedges {
1290 if he.end_vertex == vertex_id
1291 && let Some(twin_id) = he.twin
1292 && self.halfedges.contains_key(twin_id)
1293 {
1294 list.push(twin_id);
1295 }
1296 }
1297
1298 if let Some(entry) = self.outgoing_halfedges.get_mut(vertex_id) {
1299 *entry = list;
1300 }
1301
1302 if let Some(vertex) = self.vertices.get_mut(vertex_id)
1303 && !vertex
1304 .outgoing_halfedge
1305 .is_some_and(|he| self.halfedges.contains_key(he))
1306 {
1307 vertex.outgoing_halfedge = self
1308 .outgoing_halfedges
1309 .get(vertex_id)
1310 .and_then(|l| l.first().copied());
1311 }
1312 }
1313
1314 pub fn compute_vertex_normal(&mut self, vertex_id: VertexId) {
1316 if self.vertex_normals.is_none() {
1317 return;
1318 }
1319
1320 let vertex = unwrap_or_return!(self.vertices.get(vertex_id), "Vertex not found");
1321
1322 let mut normal = Vec3::ZERO;
1323
1324 for face_id in vertex.faces(self) {
1325 let face = unwrap_or_return!(self.faces.get(face_id), "Face not found");
1326 let face_normal = face.normal(self);
1327 normal += unwrap_or_return!(face_normal, "Face normal not found");
1328 }
1329
1330 self.vertex_normals
1331 .as_mut()
1332 .unwrap()
1333 .insert(vertex_id, normal.try_normalize().unwrap_or(Vec3::ZERO));
1334 }
1335
1336 #[instrument(skip(self))]
1338 pub fn compute_vertex_normals(&mut self) {
1339 let mut normals = SecondaryMap::with_capacity(self.vertices.len());
1340
1341 for face in self.faces.values() {
1342 let Some(&he_a) = self.halfedges.get(face.halfedge) else {
1343 error!("Halfedge not found");
1344 continue;
1345 };
1346
1347 let Some(he_b_id) = he_a.next else {
1348 error!("Halfedge has no next halfedge");
1349 continue;
1350 };
1351 let Some(he_b) = self.halfedges.get(he_b_id) else {
1352 error!("Next halfedge not found");
1353 continue;
1354 };
1355
1356 let a = match he_a.start_vertex(self) {
1357 Some(v) => v,
1358 None => {
1359 error!("Start vertex not found");
1360 continue;
1361 }
1362 };
1363 let b = he_a.end_vertex;
1364 let c = he_b.end_vertex;
1365
1366 let (Some(pos_a), Some(pos_b), Some(pos_c)) = (
1367 self.positions.get(a),
1368 self.positions.get(b),
1369 self.positions.get(c),
1370 ) else {
1371 continue;
1372 };
1373
1374 let diff_a = pos_c - pos_a;
1375 let diff_b = pos_c - pos_b;
1376
1377 let face_normal = diff_a.cross(diff_b);
1379
1380 for v_id in [a, b, c] {
1381 let Some(entry) = normals.entry(v_id) else {
1382 continue;
1383 };
1384 *entry.or_default() += face_normal;
1385 }
1386 }
1387
1388 self.vertex_normals = Some(normals);
1389 self.normalize_vertex_normals();
1390 }
1391
1392 pub fn normalize_vertex_normals(&mut self) {
1394 if let Some(normals) = &mut self.vertex_normals {
1395 for normal in normals.values_mut() {
1396 *normal = normal.normalize_or_zero();
1397 }
1398 }
1399 }
1400
1401 #[inline]
1403 pub fn optimize_bvh_incremental(&mut self) {
1404 self.bvh.optimize_incremental(&mut self.bvh_workspace);
1405 }
1406
1407 #[inline]
1409 pub fn refit_bvh(&mut self) {
1410 self.bvh.refit(&mut self.bvh_workspace);
1411 }
1412
1413 #[inline]
1415 pub fn rebuild_bvh(&mut self) {
1416 self.bvh = Bvh::new();
1417 self.bvh_workspace = BvhWorkspace::default();
1418
1419 for face in self.faces.values() {
1420 self.bvh
1421 .insert_or_update_partially(face.aabb(self), face.index, 0.0);
1422 }
1423 self.bvh
1424 .rebuild(&mut self.bvh_workspace, Default::default());
1425 }
1426
1427 #[instrument(skip_all)]
1428 pub fn repair_face_pointers(&mut self) {
1436 let face_ids: Vec<FaceId> = self.faces.keys().collect();
1437 let mut visited: hashbrown::HashMap<HalfedgeId, FaceId> = hashbrown::HashMap::new();
1438
1439 for face_id in face_ids {
1440 let Some(start_he) = self.faces.get(face_id).map(|f| f.halfedge) else {
1441 continue;
1442 };
1443
1444 let mut he_id = start_he;
1445 for _ in 0..32 {
1446 let Some(he) = self.halfedges.get_mut(he_id) else {
1447 break;
1448 };
1449 he.face = Some(face_id);
1450 visited.insert(he_id, face_id);
1451
1452 let Some(next) = he.next else {
1453 break;
1454 };
1455 if next == start_he {
1456 break;
1457 }
1458 he_id = next;
1459 }
1460 }
1461
1462 let orphan_ids: Vec<HalfedgeId> = self
1466 .halfedges
1467 .iter()
1468 .filter(|(he_id, he)| he.face.is_some() && !visited.contains_key(he_id))
1469 .map(|(he_id, _)| he_id)
1470 .collect();
1471
1472 for he_id in orphan_ids {
1473 if let Some(he) = self.halfedges.get_mut(he_id) {
1474 he.face = None;
1475 }
1476 }
1477 }
1478
1479 pub fn rebuild_outgoing_halfedges(&mut self) {
1480 self.outgoing_halfedges.clear();
1481
1482 for vertex_id in self.vertices.keys() {
1487 self.outgoing_halfedges.insert(vertex_id, Vec::new());
1488 }
1489
1490 for halfedge in self.halfedges.values() {
1491 let Some(twin_id) = halfedge.twin else {
1492 error!("Halfedge has no twin");
1493 continue;
1494 };
1495
1496 let Some(entry) = self.outgoing_halfedges.entry(halfedge.end_vertex) else {
1497 error!("Vertex key invalid");
1498 continue;
1499 };
1500
1501 entry.or_default().push(twin_id);
1502 }
1503
1504 for (v_id, vertex) in &mut self.vertices {
1509 let stored_seed = vertex.outgoing_halfedge;
1510 let live_seed = stored_seed.filter(|he| self.halfedges.contains_key(*he));
1511 vertex.outgoing_halfedge = live_seed.or_else(|| {
1512 self.outgoing_halfedges
1513 .get(v_id)
1514 .and_then(|list| list.first().copied())
1515 });
1516 }
1517 }
1518}
1519
1520#[cfg(all(test, feature = "instrumentation"))]
1521mod integrity_violation_tests {
1522 use super::{
1523 integrity_violation_reported, mark_integrity_violation, reset_integrity_violation,
1524 };
1525
1526 #[test]
1529 fn violation_flag_is_per_thread_and_resettable() {
1530 reset_integrity_violation();
1531 assert!(!integrity_violation_reported());
1532
1533 std::thread::spawn(|| {
1535 mark_integrity_violation();
1536 assert!(
1537 integrity_violation_reported(),
1538 "flag must set on its own thread"
1539 );
1540 })
1541 .join()
1542 .expect("probe thread panicked");
1543 assert!(
1544 !integrity_violation_reported(),
1545 "another thread's violation leaked into this thread"
1546 );
1547
1548 mark_integrity_violation();
1549 assert!(integrity_violation_reported());
1550
1551 reset_integrity_violation();
1552 assert!(!integrity_violation_reported(), "reset must clear the flag");
1553 }
1554}