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 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 08:24:25 -04:00
committed by Irakli Kourtis
parent 4cb0d6ee96
commit e197856205
3 changed files with 95 additions and 24 deletions
+77 -15
View File
@@ -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<Part, 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(&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<usize>)> {
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::<Option<Vec<usize>>>().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::<Option<Vec<u64>>>().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<usize>), 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<Vec<usize>> {
let h = body.history()?;
let from: Vec<usize> = h
.faces
.iter()
.map(|f| match f.origins.as_slice() {
[crate::history::FaceOrigin::Face { operand: 0, face }] => Some(*face),
_ => None,
})
.collect::<Option<_>>()?;
(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;
+17 -4
View File
@@ -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<bool> = 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);
+1 -5
View File
@@ -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<Vec<(u32, bool)>> = 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).