From e19785620503f54f61f6b71e4e12ce4e01bae634 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:24:25 -0400 Subject: [PATCH] STEP import: a refused solid's surface body faces as the file has it SMLib holds both sides of a sheet's faces and gives each back on its surface's natural side, so a sheet whose file uses faces the other way came back renumbered. The reader no longer turns them (that lost the file's side); instead SMLib is given the sheet with those faces turned over exactly on the record, and they are turned back on SMLib's body, each face mapped through. Co-Authored-By: Claude Opus 5.5 --- crates/kernel/src/step_in/parallel.rs | 92 ++++++++++++++++++++++----- crates/kernel/src/step_in/tests.rs | 21 ++++-- crates/step/src/topology.rs | 6 +- 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/crates/kernel/src/step_in/parallel.rs b/crates/kernel/src/step_in/parallel.rs index b29408ade8..19da2586e7 100644 --- a/crates/kernel/src/step_in/parallel.rs +++ b/crates/kernel/src/step_in/parallel.rs @@ -100,28 +100,90 @@ pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result: } /// The part SMLib makes of a solid (or sheet) record the reader made. +/// +/// SMLib holds both sides of a sheet's faces and gives each back facing its surface's natural +/// normal, so a sheet whose file uses some faces the other way is given to SMLib with those faces +/// turned over (exactly, on the record) and they are turned back on SMLib's body: the body faces +/// as the file does, and each of its faces is known to be the record's face it was. fn solid_part(solid: &solidcraft_step::topology::Solid) -> std::result::Result { let failed = |e: KernelError| Failure { read: false, msg: e.to_string().trim_start_matches("the operation failed: ").to_string() }; - let (brep, smlib_order) = crate::native::smlib_record_ordered(&solid.brep).map_err(failed)?; - let (body, body_order) = crate::native::body_from_smlib_ordered(&brep).map_err(failed)?; - // The file face of each body face: SMLib's face i is the record's `smlib_order` face and the - // body's `body_order` face (an empty order is the same one). - let (Some(smlib_order), Some(body_order)) = (smlib_order, body_order) else { - return Err(Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() }); - }; - let at = |order: &[usize], i: usize| if order.is_empty() { Some(i) } else { order.get(i).copied() }; - let faces = brep.counts().map_err(|e| failed(KernelError::Failed(format!("SMLib: {e}"))))?[0]; - let mut face_origin = vec![0u64; body.face_count()]; - for i in 0..faces { - let file_face = at(&smlib_order.faces, i).and_then(|r| solid.face_origin.get(r)).copied(); - match (at(&body_order.faces, i).and_then(|b| face_origin.get_mut(b)), file_face) { - (Some(slot), Some(f)) => *slot = f, - _ => return Err(Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() }), + let r = &solid.brep; + let mut reversed = vec![false; r.faces.len()]; + if solid.sheet { + for s in &r.shells { + if let solidcraft_geom::brep::Shell::Faces { faces, .. } = s { + for f in faces.iter().filter(|f| !f.forward) { + if let Some(slot) = reversed.get_mut(f.face as usize) { + *slot = true; + } + } + } } } + let (body, of_record) = if reversed.iter().any(|r| *r) { + let turn = |body: &Body, by: &dyn Fn(usize) -> bool| -> crate::Result<(Body, Vec)> { + let picked: Vec<(usize, bool)> = (0..body.face_count()).map(|f| (f, by(f))).collect(); + let turned = crate::surfaces::taken(body, body.record(), &picked)?; + let from = kept_faces(&turned).ok_or_else(|| KernelError::Failed("a face turned over from no one face".into()))?; + Ok((turned, from)) + }; + let as_read = Body::from_record(r.clone()).map_err(failed)?; + // `natural`'s face j is the record's `to_record[j]`; SMLib's body's face k is `natural`'s + // `of_natural[k]`; the body turned back's face i is SMLib's `back[i]`. + let (natural, to_record) = turn(&as_read, &|f| reversed.get(f).copied().unwrap_or(false)).map_err(failed)?; + let (smlib, of_natural) = smlib_body(natural.record())?; + let record_of = |k: usize| of_natural.get(k).and_then(|j| to_record.get(*j)).copied(); + let (back, from) = turn(&smlib, &|k| record_of(k).and_then(|f| reversed.get(f)).copied().unwrap_or(false)).map_err(failed)?; + let of_record = from.iter().map(|k| record_of(*k)).collect::>>().ok_or_else(renumbered)?; + (back.without_history(), of_record) + } else { + smlib_body(r)? + }; + let face_origin = of_record.iter().map(|f| solid.face_origin.get(*f).copied()).collect::>>().ok_or_else(renumbered)?; Ok(Part { body, closed: !solid.sheet, file_faces: solid.face_origin.len(), face_origin, refused: None }) } +fn renumbered() -> Failure { + Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() } +} + +/// The body SMLib makes of `record` as given, and the record's face each of its faces is. +fn smlib_body(record: &solidcraft_geom::brep::Brep) -> std::result::Result<(Body, Vec), Failure> { + let failed = |e: KernelError| Failure { read: false, msg: e.to_string().trim_start_matches("the operation failed: ").to_string() }; + let (brep, smlib_order) = crate::native::smlib_record_ordered(record).map_err(failed)?; + let (body, body_order) = crate::native::body_from_smlib_ordered(&brep).map_err(failed)?; + // SMLib's face i is the record's `smlib_order` face and the body's `body_order` face (an + // empty order is the same one). + let (Some(smlib_order), Some(body_order)) = (smlib_order, body_order) else { return Err(renumbered()) }; + let at = |order: &[usize], i: usize| if order.is_empty() { Some(i) } else { order.get(i).copied() }; + let faces = brep.counts().map_err(|e| failed(KernelError::Failed(format!("SMLib: {e}"))))?[0]; + let mut of_record = vec![usize::MAX; body.face_count()]; + for i in 0..faces { + match (at(&body_order.faces, i).and_then(|b| of_record.get_mut(b)), at(&smlib_order.faces, i).filter(|r| *r < record.faces.len())) { + (Some(slot), Some(r)) => *slot = r, + _ => return Err(renumbered()), + } + } + if of_record.contains(&usize::MAX) { + return Err(renumbered()); + } + Ok((body, of_record)) +} + +/// The face of its one operand each face of `body` is, from its history. +fn kept_faces(body: &Body) -> Option> { + let h = body.history()?; + let from: Vec = h + .faces + .iter() + .map(|f| match f.origins.as_slice() { + [crate::history::FaceOrigin::Face { operand: 0, face }] => Some(*face), + _ => None, + }) + .collect::>()?; + (from.len() == body.face_count()).then_some(from) +} + /// Most reads of a refused solid without faces, each leaving out the face the refusal before names. const MAX_LEFT_OUT_ROUNDS: usize = 8; diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index d3c4688a53..6deee12752 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -2164,8 +2164,8 @@ fn a_patch_side_shrunk_within_its_vertexs_tolerance_is_a_pole_of_the_loop() { assert!(rel(m.area, area) < 1e-6, "{} vs {area}", m.area); } -/// A can of radius 5 and height 10: its bottom, its wall, and its lid on the plane `lid_z` -/// (10 where the lid's rim lies on it). +/// A can of radius 5 and height 10: its bottom (on a plane facing up, the face against it), its +/// wall, and its lid on the plane `lid_z` (10 where the lid's rim lies on it). fn can_with_its_lid_at(lid_z: f64) -> String { format!( "{HEAD} @@ -2182,8 +2182,8 @@ fn can_with_its_lid_at(lid_z: f64) -> String { #11=ORIENTED_EDGE('',*,*,#10,.F.); #12=EDGE_LOOP('',(#11)); #13=FACE_OUTER_BOUND('',#12,.T.); -#14=PLANE('',#6); -#15=ADVANCED_FACE('Bottom',(#13),#14,.T.); +#14=PLANE('',#5); +#15=ADVANCED_FACE('Bottom',(#13),#14,.F.); #16=ORIENTED_EDGE('',*,*,#10,.T.); #17=EDGE_LOOP('',(#16)); #18=FACE_BOUND('',#17,.T.); @@ -2234,6 +2234,19 @@ fn a_refused_solid_comes_in_as_a_surface_body_of_the_faces_that_read() { assert!(why.contains("face #45") && why.contains("off the face's surface"), "{why}"); assert_eq!((can.file_faces, can.body.face_count()), (3, 2)); assert_eq!(can.face_origin, [15, 20]); + // The bottom faces down as the file has it, against its plane's natural normal (SMLib gives + // a sheet's faces back on their natural side; the import turns it back). + let r = can.body.record(); + let bottom = can.face_origin.iter().position(|f| *f == 15).unwrap(); + let uses: Vec = r + .shells + .iter() + .flat_map(|s| match s { + solidcraft_geom::brep::Shell::Faces { faces, .. } => faces.iter().filter(|f| f.face as usize == bottom).map(|f| f.forward).collect(), + _ => Vec::new(), + }) + .collect(); + assert_eq!(uses, [false], "the bottom's use"); // The report keeps the refusal and names the face left out; no "open body" warning on top. assert!(imp.warnings.iter().any(|w| w.contains("'Can': imported as surfaces: ") && w.contains(why)), "{:?}", imp.warnings); assert!(imp.warnings.iter().any(|w| w.contains("'Can': left out: ") && w.contains("face #45")), "{:?}", imp.warnings); diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index 4ca1751e88..5ca4b5d35b 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -966,13 +966,9 @@ impl<'b, 'a> Builder<'b, 'a> { if flipped > 0 { notes.push(format!("{flipped} faces turned over: their loops disagree with their same_sense")); } - // What is left of a solid with faces left out is a sheet whose faces face their surfaces' - // natural normals: SMLib holds both sides of a sheet's faces and gives each back on that - // side, so the record is the one SMLib's body of it numbers alike. - let open = kind == Kind::Open; let shells: Vec> = shells .into_iter() - .map(|s| s.into_iter().map(|(f, fwd)| (f, open || if turned.get(f as usize).copied().unwrap_or(false) { !fwd } else { fwd })).collect()) + .map(|s| s.into_iter().map(|(f, fwd)| (f, if turned.get(f as usize).copied().unwrap_or(false) { !fwd } else { fwd })).collect()) .collect(); // Regions: which shells are closed (every edge used at least twice among its faces).