STEP import: a surface model's closed shell beside a sheet is a solid of its own

A SHELL_BASED_SURFACE_MODEL with both closed and open shells (Poppy Humanoid's
thighs: the part and a two-face sheet on it) came in as one body, open for the
sheet's edges. Each closed shell is now read as an item of its own, a solid in
the model's colour, and the model's open shells as a surface body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
jyviko
2026-10-28 15:14:34 -04:00
committed by Irakli Kourtis
parent 61a1f8bf26
commit cd4f725729
3 changed files with 105 additions and 11 deletions
+16 -4
View File
@@ -624,10 +624,22 @@ impl Reader {
self.warn(format!("more than {MAX_BODIES} bodies: the rest are left out"));
return;
}
// The node lists the planned solid; it becomes a body index once converted.
out.push(self.plan.len());
self.plan.push(Planned { item, units: u, batch: id });
*self.uses.entry(item).or_default() += 1;
// The node lists the planned solids (a surface model's closed shells
// beside its sheet are solids of their own, in the model's colour); each
// becomes a body index once converted.
for piece in solidcraft_step::topology::items(&self.ex, item) {
if piece != item {
if let Some(c) = self.colors.get(&item).copied() {
self.colors.entry(piece).or_insert(c);
}
if let Some(o) = self.opacity.get(&item).copied() {
self.opacity.entry(piece).or_insert(o);
}
}
out.push(self.plan.len());
self.plan.push(Planned { item: piece, units: u, batch: id });
*self.uses.entry(piece).or_default() += 1;
}
}
}
"MAPPED_ITEM" => {
+40
View File
@@ -570,6 +570,46 @@ fn product(pd: u64, name: &str) -> String {
)
}
/// A surface model of a closed shell and an open one beside it (Poppy Humanoid's thighs: the
/// part, and a sheet of two faces on it) comes in as two bodies: the closed shell a solid of its
/// own, the open one a surface body, each in the model's colour. It once came in as one body,
/// open for its sheet's edges.
#[test]
fn a_surface_models_closed_shell_beside_a_sheet_is_a_solid_of_its_own() {
let mut t = String::from(HEAD);
t += &cube_entities();
// A unit square sheet at z = 5.
let corners = [(0, 0), (1, 0), (1, 1), (0, 1)];
for (i, (x, y)) in corners.iter().enumerate() {
t += &format!("#{}=CARTESIAN_POINT('',({x}.,{y}.,5.));\n#{}=VERTEX_POINT('',#{});\n", 800 + i, 810 + i, 800 + i);
}
let mut oes = Vec::new();
for i in 0..4 {
let (a, b) = (corners[i], corners[(i + 1) % 4]);
t += &format!("#{}=DIRECTION('',({}.,{}.,0.));\n#{}=VECTOR('',#{},1.);\n", 820 + 2 * i, b.0 - a.0, b.1 - a.1, 821 + 2 * i, 820 + 2 * i);
t += &format!("#{}=LINE('',#{},#{});\n", 830 + i, 800 + i, 821 + 2 * i);
t += &format!("#{}=EDGE_CURVE('',#{},#{},#{},.T.);\n", 840 + i, 810 + i, 810 + (i + 1) % 4, 830 + i);
t += &format!("#{}=ORIENTED_EDGE('',*,*,#{},.T.);\n", 850 + i, 840 + i);
oes.push(format!("#{}", 850 + i));
}
t += &format!("#860=EDGE_LOOP('',({}));\n#861=FACE_OUTER_BOUND('',#860,.T.);\n", oes.join(","));
t += "#862=CARTESIAN_POINT('',(0.,0.,5.));\n#863=DIRECTION('',(0.,0.,1.));\n#864=AXIS2_PLACEMENT_3D('',#862,#863,$);\n#865=PLANE('',#864);\n";
t += "#866=ADVANCED_FACE('',(#861),#865,.T.);\n#130=OPEN_SHELL('',(#866));\n#110=SHELL_BASED_SURFACE_MODEL('Thigh',(#100,#130));\n";
t += &context(20, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))");
t += "#60=MANIFOLD_SURFACE_SHAPE_REPRESENTATION('',(#110),#20);\n";
t += "#70=STYLED_ITEM('',(#71),#110);\n#71=PRESENTATION_STYLE_ASSIGNMENT((#72));\n#72=SURFACE_STYLE_USAGE(.BOTH.,#73);\n#73=SURFACE_SIDE_STYLE('',(#74));\n#74=SURFACE_STYLE_FILL_AREA(#75);\n#75=FILL_AREA_STYLE('',(#76));\n#76=FILL_AREA_STYLE_COLOUR('',#77);\n#77=COLOUR_RGB('',1.,0.5,0.);\n";
t += TAIL;
let imp = step_import(&t).unwrap();
assert_eq!(imp.bodies.len(), 2, "{:?}", imp.warnings);
let (solid, sheet) = (&imp.bodies[0], &imp.bodies[1]);
assert!(solid.closed && solid.refused.is_none() && !solid.body.is_surface(), "{:?}", imp.warnings);
assert_eq!((solid.source_item, solid.file_faces), (100, 6));
assert!(rel(measure(&solid.body).unwrap().volume, 1.0) < 1e-9);
assert!(!sheet.closed && sheet.body.is_surface() && sheet.body.face_count() == 1);
assert_eq!((sheet.source_item, sheet.name.as_str()), (110, "Thigh"));
assert!(imp.bodies.iter().all(|b| b.color == Some([1.0, 0.5, 0.0])));
}
#[test]
fn hand_written_part_in_inches_with_colour() {
let mut t = String::from(HEAD);
+49 -7
View File
@@ -248,9 +248,24 @@ pub fn read_solids_with(ex: &Exchange, trims: &dyn Trims) -> Solids {
}
fn read_solids_in(ex: &Exchange, trims: Option<&dyn Trims>) -> Solids {
let mut items: Vec<u64> = ex.entities.iter().filter(|(_, e)| SOLID_ITEMS.contains(&e.name())).map(|(id, _)| *id).collect();
items.sort_unstable();
let index = Index::new(ex);
let mut solids: Vec<u64> = ex.entities.iter().filter(|(_, e)| SOLID_ITEMS.contains(&e.name())).map(|(id, _)| *id).collect();
solids.sort_unstable();
let mut index = Index::new(ex);
// A surface model's closed shells read on their own sit where the model does.
let mut items = Vec::with_capacity(solids.len());
for solid in solids {
for piece in self::items(ex, solid) {
if piece != solid {
if let Some(c) = index.context.get(&solid).copied() {
index.context.insert(piece, c);
}
if let Some(p) = index.product.get(&solid).cloned() {
index.product.insert(piece, p);
}
}
items.push(piece);
}
}
let mut out = Solids::default();
let mut budget = MAX_FILE_FACES;
let mut readers: HashMap<u64, (Reader<'_>, units::Context)> = HashMap::new();
@@ -334,7 +349,7 @@ fn read_with<'a>(
let mut b = Builder::new(ex, reader, tolerance(&context), budget, trims);
// The file's faces, edges, vertices and their geometry read into the builder.
let topology = solidcraft_profile::span("import", "step.topology");
let (ids, kind) = item_shells(e)?;
let (ids, kind) = item_shells(ex, item)?;
let mut shells = Vec::with_capacity(ids.len());
let mut skipped = false;
for s in ids {
@@ -362,7 +377,7 @@ pub struct FaceRead {
/// as [`read_solid_with`] reads it, with its own edges and vertices (so its tolerances cover its
/// own gaps only); a face that cannot be read on its own says why and the others are still read.
pub fn read_faces_with(ex: &Exchange, item: u64, context: units::Context, trims: &dyn Trims) -> R<Vec<FaceRead>> {
let (shells, _) = item_shells(entity(ex, item)?)?;
let (shells, _) = item_shells(ex, item)?;
let mut reader = Reader::new(ex, context.units);
let mut budget = MAX_FILE_FACES;
let mut out = Vec::new();
@@ -384,8 +399,31 @@ pub fn read_faces_with(ex: &Exchange, item: u64, context: units::Context, trims:
Ok(out)
}
/// A surface model's shells the file calls closed and the others, in its order.
fn model_shells(ex: &Exchange, shells: &[u64]) -> (Vec<u64>, Vec<u64>) {
shells.iter().partition(|s| ex.get(**s).is_some_and(|e| matches!(e.name(), "CLOSED_SHELL" | "ORIENTED_CLOSED_SHELL")))
}
/// The items a solid item is read as, each one body: the item itself, but for a surface model
/// with both closed and open shells (a part with a sheet beside it) each closed shell on its own
/// (a solid), then the model (its open shells, a sheet).
pub fn items(ex: &Exchange, item: u64) -> Vec<u64> {
let shells = match ex.get(item) {
Some(e) if e.name() == "SHELL_BASED_SURFACE_MODEL" => {
e.params().get(1).and_then(Param::as_list).unwrap_or(&[]).iter().filter_map(Param::as_ref_id).collect()
}
_ => Vec::new(),
};
let (closed, open) = model_shells(ex, &shells);
if closed.is_empty() || open.is_empty() {
return vec![item];
}
closed.into_iter().chain([item]).collect()
}
/// The shells of a solid item (the outer shell first, then its voids), and what kind it is.
fn item_shells(e: &Entity) -> R<(Vec<u64>, Kind)> {
fn item_shells(ex: &Exchange, item: u64) -> R<(Vec<u64>, Kind)> {
let e = entity(ex, item)?;
let p = e.params();
match e.name() {
"MANIFOLD_SOLID_BREP" => Ok((vec![rid(p.get(1), "outer shell")?], Kind::Solid)),
@@ -403,8 +441,12 @@ fn item_shells(e: &Entity) -> R<(Vec<u64>, Kind)> {
if ids.is_empty() || ids.len() > brep::MAX_SHELLS {
return Err(format!("a surface model with {} shells", ids.len()));
}
Ok((ids, Kind::Model))
// Its closed shells, where it has open ones too, are items of their own ([`items`]).
let (closed, open) = model_shells(ex, &ids);
Ok((if closed.is_empty() || open.is_empty() { ids } else { open }, Kind::Model))
}
// A closed shell of a surface model that has open shells too ([`items`]).
"CLOSED_SHELL" | "ORIENTED_CLOSED_SHELL" => Ok((vec![item], Kind::Model)),
n => Err(unsupported(n)),
}
}