Files
OneCAD/crates/kernel/src/meshbody.rs
T
bherbruckandClaude Opus 5.5 294597cc96 Face colours and opacity in STEP and 3MF export (and STEP import)
- Bodies carry a Paint for export (opacity, per-face colours), set by
  Document::painted from the appearance model. The paint comes from a
  face appearance, a body/component appearance's opacity or a material
  look (Glass), and falls back to the paint the body was imported with.
- STEP: faces with colours of their own get a STYLED_ITEM on their
  ADVANCED_FACE. See-through looks add SURFACE_STYLE_TRANSPARENT.
  Differently coloured pieces are not merged into one face.
- STEP import reads face styles and transparency. The reader tracks the
  file face of each kernel face (bands split in two keep it), so imported
  face colours land on the right faces; the design shows them through
  face_colors.
- 3MF: per-triangle colours (pid/p1 into the base materials, carried
  through welding) and the object's alpha (#RRGGBBAA).
- Tests: a blue half-transparent cube with a red top exports, re-imports
  with its opacity and the red top face, and keeps it when opened and
  exported again. 3MF writes the alpha and the face entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 14:02:10 +00:00

148 lines
5.9 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 solvecraft_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()));
}
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())), mesh: Some(Arc::new(out)), color: None, paint: None })
}
/// Every triangle side is shared by exactly two triangles in opposite directions.
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(), mesh: Some(Arc::new(out)), color: self.color, paint: self.paint.clone() })
}
}