mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
STEP import: an edge off its face's surface carries its gap as its tolerance
An edge's curve as a file writes it may lie off a face's surface by more than the
file's stated uncertainty: Poppy Humanoid's intersection curves lie up to 9 µm off
surfaces in a file that states 0.01 µm, and 24 of its solids were refused by the
record check ("pcurve … lifts off its edge's curve"). Each use's gap, between its
pcurve on the surface and its edge's curve, is now measured along the edge at 17
points as the record check measures it, and the edge's tolerance covers it (1.5 ×
the gap, as a vertex's covers its gap; more than 1 mm is refused). The pcurves are as
made, and each solid's notes say how many edges were raised and how far.
The pcurve fit's tenfold retries went in 6cc0401fa; this measures instead of
retrying. On the corpus (the repository's STEP/IGES files, the models repository,
Poppy): 50 → 53 of 56 files import; Poppy 132 → 194 bodies.
Test: a hand-written domed patch whose edges are written 0.2 µm above it (SMLib makes
its pcurves) imports with them covered; 1.5 mm off is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
@@ -1738,3 +1738,71 @@ fn step_conformance_parts_read_into_smlib() {
|
||||
assert_eq!(n, faces, "{name}: faces");
|
||||
}
|
||||
}
|
||||
|
||||
/// A domed cubic patch 30 mm across whose four edges, cubic B-splines on its boundary rows,
|
||||
/// are written `dz` mm above it, in a file that states 1e-6 mm: an intersection curve written
|
||||
/// 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 };
|
||||
let mut t = String::from(HEAD);
|
||||
let mut rows = Vec::new();
|
||||
for i in 0..4 {
|
||||
let row: Vec<String> = (0..4).map(|j| format!("#{}", 100 + i * 4 + j)).collect();
|
||||
for j in 0..4 {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',({}.,{}.,{}));\n", 100 + i * 4 + j, i * 10, j * 10, z(i, j));
|
||||
}
|
||||
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(",")
|
||||
);
|
||||
// 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] =
|
||||
[[(0, 0), (1, 0), (2, 0), (3, 0)], [(3, 0), (3, 1), (3, 2), (3, 3)], [(3, 3), (2, 3), (1, 3), (0, 3)], [(0, 3), (0, 2), (0, 1), (0, 0)]];
|
||||
let p = |(i, j): (usize, usize)| format!("({}.,{}.,{})", i * 10, j * 10, z(i, j) + dz);
|
||||
for (k, side) in sides.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n#{}=VERTEX_POINT('',#{});\n", 200 + k, p(side[0]), 210 + k, 200 + k);
|
||||
let pts: Vec<String> = (0..4).map(|m| format!("#{}", 300 + k * 4 + m)).collect();
|
||||
for (m, &q) in side.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n", 300 + k * 4 + m, p(q));
|
||||
}
|
||||
t += &format!("#{}=B_SPLINE_CURVE_WITH_KNOTS('',3,({}),.UNSPECIFIED.,.F.,.F.,(4,4),(0.,1.),.UNSPECIFIED.);\n", 400 + k, pts.join(","));
|
||||
}
|
||||
for k in 0..4 {
|
||||
t += &format!(
|
||||
"#{}=EDGE_CURVE('',#{},#{},#{},.T.);\n#{}=ORIENTED_EDGE('',*,*,#{},.T.);\n",
|
||||
500 + k,
|
||||
210 + k,
|
||||
210 + (k + 1) % 4,
|
||||
400 + k,
|
||||
510 + k,
|
||||
500 + k
|
||||
);
|
||||
}
|
||||
t += "#520=EDGE_LOOP('',(#510,#511,#512,#513));\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('Dome',(#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 edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
// 0.2 µm off, 200 times the file's stated uncertainty: each edge's tolerance covers its
|
||||
// gap and the face imports, its notes saying how many edges and how far.
|
||||
let imp = step_import(&dome_with_edges_raised(2e-4)).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 r = imp.bodies[0].body.record();
|
||||
assert!(r.edges.iter().all(|e| e.tolerance >= 2e-4 && e.tolerance < 1e-3), "{:?}", r.edges.iter().map(|e| e.tolerance).collect::<Vec<_>>());
|
||||
// Far off (1.5 mm) is refused, naming the gap.
|
||||
let far = step_import(&dome_with_edges_raised(1.5));
|
||||
let refused = match &far {
|
||||
Ok(i) => i.bodies.is_empty() && i.warnings.iter().any(|w| w.contains("off the face's surface")),
|
||||
Err(e) => e.to_string().contains("off the face's surface"),
|
||||
};
|
||||
assert!(refused, "{far:?}");
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
//!
|
||||
//! Tolerances: the representation context's distance uncertainty (default [`DEFAULT_TOLERANCE`]
|
||||
//! when the file has none, never below [`MIN_TOLERANCE`]) is the brep's tolerance and every
|
||||
//! vertex's and edge's to start with. A vertex measurably off its curve or surface gets that
|
||||
//! distance (with margin) as its tolerance; a face whose edges lie off its surface gets its
|
||||
//! pcurves fitted at a looser tolerance, which its edges then carry.
|
||||
//! vertex's and edge's to start with. A vertex measurably off its curve or surface, or an edge
|
||||
//! whose curve lies measurably off a face's surface, gets that distance (with margin) as its
|
||||
//! tolerance, and the solid's notes say how many and how far.
|
||||
//!
|
||||
//! Pcurves are the closed forms the 3D edges have on our surfaces ([`pcurve::exact_pcurves`]); a
|
||||
//! use with none gets its pcurve from the kernel ([`read_solids_with`], SMLib's own trims). The
|
||||
@@ -79,6 +79,12 @@ const MAX_SPLINE_DEGREE: usize = 16;
|
||||
const MAX_SAMPLES: usize = 4096;
|
||||
/// How far (mm) a vertex may lie from its curve before the edge is refused.
|
||||
const MAX_VERTEX_GAP: f64 = 1.0;
|
||||
/// How far (mm) an edge's curve may lie from a face's surface before the face is refused.
|
||||
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.
|
||||
const EDGE_GAP_MARGIN: f64 = 1.5;
|
||||
|
||||
/// A solid (or sheet) read from the file.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -385,6 +391,8 @@ struct Builder<'b, 'a> {
|
||||
face_ids: HashSet<u64>,
|
||||
faces: Vec<FaceB>,
|
||||
sense_mismatches: usize,
|
||||
/// Edges whose tolerance was raised to the gap their curve has to a face's surface.
|
||||
raised_edges: HashSet<u32>,
|
||||
}
|
||||
|
||||
impl<'b, 'a> Builder<'b, 'a> {
|
||||
@@ -402,6 +410,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
face_ids: HashSet::new(),
|
||||
faces: Vec::new(),
|
||||
sense_mismatches: 0,
|
||||
raised_edges: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +445,50 @@ 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<()> {
|
||||
let Some(x) = self.edges.get_mut(e as usize) else { return Err("edge reference".into()) };
|
||||
if 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;
|
||||
self.raised_edges.insert(e);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An edge's curve as the file has it may lie off a face's surface by more than the file's
|
||||
/// uncertainty states (an intersection curve written looser than its context). Each use's
|
||||
/// gap, between its pcurve on the surface and its edge's curve, is measured along the edge as
|
||||
/// the record check measures it, and the edge's tolerance covers it, as a vertex's covers its
|
||||
/// gap: the pcurves are as made and the record says how far its geometry agrees.
|
||||
fn raise_edges(&mut self, surface: &BSurface, edge_loops: &[Vec<(u32, bool)>], pcs: &[Vec<Curve2>]) -> R<()> {
|
||||
let on = SurfaceEval::new(surface).map_err(|m| m.to_string())?;
|
||||
for (l, pc) in edge_loops.iter().zip(pcs) {
|
||||
for (&(e, _), c) in l.iter().zip(pc) {
|
||||
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);
|
||||
// A point a pcurve or surface lacks is the record check's to name.
|
||||
let (Some(q), Some(p)) = (c.eval(t), eval.point(t)) else { continue };
|
||||
if let Some(s) = on.point(q) {
|
||||
gap = gap.max(s.dist(p));
|
||||
}
|
||||
}
|
||||
self.cover_edge(e, gap)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The schema edge of an EDGE_CURVE, and whether it runs with the STEP edge.
|
||||
fn edge(&mut self, id: u64) -> R<(u32, bool)> {
|
||||
if let Some(e) = self.edge_ids.get(&id) {
|
||||
@@ -749,6 +802,14 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
if let Some(first) = first_failure {
|
||||
return Err(format!("pcurves failed on {failed} of {} faces; first: {first}", self.faces.len()));
|
||||
}
|
||||
if !self.raised_edges.is_empty() {
|
||||
let most = self.raised_edges.iter().filter_map(|&e| self.edges.get(e as usize)).map(|e| e.tolerance).fold(0.0, f64::max);
|
||||
notes.push(format!(
|
||||
"{} edges lie off their faces' surfaces by more than {} mm: their tolerances are the gaps they have (up to {most:.3e} mm)",
|
||||
self.raised_edges.len(),
|
||||
self.tol
|
||||
));
|
||||
}
|
||||
let flipped = turned.iter().filter(|t| **t).count();
|
||||
if flipped > 0 {
|
||||
notes.push(format!("{flipped} faces turned over: their loops disagree with their same_sense"));
|
||||
@@ -1036,6 +1097,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
};
|
||||
let pcs =
|
||||
pcurve::pcurves_within(&surface, &uses_of(&self.edges, &edge_loops, made.as_ref())?, tol, box_on_form).map_err(|e| e.to_string())?;
|
||||
self.raise_edges(&surface, &edge_loops, &pcs)?;
|
||||
// Signed areas of the loops that close in (u, v).
|
||||
let periods = natural_periods(&surface);
|
||||
// Each use starts near where the one before it ends, along a bounded direction that
|
||||
|
||||
Reference in New Issue
Block a user