Files
OneCAD/crates/kernel/src/meshbody.rs
T
bherbruckandClaude Opus 5.5 9b0f028f13 STEP assemblies: mesh a repeated part once and place it for its instances (#1152)
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 <noreply@anthropic.com>
2026-10-14 14:23:33 -04:00

217 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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,
})
}
}