//! Mesh bodies: triangles without a B-rep (imported 3MF/STL). They render, measure, move and //! export; B-rep features refuse them. use std::collections::HashMap; use std::sync::Arc; use solidcraft_geom::{Mesh, Vec3}; use crate::body::Body; use crate::{KernelError, Result}; /// Most triangles in one mesh body. pub const MAX_TRIANGLES: usize = 20_000_000; /// Indexed triangles with shared vertices, wound counter-clockwise seen from outside. #[derive(Clone, Debug, Default)] pub struct TriMesh { pub positions: Vec, pub triangles: Vec<[u32; 3]>, } impl TriMesh { /// Display/measure mesh: corners split per triangle so normals stay flat. pub(crate) fn to_mesh(&self) -> Mesh { let mut out = Mesh::default(); out.positions.reserve(self.triangles.len() * 3); for t in &self.triangles { let (Some(a), Some(b), Some(c)) = (self.positions.get(t[0] as usize), self.positions.get(t[1] as usize), self.positions.get(t[2] as usize)) else { continue; }; let n = (*b - *a).cross(*c - *a).normalized().unwrap_or(Vec3::Z); let base = u32::try_from(out.positions.len()).unwrap_or(u32::MAX); out.positions.extend([*a, *b, *c]); out.normals.extend([n, n, n]); out.triangles.push([base, base.saturating_add(1), base.saturating_add(2)]); out.tri_face.push(0); } out } /// Distinct undirected triangle sides. pub(crate) fn edge_count(&self) -> usize { let mut set = std::collections::HashSet::new(); for t in &self.triangles { for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] { set.insert((a.min(b), a.max(b))); } } set.len() } /// Signed volume ×6. fn volume6(&self) -> f64 { self.triangles .iter() .filter_map(|t| Some((self.positions.get(t[0] as usize)?, self.positions.get(t[1] as usize)?, self.positions.get(t[2] as usize)?))) .map(|(a, b, c)| a.dot(b.cross(*c))) .sum() } } /// A mesh body from triangles (`positions` in mm). Coincident vertices are merged, degenerate /// triangles dropped, and a closed mesh with inward winding is turned outward. pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result { if triangles.len() > MAX_TRIANGLES { return Err(KernelError::Invalid(format!("mesh with {} triangles (limit {MAX_TRIANGLES})", triangles.len()))); } if positions.iter().any(|p| !(p.is_finite() && p.x.abs() < 1e9 && p.y.abs() < 1e9 && p.z.abs() < 1e9)) { return Err(KernelError::Invalid("mesh with non-finite or huge coordinates".into())); } // Merge vertices that are exactly equal (bit pattern of the coordinates). let mut map: HashMap<[u64; 3], u32> = HashMap::new(); let mut remap: Vec = Vec::with_capacity(positions.len()); let mut out = TriMesh::default(); for p in positions { let key = [(p.x + 0.0).to_bits(), (p.y + 0.0).to_bits(), (p.z + 0.0).to_bits()]; let next = u32::try_from(out.positions.len()).map_err(|_| KernelError::Invalid("too many vertices".into()))?; let i = *map.entry(key).or_insert_with(|| { out.positions.push(*p); next }); remap.push(i); } for t in triangles { let (Some(&a), Some(&b), Some(&c)) = (remap.get(t[0] as usize), remap.get(t[1] as usize), remap.get(t[2] as usize)) else { return Err(KernelError::Invalid("mesh triangle refers to a missing vertex".into())); }; if a == b || b == c || c == a { continue; } out.triangles.push([a, b, c]); } if out.triangles.is_empty() { return Err(KernelError::Invalid("mesh has no triangles".into())); } unify_winding(&mut out); if out.volume6() < 0.0 && is_closed(&out) { for t in &mut out.triangles { t.swap(1, 2); } } Ok(Body { solid: Arc::new(crate::body::Solid::new_unchecked(Vec::new())), size: 0.0, volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))), coarse_missing: None, mesh: Some(Arc::new(out)), color: None, paint: None, splits: None, face_sources: None, history: None, display_of: None, regularization_protected: false, }) } /// Wind every connected piece one way: a triangle that runs a side in the same direction as /// its neighbour is turned over (files with stray inverted facets). Sides shared by more than /// two triangles are left alone. fn unify_winding(m: &mut TriMesh) { let mut sides: HashMap<(u32, u32), Vec> = HashMap::new(); for (i, t) in m.triangles.iter().enumerate() { for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] { sides.entry((a.min(b), a.max(b))).or_default().push(i); } } let runs = |t: &[u32; 3], a: u32, b: u32| [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])].contains(&(a, b)); let mut done = vec![false; m.triangles.len()]; for start in 0..m.triangles.len() { if done[start] { continue; } done[start] = true; let mut stack = vec![start]; while let Some(i) = stack.pop() { let Some(t) = m.triangles.get(i).copied() else { continue }; for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] { let Some(list) = sides.get(&(a.min(b), a.max(b))) else { continue }; if list.len() != 2 { continue; } for &j in list { if j == i || done[j] { continue; } done[j] = true; if let Some(u) = m.triangles.get_mut(j) && runs(u, a, b) { u.swap(1, 2); } stack.push(j); } } } } } /// Every triangle side is shared by exactly two triangles in opposite directions. pub(crate) fn is_closed(m: &TriMesh) -> bool { let mut count: HashMap<(u32, u32), i32> = HashMap::new(); for t in &m.triangles { for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] { *count.entry((a, b)).or_default() += 1; } } count.iter().all(|(&(a, b), &n)| n == 1 && count.get(&(b, a)) == Some(&1)) } impl Body { /// Shared-vertex triangles of a mesh body. pub fn triangle_mesh(&self) -> Option<(&[Vec3], &[[u32; 3]])> { self.mesh.as_ref().map(|m| (m.positions.as_slice(), m.triangles.as_slice())) } /// Does a mesh body enclose a volume (every side shared by two triangles)? pub fn is_closed_mesh(&self) -> bool { self.mesh.as_ref().is_some_and(|m| is_closed(m)) } /// A mesh body moved by a column-major affine matrix (reflections keep outward winding). pub(crate) fn mesh_transformed(&self, m: &[[f64; 4]; 4]) -> Option { let tm = self.mesh.as_ref()?; let f = |p: Vec3| { Vec3::new( m[0][0] * p.x + m[1][0] * p.y + m[2][0] * p.z + m[3][0], m[0][1] * p.x + m[1][1] * p.y + m[2][1] * p.z + m[3][1], m[0][2] * p.x + m[1][2] * p.y + m[2][2] * p.z + m[3][2], ) }; let det = m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2]) + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2]); let mut out = TriMesh { positions: tm.positions.iter().map(|p| f(*p)).collect(), triangles: tm.triangles.clone() }; if det < 0.0 { for t in &mut out.triangles { t.swap(1, 2); } } Some(Body { solid: self.solid.clone(), size: 0.0, volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))), coarse_missing: None, mesh: Some(Arc::new(out)), color: self.color, paint: self.paint.clone(), splits: self.splits.clone(), face_sources: None, history: None, display_of: None, regularization_protected: self.regularization_protected, }) } }