The reader converts each item once, but every instance made its own display mesh (tessellation, seam and tangent flags), the most expensive step of opening a large assembly. A later instance of an item now shows its first instance's mesh, placed by the difference of the two placements. Only rigid placements share; split lines and any operation drop the link. Measurements and export read each instance's own geometry, so eval output is unchanged. Generated assemblies: mesh phase 2,215 -> 139 ms (901 bodies) and 1,028 -> 85 ms (5,401 bodies), with the same triangle counts. Co-authored-by: Claude Opus 5.5 <[email protected]>
217 lines
8.2 KiB
Rust
217 lines
8.2 KiB
Rust
//! 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<Vec3>,
|
||
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<Body> {
|
||
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<u32> = 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<usize>> = 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<Body> {
|
||
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,
|
||
})
|
||
}
|
||
}
|