mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
STEP import: a refused solid comes in as a surface body of its exact faces
A solid the reader or SMLib refuses was shown as a mesh of its faces (#8223): exactness lost, cracks between faces, and no modelling. It now comes in as an open surface body of the faces that read, sharing their edges as the file has them; each face left out is named. - The reader reads a solid with a set of faces left out (read_solid_without): a sheet whose faces face their surfaces' natural normals, as SMLib gives a sheet's faces back, so SMLib's body numbers the faces as the record does. - Tier 1: the faces that cannot be read alone and the face each refusal names (named_face) are left out, up to 8 reads. Tier 2, when a refusal names no face of the solid: the one-face bodies stitched by SMLib. Tier 3, when SMLib cannot stitch them: the mesh of them, as before. - Report: "imported as surfaces: <why>" and "left out: face #..."; the ImportedBody field shown_as_mesh is now refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,10 +55,10 @@ pub struct ImportedBody {
|
||||
pub closed: bool,
|
||||
/// Faces read from the file (each body face is one of them: `face_origin`).
|
||||
pub file_faces: usize,
|
||||
/// No B-rep could be made of the solid: `body` is a mesh body of the faces that could be
|
||||
/// read and meshed on their own, shown so the solid is not lost, and this is why the solid
|
||||
/// was refused (the import's warnings say so too).
|
||||
pub shown_as_mesh: Option<String>,
|
||||
/// No B-rep could be made of the whole solid, and this is why (the import's warnings say so
|
||||
/// too, naming each face left out): `body` is an open surface body of the solid's exact faces
|
||||
/// but those, or a mesh body of them where no surface body could be made of them.
|
||||
pub refused: Option<String>,
|
||||
}
|
||||
|
||||
/// One product occurrence in the file's assembly tree.
|
||||
@@ -720,7 +720,8 @@ impl Reader {
|
||||
if let Some(failed) = read {
|
||||
let label = self.label(item);
|
||||
if let Some(why) = &part.refused {
|
||||
self.warn(format!("{label}: shown as a mesh: {why}"));
|
||||
let kind = if part.body.is_mesh() { "shown as a mesh" } else { "imported as surfaces" };
|
||||
self.warn(format!("{label}: {kind}: {why}"));
|
||||
}
|
||||
for f in &failed {
|
||||
self.warn(format!("{label}: {f}"));
|
||||
@@ -780,7 +781,7 @@ impl Reader {
|
||||
file_faces,
|
||||
source_item: item,
|
||||
face_origin,
|
||||
shown_as_mesh: part.refused.clone(),
|
||||
refused: part.refused.clone(),
|
||||
});
|
||||
}
|
||||
Err(parallel::Failure { read, msg }) => {
|
||||
|
||||
@@ -26,8 +26,9 @@ pub(super) struct Part {
|
||||
pub file_faces: usize,
|
||||
/// The file face of each kernel face.
|
||||
pub face_origin: Vec<u64>,
|
||||
/// No B-rep could be made of the solid: the body is a mesh of the faces that could be read
|
||||
/// on their own ([`mesh_part`]), and this is why the solid was refused.
|
||||
/// No B-rep could be made of the whole solid, and this is why: the body is a surface body of
|
||||
/// the faces that read ([`surface_part`]), or, where none could be made of them, a mesh of
|
||||
/// them ([`mesh_part`]).
|
||||
pub refused: Option<String>,
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ fn prepare(ex: &Exchange, job: &Job) -> Prepared {
|
||||
let (part, read) = match &job.source {
|
||||
Source::Oriented(part) => (part.clone(), None),
|
||||
Source::Item { item, units } => {
|
||||
let (part, read) = smlib_part(ex, *item, *units).or_else(|refused| mesh_part(ex, *item, *units, refused))?;
|
||||
let (part, read) = smlib_part(ex, *item, *units).or_else(|refused| surface_part(ex, *item, *units, refused))?;
|
||||
(Arc::new(part), Some(read))
|
||||
}
|
||||
};
|
||||
@@ -88,12 +89,18 @@ fn placed(body: &Body, m: &M4) -> crate::Result<Body> {
|
||||
|
||||
/// A solid item read with the STEP reader into its schema 4 record, then into SMLib as given,
|
||||
/// and brought back as a body: the part SMLib made of the file. A solid the reader or SMLib
|
||||
/// refuses fails with the reason ([`mesh_part`] then shows what it can of it).
|
||||
/// refuses fails with the reason ([`surface_part`] then imports what it can of it).
|
||||
pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result::Result<(Part, Vec<String>), Failure> {
|
||||
let solid = solidcraft_profile::scope("import", "step.read_solid", || {
|
||||
solidcraft_step::topology::read_solid_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims)
|
||||
})
|
||||
.map_err(|msg| Failure { read: true, msg })?;
|
||||
let part = solid_part(&solid)?;
|
||||
Ok((part, solid.notes))
|
||||
}
|
||||
|
||||
/// The part SMLib makes of a solid (or sheet) record the reader made.
|
||||
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)?;
|
||||
@@ -112,43 +119,129 @@ pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result:
|
||||
_ => return Err(Failure { read: false, msg: "not supported yet: SMLib renumbered the solid's faces".into() }),
|
||||
}
|
||||
}
|
||||
let part = Part { body, closed: !solid.sheet, file_faces: solid.face_origin.len(), face_origin, refused: None };
|
||||
Ok((part, solid.notes))
|
||||
Ok(Part { body, closed: !solid.sheet, file_faces: solid.face_origin.len(), face_origin, refused: None })
|
||||
}
|
||||
|
||||
/// A solid the reader or SMLib refuses, still shown: each of its faces read on its own
|
||||
/// ([`solidcraft_step::topology::read_faces_with`]) and meshed by the planner as a one-face
|
||||
/// surface body, the faces' triangles together a mesh body. It is no B-rep (modelling
|
||||
/// operations refuse it); `refused` says why. Faces that cannot be read or meshed on their own
|
||||
/// are left out, each named in the notes. With no face to show, the solid stays refused.
|
||||
fn mesh_part(ex: &Exchange, item: u64, units: Units, refused: Failure) -> std::result::Result<(Part, Vec<String>), Failure> {
|
||||
let _span = solidcraft_profile::span("import", "step.refused_mesh");
|
||||
let faces = match solidcraft_step::topology::read_faces_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims) {
|
||||
Ok(faces) => faces,
|
||||
Err(_) => return Err(refused),
|
||||
};
|
||||
let mut notes = Vec::new();
|
||||
let mut bodies = Vec::with_capacity(faces.len());
|
||||
for f in &faces {
|
||||
match f.record.clone().map_err(KernelError::Failed).and_then(Body::from_record) {
|
||||
Ok(b) => bodies.push((f.face, b)),
|
||||
Err(e) => notes.push(format!("not shown: {}", e.to_string().trim_start_matches("the operation failed: "))),
|
||||
/// Most reads of a refused solid without faces, each leaving out the face the refusal before names.
|
||||
const MAX_LEFT_OUT_ROUNDS: usize = 8;
|
||||
|
||||
/// A solid the reader or SMLib refuses, imported as an open surface body of its exact faces, the
|
||||
/// faces that make it refused left out and named in the notes; `refused` says why the solid was.
|
||||
/// Each face is read on its own first ([`solidcraft_step::topology::read_faces_with`]); then:
|
||||
/// 1. the solid is read again without the faces that cannot be read alone and the face the
|
||||
/// refusal names, as one sheet whose faces share their edges as the file has them, and made
|
||||
/// by SMLib; a refusal of that naming another face of the solid leaves it out too (up to
|
||||
/// [`MAX_LEFT_OUT_ROUNDS`] reads). A face is only ever left out where a refusal names it.
|
||||
/// 2. Where a refusal names no face of the solid: the faces that read alone, stitched by SMLib
|
||||
/// into one surface body (their edges sewn at ten times their tolerance).
|
||||
/// 3. Where SMLib cannot stitch them either: a mesh of them ([`mesh_part`]).
|
||||
///
|
||||
/// With no face to import, the solid stays refused.
|
||||
fn surface_part(ex: &Exchange, item: u64, units: Units, refused: Failure) -> std::result::Result<(Part, Vec<String>), Failure> {
|
||||
let _span = solidcraft_profile::span("import", "step.refused_surfaces");
|
||||
let trims = &solidcraft_smlib::pcurves::SmlibTrims;
|
||||
let Ok(faces) = solidcraft_step::topology::read_faces_with(ex, item, units, trims) else { return Err(refused) };
|
||||
let of_solid: std::collections::HashSet<u64> = faces.iter().map(|f| f.face).collect();
|
||||
// The faces left out, each with the refusal that names it, in the order they were found.
|
||||
let mut left_out: Vec<(u64, String)> = faces.iter().filter_map(|f| f.record.as_ref().err().map(|m| (f.face, m.clone()))).collect();
|
||||
let mut why = refused.msg.clone();
|
||||
let mut rounds = 0;
|
||||
loop {
|
||||
// The face the last refusal names, if it is one of the solid's still in, is left out too.
|
||||
let named = solidcraft_step::topology::named_face(&why).filter(|f| of_solid.contains(f) && !left_out.iter().any(|(g, _)| g == f));
|
||||
match named {
|
||||
Some(f) => left_out.push((f, why.clone())),
|
||||
None if rounds > 0 => break,
|
||||
None => {}
|
||||
}
|
||||
if left_out.is_empty() || left_out.len() >= faces.len() || rounds >= MAX_LEFT_OUT_ROUNDS {
|
||||
break;
|
||||
}
|
||||
rounds += 1;
|
||||
let skip: std::collections::HashSet<u64> = left_out.iter().map(|(f, _)| *f).collect();
|
||||
let read =
|
||||
solidcraft_profile::scope("import", "step.read_without", || solidcraft_step::topology::read_solid_without(ex, item, units, trims, &skip));
|
||||
match read.map_err(|msg| Failure { read: true, msg }).and_then(|solid| Ok((solid_part(&solid)?, solid.notes))) {
|
||||
Ok((mut part, mut notes)) => {
|
||||
part.file_faces = faces.len();
|
||||
part.refused = Some(refused.msg);
|
||||
notes.extend(left_out.iter().map(|(_, m)| format!("left out: {m}")));
|
||||
return Ok((part, notes));
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!(target: "step", "item #{item}: read without {} faces, refused: {}", skip.len(), e.msg);
|
||||
why = e.msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The faces that read alone, each a one-face surface body.
|
||||
let mut notes = Vec::new();
|
||||
let mut pieces = Vec::with_capacity(faces.len());
|
||||
for f in &faces {
|
||||
match f.record.clone().map_err(KernelError::Failed).and_then(Body::from_record) {
|
||||
Ok(b) => pieces.push((f.face, b)),
|
||||
Err(e) => notes.push(format!("left out: {}", e.to_string().trim_start_matches("the operation failed: "))),
|
||||
}
|
||||
}
|
||||
match stitched_part(&pieces, faces.len(), &refused.msg) {
|
||||
Ok(part) => Ok((part, notes)),
|
||||
Err(e) => {
|
||||
log::debug!(target: "step", "item #{item}: its faces not stitched ({e}); meshed instead");
|
||||
mesh_part(&pieces, faces.len(), notes, refused)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-face surface bodies `pieces` (each with its file face) stitched by SMLib into one
|
||||
/// surface body, where their edges coincide within ten times the pieces' tolerance.
|
||||
fn stitched_part(pieces: &[(u64, Body)], file_faces: usize, refused: &str) -> crate::Result<Part> {
|
||||
if pieces.is_empty() {
|
||||
return Err(KernelError::Failed("no face read".into()));
|
||||
}
|
||||
let tol = 10.0 * pieces.iter().map(|(_, b)| b.record().tolerance).fold(0.0, f64::max);
|
||||
let bodies: Vec<&Body> = pieces.iter().map(|(_, b)| b).collect();
|
||||
let body = crate::surfaces::stitch(&bodies, tol)?;
|
||||
// Each stitched face is the one face of the piece it was sewn from.
|
||||
let history = body.history().ok_or_else(|| KernelError::Failed("the stitch kept no history".into()))?;
|
||||
let face_origin = history
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| match f.origins.as_slice() {
|
||||
[crate::history::FaceOrigin::Face { operand, .. }] => pieces.get(*operand).map(|(face, _)| *face),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<u64>>>()
|
||||
.filter(|o| o.len() == body.face_count())
|
||||
.ok_or_else(|| KernelError::Failed("a stitched face from no one face".into()))?;
|
||||
let closed = !body.is_surface();
|
||||
Ok(Part { body, closed, file_faces, face_origin, refused: Some(refused.to_string()) })
|
||||
}
|
||||
|
||||
/// The one-face surface bodies `pieces` meshed by the planner, their triangles together a mesh
|
||||
/// body: no B-rep (modelling operations refuse it), for a solid whose faces SMLib could not
|
||||
/// stitch either. Faces that cannot be meshed are left out, each named in the notes; with no
|
||||
/// face to show, the solid stays refused.
|
||||
fn mesh_part(
|
||||
pieces: &[(u64, Body)],
|
||||
file_faces: usize,
|
||||
mut notes: Vec<String>,
|
||||
refused: Failure,
|
||||
) -> std::result::Result<(Part, Vec<String>), Failure> {
|
||||
let _span = solidcraft_profile::span("import", "step.refused_mesh");
|
||||
// Every face at the display tolerance of the whole solid.
|
||||
let bounds: solidcraft_geom::Aabb3 = bodies.iter().flat_map(|(_, b)| b.record().vertices.iter().map(|v| crate::body::v3(v.point))).collect();
|
||||
let bounds: solidcraft_geom::Aabb3 = pieces.iter().flat_map(|(_, b)| b.record().vertices.iter().map(|v| crate::body::v3(v.point))).collect();
|
||||
let size = if bounds.is_empty() { 0.0 } else { bounds.diagonal() };
|
||||
let tol = crate::MeshTolerance::display_of(if size.is_finite() { size } else { 0.0 });
|
||||
let (mut positions, mut triangles) = (Vec::new(), Vec::new());
|
||||
for (face, b) in &bodies {
|
||||
for (face, b) in pieces {
|
||||
match b.mesh(&tol) {
|
||||
Ok(m) if !m.triangles.is_empty() => {
|
||||
let Ok(base) = u32::try_from(positions.len()) else { break };
|
||||
positions.extend_from_slice(&m.positions);
|
||||
triangles.extend(m.triangles.iter().map(|t| t.map(|i| i.saturating_add(base))));
|
||||
}
|
||||
Ok(_) => notes.push(format!("not shown: face #{face}: no triangles")),
|
||||
Err(e) => notes.push(format!("not shown: face #{face}: {}", e.to_string().trim_start_matches("the operation failed: "))),
|
||||
Ok(_) => notes.push(format!("left out: face #{face}: no triangles")),
|
||||
Err(e) => notes.push(format!("left out: face #{face}: {}", e.to_string().trim_start_matches("the operation failed: "))),
|
||||
}
|
||||
}
|
||||
if triangles.is_empty() {
|
||||
@@ -157,7 +250,7 @@ fn mesh_part(ex: &Exchange, item: u64, units: Units, refused: Failure) -> std::r
|
||||
let body = crate::meshbody::mesh_body(&positions, &triangles)
|
||||
.map_err(|e| Failure { msg: format!("{} (its mesh: {e})", refused.msg), read: refused.read })?;
|
||||
let closed = body.is_closed_mesh();
|
||||
Ok((Part { body, closed, file_faces: faces.len(), face_origin: Vec::new(), refused: Some(refused.msg) }, notes))
|
||||
Ok((Part { body, closed, file_faces, face_origin: Vec::new(), refused: Some(refused.msg) }, notes))
|
||||
}
|
||||
|
||||
/// Prepare `jobs`, the results in the jobs' order. Solids still to read and orient (the slow
|
||||
|
||||
@@ -2164,14 +2164,10 @@ 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 solid no B-rep can be made of is still shown: a cup whose wall, a cylinder face, has one
|
||||
/// bound (the rim it shares with its bottom) and no far one is refused, as Poppy Humanoid's
|
||||
/// stock head is. Each face is read and meshed on its own instead: the bottom shows, as a mesh
|
||||
/// body that says why the solid was refused; the wall, which cannot be read on its own either,
|
||||
/// is left out and named. Modelling operations refuse the mesh body.
|
||||
#[test]
|
||||
fn a_refused_solid_is_shown_as_a_mesh_of_the_faces_that_read() {
|
||||
let text = format!(
|
||||
/// 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).
|
||||
fn can_with_its_lid_at(lid_z: f64) -> String {
|
||||
format!(
|
||||
"{HEAD}
|
||||
#1=CARTESIAN_POINT('',(0.,0.,0.));
|
||||
#2=DIRECTION('',(0.,0.,1.));
|
||||
@@ -2190,34 +2186,63 @@ fn a_refused_solid_is_shown_as_a_mesh_of_the_faces_that_read() {
|
||||
#15=ADVANCED_FACE('Bottom',(#13),#14,.T.);
|
||||
#16=ORIENTED_EDGE('',*,*,#10,.T.);
|
||||
#17=EDGE_LOOP('',(#16));
|
||||
#18=FACE_OUTER_BOUND('',#17,.T.);
|
||||
#18=FACE_BOUND('',#17,.T.);
|
||||
#19=CYLINDRICAL_SURFACE('',#5,5.);
|
||||
#20=ADVANCED_FACE('Wall',(#18),#19,.T.);
|
||||
#21=CLOSED_SHELL('',(#15,#20));
|
||||
#22=MANIFOLD_SOLID_BREP('Cup',#21);
|
||||
#20=ADVANCED_FACE('Wall',(#18,#37),#19,.T.);
|
||||
#30=CARTESIAN_POINT('',(0.,0.,10.));
|
||||
#31=AXIS2_PLACEMENT_3D('',#30,#2,#3);
|
||||
#32=CARTESIAN_POINT('',(5.,0.,10.));
|
||||
#33=VERTEX_POINT('',#32);
|
||||
#34=CIRCLE('',#31,5.);
|
||||
#35=EDGE_CURVE('',#33,#33,#34,.T.);
|
||||
#36=ORIENTED_EDGE('',*,*,#35,.F.);
|
||||
#37=FACE_BOUND('',#38,.T.);
|
||||
#38=EDGE_LOOP('',(#36));
|
||||
#39=ORIENTED_EDGE('',*,*,#35,.T.);
|
||||
#40=EDGE_LOOP('',(#39));
|
||||
#41=FACE_OUTER_BOUND('',#40,.T.);
|
||||
#42=CARTESIAN_POINT('',(0.,0.,{lid_z:?}));
|
||||
#43=AXIS2_PLACEMENT_3D('',#42,#2,#3);
|
||||
#44=PLANE('',#43);
|
||||
#45=ADVANCED_FACE('Lid',(#41),#44,.T.);
|
||||
#21=CLOSED_SHELL('',(#15,#20,#45));
|
||||
#22=MANIFOLD_SOLID_BREP('Can',#21);
|
||||
#23=SHAPE_REPRESENTATION('',(#22),$);
|
||||
{TAIL}"
|
||||
);
|
||||
let imp = step_import(&text).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let cup = &imp.bodies[0];
|
||||
assert!(cup.body.is_mesh(), "{:?}", imp.warnings);
|
||||
let why = cup.shown_as_mesh.as_deref().unwrap_or_default();
|
||||
assert!(why.contains("face #20: a face around an open surface of revolution with one bound"), "{why}");
|
||||
assert_eq!(cup.file_faces, 2);
|
||||
// The report keeps the refusal and names the face left out.
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("'Cup': shown as a mesh: ") && w.contains(why)), "{:?}", imp.warnings);
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("'Cup': not shown: face #20: ")), "{:?}", imp.warnings);
|
||||
// The bottom, a 5 mm disk facing down.
|
||||
let m = measure(&cup.body).unwrap();
|
||||
assert!(rel(m.area, std::f64::consts::PI * 25.0) < 1e-2, "area {}", m.area);
|
||||
let (positions, triangles) = cup.body.triangle_mesh().unwrap();
|
||||
assert!(positions.iter().all(|p| p.z.abs() < 1e-9 && p.x.hypot(p.y) <= 5.0 + 1e-6), "the bottom only");
|
||||
let down = triangles
|
||||
.iter()
|
||||
.map(|t| (positions[t[1] as usize] - positions[t[0] as usize]).cross(positions[t[2] as usize] - positions[t[0] as usize]).z)
|
||||
.sum::<f64>();
|
||||
assert!(down < 0.0, "the bottom faces out of the cup: {down}");
|
||||
let refused = fillet(&cup.body, &[Vec3::new(5.0, 0.0, 0.0)], 1.0).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(refused.contains("needs a solid body"), "{refused}");
|
||||
)
|
||||
}
|
||||
|
||||
/// A solid no B-rep can be made of comes in as an open surface body of its exact faces, the
|
||||
/// faces that make it refused left out and named, as Poppy Humanoid's stock head (a face around
|
||||
/// an open surface of revolution with one bound) and Congé19 (an edge further off its face than
|
||||
/// any tolerance SMLib takes) do. A can whose lid lies 1.5 mm off its rim: the lid is left out,
|
||||
/// the bottom and the wall come in sharing their rim, and the existing surface tools close the
|
||||
/// can again (the rim patched, the patch stitched on): a solid with the can's volume.
|
||||
#[test]
|
||||
fn a_refused_solid_comes_in_as_a_surface_body_of_the_faces_that_read() {
|
||||
let pi = std::f64::consts::PI;
|
||||
let whole = step_import(&can_with_its_lid_at(10.0)).unwrap();
|
||||
assert_eq!(whole.bodies.len(), 1, "{:?}", whole.warnings);
|
||||
assert!(whole.bodies[0].closed && whole.bodies[0].refused.is_none(), "{:?}", whole.warnings);
|
||||
assert!(rel(measure(&whole.bodies[0].body).unwrap().volume, 250.0 * pi) < 1e-6);
|
||||
|
||||
let imp = step_import(&can_with_its_lid_at(11.5)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let can = &imp.bodies[0];
|
||||
assert!(!can.body.is_mesh() && can.body.is_surface() && !can.closed, "{:?}", imp.warnings);
|
||||
let why = can.refused.as_deref().unwrap_or_default();
|
||||
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 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);
|
||||
assert!(!imp.warnings.iter().any(|w| w.contains("open body")), "{:?}", imp.warnings);
|
||||
// Exact: the bottom's disk and the wall's 10 mm of cylinder.
|
||||
assert!(rel(measure(&can.body).unwrap().area, 125.0 * pi) < 1e-6, "{}", measure(&can.body).unwrap().area);
|
||||
// Closed again with the existing tools.
|
||||
let lid = crate::patch_edges(&can.body, &[Vec3::new(0.0, 5.0, 10.0)]).unwrap();
|
||||
let back = crate::stitch(&[&can.body, &lid], 0.0).unwrap();
|
||||
assert!(!back.is_surface(), "closed");
|
||||
assert!(rel(measure(&back).unwrap().volume, 250.0 * pi) < 1e-6, "{}", measure(&back).unwrap().volume);
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ fn read_solids_in(ex: &Exchange, trims: Option<&dyn Trims>) -> Solids {
|
||||
(Reader::new(ex, c.units), c)
|
||||
});
|
||||
let context = *context;
|
||||
match read_with(ex, reader, item, context, &mut budget, trims) {
|
||||
match read_with(ex, reader, item, context, &mut budget, trims, &HashSet::new()) {
|
||||
Ok(mut s) => {
|
||||
s.product = index.product.get(&item).cloned();
|
||||
if s.name.is_empty()
|
||||
@@ -286,14 +286,33 @@ fn read_solids_in(ex: &Exchange, trims: Option<&dyn Trims>) -> Solids {
|
||||
pub fn read_solid(ex: &Exchange, item: u64, context: units::Context) -> R<Solid> {
|
||||
let mut reader = Reader::new(ex, context.units);
|
||||
let mut budget = MAX_FILE_FACES;
|
||||
read_with(ex, &mut reader, item, context, &mut budget, None)
|
||||
read_with(ex, &mut reader, item, context, &mut budget, None, &HashSet::new())
|
||||
}
|
||||
|
||||
/// [`read_solid`], with `trims` making the pcurves of uses that have no closed form.
|
||||
pub fn read_solid_with(ex: &Exchange, item: u64, context: units::Context, trims: &dyn Trims) -> R<Solid> {
|
||||
let mut reader = Reader::new(ex, context.units);
|
||||
let mut budget = MAX_FILE_FACES;
|
||||
read_with(ex, &mut reader, item, context, &mut budget, Some(trims))
|
||||
read_with(ex, &mut reader, item, context, &mut budget, Some(trims), &HashSet::new())
|
||||
}
|
||||
|
||||
/// [`read_solid_with`] with the faces `left_out` (file face entities: the ADVANCED_FACE or
|
||||
/// FACE_SURFACE, or the ORIENTED_FACE a shell lists) left out of their shells: what is left of a
|
||||
/// solid that cannot be read or made whole, its faces still sharing their edges. With any face
|
||||
/// left out it is a surface model of open shells (`sheet`), none of them bounding a region.
|
||||
pub fn read_solid_without(ex: &Exchange, item: u64, context: units::Context, trims: &dyn Trims, left_out: &HashSet<u64>) -> R<Solid> {
|
||||
let mut reader = Reader::new(ex, context.units);
|
||||
let mut budget = MAX_FILE_FACES;
|
||||
read_with(ex, &mut reader, item, context, &mut budget, Some(trims), left_out)
|
||||
}
|
||||
|
||||
/// The file face a refusal of the reader names first (its "face #id"), if it names one.
|
||||
pub fn named_face(message: &str) -> Option<u64> {
|
||||
message.match_indices("face #").find_map(|(at, m)| {
|
||||
let rest = message.get(at + m.len()..)?;
|
||||
let digits = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
|
||||
rest.get(..digits)?.parse().ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// The base tolerance of a context.
|
||||
@@ -308,6 +327,7 @@ fn read_with<'a>(
|
||||
context: units::Context,
|
||||
budget: &mut usize,
|
||||
trims: Option<&dyn Trims>,
|
||||
left_out: &HashSet<u64>,
|
||||
) -> R<Solid> {
|
||||
let e = entity(ex, item)?;
|
||||
let name = e.params().first().and_then(Param::as_str).unwrap_or("").to_string();
|
||||
@@ -316,11 +336,14 @@ fn read_with<'a>(
|
||||
let topology = solidcraft_profile::span("import", "step.topology");
|
||||
let (ids, kind) = item_shells(e)?;
|
||||
let mut shells = Vec::with_capacity(ids.len());
|
||||
let mut skipped = false;
|
||||
for s in ids {
|
||||
shells.push(b.shell(s)?);
|
||||
let (faces, any) = b.shell(s, left_out)?;
|
||||
skipped |= any;
|
||||
shells.push(faces);
|
||||
}
|
||||
drop(topology);
|
||||
b.finish(item, name, shells, kind)
|
||||
b.finish(item, name, shells, if skipped { Kind::Open } else { kind })
|
||||
}
|
||||
|
||||
/// A face of a solid read on its own ([`read_faces_with`]).
|
||||
@@ -417,6 +440,8 @@ enum Kind {
|
||||
Solid,
|
||||
Voids,
|
||||
Model,
|
||||
/// A solid with faces left out ([`read_solid_without`]): its shells are sheets only.
|
||||
Open,
|
||||
}
|
||||
|
||||
/// A curve the edge can be read on, and how its parameter relates to its vertices.
|
||||
@@ -838,14 +863,32 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
Ok(ext)
|
||||
}
|
||||
|
||||
fn shell(&mut self, id: u64) -> R<Vec<(u32, bool)>> {
|
||||
/// The faces of shell `id` but those `left_out`, and whether any was left out.
|
||||
fn shell(&mut self, id: u64, left_out: &HashSet<u64>) -> R<(Vec<(u32, bool)>, bool)> {
|
||||
let (faces, flip) = shell_faces(self.ex, id)?;
|
||||
let mut out = Vec::with_capacity(faces.len());
|
||||
let mut skipped = false;
|
||||
for f in faces {
|
||||
if !left_out.is_empty() && (left_out.contains(&f) || left_out.contains(&self.face_entity(f, 0)?)) {
|
||||
skipped = true;
|
||||
continue;
|
||||
}
|
||||
let (face, forward) = self.face(f, 0)?;
|
||||
out.push((face, forward != flip));
|
||||
}
|
||||
Ok(out)
|
||||
Ok((out, skipped))
|
||||
}
|
||||
|
||||
/// The ADVANCED_FACE or FACE_SURFACE a shell's face `id` is (through its ORIENTED_FACEs).
|
||||
fn face_entity(&self, id: u64, depth: usize) -> R<u64> {
|
||||
if depth > MAX_NESTING {
|
||||
return Err("oriented faces nested too deeply".into());
|
||||
}
|
||||
let e = entity(self.ex, id)?;
|
||||
if e.name() == "ORIENTED_FACE" {
|
||||
return self.face_entity(rid(e.params().get(2), "oriented face element")?, depth + 1);
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Pcurves, outer loops, domains and the canonical order: the finished record.
|
||||
@@ -923,9 +966,13 @@ 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, if turned.get(f as usize).copied().unwrap_or(false) { !fwd } else { fwd })).collect())
|
||||
.map(|s| s.into_iter().map(|(f, fwd)| (f, open || 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).
|
||||
@@ -942,6 +989,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
regions.push(Region { shells: vec![index(v)?], void: true });
|
||||
}
|
||||
}
|
||||
Kind::Open => regions[0].shells = (0..index(shells.len())?).collect(),
|
||||
Kind::Model => {
|
||||
regions[0].shells = (0..index(shells.len())?).collect();
|
||||
for (k, c) in closed.iter().enumerate() {
|
||||
@@ -951,7 +999,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let sheet = !closed.iter().any(|c| *c);
|
||||
let sheet = kind == Kind::Open || !closed.iter().any(|c| *c);
|
||||
|
||||
// Canonical order: faces walking regions → shells → oriented faces.
|
||||
let nf = self.faces.len();
|
||||
|
||||
Reference in New Issue
Block a user