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 <[email protected]> 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
@@ -125,7 +125,15 @@ impl Document {
|
||||
/// The faces of a body with appearances of their own: (B-rep face index, look). A later
|
||||
/// assignment to the same face wins.
|
||||
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
|
||||
let mut out: Vec<(usize, Look)> = Vec::new();
|
||||
// Face colours the body was imported with, under the design's own.
|
||||
let mut out: Vec<(usize, Look)> = b
|
||||
.body
|
||||
.paint()
|
||||
.map(|p| {
|
||||
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
p.faces.iter().map(|f| (f.face, Look::custom(f.color.map(to8), f.opacity as f64))).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let names = crate::naming::face_names(b);
|
||||
for f in self.appearances.faces.iter().filter(|f| f.body == b.name) {
|
||||
let (p, look) = (f.point, f.look.clone());
|
||||
|
||||
@@ -1197,13 +1197,22 @@ impl Document {
|
||||
self.body_look(&b.name, b.feature).map(|l| l.rgb()).or_else(|| b.body.color())
|
||||
}
|
||||
|
||||
/// The model with each body carrying the colour it is shown in (for export).
|
||||
/// The model with each body carrying the colour, opacity and face colours it is shown with
|
||||
/// (for export).
|
||||
pub fn painted(&self, state: &crate::ModelState) -> crate::ModelState {
|
||||
let mut st = state.clone();
|
||||
for b in &mut st.bodies {
|
||||
let c = self.body_color(b);
|
||||
if c != b.body.color() {
|
||||
b.body = b.body.clone().with_color(c);
|
||||
let look = self.body_look(&b.name, b.feature);
|
||||
let faces: Vec<solvecraft_kernel::FacePaint> = self
|
||||
.face_colors(b)
|
||||
.into_iter()
|
||||
.map(|(face, l)| solvecraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
|
||||
.collect();
|
||||
let opacity = look.map(|l| l.opacity as f32).or_else(|| b.body.paint().map(|p| p.opacity)).unwrap_or(1.0);
|
||||
let paint = solvecraft_kernel::Paint { opacity, faces };
|
||||
if c != b.body.color() || b.body.paint() != Some(&paint) {
|
||||
b.body = b.body.clone().with_color(c).with_paint(Some(paint));
|
||||
}
|
||||
}
|
||||
st
|
||||
|
||||
@@ -987,6 +987,35 @@ fn material_and_appearance_colours_export_to_step() {
|
||||
check(&step_round_trip(&mut s2, "colours2"));
|
||||
}
|
||||
|
||||
/// Face appearances and see-through bodies export: STEP styles on the faces with a
|
||||
/// transparency, read back as face colours.
|
||||
#[test]
|
||||
fn face_colours_and_opacity_export() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Cube"}));
|
||||
run(&mut s, "AppearanceCommand", json!({"bodies": ["Cube"], "color": "#2040c0", "opacity": 0.5}));
|
||||
run(&mut s, "AppearanceCommand", json!({"faces": [[5, 5, 10]], "color": "#ff0000"}));
|
||||
let imp = step_round_trip(&mut s, "facecol");
|
||||
let b = &imp.bodies[0];
|
||||
let paint = b.body.paint().expect("paint read back");
|
||||
assert!((paint.opacity - 0.5).abs() < 1e-3, "{paint:?}");
|
||||
let faces = b.body.faces(0.01).unwrap();
|
||||
let top = faces.iter().position(|f| (f.centroid.z - 10.0).abs() < 1e-6).unwrap();
|
||||
assert_eq!(paint.faces.len(), 1, "{paint:?}");
|
||||
assert_eq!(paint.faces[0].face, top);
|
||||
assert!((paint.faces[0].color[0] - 1.0).abs() < 1e-3 && paint.faces[0].color[1].abs() < 1e-3, "{paint:?}");
|
||||
// Opened again, the face keeps its colour (shown and exported).
|
||||
let p = std::env::temp_dir().join(format!("solvecraft-facecol-{}.step", std::process::id()));
|
||||
run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()}));
|
||||
let mut s2 = Session::default();
|
||||
run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()}));
|
||||
let _ = std::fs::remove_file(&p);
|
||||
let st = s2.world_state();
|
||||
let looks = s2.doc.face_colors(&st.bodies[0]);
|
||||
assert_eq!(looks.len(), 1);
|
||||
assert_eq!(looks[0].1.color, [255, 0, 0]);
|
||||
}
|
||||
|
||||
/// Components export as a STEP assembly: products, occurrences, placements.
|
||||
#[test]
|
||||
fn components_export_as_step_assembly() {
|
||||
|
||||
+14
-3
@@ -119,7 +119,7 @@ pub fn mesh_import_feature(bytes: &[u8], file: &str) -> Result<MeshFeature> {
|
||||
let positions: Vec<Vec3> = tris.iter().flatten().map(|v| Vec3::new(v[0] as f64, v[1] as f64, v[2] as f64)).collect();
|
||||
let n = u32::try_from(tris.len()).map_err(|_| IoError::Invalid("STL too large".into()))?;
|
||||
let triangles = (0..n).map(|i| [i * 3, i * 3 + 1, i * 3 + 2]).collect();
|
||||
(vec![MeshObject { name: stem.clone(), positions, triangles, color: None }], Vec::new())
|
||||
(vec![MeshObject { name: stem.clone(), positions, triangles, ..Default::default() }], Vec::new())
|
||||
} else {
|
||||
read_3mf(bytes)?
|
||||
};
|
||||
@@ -269,8 +269,19 @@ pub fn export(state: &ModelState, bodies: &[String], format: Format, name: &str)
|
||||
.map(|b| {
|
||||
let m = export_mesh(&b.body)?;
|
||||
let eps = (b.body.size() * 1e-6).max(1e-7);
|
||||
let (positions, triangles) = threemf::weld(&m.positions, &m.triangles, eps);
|
||||
Ok(MeshObject { name: b.name.clone(), positions, triangles, color: b.body.color() })
|
||||
// Faces with colours of their own tag their triangles.
|
||||
let paint = b.body.paint();
|
||||
let mut looks = Vec::new();
|
||||
let mut by_face = std::collections::HashMap::new();
|
||||
for f in paint.map(|p| p.faces.as_slice()).unwrap_or_default() {
|
||||
looks.push((f.color, 1.0 - f.opacity));
|
||||
by_face.insert(f.face, looks.len() as u32);
|
||||
}
|
||||
let tags: Vec<u32> = m.tri_face.iter().map(|f| by_face.get(&(*f as usize)).copied().unwrap_or(0)).collect();
|
||||
let (positions, triangles, tri_looks) = threemf::weld_tagged(&m.positions, &m.triangles, &tags, eps);
|
||||
let tri_looks = if looks.is_empty() { Vec::new() } else { tri_looks };
|
||||
let transparency = 1.0 - paint.map(|p| p.opacity).unwrap_or(1.0);
|
||||
Ok(MeshObject { name: b.name.clone(), positions, triangles, color: b.body.color(), transparency, looks, tri_looks })
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
write_3mf(&objs)?
|
||||
|
||||
+78
-17
@@ -36,6 +36,13 @@ pub struct MeshObject {
|
||||
pub positions: Vec<Vec3>,
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
pub color: Option<[f32; 3]>,
|
||||
/// 0 = opaque, 1 = invisible (written as the colour's alpha).
|
||||
pub transparency: f32,
|
||||
/// Colours of their own for some triangles (faces with appearances): colour 0..1 and
|
||||
/// transparency; `tri_looks` gives each triangle's entry + 1 (0: the object's own), and is
|
||||
/// empty when no triangle has one.
|
||||
pub looks: Vec<([f32; 3], f32)>,
|
||||
pub tri_looks: Vec<u32>,
|
||||
}
|
||||
|
||||
fn esc(s: &str) -> String {
|
||||
@@ -55,7 +62,7 @@ fn esc(s: &str) -> String {
|
||||
}
|
||||
|
||||
/// Shared vertices (merged at single precision, as written) and non-degenerate triangles.
|
||||
fn indexed(m: &MeshObject) -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
|
||||
fn indexed(m: &MeshObject) -> (Vec<[f32; 3]>, Vec<([u32; 3], u32)>) {
|
||||
let mut map: HashMap<[u32; 3], u32> = HashMap::new();
|
||||
let mut verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut remap = Vec::with_capacity(m.positions.len());
|
||||
@@ -72,9 +79,10 @@ fn indexed(m: &MeshObject) -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
|
||||
let tris = m
|
||||
.triangles
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
.enumerate()
|
||||
.filter_map(|(k, t)| {
|
||||
let (a, b, c) = (*remap.get(t[0] as usize)?, *remap.get(t[1] as usize)?, *remap.get(t[2] as usize)?);
|
||||
(a != b && b != c && c != a).then_some([a, b, c])
|
||||
(a != b && b != c && c != a).then_some(([a, b, c], m.tri_looks.get(k).copied().unwrap_or(0)))
|
||||
})
|
||||
.collect();
|
||||
(verts, tris)
|
||||
@@ -83,6 +91,13 @@ fn indexed(m: &MeshObject) -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
|
||||
/// Make a tessellation watertight: merge vertices closer than `eps` and split triangles at
|
||||
/// vertices lying on their open sides (T-junctions between separately meshed faces).
|
||||
pub fn weld(positions: &[Vec3], triangles: &[[u32; 3]], eps: f64) -> (Vec<Vec3>, Vec<[u32; 3]>) {
|
||||
let (v, t, _) = weld_tagged(positions, triangles, &[], eps);
|
||||
(v, t)
|
||||
}
|
||||
|
||||
/// [`weld`] keeping a tag per triangle (pieces of a split triangle keep its tag; missing tags
|
||||
/// are 0).
|
||||
pub fn weld_tagged(positions: &[Vec3], triangles: &[[u32; 3]], tags: &[u32], eps: f64) -> (Vec<Vec3>, Vec<[u32; 3]>, Vec<u32>) {
|
||||
// Merge on a grid of cell `eps` (a vertex joins one already in its own or a neighbour cell).
|
||||
let cell = |p: Vec3| [(p.x / eps).round() as i64, (p.y / eps).round() as i64, (p.z / eps).round() as i64];
|
||||
let mut grid: HashMap<[i64; 3], Vec<u32>> = HashMap::new();
|
||||
@@ -114,13 +129,14 @@ pub fn weld(positions: &[Vec3], triangles: &[[u32; 3]], eps: f64) -> (Vec<Vec3>,
|
||||
};
|
||||
remap.push(i);
|
||||
}
|
||||
let mut tris: Vec<[u32; 3]> = triangles
|
||||
let (mut tris, mut tags): (Vec<[u32; 3]>, Vec<u32>) = triangles
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
.enumerate()
|
||||
.filter_map(|(k, t)| {
|
||||
let (a, b, c) = (*remap.get(t[0] as usize)?, *remap.get(t[1] as usize)?, *remap.get(t[2] as usize)?);
|
||||
(a != b && b != c && c != a).then_some([a, b, c])
|
||||
(a != b && b != c && c != a).then_some(([a, b, c], tags.get(k).copied().unwrap_or(0)))
|
||||
})
|
||||
.collect();
|
||||
.unzip();
|
||||
// T-junctions: split open sides at open-side vertices lying on them.
|
||||
for _ in 0..8 {
|
||||
let mut half: HashMap<(u32, u32), usize> = HashMap::new();
|
||||
@@ -163,7 +179,8 @@ pub fn weld(positions: &[Vec3], triangles: &[[u32; 3]], eps: f64) -> (Vec<Vec3>,
|
||||
break;
|
||||
}
|
||||
let mut next = Vec::with_capacity(tris.len() + splits.len() * 2);
|
||||
for t in &tris {
|
||||
let mut next_tags = Vec::with_capacity(next.capacity());
|
||||
for (t, tag) in tris.iter().zip(&tags) {
|
||||
// Rotate so the split side (if any) is (t0, t1); split one side per pass.
|
||||
let rot = [[t[0], t[1], t[2]], [t[1], t[2], t[0]], [t[2], t[0], t[1]]];
|
||||
match rot.iter().find_map(|r| splits.get(&(r[0], r[1])).map(|s| (r, s))) {
|
||||
@@ -171,16 +188,22 @@ pub fn weld(positions: &[Vec3], triangles: &[[u32; 3]], eps: f64) -> (Vec<Vec3>,
|
||||
let mut prev = r[0];
|
||||
for (_, v) in s {
|
||||
next.push([prev, *v, r[2]]);
|
||||
next_tags.push(*tag);
|
||||
prev = *v;
|
||||
}
|
||||
next.push([prev, r[1], r[2]]);
|
||||
next_tags.push(*tag);
|
||||
}
|
||||
None => {
|
||||
next.push(*t);
|
||||
next_tags.push(*tag);
|
||||
}
|
||||
None => next.push(*t),
|
||||
}
|
||||
}
|
||||
tris = next;
|
||||
tags = next_tags;
|
||||
}
|
||||
(verts, tris)
|
||||
(verts, tris, tags)
|
||||
}
|
||||
|
||||
/// The model part's XML.
|
||||
@@ -189,19 +212,34 @@ pub fn model_xml(objects: &[MeshObject]) -> String {
|
||||
x.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
let _ = writeln!(x, "<model unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"{CORE_NS}\">");
|
||||
x.push_str(" <metadata name=\"Application\">SolveCraft</metadata>\n <resources>\n");
|
||||
let colored = objects.iter().any(|o| o.color.is_some());
|
||||
// Base material group id 1; objects from 2.
|
||||
let colored = objects.iter().any(|o| o.color.is_some() || o.transparency > 0.0 || !o.looks.is_empty());
|
||||
// Base material group id 1 (each object's own colour, then its faces' colours); objects
|
||||
// from 2. A see-through colour carries its alpha (#RRGGBBAA).
|
||||
let display = |c: [f32; 3], transparency: f32| {
|
||||
let c = c.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
let alpha = if transparency > 0.0 { format!("{:02X}", ((1.0 - transparency.clamp(0.0, 1.0)) * 255.0).round() as u8) } else { String::new() };
|
||||
format!("#{:02X}{:02X}{:02X}{alpha}", c[0], c[1], c[2])
|
||||
};
|
||||
let mut first = Vec::with_capacity(objects.len());
|
||||
if colored {
|
||||
x.push_str(" <basematerials id=\"1\">\n");
|
||||
let mut n = 0usize;
|
||||
for o in objects {
|
||||
let c = o.color.unwrap_or([0.7, 0.7, 0.7]).map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
let _ = writeln!(x, " <base name=\"{}\" displaycolor=\"#{:02X}{:02X}{:02X}\"/>", esc(&o.name), c[0], c[1], c[2]);
|
||||
first.push(n);
|
||||
let _ =
|
||||
writeln!(x, " <base name=\"{}\" displaycolor=\"{}\"/>", esc(&o.name), display(o.color.unwrap_or([0.7, 0.7, 0.7]), o.transparency));
|
||||
n += 1;
|
||||
for (i, (c, t)) in o.looks.iter().enumerate() {
|
||||
let _ = writeln!(x, " <base name=\"{} face {}\" displaycolor=\"{}\"/>", esc(&o.name), i + 1, display(*c, *t));
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
x.push_str(" </basematerials>\n");
|
||||
}
|
||||
for (k, o) in objects.iter().enumerate() {
|
||||
let id = k + 2;
|
||||
let mat = if colored { format!(" pid=\"1\" pindex=\"{k}\"") } else { String::new() };
|
||||
let own = first.get(k).copied().unwrap_or(0);
|
||||
let mat = if colored { format!(" pid=\"1\" pindex=\"{own}\"") } else { String::new() };
|
||||
let _ = writeln!(x, " <object id=\"{id}\" type=\"model\" name=\"{}\"{mat}>", esc(&o.name));
|
||||
x.push_str(" <mesh>\n <vertices>\n");
|
||||
let (verts, tris) = indexed(o);
|
||||
@@ -209,8 +247,15 @@ pub fn model_xml(objects: &[MeshObject]) -> String {
|
||||
let _ = writeln!(x, " <vertex x=\"{}\" y=\"{}\" z=\"{}\"/>", v[0], v[1], v[2]);
|
||||
}
|
||||
x.push_str(" </vertices>\n <triangles>\n");
|
||||
for t in &tris {
|
||||
let _ = writeln!(x, " <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\"/>", t[0], t[1], t[2]);
|
||||
for (t, look) in &tris {
|
||||
match look {
|
||||
l if colored && *l > 0 && (*l as usize) <= o.looks.len() => {
|
||||
let _ = writeln!(x, " <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\" pid=\"1\" p1=\"{}\"/>", t[0], t[1], t[2], own + *l as usize);
|
||||
}
|
||||
_ => {
|
||||
let _ = writeln!(x, " <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\"/>", t[0], t[1], t[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
x.push_str(" </triangles>\n </mesh>\n </object>\n");
|
||||
}
|
||||
@@ -573,9 +618,25 @@ mod tests {
|
||||
positions: vec![Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), Vec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, 0.0, 10.0)],
|
||||
triangles: vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]],
|
||||
color: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A see-through object writes its alpha; triangles of faces with colours of their own
|
||||
/// point at their entries.
|
||||
#[test]
|
||||
fn face_colours_and_alpha() {
|
||||
let mut t = tetra("T");
|
||||
t.color = Some([0.125, 0.25, 0.75]);
|
||||
t.transparency = 0.5;
|
||||
t.looks = vec![([1.0, 0.0, 0.0], 0.0)];
|
||||
t.tri_looks = vec![0, 1, 0, 0];
|
||||
let model = model_xml(&[t]);
|
||||
assert!(model.contains("displaycolor=\"#2040BF80\""), "{model}");
|
||||
assert!(model.contains("displaycolor=\"#FF0000\""), "{model}");
|
||||
assert_eq!(model.matches("p1=\"1\"").count(), 1, "{model}");
|
||||
}
|
||||
|
||||
fn package(model: &str, rels: Option<&str>) -> Vec<u8> {
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default();
|
||||
|
||||
@@ -17,6 +17,24 @@ pub struct Body {
|
||||
pub(crate) mesh: Option<std::sync::Arc<crate::meshbody::TriMesh>>,
|
||||
/// Display colour (linear RGB 0..1) from an imported file; operations drop it.
|
||||
pub(crate) color: Option<[f32; 3]>,
|
||||
/// Opacity and face colours for export (set by the document; operations drop it).
|
||||
pub(crate) paint: Option<std::sync::Arc<Paint>>,
|
||||
}
|
||||
|
||||
/// How a body looks beyond its colour, for export: its opacity and its faces' own colours.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Paint {
|
||||
/// 1 = opaque.
|
||||
pub opacity: f32,
|
||||
pub faces: Vec<FacePaint>,
|
||||
}
|
||||
|
||||
/// A face's own colour: the B-rep face index (`face_iter` order), sRGB as 0..1, opacity.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FacePaint {
|
||||
pub face: usize,
|
||||
pub color: [f32; 3],
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
/// A B-rep edge as seen by the rest of the application.
|
||||
@@ -259,6 +277,7 @@ impl Body {
|
||||
_ => false,
|
||||
}
|
||||
&& self.color == other.color
|
||||
&& self.paint == other.paint
|
||||
}
|
||||
|
||||
pub(crate) fn new(solid: Solid) -> Result<Body> {
|
||||
@@ -283,7 +302,7 @@ impl Body {
|
||||
if v < 0.0 {
|
||||
solid.not();
|
||||
}
|
||||
Ok(Body { solid: std::sync::Arc::new(solid), mesh: None, color: None })
|
||||
Ok(Body { solid: std::sync::Arc::new(solid), mesh: None, color: None, paint: None })
|
||||
}
|
||||
|
||||
/// A deep copy of the solid that can be mutated without affecting other bodies.
|
||||
@@ -316,6 +335,30 @@ impl Body {
|
||||
self
|
||||
}
|
||||
|
||||
/// Opacity and face colours for export, if any.
|
||||
pub fn paint(&self) -> Option<&Paint> {
|
||||
self.paint.as_deref()
|
||||
}
|
||||
|
||||
/// The same body with opacity and face colours for export (`None` or opaque with no face
|
||||
/// colours clears them).
|
||||
pub fn with_paint(mut self, p: Option<Paint>) -> Body {
|
||||
let clean = |x: f32| if x.is_finite() { x.clamp(0.0, 1.0) } else { 1.0 };
|
||||
self.paint = p
|
||||
.map(|mut p| {
|
||||
p.opacity = clean(p.opacity);
|
||||
p.faces.retain(|f| f.color.iter().all(|x| x.is_finite()));
|
||||
for f in &mut p.faces {
|
||||
f.color = f.color.map(|x| x.clamp(0.0, 1.0));
|
||||
f.opacity = clean(f.opacity);
|
||||
}
|
||||
p
|
||||
})
|
||||
.filter(|p| p.opacity < 1.0 || !p.faces.is_empty())
|
||||
.map(std::sync::Arc::new);
|
||||
self
|
||||
}
|
||||
|
||||
/// Characteristic size (bounding box diagonal) for tolerances.
|
||||
pub fn size(&self) -> f64 {
|
||||
let mut b = solvecraft_geom::Aabb3::EMPTY;
|
||||
|
||||
@@ -36,7 +36,7 @@ mod sweep_path;
|
||||
mod topo;
|
||||
|
||||
pub use blend::{chamfer, fillet};
|
||||
pub use body::{Body, EdgeInfo, FaceInfo};
|
||||
pub use body::{Body, EdgeInfo, FaceInfo, FacePaint, Paint};
|
||||
pub use build::{PathSeg, box_solid, cylinder, extrude, extrude_tapered, loft, revolve, sphere, sweep, torus};
|
||||
pub use helix::{loft_to_point, sweep_helix};
|
||||
pub use measure::{BodyMeasure, measure};
|
||||
|
||||
@@ -100,7 +100,7 @@ pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result<Body> {
|
||||
t.swap(1, 2);
|
||||
}
|
||||
}
|
||||
Ok(Body { solid: Arc::new(crate::body::Solid::new_unchecked(Vec::new())), mesh: Some(Arc::new(out)), color: None })
|
||||
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.
|
||||
@@ -142,6 +142,6 @@ impl Body {
|
||||
t.swap(1, 2);
|
||||
}
|
||||
}
|
||||
Some(Body { solid: self.solid.clone(), mesh: Some(Arc::new(out)), color: self.color })
|
||||
Some(Body { solid: self.solid.clone(), mesh: Some(Arc::new(out)), color: self.color, paint: self.paint.clone() })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -152,6 +152,8 @@ struct Out {
|
||||
/// B-spline cylinder sheets of the body being copied under merged faces, with the direction
|
||||
/// of the cylinder's parameter seam.
|
||||
merged: HashMap<u64, V3>,
|
||||
/// Faces of the body being copied: exchange id → written id.
|
||||
faces_out: HashMap<u64, u64>,
|
||||
}
|
||||
|
||||
impl Out {
|
||||
@@ -332,6 +334,9 @@ impl Out {
|
||||
let topo = TOPOLOGY.contains(&e.name());
|
||||
let new = if topo { self.add(text) } else { self.add_shared(text) };
|
||||
map.insert(id, new);
|
||||
if matches!(e.name(), "FACE_SURFACE" | "ADVANCED_FACE") {
|
||||
self.faces_out.insert(id, new);
|
||||
}
|
||||
}
|
||||
map.get(&root).copied().ok_or_else(|| "solid not copied".into())
|
||||
}
|
||||
@@ -827,7 +832,7 @@ fn face_edges(ex: &p21::Exchange, face: &p21::Entity) -> Option<Vec<Vec<(u64, bo
|
||||
/// Faces that truck splits (a revolution in two or three turns) joined back into one face per
|
||||
/// analytic surface, by dropping the edges between them. Faces whose union would lose all its
|
||||
/// edges, or whose leftover edges do not close into loops, stay apart.
|
||||
fn merge_split_faces(ex: &mut p21::Exchange) -> HashMap<u64, V3> {
|
||||
fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>) -> HashMap<u64, V3> {
|
||||
let mut merged_surfaces = std::collections::HashSet::new();
|
||||
let mut next = ex.entities.keys().max().copied().unwrap_or(0) + 1;
|
||||
let shells: Vec<u64> = ex.entities.iter().filter(|(_, e)| matches!(e.name(), "CLOSED_SHELL" | "OPEN_SHELL")).map(|(i, _)| *i).collect();
|
||||
@@ -908,6 +913,10 @@ fn merge_split_faces(ex: &mut p21::Exchange) -> HashMap<u64, V3> {
|
||||
continue;
|
||||
}
|
||||
let (Some(&fa), Some(&fb)) = (faces.get(i), faces.get(j)) else { continue };
|
||||
// Faces of different colours stay apart.
|
||||
if paint.get(&fa) != paint.get(&fb) {
|
||||
continue;
|
||||
}
|
||||
let (Some(ea), Some(eb)) = (ex.get(fa).and_then(|f| face_edges(ex, f)), ex.get(fb).and_then(|f| face_edges(ex, f))) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1100,6 +1109,10 @@ fn merge_split_faces(ex: &mut p21::Exchange) -> HashMap<u64, V3> {
|
||||
merged_surfaces.insert(sid);
|
||||
}
|
||||
ex.entities.insert(nid, p21::Entity { records: vec![p21::Record { name: old.name().to_string(), params }] });
|
||||
if let Some(look) = paint.remove(&fa) {
|
||||
paint.remove(&fb);
|
||||
paint.insert(nid, look);
|
||||
}
|
||||
let new_list: Vec<Param> = faces.iter().filter(|f| **f != fb).map(|f| Param::Ref(if *f == fa { nid } else { *f })).collect();
|
||||
if let Some(she) = ex.entities.get_mut(&sh)
|
||||
&& let Some(r) = she.records.first_mut()
|
||||
@@ -1168,14 +1181,31 @@ fn merge_split_faces(ex: &mut p21::Exchange) -> HashMap<u64, V3> {
|
||||
|
||||
/// The B-rep entities of one body as truck writes them, parsed, and the surfaces of faces
|
||||
/// merged from pieces.
|
||||
fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V3>)> {
|
||||
fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V3>, HashMap<u64, FaceLook>)> {
|
||||
let text = crate::step::truck_step(&[b], "SolveCraft")?;
|
||||
let mut ex = p21::parse(&text).map_err(|e| KernelError::Failed(format!("STEP export: {e}")))?;
|
||||
collapse_short_edges(&mut ex);
|
||||
let merged = if merge { merge_split_faces(&mut ex) } else { Default::default() };
|
||||
Ok((ex, merged))
|
||||
// Face colours by exchange face: truck writes each shell's faces in the body's face order.
|
||||
let mut paint = HashMap::new();
|
||||
if let Some(p) = b.paint()
|
||||
&& !p.faces.is_empty()
|
||||
{
|
||||
let mut shells: Vec<u64> = ex.entities.iter().filter(|(_, e)| matches!(e.name(), "CLOSED_SHELL" | "OPEN_SHELL")).map(|(i, _)| *i).collect();
|
||||
shells.sort();
|
||||
let faces: Vec<u64> = shells.iter().filter_map(|s| ex.get(*s)?.params().get(1)?.as_list()).flatten().filter_map(Param::as_ref_id).collect();
|
||||
for f in &p.faces {
|
||||
if let Some(id) = faces.get(f.face) {
|
||||
paint.insert(*id, (f.color, f.opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
let merged = if merge { merge_split_faces(&mut ex, &mut paint) } else { Default::default() };
|
||||
Ok((ex, merged, paint))
|
||||
}
|
||||
|
||||
/// A face's own colour (0..1) and opacity.
|
||||
type FaceLook = ([f32; 3], f32);
|
||||
|
||||
fn rigid_frame(m: &[[f64; 4]; 4]) -> Result<([f64; 3], [f64; 3], [f64; 3])> {
|
||||
let x = [m[0][0], m[0][1], m[0][2]];
|
||||
let y = [m[1][0], m[1][1], m[1][2]];
|
||||
@@ -1308,18 +1338,27 @@ fn products_text(products: &[ExportProduct], root: usize, header: &StepHeader, m
|
||||
if !p.bodies.is_empty() {
|
||||
let mut solids = Vec::new();
|
||||
for b in &p.bodies {
|
||||
let (ex, merged) = brep_exchange(b.body, merge)?;
|
||||
let (ex, merged, face_paint) = brep_exchange(b.body, merge)?;
|
||||
o.merged = merged;
|
||||
o.faces_out.clear();
|
||||
let mut roots: Vec<u64> =
|
||||
ex.entities.iter().filter(|(_, e)| matches!(e.name(), "MANIFOLD_SOLID_BREP" | "BREP_WITH_VOIDS")).map(|(i, _)| *i).collect();
|
||||
roots.sort();
|
||||
for r in roots {
|
||||
let id = o.copy_solid(&ex, r, &b.name).map_err(|e| KernelError::Failed(format!("STEP export: {e}")))?;
|
||||
solids.push(id);
|
||||
let opacity = b.body.paint().map(|p| p.opacity).unwrap_or(1.0);
|
||||
if let Some(c) = b.color {
|
||||
styled.push(style(&mut o, id, c));
|
||||
styled.push(style(&mut o, id, c, opacity));
|
||||
}
|
||||
}
|
||||
// Faces with colours of their own.
|
||||
let mut painted: Vec<(u64, ([f32; 3], f32))> =
|
||||
face_paint.into_iter().filter_map(|(f, look)| Some((*o.faces_out.get(&f)?, look))).collect();
|
||||
painted.sort_by_key(|(f, _)| *f);
|
||||
for (f, (c, op)) in painted {
|
||||
styled.push(style(&mut o, f, c, op));
|
||||
}
|
||||
}
|
||||
let list = solids.iter().chain(std::iter::once(&origin)).map(|i| format!("#{i}")).collect::<Vec<_>>().join(",");
|
||||
let absr = o.add(format!("ADVANCED_BREP_SHAPE_REPRESENTATION({},({list}),#{ctx})", string(&name)));
|
||||
@@ -1380,12 +1419,19 @@ fn products_text(products: &[ExportProduct], root: usize, header: &StepHeader, m
|
||||
}
|
||||
|
||||
/// A surface colour for a solid (STYLED_ITEM chain).
|
||||
fn style(o: &mut Out, item: u64, c: [f32; 3]) -> u64 {
|
||||
fn style(o: &mut Out, item: u64, c: [f32; 3], opacity: f32) -> u64 {
|
||||
let rgb = o.add_shared(format!("COLOUR_RGB('',{},{},{})", real(c[0] as f64), real(c[1] as f64), real(c[2] as f64)));
|
||||
let fasc = o.add_shared(format!("FILL_AREA_STYLE_COLOUR('',#{rgb})"));
|
||||
let fas = o.add_shared(format!("FILL_AREA_STYLE('',(#{fasc}))"));
|
||||
let ssfa = o.add_shared(format!("SURFACE_STYLE_FILL_AREA(#{fas})"));
|
||||
let sss = o.add_shared(format!("SURFACE_SIDE_STYLE('',(#{ssfa}))"));
|
||||
// See-through looks add a transparency (1 = invisible) to the side style.
|
||||
let sides = if opacity < 1.0 {
|
||||
let tr = o.add_shared(format!("SURFACE_STYLE_TRANSPARENT({})", real((1.0 - opacity as f64).clamp(0.0, 1.0))));
|
||||
format!("#{ssfa},#{tr}")
|
||||
} else {
|
||||
format!("#{ssfa}")
|
||||
};
|
||||
let sss = o.add_shared(format!("SURFACE_SIDE_STYLE('',({sides}))"));
|
||||
let ssu = o.add_shared(format!("SURFACE_STYLE_USAGE(.BOTH.,#{sss})"));
|
||||
let psa = o.add_shared(format!("PRESENTATION_STYLE_ASSIGNMENT((#{ssu}))"));
|
||||
o.add(format!("STYLED_ITEM('color',(#{psa}),#{item})"))
|
||||
|
||||
Reference in new issue
Block a user