mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user