mod access;
mod elements;
pub mod integrations;
mod iter;
mod ops;
mod plane_slice;
pub mod primitives;
#[cfg(feature = "rerun")]
mod rerun_impl;
mod selection;
#[cfg(feature = "serde")]
mod serialize;
pub mod utils;
pub use elements::*;
pub use iter::*;
pub use ops::*;
pub use plane_slice::*;
pub use selection::*;
use hashbrown::HashMap;
use parry3d::partitioning::{Bvh, BvhWorkspace};
use glam::Vec3;
use slotmap::{SecondaryMap, SlotMap};
use tracing::{error, instrument};
use crate::utils::unwrap_or_return;
#[cfg(feature = "instrumentation")]
thread_local! {
static CURRENT_OP: std::cell::Cell<&'static str> = const { std::cell::Cell::new("unknown") };
}
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn set_current_op(op: &'static str) {
CURRENT_OP.with(|cell| cell.set(op));
}
#[cfg(feature = "instrumentation")]
static FACE_DEATH_LEDGER: std::sync::Mutex<std::collections::VecDeque<(FaceId, &'static str)>> =
std::sync::Mutex::new(std::collections::VecDeque::new());
#[cfg(feature = "instrumentation")]
const FACE_DEATH_LEDGER_CAP: usize = 64;
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn record_face_death(face_id: FaceId) {
let op = CURRENT_OP.with(|cell| cell.get());
if let Ok(mut ledger) = FACE_DEATH_LEDGER.lock() {
ledger.push_back((face_id, op));
if ledger.len() > FACE_DEATH_LEDGER_CAP {
ledger.pop_front();
}
}
}
#[cfg(feature = "instrumentation")]
thread_local! {
static OP_BOUNDARY: std::cell::RefCell<Option<hashbrown::HashSet<(HalfedgeId, HalfedgeId)>>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(feature = "instrumentation")]
pub(crate) fn hole_check_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("MESH_GRAPH_HOLE_CHECK").is_some())
}
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn probe_chain_begin(mesh: &MeshGraph) {
if !hole_check_enabled() {
return;
}
let boundary = mesh.boundary_edge_set();
OP_BOUNDARY.with(|b| *b.borrow_mut() = Some(boundary));
}
#[cfg(feature = "instrumentation")]
fn dump_boundary_delta(added: &[(HalfedgeId, HalfedgeId)], removed: &[(HalfedgeId, HalfedgeId)]) {
if !added.is_empty() {
eprintln!(" added {}:", added.len());
for (a, b) in added.iter().take(8) {
eprintln!(" edge ({a:?}, {b:?})");
}
}
if !removed.is_empty() {
eprintln!(" removed {}:", removed.len());
for (a, b) in removed.iter().take(8) {
eprintln!(" edge ({a:?}, {b:?})");
}
}
}
#[cfg(feature = "instrumentation")]
pub(crate) fn dump_face_death_ledger() {
if let Ok(ledger) = FACE_DEATH_LEDGER.lock() {
for (face_id, op) in ledger.iter() {
eprintln!(" face {face_id:?} removed by '{op}'");
}
}
}
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn report_dead_halfedge_in_collapse_check(mesh_graph: &MeshGraph, dead_id: HalfedgeId) {
static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if REPORTED.set(()).is_err() {
return;
}
mark_integrity_violation();
eprintln!("DEAD ID IN COLLAPSE CHECK: {dead_id:?} was inserted via a live halfedge's twin");
for (he_id, he) in &mesh_graph.halfedges {
if he.twin == Some(dead_id) {
let face_alive = he.face.is_some_and(|f| mesh_graph.faces.contains_key(f));
eprintln!(
" violator {he_id:?}: face={:?} (alive={face_alive}) next={:?} twin={:?} end={:?}",
he.face, he.next, he.twin, he.end_vertex
);
}
}
eprintln!("{}", std::backtrace::Backtrace::force_capture());
eprintln!("recent face deaths (oldest first):");
dump_face_death_ledger();
state_history_dump(
"dead_id_in_collapse_check",
Some(mesh_graph),
Some(&dead_id),
);
}
#[cfg(feature = "instrumentation")]
pub(crate) static OP_TRACE: std::sync::Mutex<std::collections::VecDeque<String>> =
std::sync::Mutex::new(std::collections::VecDeque::new());
#[cfg(feature = "instrumentation")]
pub(crate) const OP_TRACE_CAP: usize = 2000;
#[cfg(feature = "instrumentation")]
pub(crate) fn op_trace_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("MESH_GRAPH_TRACE").is_some())
}
#[cfg(feature = "instrumentation")]
#[doc(hidden)]
#[macro_export]
macro_rules! record_op_trace {
($($arg:tt)*) => {
if $crate::op_trace_enabled() {
$crate::record_op_trace_impl(format_args!($($arg)*).to_string());
}
};
}
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn record_op_trace_impl(event: String) {
if let Ok(mut trace) = OP_TRACE.lock() {
trace.push_back(event);
while trace.len() > OP_TRACE_CAP {
trace.pop_front();
}
}
}
#[cfg(feature = "instrumentation")]
static REPLAY_POSITION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[cfg(feature = "instrumentation")]
pub fn set_replay_position(pos: u64) {
REPLAY_POSITION.store(pos, std::sync::atomic::Ordering::Relaxed);
}
#[cfg(feature = "instrumentation")]
fn replay_position() -> u64 {
REPLAY_POSITION.load(std::sync::atomic::Ordering::Relaxed)
}
#[cfg(feature = "instrumentation")]
thread_local! {
static INTEGRITY_VIOLATION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(feature = "instrumentation")]
pub(crate) fn mark_integrity_violation() {
INTEGRITY_VIOLATION.with(|cell| cell.set(true));
}
#[cfg(feature = "instrumentation")]
pub fn integrity_violation_reported() -> bool {
INTEGRITY_VIOLATION.with(|cell| cell.get())
}
#[cfg(feature = "instrumentation")]
pub fn reset_integrity_violation() {
INTEGRITY_VIOLATION.with(|cell| cell.set(false));
}
#[cfg(feature = "instrumentation")]
struct StateSnapshot {
pos: u64,
op: &'static str,
mesh: MeshGraph,
}
#[cfg(feature = "instrumentation")]
pub(crate) static STATE_RING: std::sync::Mutex<std::collections::VecDeque<StateSnapshot>> =
std::sync::Mutex::new(std::collections::VecDeque::new());
#[cfg(feature = "instrumentation")]
thread_local! {
static STATE_RING_CAP: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
}
#[cfg(feature = "instrumentation")]
static STATE_DUMP_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
#[cfg(feature = "instrumentation")]
pub fn state_dump_dir() -> Option<std::path::PathBuf> {
STATE_DUMP_DIR.get().cloned()
}
#[cfg(feature = "instrumentation")]
fn state_ring_cap() -> usize {
STATE_RING_CAP.with(|cap| match cap.get() {
Some(cap) => cap,
None => {
let from_env = std::env::var_os("MESH_GRAPH_STATE_HISTORY_LEN")
.and_then(|s| s.to_str().and_then(|s| s.parse::<usize>().ok()))
.unwrap_or(0);
cap.set(Some(from_env));
from_env
}
})
}
#[cfg(feature = "instrumentation")]
pub fn set_state_history_len(len: usize) {
STATE_RING_CAP.with(|cap| cap.set(Some(len)));
}
#[cfg(feature = "instrumentation")]
fn state_dump_at_pos() -> Option<u64> {
static AT_POS: std::sync::OnceLock<Option<u64>> = std::sync::OnceLock::new();
*AT_POS.get_or_init(|| {
std::env::var_os("MESH_GRAPH_STATE_DUMP_AT_POS")
.and_then(|s| s.to_str().and_then(|s| s.parse::<u64>().ok()))
})
}
#[cfg(feature = "instrumentation")]
#[inline]
pub(crate) fn state_history_push(mesh: &MeshGraph, op: &'static str) {
let cap = state_ring_cap();
if cap == 0 {
return;
}
let snapshot = StateSnapshot {
pos: replay_position(),
op,
mesh: mesh.clone(),
};
if let Ok(mut ring) = STATE_RING.lock() {
ring.push_back(snapshot);
while ring.len() > cap {
ring.pop_front();
}
}
if let Some(target) = state_dump_at_pos()
&& replay_position() >= target
{
state_history_dump("at_position", Some(mesh), None);
}
}
#[cfg(feature = "instrumentation")]
pub(crate) fn state_history_dump(
reason: &str,
current: Option<&MeshGraph>,
context: Option<&dyn std::fmt::Debug>,
) {
static DUMPED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if DUMPED.set(()).is_err() {
return;
}
let dir = std::env::var_os("MESH_GRAPH_STATE_DUMP_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
std::path::PathBuf::from(format!("mesh_graph_state_dump_{}", std::process::id()))
});
if let Err(e) = std::fs::create_dir_all(&dir) {
eprintln!("state dump: could not create {}: {e:?}", dir.display());
return;
}
let _ = STATE_DUMP_DIR.set(dir.clone());
let mut meta = String::new();
meta.push_str(&format!(
"reason: {reason}\ncurrent replay position: {}\n",
replay_position()
));
if let Some(context) = context {
meta.push_str(&format!("context: {context:?}\n"));
}
if let Ok(ring) = STATE_RING.lock() {
meta.push_str("ring entries (oldest first):\n");
for (i, snap) in ring.iter().enumerate() {
meta.push_str(&format!(
" state_{i:02}: pos={} op={}\n",
snap.pos, snap.op
));
}
}
if let Some(current) = current {
let path = dir.join("current.json");
if let Err(e) = current.save_state(&path) {
eprintln!("state dump: could not write {}: {e:?}", path.display());
}
meta.push_str(&format!(
"current.json: current broken state (pos {})\n",
replay_position()
));
}
if let Ok(ring) = STATE_RING.lock() {
for (i, snap) in ring.iter().enumerate() {
let path = dir.join(format!("state_{i:02}_pos_{:06}_{}.json", snap.pos, snap.op));
if let Err(e) = snap.mesh.save_state(&path) {
eprintln!("state dump: could not write {}: {e:?}", path.display());
}
}
}
if let Err(e) = std::fs::write(dir.join("meta.txt"), meta) {
eprintln!("state dump: could not write meta.txt: {e:?}");
}
eprintln!("state history dumped to {}", dir.display());
}
#[cfg(feature = "rerun")]
lazy_static::lazy_static! {
pub static ref RR: rerun::RecordingStream = rerun::RecordingStreamBuilder::new("mesh_graph").spawn().unwrap();
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "bevy", derive(bevy::prelude::Component))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(from = "crate::serialize::MeshGraphIntermediate")
)]
pub struct MeshGraph {
#[cfg_attr(feature = "serde", serde(skip))]
pub bvh: Bvh,
#[cfg_attr(feature = "serde", serde(skip))]
pub bvh_workspace: BvhWorkspace,
#[cfg_attr(feature = "serde", serde(skip))]
pub index_to_face_id: HashMap<u32, FaceId>,
#[cfg_attr(feature = "serde", serde(skip))]
pub next_index: u32,
pub vertices: SlotMap<VertexId, Vertex>,
pub halfedges: SlotMap<HalfedgeId, Halfedge>,
pub faces: SlotMap<FaceId, Face>,
pub positions: SecondaryMap<VertexId, Vec3>,
pub vertex_normals: Option<SecondaryMap<VertexId, Vec3>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub outgoing_halfedges: SecondaryMap<VertexId, Vec<HalfedgeId>>,
}
impl MeshGraph {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn triangles(vertex_positions: &[Vec3]) -> Option<Self> {
if !vertex_positions.len().is_multiple_of(3) {
return None;
}
let mut unique_positions: Vec<Vec3> = Vec::with_capacity(vertex_positions.len() / 3);
let mut face_indices = Vec::with_capacity(vertex_positions.len());
for vertex_pos in vertex_positions {
let mut idx = None;
for (j, pos) in unique_positions.iter().enumerate() {
const EPSILON: f32 = 1e-5;
if pos.distance_squared(*vertex_pos) < EPSILON {
idx = Some(j);
break;
}
}
let vertex_idx = if let Some(idx) = idx {
idx
} else {
let new_idx = unique_positions.len();
unique_positions.push(*vertex_pos);
#[cfg(feature = "rerun")]
RR.log(
"meshgraph/construct/vertices",
&rerun::Points3D::new(unique_positions.iter().map(crate::utils::vec3_array)),
)
.unwrap();
new_idx
};
face_indices.push(vertex_idx);
}
Some(Self::indexed_triangles(&unique_positions, &face_indices))
}
#[instrument]
pub fn indexed_triangles_with_custom_attribute<T>(
vertex_positions: &[Vec3],
face_indices: &[usize],
custom_attribute: &[T],
) -> (Self, SecondaryMap<VertexId, T>)
where
T: Clone + std::fmt::Debug,
{
let (mesh_graph, vertex_ids) =
Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices);
let mut custom_attribute_map = SecondaryMap::with_capacity(custom_attribute.len());
for (attr, vertex_id) in custom_attribute.iter().zip(vertex_ids) {
custom_attribute_map.insert(vertex_id, attr.clone());
}
(mesh_graph, custom_attribute_map)
}
#[inline]
pub fn indexed_triangles(vertex_positions: &[Vec3], face_indices: &[usize]) -> Self {
Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices).0
}
#[instrument]
pub fn indexed_triangles_and_vertex_ids(
vertex_positions: &[Vec3],
face_indices: &[usize],
) -> (Self, Vec<VertexId>) {
let mut mesh_graph = Self {
bvh: Bvh::new(),
bvh_workspace: BvhWorkspace::default(),
index_to_face_id: HashMap::with_capacity(face_indices.len() / 3),
next_index: 0,
vertices: SlotMap::with_capacity_and_key(vertex_positions.len()),
halfedges: SlotMap::with_capacity_and_key(face_indices.len()),
faces: SlotMap::with_capacity_and_key(face_indices.len() / 3),
positions: SecondaryMap::with_capacity(vertex_positions.len()),
vertex_normals: None,
outgoing_halfedges: SecondaryMap::with_capacity(vertex_positions.len()),
};
let mut vertex_ids = Vec::with_capacity(vertex_positions.len());
for pos in vertex_positions {
vertex_ids.push(mesh_graph.add_vertex(*pos));
}
for chunk in face_indices.as_chunks::<3>().0 {
let a = vertex_ids[chunk[0]];
let b = vertex_ids[chunk[1]];
let c = vertex_ids[chunk[2]];
if a == b || b == c || c == a {
#[cfg(feature = "rerun")]
RR.log(
"meshgraph/construct/zero_face",
&rerun::Points3D::new(
[
mesh_graph.positions[a],
mesh_graph.positions[b],
mesh_graph.positions[c],
]
.iter()
.map(crate::utils::vec3_array),
),
)
.unwrap();
continue;
}
let he_a_id = mesh_graph.add_or_get_edge(a, b).unwrap().start_to_end_he_id;
let he_b_id = mesh_graph.add_or_get_edge(b, c).unwrap().start_to_end_he_id;
let he_c_id = mesh_graph.add_or_get_edge(c, a).unwrap().start_to_end_he_id;
let _face_id = mesh_graph.add_face(he_a_id, he_b_id, he_c_id);
}
mesh_graph.make_all_outgoing_halfedges_boundary_if_possible();
mesh_graph.rebuild_bvh();
(mesh_graph, vertex_ids)
}
#[instrument(skip(self))]
fn pair_with_fresh_boundary_half(
&mut self,
survivor_id: HalfedgeId,
survivor_start_v: VertexId,
) -> Option<HalfedgeId> {
let survivor = self
.halfedges
.get(survivor_id)
.or_else(error_none!("survivor he not found"))?;
let survivor_end = survivor.end_vertex;
let boundary_id = self.add_halfedge(survivor_end, survivor_start_v)?;
self.halfedges[survivor_id].twin = Some(boundary_id);
self.halfedges[boundary_id].twin = Some(survivor_id);
#[cfg(feature = "instrumentation")]
crate::record_op_trace!(
"fresh boundary {boundary_id:?} ({survivor_end:?}->{survivor_start_v:?}) paired with survivor {survivor_id:?}"
);
Some(boundary_id)
}
fn reseed_outgoing_if_dead(&mut self, vertex_id: VertexId) {
let Some(vertex) = self.vertices.get(vertex_id) else {
return;
};
if vertex
.outgoing_halfedge
.is_some_and(|he| self.halfedges.contains_key(he))
{
return;
}
let new_seed = self.outgoing_halfedges.get(vertex_id).and_then(|list| {
list.iter()
.copied()
.find(|he| self.halfedges.contains_key(*he))
});
if let Some(v) = self.vertices.get_mut(vertex_id) {
v.outgoing_halfedge = new_seed;
}
}
#[cfg(feature = "instrumentation")]
pub(crate) fn probe_live_face_removal(&self, removed_ids: &[HalfedgeId], op: &str) {
if removed_ids.is_empty() {
return;
}
static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if !matches!(op, "remove_face_tail" | "remove_halfedge_face") {
for id in removed_ids {
if let Some(he) = self.halfedges.get(*id)
&& let Some(face_id) = he.face
&& self.faces.contains_key(face_id)
{
let other_members: Vec<HalfedgeId> = self
.halfedges
.iter()
.filter(|(h_id, h)| {
h.face == Some(face_id) && *h_id != *id && !removed_ids.contains(h_id)
})
.map(|(h_id, _)| h_id)
.take(4)
.collect();
if !other_members.is_empty() && REPORTED.set(()).is_ok() {
mark_integrity_violation();
eprintln!(
"REMOVING LIVE-FACE MEMBER {id:?} of face {face_id:?} (surviving members {other_members:?})"
);
eprintln!("{}", std::backtrace::Backtrace::force_capture());
state_history_dump("live_face_member_removal", Some(self), Some(&id));
}
}
}
}
}
#[cfg(feature = "instrumentation")]
pub(crate) fn probe_chain_integrity(&self, op: &str) -> bool {
static REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
let mut violations: Vec<(HalfedgeId, &'static str)> = Vec::new();
let mut dead_face_siblings: Vec<FaceId> = Vec::new();
for (he_id, he) in &self.halfedges {
let Some(face_id) = he.face else {
continue;
};
let verdict = if !self.faces.contains_key(face_id) {
if !dead_face_siblings.contains(&face_id) {
dead_face_siblings.push(face_id);
}
Some("member of a removed face")
} else {
match he.next {
None => Some("member of a live face with next=None"),
Some(next_id) => match self.halfedges.get(next_id) {
None => Some("next references a removed halfedge"),
Some(next_he) if next_he.face != Some(face_id) => {
Some("next references a halfedge of another/no face")
}
Some(_) => None,
},
}
};
if let Some(reason) = verdict {
violations.push((he_id, reason));
if violations.len() >= 12 {
break;
}
}
}
let mut fork: Option<(FaceId, Vec<HalfedgeId>, Vec<HalfedgeId>)> = None;
let mut family_by_face: hashbrown::HashMap<FaceId, Vec<HalfedgeId>> =
hashbrown::HashMap::new();
for (h_id, h) in &self.halfedges {
if let Some(f) = h.face {
family_by_face.entry(f).or_default().push(h_id);
}
}
for (face_id, face) in &self.faces {
let Some(family) = family_by_face.get(&face_id) else {
continue;
};
if family.len() < 3 {
continue;
}
let mut chain = Vec::with_capacity(family.len());
let mut cur = Some(face.halfedge);
let mut steps = 0;
while let Some(he_id) = cur {
if !self.halfedges.contains_key(he_id) || chain.len() > family.len() + 2 {
break;
}
if chain.contains(&he_id) {
break;
}
chain.push(he_id);
cur = self.halfedges[he_id].next;
steps += 1;
if steps > 16 {
break;
}
}
if chain.len() != family.len() || family.iter().any(|h_id| !chain.contains(h_id)) {
fork = Some((face_id, family.clone(), chain));
break;
}
}
let fmt_ids = |ids: &[HalfedgeId]| -> String {
if ids.len() <= 8 {
format!("{ids:?}")
} else {
format!("{:?}... ({} ids)", &ids[..8], ids.len())
}
};
let mut twin_problems: Vec<String> = Vec::new();
let mut membership_problems: Vec<String> = Vec::new();
let mut order_deviation_count: usize = 0;
let mut first_order_sample: Option<String> = None;
struct OutScratch {
expected: hashbrown::HashMap<VertexId, Vec<HalfedgeId>>,
counts: hashbrown::HashMap<HalfedgeId, usize>,
}
static OUT_SCRATCH: std::sync::Mutex<Option<OutScratch>> = std::sync::Mutex::new(None);
let mut scratch = OUT_SCRATCH.lock().unwrap();
let scratch = scratch.get_or_insert_with(|| OutScratch {
expected: hashbrown::HashMap::new(),
counts: hashbrown::HashMap::new(),
});
for list in scratch.expected.values_mut() {
list.clear();
}
scratch.expected.clear();
scratch.counts.clear();
for (he_id, he) in &self.halfedges {
match he.twin {
None => twin_problems.push(format!("halfedge {he_id:?} has twin=None")),
Some(twin_id) => {
if !self.halfedges.contains_key(twin_id) {
twin_problems.push(format!(
"halfedge {he_id:?} has twin {twin_id:?} which is removed"
));
} else if self.halfedges[twin_id].twin != Some(he_id) {
twin_problems.push(format!(
"halfedge {he_id:?} has twin {twin_id:?} which does not point back"
));
}
}
}
if let Some(twin_id) = he.twin {
scratch
.expected
.entry(he.end_vertex)
.or_default()
.push(twin_id);
}
}
for (v_id, expected) in scratch.expected.iter() {
let actual: &[HalfedgeId] = self
.outgoing_halfedges
.get(*v_id)
.map(Vec::as_slice)
.unwrap_or(&[]);
scratch.counts.clear();
for &a in actual {
*scratch.counts.entry(a).or_default() += 1;
}
let mut missing: Vec<HalfedgeId> = Vec::new();
for &exp in expected {
match scratch.counts.get_mut(&exp) {
Some(c) if *c > 0 => *c -= 1,
_ => missing.push(exp),
}
}
let mut extra: Vec<HalfedgeId> = Vec::new();
for &a in actual {
if scratch.counts.get(&a) != Some(&0) {
extra.push(a);
}
}
if !missing.is_empty() || !extra.is_empty() {
let mut msg = format!("vertex {v_id:?}: outgoing deviates from ground truth");
if !missing.is_empty() {
msg += &format!(", missing {}", fmt_ids(&missing));
}
if !extra.is_empty() {
msg += &format!(", extra {}", fmt_ids(&extra));
}
membership_problems.push(msg);
} else if actual != expected.as_slice() {
order_deviation_count += 1;
if first_order_sample.is_none() {
first_order_sample = Some(format!(
"vertex {v_id:?}: outgoing order differs from rebuild order"
));
}
}
}
for (v_id, actual) in &self.outgoing_halfedges {
if !scratch.expected.contains_key(&v_id) && !actual.is_empty() {
membership_problems.push(format!(
"vertex {v_id:?}: outgoing {} but no live halfedge ends at it",
fmt_ids(actual)
));
}
}
let mut seed_deviations: Vec<String> = Vec::new();
for (v_id, vertex) in &self.vertices {
let stored = vertex.outgoing_halfedge;
let rebuilt_seed = stored
.filter(|he| self.halfedges.contains_key(*he))
.or_else(|| scratch.expected.get(&v_id).and_then(|l| l.first().copied()));
if stored != rebuilt_seed {
seed_deviations.push(format!(
"vertex {v_id:?}: seed {stored:?} != rebuilt {rebuilt_seed:?}"
));
}
}
let clean =
violations.is_empty() && twin_problems.is_empty() && membership_problems.is_empty();
if !clean && REPORTED.set(()).is_ok() {
mark_integrity_violation();
eprintln!(
"CHAIN CORRUPTION detected at end of op '{op}' ({} violations shown):",
violations.len()
);
for (he_id, reason) in violations {
let detail = self.halfedges.get(he_id).map(|he| {
format!(
"face={:?} next={:?} twin={:?} end={:?}",
he.face, he.next, he.twin, he.end_vertex
)
});
eprintln!(" halfedge {he_id:?}: {reason}; {detail:?}");
if let Some(he) = self.halfedges.get(he_id) {
if let Some(next_id) = he.next
&& let Some(next_he) = self.halfedges.get(next_id)
{
eprintln!(
" next {next_id:?}: face={:?} next={:?} twin={:?} end={:?}",
next_he.face, next_he.next, next_he.twin, next_he.end_vertex
);
}
if let Some(twin_id) = he.twin
&& let Some(twin_he) = self.halfedges.get(twin_id)
{
eprintln!(
" twin {twin_id:?}: face={:?} next={:?} twin={:?} end={:?}",
twin_he.face, twin_he.next, twin_he.twin, twin_he.end_vertex
);
}
if let Some(start_v) = he.start_vertex(self) {
let out: Vec<HalfedgeId> = self
.outgoing_halfedges
.get(start_v)
.map(|l| l.iter().copied().take(6).collect())
.unwrap_or_default();
let out_desc: Vec<String> = out
.iter()
.filter_map(|id| {
self.halfedges
.get(*id)
.map(|h| format!("{id:?}(face={:?},next={:?})", h.face, h.next))
})
.collect();
eprintln!(" start vertex {start_v:?} outgoing: {out_desc:?}");
}
}
}
for dead_face_id in &dead_face_siblings {
let family: Vec<HalfedgeId> = self
.halfedges
.iter()
.filter(|(_, h)| h.face == Some(*dead_face_id))
.map(|(h_id, _)| h_id)
.collect();
eprintln!(" halfedges claiming removed face {dead_face_id:?}: {family:?}");
}
eprintln!("{}", std::backtrace::Backtrace::force_capture());
eprintln!("recent face deaths (oldest first):");
dump_face_death_ledger();
if let Some((fork_face, fork_family, fork_chain)) = fork {
eprintln!("family-vs-chain for face {fork_face:?}:");
eprintln!(" chain (walked): {fork_chain:?}");
eprintln!(" family (face field): {fork_family:?}");
}
for problem in &twin_problems {
eprintln!("TWIN: {problem}");
}
for problem in membership_problems.iter().take(12) {
eprintln!("OUTGOING: {problem}");
}
if let Ok(trace) = OP_TRACE.lock() {
eprintln!("op trace (oldest first):");
for event in trace.iter() {
eprintln!(" {event}");
}
}
state_history_dump("chain_integrity", Some(self), Some(&op));
}
if hole_check_enabled() {
static HOLE_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
static RIM_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
let current = self.boundary_edge_set();
let begin = OP_BOUNDARY.with(|b| b.borrow().clone());
if let Some(begin) = begin {
let added: Vec<(HalfedgeId, HalfedgeId)> =
current.difference(&begin).copied().collect();
let removed: Vec<(HalfedgeId, HalfedgeId)> =
begin.difference(¤t).copied().collect();
if !added.is_empty() || !removed.is_empty() {
let boundary_count =
self.halfedges.values().filter(|h| h.face.is_none()).count();
if begin.is_empty() {
if HOLE_REPORTED.set(()).is_ok() {
mark_integrity_violation();
eprintln!(
"HOLE DELTA: op '{op}' changed the boundary edge set of a closed region (now {boundary_count} boundary halfedges):"
);
dump_boundary_delta(&added, &removed);
eprintln!("{}", std::backtrace::Backtrace::force_capture());
if let Ok(trace) = OP_TRACE.lock() {
eprintln!("op trace (oldest first):");
for event in trace.iter() {
eprintln!(" {event}");
}
}
state_history_dump("hole", Some(self), Some(&op));
}
} else if RIM_REPORTED.set(()).is_ok() {
eprintln!(
"RIM DELTA: op '{op}' changed the boundary edge set of an open region (now {boundary_count} boundary halfedges) — expected during punch cleanup:"
);
dump_boundary_delta(&added, &removed);
}
}
}
}
if order_deviation_count > 0 {
static ORDER_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if ORDER_REPORTED.set(()).is_ok() {
eprintln!(
"OUTGOING ORDER deviates from rebuild order at {order_deviation_count} vertices (first: {})",
first_order_sample.as_deref().unwrap_or("")
);
}
}
if !seed_deviations.is_empty() {
static SEED_REPORTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if SEED_REPORTED.set(()).is_ok() {
eprintln!(
"SEED deviates from rebuild at op '{op}' at {} vertices (first: {})",
seed_deviations.len(),
seed_deviations[0]
);
}
}
clean
}
#[cfg(feature = "instrumentation")]
fn boundary_edge_set(&self) -> hashbrown::HashSet<(HalfedgeId, HalfedgeId)> {
let mut edges = hashbrown::HashSet::new();
for (he_id, he) in &self.halfedges {
if he.face.is_none()
&& let Some(twin_id) = he.twin
{
edges.insert(if twin_id < he_id {
(twin_id, he_id)
} else {
(he_id, twin_id)
});
}
}
edges
}
pub fn rebuild_vertex_outgoing_list(&mut self, vertex_id: VertexId) {
let mut list: Vec<HalfedgeId> = Vec::new();
for (_, he) in &self.halfedges {
if he.end_vertex == vertex_id
&& let Some(twin_id) = he.twin
&& self.halfedges.contains_key(twin_id)
{
list.push(twin_id);
}
}
if let Some(entry) = self.outgoing_halfedges.get_mut(vertex_id) {
*entry = list;
}
if let Some(vertex) = self.vertices.get_mut(vertex_id)
&& !vertex
.outgoing_halfedge
.is_some_and(|he| self.halfedges.contains_key(he))
{
vertex.outgoing_halfedge = self
.outgoing_halfedges
.get(vertex_id)
.and_then(|l| l.first().copied());
}
}
pub fn compute_vertex_normal(&mut self, vertex_id: VertexId) {
if self.vertex_normals.is_none() {
return;
}
let vertex = unwrap_or_return!(self.vertices.get(vertex_id), "Vertex not found");
let mut normal = Vec3::ZERO;
for face_id in vertex.faces(self) {
let face = unwrap_or_return!(self.faces.get(face_id), "Face not found");
let face_normal = face.normal(self);
normal += unwrap_or_return!(face_normal, "Face normal not found");
}
self.vertex_normals
.as_mut()
.unwrap()
.insert(vertex_id, normal.try_normalize().unwrap_or(Vec3::ZERO));
}
#[instrument(skip(self))]
pub fn compute_vertex_normals(&mut self) {
let mut normals = SecondaryMap::with_capacity(self.vertices.len());
for face in self.faces.values() {
let Some(&he_a) = self.halfedges.get(face.halfedge) else {
error!("Halfedge not found");
continue;
};
let Some(he_b_id) = he_a.next else {
error!("Halfedge has no next halfedge");
continue;
};
let Some(he_b) = self.halfedges.get(he_b_id) else {
error!("Next halfedge not found");
continue;
};
let a = match he_a.start_vertex(self) {
Some(v) => v,
None => {
error!("Start vertex not found");
continue;
}
};
let b = he_a.end_vertex;
let c = he_b.end_vertex;
let (Some(pos_a), Some(pos_b), Some(pos_c)) = (
self.positions.get(a),
self.positions.get(b),
self.positions.get(c),
) else {
continue;
};
let diff_a = pos_c - pos_a;
let diff_b = pos_c - pos_b;
let face_normal = diff_a.cross(diff_b);
for v_id in [a, b, c] {
let Some(entry) = normals.entry(v_id) else {
continue;
};
*entry.or_default() += face_normal;
}
}
self.vertex_normals = Some(normals);
self.normalize_vertex_normals();
}
pub fn normalize_vertex_normals(&mut self) {
if let Some(normals) = &mut self.vertex_normals {
for normal in normals.values_mut() {
*normal = normal.normalize_or_zero();
}
}
}
#[inline]
pub fn optimize_bvh_incremental(&mut self) {
self.bvh.optimize_incremental(&mut self.bvh_workspace);
}
#[inline]
pub fn refit_bvh(&mut self) {
self.bvh.refit(&mut self.bvh_workspace);
}
#[inline]
pub fn rebuild_bvh(&mut self) {
self.bvh = Bvh::new();
self.bvh_workspace = BvhWorkspace::default();
for face in self.faces.values() {
self.bvh
.insert_or_update_partially(face.aabb(self), face.index, 0.0);
}
self.bvh
.rebuild(&mut self.bvh_workspace, Default::default());
}
#[instrument(skip_all)]
pub fn repair_face_pointers(&mut self) {
let face_ids: Vec<FaceId> = self.faces.keys().collect();
let mut visited: hashbrown::HashMap<HalfedgeId, FaceId> = hashbrown::HashMap::new();
for face_id in face_ids {
let Some(start_he) = self.faces.get(face_id).map(|f| f.halfedge) else {
continue;
};
let mut he_id = start_he;
for _ in 0..32 {
let Some(he) = self.halfedges.get_mut(he_id) else {
break;
};
he.face = Some(face_id);
visited.insert(he_id, face_id);
let Some(next) = he.next else {
break;
};
if next == start_he {
break;
}
he_id = next;
}
}
let orphan_ids: Vec<HalfedgeId> = self
.halfedges
.iter()
.filter(|(he_id, he)| he.face.is_some() && !visited.contains_key(he_id))
.map(|(he_id, _)| he_id)
.collect();
for he_id in orphan_ids {
if let Some(he) = self.halfedges.get_mut(he_id) {
he.face = None;
}
}
}
pub fn rebuild_outgoing_halfedges(&mut self) {
self.outgoing_halfedges.clear();
for vertex_id in self.vertices.keys() {
self.outgoing_halfedges.insert(vertex_id, Vec::new());
}
for halfedge in self.halfedges.values() {
let Some(twin_id) = halfedge.twin else {
error!("Halfedge has no twin");
continue;
};
let Some(entry) = self.outgoing_halfedges.entry(halfedge.end_vertex) else {
error!("Vertex key invalid");
continue;
};
entry.or_default().push(twin_id);
}
for (v_id, vertex) in &mut self.vertices {
let stored_seed = vertex.outgoing_halfedge;
let live_seed = stored_seed.filter(|he| self.halfedges.contains_key(*he));
vertex.outgoing_halfedge = live_seed.or_else(|| {
self.outgoing_halfedges
.get(v_id)
.and_then(|list| list.first().copied())
});
}
}
}
#[cfg(all(test, feature = "instrumentation"))]
mod integrity_violation_tests {
use super::{
integrity_violation_reported, mark_integrity_violation, reset_integrity_violation,
};
#[test]
fn violation_flag_is_per_thread_and_resettable() {
reset_integrity_violation();
assert!(!integrity_violation_reported());
std::thread::spawn(|| {
mark_integrity_violation();
assert!(
integrity_violation_reported(),
"flag must set on its own thread"
);
})
.join()
.expect("probe thread panicked");
assert!(
!integrity_violation_reported(),
"another thread's violation leaked into this thread"
);
mark_integrity_violation();
assert!(integrity_violation_reported());
reset_integrity_violation();
assert!(!integrity_violation_reported(), "reset must clear the flag");
}
}