STEP import: 65/66 Fusion files; bounded meshing
Meshing (vendored truck-meshalgo) works within a budget so hostile files cannot hang: bounded edge and surface divisions (point caps, depth and round limits, NaN-safe), and a grid budget per face shared out per shell. Surface grids refine only the direction that bends; rulings left undivided get cells comparable to the other direction, so trimmed cylinders (holes, tee joints) no longer lose volume to long triangles. The helical sweep imports in 12 s instead of 7 minutes. Small sphere patches get poles away from them even when the file runs a seam through the patch (a pyramid's corner blend). Faces the mesher cannot mesh are reported as import warnings. Test: wild B-spline and far-off boundaries import in bounded time. STEP corpus 65/66 (50-sweep-tight-bend: a horn-torus bend whose inner side collapses to a point; Fusion's own area for it is also off). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
7a0cc85392
commit
1c10d1e56a
7 files changed
+359
-76
No files matched your search
@@ -291,6 +291,21 @@ impl Body {
|
||||
})
|
||||
}
|
||||
|
||||
/// Faces with no triangles in a coarse tessellation (the mesher failed or gave up).
|
||||
pub fn unmeshed_faces(&self) -> usize {
|
||||
if self.is_mesh() {
|
||||
return 0;
|
||||
}
|
||||
let Ok(m) = self.tessellate((self.size() * 1e-2).max(1e-3)) else { return self.face_count() };
|
||||
let mut has = vec![false; self.face_count()];
|
||||
for f in &m.tri_face {
|
||||
if let Some(h) = has.get_mut(*f as usize) {
|
||||
*h = true;
|
||||
}
|
||||
}
|
||||
has.iter().filter(|h| !**h).count()
|
||||
}
|
||||
|
||||
/// Display mesh: like [`Body::tessellate`], with seam edges flagged.
|
||||
pub fn display_mesh(&self, tol: f64) -> Result<Mesh> {
|
||||
let mut m = self.tessellate(tol)?;
|
||||
|
||||
@@ -566,6 +566,23 @@ fn seam_frame(f: &Frame, extent: &[mt::Point3], seam: &[mt::Point3]) -> Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Length of the mean unit direction from the frame origin to the points (1: all in one
|
||||
/// direction, 0: spread all round).
|
||||
fn mean_direction(f: &Frame, pts: &[mt::Point3]) -> f64 {
|
||||
let dirs: Vec<mt::Vector3> = pts
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let d = p - f.o;
|
||||
let m = d.magnitude();
|
||||
(m > 1e-12).then(|| d / m)
|
||||
})
|
||||
.collect();
|
||||
if dirs.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
(dirs.iter().fold(mt::Vector3::new(0.0, 0.0, 0.0), |a, d| a + d) / dirs.len() as f64).magnitude()
|
||||
}
|
||||
|
||||
/// Polar axis for a sphere: far from every boundary point, and (for a face concentrated on
|
||||
/// one side) with both poles outside the face, so every loop closes in parameter space.
|
||||
fn sphere_axis(f: &Frame, extent: &[mt::Point3]) -> mt::Vector3 {
|
||||
@@ -810,7 +827,10 @@ pub(crate) fn surface(cx: &Ctx, id: u64, extent: &[mt::Point3], seam: &[mt::Poin
|
||||
}
|
||||
// With seam edges the file's own parameterisation fits the loops; else pick poles
|
||||
// away from the boundary.
|
||||
let f = if seam.is_empty() {
|
||||
// A small patch (a corner blend) gets poles away from it even when the file runs a
|
||||
// seam through it: a seam ending at a pole on the patch boundary leaves the mesher
|
||||
// no way to tell the patch from the rest of the sphere.
|
||||
let f = if seam.is_empty() || mean_direction(&f, extent) > 0.3 {
|
||||
let z = sphere_axis(&f, extent);
|
||||
let x = any_perpendicular(z);
|
||||
seam_frame(&Frame { o: f.o, x, y: z.cross(x), z }, extent, seam)
|
||||
|
||||
@@ -233,6 +233,8 @@ struct Reader<'a> {
|
||||
node_items: HashSet<u64>,
|
||||
/// Occurrences and mapped representations visited so far.
|
||||
visits: usize,
|
||||
/// Items whose meshing was checked.
|
||||
unmeshed_checked: HashSet<u64>,
|
||||
bodies: Vec<ImportedBody>,
|
||||
}
|
||||
|
||||
@@ -454,6 +456,13 @@ impl<'a> Reader<'a> {
|
||||
Ok(body) => {
|
||||
let own = e.params().first().and_then(Param::as_str).unwrap_or("").trim().to_string();
|
||||
let name = if own.is_empty() { product.to_string() } else { own };
|
||||
// Faces the mesher gives up on (or that the budget cut short) are reported.
|
||||
if self.unmeshed_checked.insert(item) {
|
||||
let n = body.unmeshed_faces();
|
||||
if n > 0 {
|
||||
self.warn(format!("solid '{name}': {n} face(s) could not be meshed and are not shown"));
|
||||
}
|
||||
}
|
||||
out.push(self.bodies.len());
|
||||
let color = self.colors.get(&item).copied();
|
||||
self.bodies.push(ImportedBody { name, body: body.with_color(color), color, path: path.to_vec(), closed, file_faces });
|
||||
@@ -682,6 +691,7 @@ pub fn step_import(text: &str) -> Result<StepImport> {
|
||||
used_items: HashSet::new(),
|
||||
node_items: HashSet::new(),
|
||||
visits: 0,
|
||||
unmeshed_checked: HashSet::new(),
|
||||
bodies: Vec::new(),
|
||||
};
|
||||
rd.index();
|
||||
|
||||
@@ -367,3 +367,70 @@ fn mirrored_revolved_faces_round_trip() {
|
||||
round_trip(&boolean(&plate, &tool, BoolOp::Cut).unwrap().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces whose boundaries are nowhere near their surfaces, or wild B-spline surfaces, mesh
|
||||
/// within bounded work (the mesher once looped for ever on such input) and are reported if
|
||||
/// they cannot be meshed.
|
||||
#[test]
|
||||
fn hostile_boundaries_mesh_in_bounded_time() {
|
||||
let t0 = std::time::Instant::now();
|
||||
// A wild 24 × 24 B-spline patch (alternating ±50 mm) bounded by four straight edges that
|
||||
// lie far from it, plus a plane face bounded by a circle 500 mm off the plane.
|
||||
let n = 24;
|
||||
let mut t = String::from(HEAD);
|
||||
let mut rows = Vec::new();
|
||||
for i in 0..n {
|
||||
let mut row = Vec::new();
|
||||
for j in 0..n {
|
||||
let id = 1000 + i * n + j;
|
||||
let z = if (i * 7 + j * 13) % 2 == 0 { 50.0 } else { -50.0 };
|
||||
t += &format!("#{id}=CARTESIAN_POINT('',({}.,{}.,{z}));\n", i * 10, j * 10);
|
||||
row.push(format!("#{id}"));
|
||||
}
|
||||
rows.push(format!("({})", row.join(",")));
|
||||
}
|
||||
let knots = |m: usize| {
|
||||
let mut mult = vec!["4".to_string()];
|
||||
mult.extend(std::iter::repeat_n("1".to_string(), m - 4));
|
||||
mult.push("4".into());
|
||||
let ks: Vec<String> = (0..=(m - 3)).map(|k| format!("{k}.")).collect();
|
||||
(mult.join(","), ks.join(","))
|
||||
};
|
||||
let (mu, ku) = knots(n);
|
||||
t += &format!(
|
||||
"#10=B_SPLINE_SURFACE_WITH_KNOTS('',3,3,({}),.UNSPECIFIED.,.F.,.F.,.F.,({mu}),({mu}),({ku}),({ku}),.UNSPECIFIED.);\n",
|
||||
rows.join(",")
|
||||
);
|
||||
let corners = [(0, 0), (390, 0), (390, 390), (0, 390)];
|
||||
for (k, (x, y)) in corners.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',({x}.,{y}.,300.));\n#{}=VERTEX_POINT('',#{});\n", 20 + k, 30 + k, 20 + k);
|
||||
}
|
||||
t += "#40=DIRECTION('',(1.,0.,0.));\n#41=VECTOR('',#40,1.);\n";
|
||||
for k in 0..4 {
|
||||
t += &format!(
|
||||
"#{}=LINE('',#{},#41);\n#{}=EDGE_CURVE('',#{},#{},#{},.T.);\n#{}=ORIENTED_EDGE('',*,*,#{},.T.);\n",
|
||||
50 + k,
|
||||
20 + k,
|
||||
60 + k,
|
||||
30 + k,
|
||||
30 + (k + 1) % 4,
|
||||
50 + k,
|
||||
70 + k,
|
||||
60 + k
|
||||
);
|
||||
}
|
||||
t += "#80=EDGE_LOOP('',(#70,#71,#72,#73));\n#81=FACE_OUTER_BOUND('',#80,.T.);\n#82=ADVANCED_FACE('',(#81),#10,.T.);\n";
|
||||
t += "#90=CARTESIAN_POINT('',(0.,0.,0.));\n#91=DIRECTION('',(0.,0.,1.));\n#92=AXIS2_PLACEMENT_3D('',#90,#91,#40);\n#93=PLANE('',#92);\n";
|
||||
t += "#94=CARTESIAN_POINT('',(0.,0.,500.));\n#95=AXIS2_PLACEMENT_3D('',#94,#91,#40);\n#96=CIRCLE('',#95,30.);\n#97=CARTESIAN_POINT('',(30.,0.,500.));\n#98=VERTEX_POINT('',#97);\n";
|
||||
t += "#99=EDGE_CURVE('',#98,#98,#96,.T.);\n#100=ORIENTED_EDGE('',*,*,#99,.T.);\n#101=EDGE_LOOP('',(#100));\n#102=FACE_OUTER_BOUND('',#101,.T.);\n#103=ADVANCED_FACE('',(#102),#93,.T.);\n";
|
||||
t += "#110=OPEN_SHELL('',(#82,#103));\n#111=SHELL_BASED_SURFACE_MODEL('Wild',(#110));\n#112=SHAPE_REPRESENTATION('',(#111),$);\n";
|
||||
t += TAIL;
|
||||
let r = std::panic::catch_unwind(|| step_import(&t));
|
||||
assert!(r.is_ok(), "panicked");
|
||||
if let Ok(Ok(imp)) = r {
|
||||
for b in &imp.bodies {
|
||||
let _ = measure(&b.body);
|
||||
}
|
||||
}
|
||||
assert!(t0.elapsed().as_secs_f64() < 60.0, "took {:?}", t0.elapsed());
|
||||
}
|
||||
Reference in new issue
Block a user