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
This commit is contained in:
1 parent
8045904c54
commit
294597cc96
12 files changed
+313
-49
No files matched your search
@@ -26,6 +26,8 @@ pub(crate) struct BrepOut {
|
||||
pub closed: bool,
|
||||
/// Faces read from the file (before band faces are split for the kernel).
|
||||
pub file_faces: usize,
|
||||
/// The file face of each kernel face, in face order.
|
||||
pub face_origin: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -280,7 +282,7 @@ impl Builder {
|
||||
}
|
||||
}
|
||||
|
||||
fn shell(&mut self, cx: &Ctx, id: u64, failed: &mut Vec<String>) -> R<(mt::Shell, bool, usize)> {
|
||||
fn shell(&mut self, cx: &Ctx, id: u64, failed: &mut Vec<String>) -> R<(mt::Shell, bool, usize, Vec<u64>)> {
|
||||
let e = entity(cx, id)?;
|
||||
let p = e.params();
|
||||
let (faces, flip, closed) = match e.name() {
|
||||
@@ -296,6 +298,7 @@ impl Builder {
|
||||
return Err(format!("shell with {} faces", faces.len()));
|
||||
}
|
||||
let mut out = Vec::with_capacity(faces.len());
|
||||
let mut origin = Vec::with_capacity(faces.len());
|
||||
for f in faces {
|
||||
match self.face(cx, f, 0) {
|
||||
Ok(mut face) => {
|
||||
@@ -303,6 +306,7 @@ impl Builder {
|
||||
face.invert();
|
||||
}
|
||||
out.push(face);
|
||||
origin.push(f);
|
||||
}
|
||||
Err(err) => {
|
||||
if self.faces > MAX_FACES {
|
||||
@@ -316,8 +320,8 @@ impl Builder {
|
||||
return Err("no face could be imported".into());
|
||||
}
|
||||
let read = out.len();
|
||||
let out = super::split::split_bands(out, failed);
|
||||
Ok((out.into(), closed, read))
|
||||
let out = super::split::split_bands(out, &mut origin, failed);
|
||||
Ok((out.into(), closed, read, origin))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,19 +359,23 @@ pub(crate) fn read_item(cx: &Ctx, id: u64) -> R<BrepOut> {
|
||||
let mut shells = Vec::new();
|
||||
let mut closed = true;
|
||||
let mut file_faces = 0;
|
||||
// The file face each kernel face comes from (in face order).
|
||||
let mut face_origin = Vec::new();
|
||||
match e.name() {
|
||||
"MANIFOLD_SOLID_BREP" | "FACETED_BREP" | "BREP_WITH_VOIDS" => {
|
||||
let (s, c, n) = b.shell(cx, rid(p.get(1), "brep outer shell")?, &mut failed)?;
|
||||
let (s, c, n, o) = b.shell(cx, rid(p.get(1), "brep outer shell")?, &mut failed)?;
|
||||
closed &= c;
|
||||
file_faces += n;
|
||||
shells.push(s);
|
||||
face_origin.extend(o);
|
||||
if e.name() == "BREP_WITH_VOIDS" {
|
||||
for v in refs(p.get(2)) {
|
||||
match b.shell(cx, v, &mut failed) {
|
||||
Ok((s, c, n)) => {
|
||||
Ok((s, c, n, o)) => {
|
||||
closed &= c;
|
||||
file_faces += n;
|
||||
shells.push(s);
|
||||
face_origin.extend(o);
|
||||
}
|
||||
Err(err) => failed.push(format!("void shell #{v}: {err}")),
|
||||
}
|
||||
@@ -377,10 +385,11 @@ pub(crate) fn read_item(cx: &Ctx, id: u64) -> R<BrepOut> {
|
||||
"SHELL_BASED_SURFACE_MODEL" => {
|
||||
for s in refs(p.get(1)) {
|
||||
match b.shell(cx, s, &mut failed) {
|
||||
Ok((s, c, n)) => {
|
||||
Ok((s, c, n, o)) => {
|
||||
closed &= c;
|
||||
file_faces += n;
|
||||
shells.push(s);
|
||||
face_origin.extend(o);
|
||||
}
|
||||
Err(err) => failed.push(format!("shell #{s}: {err}")),
|
||||
}
|
||||
@@ -398,5 +407,5 @@ pub(crate) fn read_item(cx: &Ctx, id: u64) -> R<BrepOut> {
|
||||
mt::Solid::new_unchecked(shells)
|
||||
}
|
||||
};
|
||||
Ok(BrepOut { solid, failed, closed, file_faces })
|
||||
Ok(BrepOut { solid, failed, closed, file_faces, face_origin })
|
||||
}
|
||||
@@ -227,8 +227,10 @@ struct Reader<'a> {
|
||||
placements: HashMap<u64, u64>,
|
||||
/// Styled item → colour.
|
||||
colors: HashMap<u64, [f32; 3]>,
|
||||
/// Opacity of styled items with a transparency (1 = opaque).
|
||||
opacity: HashMap<u64, f32>,
|
||||
/// Converted items (brep id → solid, or the error).
|
||||
solids: HashMap<u64, std::result::Result<(mt::Solid, bool, usize), String>>,
|
||||
solids: HashMap<u64, std::result::Result<(mt::Solid, bool, usize, Vec<u64>), String>>,
|
||||
used_items: HashSet<u64>,
|
||||
/// Items already instanced for the product being read.
|
||||
node_items: HashSet<u64>,
|
||||
@@ -291,6 +293,9 @@ impl<'a> Reader<'a> {
|
||||
&& let Some(c) = self.find_colour(p.get(1))
|
||||
{
|
||||
self.colors.entry(item).or_insert(c);
|
||||
if let Some(t) = self.find_transparency(p.get(1)) {
|
||||
self.opacity.entry(item).or_insert((1.0 - t).clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -314,6 +319,34 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
/// First colour reachable from a style list (bounded search).
|
||||
/// SURFACE_STYLE_TRANSPARENT under a style list (0 = opaque, 1 = invisible).
|
||||
fn find_transparency(&self, styles: Option<&Param>) -> Option<f32> {
|
||||
let mut queue: Vec<u64> = styles?.as_list()?.iter().filter_map(Param::as_ref_id).collect();
|
||||
let mut seen = HashSet::new();
|
||||
while let Some(id) = queue.pop() {
|
||||
if seen.len() > 64 || !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let e = self.ex.get(id)?;
|
||||
if e.name() == "SURFACE_STYLE_TRANSPARENT" {
|
||||
return e.params().first().and_then(Param::as_f64).filter(|x| x.is_finite()).map(|x| x.clamp(0.0, 1.0) as f32);
|
||||
}
|
||||
if e.name() == "COLOUR_RGB" {
|
||||
continue;
|
||||
}
|
||||
for r in &e.records {
|
||||
for p in &r.params {
|
||||
match p {
|
||||
Param::Ref(x) => queue.push(*x),
|
||||
Param::List(v) => queue.extend(v.iter().filter_map(Param::as_ref_id)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_colour(&self, styles: Option<&Param>) -> Option<[f32; 3]> {
|
||||
let mut queue: Vec<u64> = styles?.as_list()?.iter().filter_map(Param::as_ref_id).collect();
|
||||
let mut seen = HashSet::new();
|
||||
@@ -387,7 +420,7 @@ impl<'a> Reader<'a> {
|
||||
seen
|
||||
}
|
||||
|
||||
fn item_solid(&mut self, item: u64, u: Units) -> Option<(mt::Solid, bool, usize)> {
|
||||
fn item_solid(&mut self, item: u64, u: Units) -> Option<(mt::Solid, bool, usize, Vec<u64>)> {
|
||||
if let Some(r) = self.solids.get(&item) {
|
||||
return r.as_ref().ok().cloned();
|
||||
}
|
||||
@@ -401,7 +434,7 @@ impl<'a> Reader<'a> {
|
||||
if !out.closed {
|
||||
self.warn(format!("{label}: imported as an open body (the shell is not closed)"));
|
||||
}
|
||||
Ok((out.solid, out.closed, out.file_faces))
|
||||
Ok((out.solid, out.closed, out.file_faces, out.face_origin))
|
||||
}
|
||||
Err(e) => {
|
||||
let label = self.label(item);
|
||||
@@ -444,7 +477,7 @@ impl<'a> Reader<'a> {
|
||||
self.warn(format!("more than {MAX_BODIES} bodies: the rest are left out"));
|
||||
return;
|
||||
}
|
||||
let Some((solid, closed, file_faces)) = self.item_solid(item, u) else { continue };
|
||||
let Some((solid, closed, file_faces, face_origin)) = self.item_solid(item, u) else { continue };
|
||||
let identity = *m == mt::Matrix4::from_scale(1.0);
|
||||
let placed = if identity {
|
||||
Ok(solid)
|
||||
@@ -466,7 +499,18 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
out.push(self.bodies.len());
|
||||
let color = self.colors.get(&item).copied();
|
||||
self.bodies.push(ImportedBody { name, body: body.with_color(color), color, path: path.to_vec(), closed, file_faces });
|
||||
// See-through bodies and faces with colours of their own.
|
||||
let faces = face_origin
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| {
|
||||
let c = self.colors.get(f)?;
|
||||
Some(crate::FacePaint { face: i, color: *c, opacity: self.opacity.get(f).copied().unwrap_or(1.0) })
|
||||
})
|
||||
.collect();
|
||||
let paint = crate::Paint { opacity: self.opacity.get(&item).copied().unwrap_or(1.0), faces };
|
||||
let body = body.with_color(color).with_paint(Some(paint));
|
||||
self.bodies.push(ImportedBody { name, body, color, path: path.to_vec(), closed, file_faces });
|
||||
}
|
||||
Err(err) => {
|
||||
let label = self.label(item);
|
||||
@@ -775,6 +819,7 @@ pub fn step_import(text: &str) -> Result<StepImport> {
|
||||
children: HashMap::new(),
|
||||
placements: HashMap::new(),
|
||||
colors: HashMap::new(),
|
||||
opacity: HashMap::new(),
|
||||
solids: HashMap::new(),
|
||||
used_items: HashSet::new(),
|
||||
node_items: HashSet::new(),
|
||||
|
||||
@@ -388,7 +388,7 @@ fn recut(face: &mt::Face, cuts: &[(mt::Edge, (mt::Edge, mt::Edge))]) -> mt::Face
|
||||
|
||||
/// Split every band face of a shell (see the module docs). Faces that cannot be split stay as
|
||||
/// they are and are reported.
|
||||
pub(crate) fn split_bands(mut faces: Vec<mt::Face>, warnings: &mut Vec<String>) -> Vec<mt::Face> {
|
||||
pub(crate) fn split_bands(mut faces: Vec<mt::Face>, origin: &mut Vec<u64>, warnings: &mut Vec<String>) -> Vec<mt::Face> {
|
||||
let n = faces.len();
|
||||
for i in 0..n {
|
||||
let Some(face) = faces.get(i).cloned() else { continue };
|
||||
@@ -399,6 +399,9 @@ pub(crate) fn split_bands(mut faces: Vec<mt::Face>, warnings: &mut Vec<String>)
|
||||
*slot = p.faces.0;
|
||||
}
|
||||
faces.push(p.faces.1);
|
||||
// The second piece comes from the same file face.
|
||||
let o = origin.get(i).copied().unwrap_or(0);
|
||||
origin.push(o);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => warnings
|
||||
|
||||
Reference in new issue
Block a user