mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
STEP import: a solid no B-rep can be made of is shown as a mesh of the faces that read
When the reader or SMLib refuses a solid, each of its faces is read on its own as a one-face surface body (solidcraft_step::topology::read_faces_with) and meshed by the planner; the triangles together are a mesh body (the existing kind STL/3MF imports make), marked ImportedBody::shown_as_mesh with the refusal. The report says 'shown as a mesh: <reason>' and names each face left out. Modelling operations refuse it as any mesh body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +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>,
|
||||
}
|
||||
|
||||
/// One product occurrence in the file's assembly tree.
|
||||
@@ -715,10 +719,13 @@ impl Reader {
|
||||
// What the read of the item found, reported once.
|
||||
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}"));
|
||||
}
|
||||
for f in &failed {
|
||||
self.warn(format!("{label}: {f}"));
|
||||
}
|
||||
if !part.closed {
|
||||
if !part.closed && part.refused.is_none() {
|
||||
self.warn(format!("{label}: imported as an open body (the shell is not closed)"));
|
||||
}
|
||||
}
|
||||
@@ -773,6 +780,7 @@ impl Reader {
|
||||
file_faces,
|
||||
source_item: item,
|
||||
face_origin,
|
||||
shown_as_mesh: part.refused.clone(),
|
||||
});
|
||||
}
|
||||
Err(parallel::Failure { read, msg }) => {
|
||||
|
||||
@@ -26,6 +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.
|
||||
pub refused: Option<String>,
|
||||
}
|
||||
|
||||
/// A prepared instance.
|
||||
@@ -63,7 +66,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)?;
|
||||
let (part, read) = smlib_part(ex, *item, *units).or_else(|refused| mesh_part(ex, *item, *units, refused))?;
|
||||
(Arc::new(part), Some(read))
|
||||
}
|
||||
};
|
||||
@@ -85,7 +88,7 @@ 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 is not imported, with the reason.
|
||||
/// refuses fails with the reason ([`mesh_part`] then shows 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)
|
||||
@@ -109,10 +112,54 @@ 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 };
|
||||
let part = Part { body, closed: !solid.sheet, file_faces: solid.face_origin.len(), face_origin, refused: None };
|
||||
Ok((part, solid.notes))
|
||||
}
|
||||
|
||||
/// 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: "))),
|
||||
}
|
||||
}
|
||||
// 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 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 {
|
||||
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: "))),
|
||||
}
|
||||
}
|
||||
if triangles.is_empty() {
|
||||
return Err(Failure { msg: format!("{} (no face could be shown as a mesh either)", refused.msg), ..refused });
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
/// Prepare `jobs`, the results in the jobs' order. Solids still to read and orient (the slow
|
||||
/// part) are shared out between up to `workers` threads, each taking the next job when it is
|
||||
/// done with one, so one large part does not hold up the rest.
|
||||
|
||||
@@ -2163,3 +2163,61 @@ fn a_patch_side_shrunk_within_its_vertexs_tolerance_is_a_pole_of_the_loop() {
|
||||
let area = 0.5 * (30.0 + side) * 30.0;
|
||||
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!(
|
||||
"{HEAD}
|
||||
#1=CARTESIAN_POINT('',(0.,0.,0.));
|
||||
#2=DIRECTION('',(0.,0.,1.));
|
||||
#3=DIRECTION('',(1.,0.,0.));
|
||||
#4=DIRECTION('',(0.,0.,-1.));
|
||||
#5=AXIS2_PLACEMENT_3D('',#1,#2,#3);
|
||||
#6=AXIS2_PLACEMENT_3D('',#1,#4,#3);
|
||||
#7=CARTESIAN_POINT('',(5.,0.,0.));
|
||||
#8=VERTEX_POINT('',#7);
|
||||
#9=CIRCLE('',#5,5.);
|
||||
#10=EDGE_CURVE('',#8,#8,#9,.T.);
|
||||
#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.);
|
||||
#16=ORIENTED_EDGE('',*,*,#10,.T.);
|
||||
#17=EDGE_LOOP('',(#16));
|
||||
#18=FACE_OUTER_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);
|
||||
#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}");
|
||||
}
|
||||
|
||||
+85
-37
@@ -310,39 +310,106 @@ fn read_with<'a>(
|
||||
trims: Option<&dyn Trims>,
|
||||
) -> R<Solid> {
|
||||
let e = entity(ex, item)?;
|
||||
let p = e.params();
|
||||
let name = p.first().and_then(Param::as_str).unwrap_or("").to_string();
|
||||
let name = e.params().first().and_then(Param::as_str).unwrap_or("").to_string();
|
||||
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 (shells, kind) = match e.name() {
|
||||
"MANIFOLD_SOLID_BREP" => (vec![b.shell(rid(p.get(1), "outer shell")?)?], Kind::Solid),
|
||||
let (ids, kind) = item_shells(e)?;
|
||||
let mut shells = Vec::with_capacity(ids.len());
|
||||
for s in ids {
|
||||
shells.push(b.shell(s)?);
|
||||
}
|
||||
drop(topology);
|
||||
b.finish(item, name, shells, kind)
|
||||
}
|
||||
|
||||
/// A face of a solid read on its own ([`read_faces_with`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FaceRead {
|
||||
/// The face's entity in the file (the ADVANCED_FACE or FACE_SURFACE, or the ORIENTED_FACE
|
||||
/// the shell lists when it could not be read).
|
||||
pub face: u64,
|
||||
/// The face as a one-face surface body, facing as its shell uses it; else why it could not
|
||||
/// be read (naming the face).
|
||||
pub record: R<Brep>,
|
||||
}
|
||||
|
||||
/// Each face of a solid item read on its own, as a one-face surface body: what can still be
|
||||
/// shown of a solid that cannot be read whole, or whose body the kernel refuses. A face is read
|
||||
/// 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 mut reader = Reader::new(ex, context.units);
|
||||
let mut budget = MAX_FILE_FACES;
|
||||
let mut out = Vec::new();
|
||||
for s in shells {
|
||||
let (faces, flip) = shell_faces(ex, s)?;
|
||||
for f in faces {
|
||||
if out.len() >= MAX_FACES {
|
||||
return Err(format!("more than {MAX_FACES} faces"));
|
||||
}
|
||||
let mut b = Builder::new(ex, &mut reader, tolerance(&context), &mut budget, Some(trims));
|
||||
let read = b.face(f, 0).and_then(|(face, forward)| b.finish(item, String::new(), vec![vec![(face, forward != flip)]], Kind::Model));
|
||||
let named = |m: String| if m.starts_with(&format!("face #{f}")) { m } else { format!("face #{f}: {m}") };
|
||||
out.push(match read {
|
||||
Ok(s) => FaceRead { face: s.face_origin.first().copied().unwrap_or(f), record: Ok(s.brep) },
|
||||
Err(m) => FaceRead { face: f, record: Err(named(m)) },
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 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)> {
|
||||
let p = e.params();
|
||||
match e.name() {
|
||||
"MANIFOLD_SOLID_BREP" => Ok((vec![rid(p.get(1), "outer shell")?], Kind::Solid)),
|
||||
"BREP_WITH_VOIDS" => {
|
||||
let mut shells = vec![b.shell(rid(p.get(1), "outer shell")?)?];
|
||||
let mut shells = vec![rid(p.get(1), "outer shell")?];
|
||||
let voids = refs(p.get(2), "voids")?;
|
||||
if voids.len() >= brep::MAX_SHELLS {
|
||||
return Err(format!("{} voids", voids.len()));
|
||||
}
|
||||
for v in voids {
|
||||
shells.push(b.shell(v)?);
|
||||
}
|
||||
(shells, Kind::Voids)
|
||||
shells.extend(voids);
|
||||
Ok((shells, Kind::Voids))
|
||||
}
|
||||
"SHELL_BASED_SURFACE_MODEL" => {
|
||||
let ids = refs(p.get(1), "surface model shells")?;
|
||||
if ids.is_empty() || ids.len() > brep::MAX_SHELLS {
|
||||
return Err(format!("a surface model with {} shells", ids.len()));
|
||||
}
|
||||
let mut shells = Vec::with_capacity(ids.len());
|
||||
for s in ids {
|
||||
shells.push(b.shell(s)?);
|
||||
}
|
||||
(shells, Kind::Model)
|
||||
Ok((ids, Kind::Model))
|
||||
}
|
||||
n => return Err(unsupported(n)),
|
||||
n => Err(unsupported(n)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The faces a shell lists, and whether the shell turns them over (an oriented shell used
|
||||
/// against its faces).
|
||||
fn shell_faces(ex: &Exchange, id: u64) -> R<(Vec<u64>, bool)> {
|
||||
let e = entity(ex, id)?;
|
||||
let p = e.params();
|
||||
let (faces, flip) = match e.name() {
|
||||
"CLOSED_SHELL" | "OPEN_SHELL" => (refs(p.get(1), "shell faces")?, false),
|
||||
"ORIENTED_CLOSED_SHELL" | "ORIENTED_OPEN_SHELL" => {
|
||||
let inner = rid(p.get(2), "oriented shell element")?;
|
||||
let inner_e = entity(ex, inner)?;
|
||||
if !matches!(inner_e.name(), "CLOSED_SHELL" | "OPEN_SHELL") {
|
||||
return Err(format!("#{inner}: {}", unsupported(format!("shell {}", inner_e.name()))));
|
||||
}
|
||||
(refs(inner_e.params().get(1), "shell faces")?, !flag(p.get(3), "oriented shell orientation")?)
|
||||
}
|
||||
n => return Err(format!("#{id}: {}", unsupported(format!("shell {n}")))),
|
||||
};
|
||||
drop(topology);
|
||||
b.finish(item, name, shells, kind)
|
||||
if faces.is_empty() {
|
||||
return Err(format!("shell #{id} has no faces"));
|
||||
}
|
||||
if faces.len() > MAX_FACES {
|
||||
return Err(format!("shell #{id} with {} faces", faces.len()));
|
||||
}
|
||||
Ok((faces, flip))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -772,26 +839,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
}
|
||||
|
||||
fn shell(&mut self, id: u64) -> R<Vec<(u32, bool)>> {
|
||||
let e = entity(self.ex, id)?;
|
||||
let p = e.params();
|
||||
let (faces, flip) = match e.name() {
|
||||
"CLOSED_SHELL" | "OPEN_SHELL" => (refs(p.get(1), "shell faces")?, false),
|
||||
"ORIENTED_CLOSED_SHELL" | "ORIENTED_OPEN_SHELL" => {
|
||||
let inner = rid(p.get(2), "oriented shell element")?;
|
||||
let inner_e = entity(self.ex, inner)?;
|
||||
if !matches!(inner_e.name(), "CLOSED_SHELL" | "OPEN_SHELL") {
|
||||
return Err(format!("#{inner}: {}", unsupported(format!("shell {}", inner_e.name()))));
|
||||
}
|
||||
(refs(inner_e.params().get(1), "shell faces")?, !flag(p.get(3), "oriented shell orientation")?)
|
||||
}
|
||||
n => return Err(format!("#{id}: {}", unsupported(format!("shell {n}")))),
|
||||
};
|
||||
if faces.is_empty() {
|
||||
return Err(format!("shell #{id} has no faces"));
|
||||
}
|
||||
if faces.len() > MAX_FACES {
|
||||
return Err(format!("shell #{id} with {} faces", faces.len()));
|
||||
}
|
||||
let (faces, flip) = shell_faces(self.ex, id)?;
|
||||
let mut out = Vec::with_capacity(faces.len());
|
||||
for f in faces {
|
||||
let (face, forward) = self.face(f, 0)?;
|
||||
|
||||
Reference in New Issue
Block a user