diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index 3485b6cab0..ebc8df0085 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -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" => { diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index 13497ab01b..3481fa9ef0 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -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); diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index 327fb38855..e89ef795bb 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -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 = 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 = 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, 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> { - 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, Vec) { + 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 { + 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, Kind)> { +fn item_shells(ex: &Exchange, item: u64) -> R<(Vec, 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, 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)), } }