mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
STEP import: an edge's distance from an analytic face is measured before its closed-form pcurve
A closed-form pcurve (a B-spline edge's control points projected onto its plane) is accepted within half its edge's tolerance, and the edge carried the file's: Poppy Humanoid has planes whose B-spline edges stand 9 µm off them in a file that states 10 µm. The closed form was refused, and a plane has no parameter box for SMLib to make pcurves in: "a face on an unbounded surface without closed-form pcurves" on 11 of its solids. On a plane, cylinder, cone, sphere or torus (nearest points in closed form) each edge's distance from the surface is now measured along it first, and its tolerance covers three times that; a spline face's gaps stay measured on its kernel's pcurves, with the 1.5 × margin of a vertex's. The pcurve code reads each use's own tolerance. Poppy: 194 → 195 bodies, the unbounded-surface refusals 11 → 3 (the solids they let through now stop at later steps); the corpus survey and the kernel tests are as before. Test: a plane face whose B-spline edges stand 0.8e-6 mm above it in a file stating 1e-6 mm imports with its closed forms; without the measure it fails as Poppy's did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
@@ -1954,7 +1954,13 @@ fn step_conformance_parts_read_into_smlib() {
|
||||
/// looser than its context. The patch has no closed-form pcurves for them, so SMLib makes them,
|
||||
/// on the surface, and they lie `dz` from their edges.
|
||||
fn dome_with_edges_raised(dz: f64) -> String {
|
||||
let z = |i: usize, j: usize| if (1..=2).contains(&i) && (1..=2).contains(&j) { 6.0 } else { 0.0 };
|
||||
patch_with_edges_raised(dz, false)
|
||||
}
|
||||
|
||||
/// [`dome_with_edges_raised`], or with `flat` the plane z = 0 in its place: its edges' pcurves
|
||||
/// are then their control points' projections onto it, in closed form.
|
||||
fn patch_with_edges_raised(dz: f64, flat: bool) -> String {
|
||||
let z = |i: usize, j: usize| if !flat && (1..=2).contains(&i) && (1..=2).contains(&j) { 6.0 } else { 0.0 };
|
||||
let mut t = String::from(HEAD);
|
||||
let mut rows = Vec::new();
|
||||
for i in 0..4 {
|
||||
@@ -1964,10 +1970,14 @@ fn dome_with_edges_raised(dz: f64) -> String {
|
||||
}
|
||||
rows.push(format!("({})", row.join(",")));
|
||||
}
|
||||
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(",")
|
||||
);
|
||||
if flat {
|
||||
t += "#10=PLANE('',#13);\n#11=CARTESIAN_POINT('',(0.,0.,0.));\n#12=DIRECTION('',(0.,0.,1.));\n#14=DIRECTION('',(1.,0.,0.));\n#13=AXIS2_PLACEMENT_3D('',#11,#12,#14);\n";
|
||||
} else {
|
||||
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(",")
|
||||
);
|
||||
}
|
||||
// The boundary, counter-clockwise from above: (i, j) runs i = 0 → 3 at j = 0, j = 0 → 3 at
|
||||
// i = 3, then back; each side's control points are the patch's, raised.
|
||||
let sides: [[(usize, usize); 4]; 4] =
|
||||
@@ -2016,3 +2026,16 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
};
|
||||
assert!(refused, "{far:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_edges_off_their_plane_by_less_than_the_file_states_get_their_closed_form() {
|
||||
// 0.8e-6 mm off, inside the file's 1e-6 mm but past the half of it a closed-form pcurve is
|
||||
// checked within: each edge's distance from the plane, measured first, is its tolerance
|
||||
// (with margin), and the face reads with its pcurves the edges' projections. A plane has no
|
||||
// parameter box for SMLib to make pcurves in, so without them the face is refused.
|
||||
let imp = step_import(&patch_with_edges_raised(0.8e-6, true)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("4 edges lie off their faces' surfaces")), "{:?}", imp.warnings);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.area, 900.0) < 1e-6, "{}", m.area);
|
||||
}
|
||||
|
||||
@@ -83,8 +83,13 @@ const MAX_VERTEX_GAP: f64 = 1.0;
|
||||
const MAX_EDGE_GAP: f64 = 1.0;
|
||||
/// Intervals an edge's curve is measured over against each face's surface it bounds.
|
||||
const EDGE_GAP_SAMPLES: usize = 16;
|
||||
/// An edge's tolerance over the largest gap measured on its samples, as a vertex's is.
|
||||
/// An edge's tolerance over the largest gap measured between its pcurves and its curve, as the
|
||||
/// record check measures it: with margin, as a vertex's.
|
||||
const EDGE_GAP_MARGIN: f64 = 1.5;
|
||||
/// An edge's tolerance over its largest distance from a face's surface, measured before its
|
||||
/// pcurve is made: a closed-form pcurve is accepted within half its edge's tolerance
|
||||
/// (`pcurve::pcurves_within`), so the tolerance goes past twice the distance.
|
||||
const EDGE_SURFACE_MARGIN: f64 = 3.0;
|
||||
|
||||
/// A solid (or sheet) read from the file.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -445,16 +450,58 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Edge `e`'s tolerance covers `gap` (with margin): refused if far off.
|
||||
fn cover_edge(&mut self, e: u32, gap: f64) -> R<()> {
|
||||
/// An edge's curve as the file has it may lie off a face's surface by more than the file's
|
||||
/// uncertainty states (a B-spline edge whose control points stand 9 µm off its plane, in a
|
||||
/// file that states 10 µm, gets no closed-form pcurve, which is checked within half that).
|
||||
/// On a surface whose nearest point is in closed form (a plane, cylinder, cone, sphere or
|
||||
/// torus) the edge's distance from it is measured along the edge before its pcurve is made,
|
||||
/// and the edge's tolerance covers it. A spline face's gaps are measured on the pcurves its
|
||||
/// kernel makes (`raise_edges`): an iterative nearest point may settle elsewhere on it.
|
||||
fn measure_edges(&mut self, surface: &BSurface, edge_loops: &[Vec<(u32, bool)>]) -> R<()> {
|
||||
if !matches!(
|
||||
surface,
|
||||
BSurface::Plane { .. }
|
||||
| BSurface::PlaneThrough { .. }
|
||||
| BSurface::Cylinder { .. }
|
||||
| BSurface::Cone { .. }
|
||||
| BSurface::Sphere { .. }
|
||||
| BSurface::Torus { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
let on = SurfaceEval::new(surface).map_err(|m| m.to_string())?;
|
||||
for l in edge_loops {
|
||||
for &(e, _) in l {
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let (curve, [t0, t1]) = match &edge.form {
|
||||
Some((c, i)) => (c, *i),
|
||||
None => (&edge.curve, edge.interval),
|
||||
};
|
||||
let eval = CurveEval::new(curve).map_err(|m| m.to_string())?;
|
||||
let mut gap = 0.0f64;
|
||||
for k in 0..=EDGE_GAP_SAMPLES {
|
||||
let t = t0 + (t1 - t0) * (k as f64 / EDGE_GAP_SAMPLES as f64);
|
||||
let Some(p) = eval.point(t) else { continue };
|
||||
// A point with no nearest point is the pcurves' to refuse, with why.
|
||||
let Some(s) = on.project(p, None, f64::INFINITY).and_then(|q| on.point(q)) else { continue };
|
||||
gap = gap.max(s.dist(p));
|
||||
}
|
||||
self.cover_edge(e, gap, EDGE_SURFACE_MARGIN)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Edge `e`'s tolerance covers `gap` with `margin`: refused if far off.
|
||||
fn cover_edge(&mut self, e: u32, gap: f64, margin: f64) -> R<()> {
|
||||
let Some(x) = self.edges.get_mut(e as usize) else { return Err("edge reference".into()) };
|
||||
if gap <= x.tolerance {
|
||||
if margin * gap <= x.tolerance {
|
||||
return Ok(());
|
||||
}
|
||||
if !(gap <= MAX_EDGE_GAP) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
x.tolerance = EDGE_GAP_MARGIN * gap;
|
||||
x.tolerance = margin * gap;
|
||||
self.raised_edges.insert(e);
|
||||
Ok(())
|
||||
}
|
||||
@@ -483,7 +530,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
gap = gap.max(s.dist(p));
|
||||
}
|
||||
}
|
||||
self.cover_edge(e, gap)?;
|
||||
self.cover_edge(e, gap, EDGE_GAP_MARGIN)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1082,6 +1129,8 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
None => (None, None),
|
||||
};
|
||||
let surface = form.clone().unwrap_or_else(|| label.clone());
|
||||
// How far each edge lies off this surface, so its closed form is checked at that.
|
||||
self.measure_edges(&label, &edge_loops)?;
|
||||
// Closed forms where the geometry has them; the kernel makes the rest (`trims`).
|
||||
let exact = pcurve::exact_pcurves(&surface, &uses_of(&self.edges, &edge_loops, None)?, tol, box_on_form).map_err(|e| e.to_string())?;
|
||||
// Without a kernel, a use with neither is refused below with why.
|
||||
@@ -1231,7 +1280,7 @@ fn uses_of<'e>(edges: &'e [EdgeB], edge_loops: &[Vec<(u32, bool)>], given: Optio
|
||||
Some(g) => Some(g.get(li).and_then(|l| l.get(p)).ok_or("a pcurve the kernel did not make")?),
|
||||
None => None,
|
||||
};
|
||||
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given });
|
||||
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: edge.tolerance, given });
|
||||
}
|
||||
uses.push(u);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user