fix: write each lump of a body as its own STEP solid (#455)
A body of several pieces (a plate and the islands a cut leaves inside its holes, such as letter counters) was written as one BREP_WITH_VOIDS whose other shells were voids. Export now writes one solid per lump, each with its face colours; a real void stays a void of its lump. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6fde5ff55b
commit
455ca86a6c
2 files changed
+85
-20
No files matched your search
@@ -115,7 +115,9 @@ fn curved_internal_void_and_disconnected_material_keep_analytic_volume() {
|
||||
}
|
||||
let imported = step_import(&step_export(&[&body], "curved nested cavities").unwrap()).unwrap();
|
||||
assert!(imported.warnings.is_empty(), "{:?}", imported.warnings);
|
||||
let imported: Vec<_> = imported.bodies.into_iter().flat_map(|b| b.body.lumps().unwrap()).collect();
|
||||
// The two pieces are written as two solids (each with its own void), so they come back as
|
||||
// two bodies.
|
||||
let imported: Vec<_> = imported.bodies.into_iter().map(|b| b.body).collect();
|
||||
assert_eq!(imported.len(), 2);
|
||||
let imported_meshes: Vec<_> = imported.iter().map(|b| b.tessellate_with(0.01, true).unwrap()).collect();
|
||||
for (p, want) in [(Vec3::ZERO, 0), (Vec3::X * 3.0, 1), (Vec3::X * 7.0, 0), (Vec3::X * 15.0, 1)] {
|
||||
|
||||
@@ -1214,6 +1214,39 @@ fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V
|
||||
Ok((ex, merged, paint))
|
||||
}
|
||||
|
||||
/// The separate pieces of a body for export, each keeping its face colours: a body of several
|
||||
/// lumps (a plate and the islands a cut left inside its holes, such as letter counters) is
|
||||
/// several solids, never one solid whose other pieces are written as voids. A body in one
|
||||
/// piece, or one whose pieces cannot be told apart, is itself.
|
||||
fn export_lumps(b: &Body) -> Vec<Body> {
|
||||
if b.mesh.is_some() || b.solid.boundaries().len() < 2 {
|
||||
return vec![b.clone()];
|
||||
}
|
||||
let lumps = match b.lumps() {
|
||||
Ok(l) if l.len() > 1 => l,
|
||||
_ => return vec![b.clone()],
|
||||
};
|
||||
let index: HashMap<_, usize> = b.solid.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect();
|
||||
lumps
|
||||
.into_iter()
|
||||
.map(|lump| {
|
||||
let paint = b.paint().map(|p| crate::body::Paint {
|
||||
opacity: p.opacity,
|
||||
faces: lump
|
||||
.solid
|
||||
.face_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(k, f)| {
|
||||
let i = index.get(&f.id())?;
|
||||
p.faces.iter().find(|fp| fp.face == *i).map(|fp| crate::body::FacePaint { face: k, ..fp.clone() })
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
lump.with_color(b.color()).with_split_lines(b.split_lines().cloned()).with_paint(paint)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Faces in the exchange's shells (fewer after merging).
|
||||
fn shell_face_count(ex: &p21::Exchange) -> usize {
|
||||
ex.entities
|
||||
@@ -1366,26 +1399,28 @@ 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, 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, opacity));
|
||||
for lump in export_lumps(b.body) {
|
||||
let (ex, merged, face_paint) = brep_exchange(&lump, 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 = lump.paint().map(|p| p.opacity).unwrap_or(1.0);
|
||||
if let Some(c) = b.color {
|
||||
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));
|
||||
}
|
||||
}
|
||||
// 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(",");
|
||||
@@ -1484,6 +1519,34 @@ pub fn step_export_bodies(bodies: &[ExportBody], header: &StepHeader) -> Result<
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn islands_are_solids_of_their_own_not_voids() {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let square = |a: f64, b: f64| Loop2::polygon(&[Vec2::new(a, a), Vec2::new(b, a), Vec2::new(b, b), Vec2::new(a, b)]);
|
||||
// A plate with a square hole, and an island inside the hole (a letter's counter).
|
||||
let plate = Region2 { outer: square(0.0, 30.0), holes: vec![square(10.0, 20.0).reversed()] };
|
||||
let island = Region2 { outer: square(12.0, 18.0), holes: vec![] };
|
||||
let body = crate::extrude_planar_regions(&Plane::XY, &[plate, island], 0.0, 2.0).unwrap();
|
||||
assert_eq!(body.shell_count(), 2);
|
||||
let text = crate::step_export(&[&body], "test").unwrap();
|
||||
assert_eq!(text.matches("MANIFOLD_SOLID_BREP(").count(), 2, "{text}");
|
||||
assert_eq!(text.matches("BREP_WITH_VOIDS").count(), 0);
|
||||
let imp = crate::step_import(&text).unwrap();
|
||||
let v: f64 = imp.bodies.iter().map(|b| crate::measure(&b.body).unwrap().volume).sum();
|
||||
assert!((v - (800.0 + 36.0) * 2.0).abs() < 1e-6, "{v}");
|
||||
// A real void stays a void.
|
||||
let hollow = crate::boolean(
|
||||
&crate::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap(),
|
||||
&crate::box_solid(Vec3::new(3.0, 3.0, 3.0), Vec3::new(7.0, 7.0, 7.0)).unwrap(),
|
||||
crate::BoolOp::Cut,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(hollow.shell_count(), 2);
|
||||
let text = crate::step_export(&[&hollow], "test").unwrap();
|
||||
assert_eq!(text.matches("BREP_WITH_VOIDS").count(), 1, "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_and_strings() {
|
||||
assert_eq!(real(1.0), "1.0");
|
||||
|
||||
Reference in new issue
Block a user