STEP import: a patch side collapsed within its vertex's tolerance is a pole of the loop

The check that a loop does not jump across a spline surface's parameters took a
pole only within the face's tolerance. A corner patch written with one side
shrunk to a few µm (Poppy Humanoid's connectors: 5 µm in a 1e-5 mm file) was
refused, though the record check takes the same junction as a pole at the
tolerance of the vertex the uses meet at. The import check now uses that
vertex's tolerance; the loop runs along the collapsed side, its boundary iso
line. A jump that is still refused says whether the surface closes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 22:22:29 -04:00
committed by Irakli Kourtis
parent f327cdb3d7
commit 5a596dd952
2 changed files with 92 additions and 5 deletions
+67
View File
@@ -2096,3 +2096,70 @@ fn an_edge_over_a_spheres_pole_is_split_there() {
let m = measure(&imp.bodies[0].body).unwrap();
assert!(rel(m.area, excess * r * r) < 1e-3, "{} vs {}", m.area, excess * r * r);
}
/// A planar bicubic patch whose v = 0 side is shrunk to `side` mm (a corner of a round written as
/// a four-sided patch, Poppy Humanoid's connectors write them), bounded by three edges, its
/// boundary iso curves counter-clockwise in (u, v): up the u = 1 side, back across v = 1, down
/// the u = 0 side. The two side edges end `side` apart at one vertex, the end of the u = 0 side,
/// whose tolerance covers that gap; the face tolerance (the file's 1e-6 mm) does not.
fn patch_with_a_side_shrunk(side: f64) -> String {
let f = |x: f64| format!("{x:.17E}");
// (i, j), i along u: the v = 0 row runs over `side`, the v = 1 row over 30 mm.
let p = |i: usize, j: usize| {
let v = j as f64 / 3.0;
[(1.0 - v) * side * i as f64 / 3.0 + v * 10.0 * i as f64, 30.0 * v, 0.0]
};
let pt = |id: usize, q: [f64; 3]| format!("#{id}=CARTESIAN_POINT('',({},{},{}));\n", f(q[0]), f(q[1]), f(q[2]));
let mut t = String::from(HEAD);
let mut rows = Vec::new();
for i in 0..4 {
rows.push(format!("({})", (0..4).map(|j| format!("#{}", 100 + i * 4 + j)).collect::<Vec<_>>().join(",")));
for j in 0..4 {
t += &pt(100 + i * 4 + j, p(i, j));
}
}
t += &format!(
"#10=B_SPLINE_SURFACE_WITH_KNOTS('',3,3,({}),.UNSPECIFIED.,.F.,.F.,.F.,(4,4),(4,4),(0.,1.),(0.,1.),.UNSPECIFIED.);\n",
rows.join(",")
);
// A at (0, 0), C at (1, 1), B at (0, 1).
for (k, q) in [p(0, 0), p(3, 3), p(0, 3)].into_iter().enumerate() {
t += &pt(200 + k, q);
t += &format!("#{}=VERTEX_POINT('',#{});\n", 210 + k, 200 + k);
}
let sides: [[(usize, usize); 4]; 3] = [[(3, 0), (3, 1), (3, 2), (3, 3)], [(3, 3), (2, 3), (1, 3), (0, 3)], [(0, 3), (0, 2), (0, 1), (0, 0)]];
for (k, side) in sides.iter().enumerate() {
for (m, &(i, j)) in side.iter().enumerate() {
t += &pt(300 + k * 4 + m, p(i, j));
}
let pts: Vec<String> = (0..4).map(|m| format!("#{}", 300 + k * 4 + m)).collect();
t += &format!("#{}=B_SPLINE_CURVE_WITH_KNOTS('',3,({}),.UNSPECIFIED.,.F.,.F.,(4,4),(0.,1.),.UNSPECIFIED.);\n", 400 + k, pts.join(","));
t += &format!(
"#{}=EDGE_CURVE('',#{},#{},#{},.T.);\n#{}=ORIENTED_EDGE('',*,*,#{},.T.);\n",
500 + k,
210 + k,
210 + (k + 1) % 3,
400 + k,
510 + k,
500 + k
);
}
t += "#520=EDGE_LOOP('',(#510,#511,#512));\n#521=FACE_OUTER_BOUND('',#520,.T.);\n#522=ADVANCED_FACE('',(#521),#10,.T.);\n";
t += "#530=OPEN_SHELL('',(#522));\n#531=SHELL_BASED_SURFACE_MODEL('Corner',(#530));\n#532=MANIFOLD_SURFACE_SHAPE_REPRESENTATION('',(#531),#540);\n";
t += &context(540, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))");
t += TAIL;
t
}
#[test]
fn a_patch_side_shrunk_within_its_vertexs_tolerance_is_a_pole_of_the_loop() {
// 4 µm: four times the face's tolerance, within the vertex's (1.5 × its 4 µm gap). The loop
// runs along the shrunk side between the side edges' ends, a pole for it, and the face reads
// with the patch's area: a trapezoid 30 mm high, 30 mm and `side` wide.
let side = 4e-3 * 1e-3;
let imp = step_import(&patch_with_a_side_shrunk(side)).unwrap();
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
let m = measure(&imp.bodies[0].body).unwrap();
let area = 0.5 * (30.0 + side) * 30.0;
assert!(rel(m.area, area) < 1e-6, "{} vs {area}", m.area);
}
+25 -5
View File
@@ -1168,10 +1168,12 @@ impl<'b, 'a> Builder<'b, 'a> {
// its domain.
let natural = surface.natural();
// At a pole (where the surface collapses to a point across a direction, to within the
// tolerance, as the pcurves closed the loop there) a loop may jump in that direction: the
// seam's two uses meet there a period apart.
// tolerance of the vertex the two uses meet at) a loop may jump in that direction: the
// seam's two uses meet there a period apart, and a side collapsed to within the vertex's
// tolerance (a corner patch with one side shrunk to a few µm) is run along between them,
// its boundary iso line. As the record check (`brep::geometry`) takes it.
let poles = SurfaceEval::new(&surface).ok();
let at_pole = |uv: [f64; 2]| poles.as_ref().is_some_and(|e| e.pole_within(uv, tol).iter().any(|p| *p));
let at_pole = |uv: [f64; 2], reach: f64| poles.as_ref().is_some_and(|e| e.pole_within(uv, reach).iter().any(|p| *p));
for (l, pc) in edge_loops.iter().zip(&pcs) {
let mut ends = Vec::with_capacity(l.len());
for (&(_, forward), c) in l.iter().zip(pc) {
@@ -1181,7 +1183,11 @@ impl<'b, 'a> Builder<'b, 'a> {
}
for k in 0..ends.len() {
let (Some((start, _)), Some((_, end))) = (ends.get(k), ends.get(if k == 0 { ends.len() - 1 } else { k - 1 })) else { continue };
if at_pole(*start) || at_pole(*end) {
// The vertex use k starts at, where the one before it ends.
let &(e, forward) = l.get(k).ok_or("edge use")?;
let v = self.edges.get(e as usize).ok_or("edge reference")?.vertices[if forward { 0 } else { 1 }];
let reach = self.vertices.get(v as usize).ok_or("vertex reference")?.tolerance.max(tol);
if at_pole(*start, reach) || at_pole(*end, reach) {
continue;
}
for d in 0..2 {
@@ -1191,7 +1197,21 @@ impl<'b, 'a> Builder<'b, 'a> {
continue;
}
if !((start[d] - end[d]).abs() <= 0.5 * (b - a)) {
return Err(unsupported("a loop across a closed spline surface's seam without a seam edge"));
// Whether the surface closes across `d` there: its two boundaries meet.
let ends_at = |x: f64| {
let mut q = *start;
q[d] = x;
poles.as_ref().and_then(|s| s.point(q))
};
let closed = matches!((ends_at(*a), ends_at(*b)), (Some(p), Some(q)) if p.dist(q) <= reach);
return Err(unsupported(if closed {
"a loop across a closed spline surface's seam without a seam edge".to_string()
} else {
format!(
"a loop that jumps across its spline surface's {} range where the surface neither closes nor collapses to a point",
if d == 0 { "u" } else { "v" }
)
}));
}
}
}