mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05c4cabc33 |
@@ -100,11 +100,19 @@ fn parabola_piece_volume() -> f64 {
|
||||
+ (0.4 * q.powf(2.5) - 2.0 / 3.0 * c * c * q.powf(1.5)) / (4.0 * c * c)
|
||||
}
|
||||
|
||||
/// The conic edge's pcurve on the cone is fitted: between its knots its image lies up to 4.9e-7
|
||||
/// mm off the edge, nearly the 5e-7 mm zone of the file's 1e-6, so the edge's tolerance covers
|
||||
/// it. The gap is the reader's, not the file's, so no note (#12757); SMLib's check passes the body.
|
||||
fn covers_the_conics_pcurve(imp: &StepImport) {
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("{e}"));
|
||||
}
|
||||
|
||||
fn import_piece(text: &str, want: f64) -> StepImport {
|
||||
assert_eq!(step_orientation_errors(text).unwrap(), Vec::<String>::new());
|
||||
let imp = step_import(text).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert_eq!(imp.bodies.len(), 1);
|
||||
covers_the_conics_pcurve(&imp);
|
||||
assert!(imp.bodies[0].closed);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert_eq!(m.faces, 3);
|
||||
@@ -118,8 +126,7 @@ fn round_trip(imp: &StepImport, want: f64) {
|
||||
let text = step_export(&[&imp.bodies[0].body], "conic").unwrap();
|
||||
step_validate(&text).unwrap();
|
||||
let back = step_import(&text).unwrap();
|
||||
assert!(back.warnings.is_empty(), "{:?}", back.warnings);
|
||||
assert_eq!(back.bodies.len(), 1);
|
||||
covers_the_conics_pcurve(&back);
|
||||
assert!(back.bodies[0].closed);
|
||||
let v = measure(&back.bodies[0].body).unwrap().volume;
|
||||
assert!(rel(v, want) < 1e-4, "round trip volume {v} vs {want}");
|
||||
|
||||
@@ -1493,7 +1493,10 @@ fn nearly_round_spindle_torus_pin_end_in_halves_imports_whole() {
|
||||
t += &context(70, "(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))").replace("LENGTH_MEASURE(1.E-6)", "LENGTH_MEASURE(1.E-5)");
|
||||
t += TAIL;
|
||||
let (volume, area) = pin_with_cylinder(major, minor);
|
||||
assert_closed_measures(&t, volume, area, &[]);
|
||||
// The apex lies 3.6e-5 mm off the pole, past the file's 1e-5: the body's tolerance covers it,
|
||||
// as SMLib checks a vertex's gap from its faces against the body's.
|
||||
let note = "solid 'Pin': the file states its geometry within 0.00001 mm, but its edges and vertices need tolerances up to 2.174e-4 mm and its vertices lie up to 3.623e-5 mm off their faces: the body's tolerance is 7.246e-5 mm";
|
||||
assert_closed_measures(&t, volume, area, &[note]);
|
||||
}
|
||||
|
||||
/// The lemon of a spindle torus (the tube's part across the axis: a closed surface with a pole
|
||||
@@ -2041,7 +2044,10 @@ fn extruded_ellipse_patch_crossing_profile_seam_stays_trimmed() {
|
||||
);
|
||||
assert!(text.contains(&old));
|
||||
let text = text.replacen(&old, &surface, 1);
|
||||
let imported = step_import(&text).unwrap();
|
||||
let mut imported = step_import(&text).unwrap();
|
||||
// The ellipse's edges lie up to 6e-4 mm off the patch, past the zone the file's 1e-3
|
||||
// gives them in SMLib, so their tolerances are raised; that note is not this test's.
|
||||
imported.warnings.retain(|w| !w.contains("edges lie off their faces' surfaces"));
|
||||
// Against its loop (same_sense false) the face is turned over, and the import says so.
|
||||
let open = "solid 'Strip': imported as an open body (the shell is not closed)";
|
||||
let turned = "solid 'Strip': 1 faces turned over: their loops disagree with their same_sense";
|
||||
@@ -2180,9 +2186,22 @@ fn dome_with_edges_raised(dz: f64) -> String {
|
||||
patch_with_edges_raised(dz, false)
|
||||
}
|
||||
|
||||
/// [`dome_with_edges_raised`] with only the edges' inner control points raised: the vertices lie
|
||||
/// on the dome's corners and each edge bows off it between them, up to `0.75 dz` at its middle,
|
||||
/// as an intersection curve written looser than its file states does.
|
||||
fn dome_with_edges_bowed(dz: f64) -> String {
|
||||
patch_with_edges(dz, false, 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 {
|
||||
patch_with_edges(dz, flat, true)
|
||||
}
|
||||
|
||||
/// [`patch_with_edges_raised`]; with `ends` false the vertices and the edges' end control points
|
||||
/// stay on the patch.
|
||||
fn patch_with_edges(dz: f64, flat: bool, ends: 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();
|
||||
@@ -2205,12 +2224,13 @@ fn patch_with_edges_raised(dz: f64, flat: bool) -> String {
|
||||
// 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);
|
||||
let p = |(i, j): (usize, usize), dz: f64| format!("({}.,{}.,{})", i * 10, j * 10, z(i, j) + dz);
|
||||
let end_dz = if ends { dz } else { 0.0 };
|
||||
for (k, side) in sides.iter().enumerate() {
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n#{}=VERTEX_POINT('',#{});\n", 200 + k, p(side[0]), 210 + k, 200 + k);
|
||||
t += &format!("#{}=CARTESIAN_POINT('',{});\n#{}=VERTEX_POINT('',#{});\n", 200 + k, p(side[0], end_dz), 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!("#{}=CARTESIAN_POINT('',{});\n", 300 + k * 4 + m, p(q, if m == 0 || m == 3 { end_dz } else { dz }));
|
||||
}
|
||||
t += &format!("#{}=B_SPLINE_CURVE_WITH_KNOTS('',3,({}),.UNSPECIFIED.,.F.,.F.,(4,4),(0.,1.),.UNSPECIFIED.);\n", 400 + k, pts.join(","));
|
||||
}
|
||||
@@ -2241,6 +2261,16 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
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<_>>());
|
||||
// A record tolerance is SMLib's intersection tolerance, twice the zone a gap must lie within,
|
||||
// and SMLib checks a vertex's gaps against half its zone (#11997): with its vertices on the
|
||||
// dome's corners and its edges bowed off between them, the body checks there (at 1.5 times
|
||||
// the gap SMLib refused it, "Bad Gap"). Edges up to 1e-4 off keep within SMLib's 1000:1 to
|
||||
// the body's 1e-6; vertices off their faces, or edges farther off, need the body's tolerance
|
||||
// raised.
|
||||
for dz in [2e-5, 1e-4] {
|
||||
let imp = step_import(&dome_with_edges_bowed(dz)).unwrap();
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("edges {dz} mm off: {e}"));
|
||||
}
|
||||
// Far off (1.5 mm) is refused, naming the gap.
|
||||
let far = step_import(&dome_with_edges_raised(1.5));
|
||||
let refused = match &far {
|
||||
@@ -2250,6 +2280,110 @@ fn edges_written_off_their_spline_face_carry_their_gap_as_their_tolerance() {
|
||||
assert!(refused, "{far:?}");
|
||||
}
|
||||
|
||||
/// A file whose edges or vertices lie farther off their faces than its stated uncertainty allows
|
||||
/// SMLib (#11997: a body written with 1e-6 whose intersection edges lie 5.6e-4 off a spline face).
|
||||
/// SMLib checks each vertex's gap from its faces against the body's tolerance, and takes no edge
|
||||
/// or vertex zone of 1000 times the body's: such a body was refused before any operation. The
|
||||
/// body's tolerance now covers both, and the body checks.
|
||||
#[test]
|
||||
fn edges_farther_off_than_the_stated_uncertainty_allows_raise_the_bodys_tolerance() {
|
||||
let tolerances = |imp: &crate::step_in::StepImport| {
|
||||
let r = imp.bodies[0].body.record();
|
||||
let widest = r.edges.iter().map(|e| e.tolerance).chain(r.vertices.iter().map(|v| v.tolerance)).fold(0.0, f64::max);
|
||||
(r.tolerance, widest)
|
||||
};
|
||||
// Edges bowed up to 3.75e-4 mm off between vertices on the dome, 375 times the file's 1e-6:
|
||||
// the body's tolerance keeps the widest edge or vertex within SMLib's ratio, no more.
|
||||
let imp = step_import(&dome_with_edges_bowed(5e-4)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap();
|
||||
let (body, widest) = tolerances(&imp);
|
||||
assert!(body >= widest / 1000.0 && body <= widest / 100.0, "body {body}, widest {widest}");
|
||||
assert!(imp.warnings.iter().any(|w| w.contains("the body's tolerance is")), "{:?}", imp.warnings);
|
||||
// Edges and vertices 5e-4 mm off: the body's tolerance covers the vertices' gaps.
|
||||
let imp = step_import(&dome_with_edges_raised(5e-4)).unwrap();
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap();
|
||||
let (body, _) = tolerances(&imp);
|
||||
assert!((5e-4..=1.1e-3).contains(&body), "body {body}");
|
||||
// Within what the stated uncertainty allows, the body keeps it.
|
||||
let imp = step_import(&dome_with_edges_bowed(5e-5)).unwrap();
|
||||
assert_eq!(tolerances(&imp).0, 1e-6, "{:?}", imp.warnings);
|
||||
}
|
||||
|
||||
/// The dome of [`dome_with_edges_bowed`] whose four edges are polylines along its flat
|
||||
/// boundary, 64 spans each, with one inner point `dz` mm above it: each edge bumps off the dome
|
||||
/// over 1/32 of its length, peaking halfway between two of the points the reader
|
||||
/// samples its gaps at (#12757).
|
||||
fn dome_with_narrow_bumps(dz: f64) -> String {
|
||||
let base = patch_with_edges(0.0, false, false);
|
||||
let sides: [[(usize, usize); 2]; 4] = [[(0, 0), (3, 0)], [(3, 0), (3, 3)], [(3, 3), (0, 3)], [(0, 3), (0, 0)]];
|
||||
let mut t = String::new();
|
||||
for line in base.lines() {
|
||||
// Each side's cubic, #400..#403, is replaced by its polyline (points from #1000).
|
||||
let side = (0..4).find(|k| line.starts_with(&format!("#{}=B_SPLINE_CURVE_WITH_KNOTS", 400 + k)));
|
||||
let Some(k) = side else {
|
||||
t += line;
|
||||
t += "\n";
|
||||
continue;
|
||||
};
|
||||
let [(i0, j0), (i1, j1)] = sides[k];
|
||||
let n = 65;
|
||||
let mut ids = Vec::new();
|
||||
for m in 0..n {
|
||||
let f = m as f64 / (n - 1) as f64;
|
||||
let (x, y) = ((i0 as f64 + (i1 as f64 - i0 as f64) * f) * 10.0, (j0 as f64 + (j1 as f64 - j0 as f64) * f) * 10.0);
|
||||
let id = 1000 + k * 100 + m;
|
||||
t += &format!("#{id}=CARTESIAN_POINT('',({x:.17E},{y:.17E},{:.17E}));\n", if m == 2 { dz } else { 0.0 });
|
||||
ids.push(format!("#{id}"));
|
||||
}
|
||||
let knots: Vec<String> = (0..n).map(|m| format!("{:.17E}", m as f64 / (n - 1) as f64)).collect();
|
||||
let mults: Vec<String> = (0..n).map(|m| if m == 0 || m == n - 1 { "2" } else { "1" }.to_string()).collect();
|
||||
t += &format!(
|
||||
"#{}=B_SPLINE_CURVE_WITH_KNOTS('',1,({}),.UNSPECIFIED.,.F.,.F.,({}),({}),.UNSPECIFIED.);\n",
|
||||
400 + k,
|
||||
ids.join(","),
|
||||
mults.join(","),
|
||||
knots.join(",")
|
||||
);
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edges_gap_between_the_points_sampled_is_covered() {
|
||||
// Each edge bumps 2e-5 mm off the dome, narrowly, between two of the reader's samples
|
||||
// (#12757: 15 edges of an imported body lay up to 1.7 times farther off than sampled, and
|
||||
// SMLib refused it, "Bad Gap"). Its tolerance covers the peak, and the body checks.
|
||||
let imp = step_import(&dome_with_narrow_bumps(2e-5)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let r = imp.bodies[0].body.record();
|
||||
assert!(r.edges.iter().all(|e| e.tolerance >= 2e-5), "{:?}", r.edges.iter().map(|e| e.tolerance).collect::<Vec<_>>());
|
||||
crate::validation::check_kept(&imp.bodies[0].body).unwrap_or_else(|e| panic!("{e}"));
|
||||
}
|
||||
|
||||
/// A vertex's tolerance is six times its gap (#11997) but never past SMLib's 1 mm: the ends of
|
||||
/// edges 0.2 mm off their face take 1 mm and are kept, as they were at 1.5 times the gap. (The
|
||||
/// vertex 0.19 mm off its LINE of #3352 needs no raise now: the edge is read as the segment
|
||||
/// between its vertices, and the part still reads.)
|
||||
#[test]
|
||||
fn raised_vertex_tolerances_stay_within_smlib_limit() {
|
||||
let slipped = step_import(include_str!("../../../../tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step")).unwrap();
|
||||
assert_eq!(slipped.bodies.len(), 1, "{:?}", slipped.warnings);
|
||||
let imp = step_import(&dome_with_edges_bowed(0.3)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let r = imp.bodies[0].body.record();
|
||||
let most = r.vertices.iter().map(|v| v.tolerance).chain(r.edges.iter().map(|e| e.tolerance)).fold(0.0, f64::max);
|
||||
assert!(most > 0.2 && most <= 1.0, "bowed edges: {most}");
|
||||
// Edges bowed about 0.6 mm off: three times the gap passes SMLib's 1 mm, so they take 1 mm and
|
||||
// are kept, as they were at 1.5 times it (a NIST FTC 07 edge 0.64 mm off its face).
|
||||
let far = step_import(&dome_with_edges_bowed(0.8)).unwrap();
|
||||
assert_eq!(far.bodies.len(), 1, "{:?}", far.warnings);
|
||||
assert!(!far.warnings.iter().any(|w| w.contains("left out") || w.contains("imported as surfaces")), "{:?}", far.warnings);
|
||||
let r = far.bodies[0].body.record();
|
||||
let most = r.edges.iter().map(|e| e.tolerance).fold(0.0, f64::max);
|
||||
assert!(most > 0.5 && most <= 1.0, "edges bowed farther: {most}");
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
@@ -66,6 +66,10 @@ fn trims(face: &TrimFace<'_>) -> Result<Vec<Vec<Curve2>>, String> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// The least margin an edge's tolerance covers its gap with where [`SURFACE_GAP_MARGIN`] would
|
||||
/// pass SMLib's limit: it takes the limit instead, as the STEP reader does.
|
||||
const LEAST_GAP_MARGIN: f64 = 1.5;
|
||||
|
||||
/// Each bare use's edge measured against the face's surface as SMLib measures it (over the
|
||||
/// surface's own domain: an edge run a few um past its patch measures how far), and its
|
||||
/// `EDGE_TOLERANCE` in `arrays` raised to cover that gap with [`SURFACE_GAP_MARGIN`]: SMLib keeps
|
||||
@@ -85,15 +89,17 @@ fn cover_gaps(body: &crate::Brep, face: &TrimFace<'_>, bare: &HashSet<(u32, u32,
|
||||
let mut raised = false;
|
||||
for e in edges {
|
||||
let gap = body.edge_face_distance(e, 0).map_err(|e| e.to_string())?;
|
||||
let need = SURFACE_GAP_MARGIN * gap;
|
||||
// The margin up to SMLib's limit; refused only where even the least margin, the reader's
|
||||
// (1.5), would pass it.
|
||||
if !(LEAST_GAP_MARGIN * gap <= crate::flat::MAX_TOLERANCE) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
let need = (SURFACE_GAP_MARGIN * gap).min(crate::flat::MAX_TOLERANCE);
|
||||
let tolerances = arrays.reals.get_mut(&crate::flat::EDGE_TOLERANCE).ok_or("edge tolerances")?;
|
||||
let given = tolerances.get_mut(e).ok_or("edge reference")?;
|
||||
if need <= *given {
|
||||
continue;
|
||||
}
|
||||
if !(need <= crate::flat::MAX_TOLERANCE) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
*given = need;
|
||||
raised = true;
|
||||
}
|
||||
|
||||
+203
-45
@@ -25,7 +25,9 @@
|
||||
//! 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, 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.
|
||||
//! tolerance, and the solid's notes say how many and how far. When those lie farther off than the
|
||||
//! file's uncertainty allows SMLib (a file that understates it), the body's tolerance is raised to
|
||||
//! cover them ([`BODY_TOLERANCE_RATIO`], [`BODY_VERTEX_GAP_MARGIN`]).
|
||||
//!
|
||||
//! 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
|
||||
@@ -83,18 +85,54 @@ const MAX_SAMPLES: usize = 4096;
|
||||
/// EDGE_TOLERANCE and VERTEX_TOLERANCE, `solidcraft_smlib::flat::MAX_TOLERANCE`). Geometry that
|
||||
/// would need more is refused, with how far off it lies.
|
||||
const MAX_TOLERANCE: f64 = 1.0;
|
||||
/// A vertex's tolerance over its largest gap from its curves and surfaces.
|
||||
const VERTEX_GAP_MARGIN: f64 = 1.5;
|
||||
/// Intervals an edge's curve is measured over against each face's surface it bounds.
|
||||
/// The most an edge's or vertex's tolerance may be over its body's. SMLib's check takes no edge
|
||||
/// or vertex zone (half its record tolerance) of 1000 times the body's or more, and its trims
|
||||
/// raise an edge's zone to no more than twice its gap, under its record tolerance: a body over
|
||||
/// `t / 1000` holds an edge or vertex of tolerance `t`, and 250 keeps a margin of four. A body
|
||||
/// whose edges lie farther off than its file's uncertainty allows (a file that understates it,
|
||||
/// #11997) gets the tolerance that keeps them within SMLib's ratio.
|
||||
const BODY_TOLERANCE_RATIO: f64 = 250.0;
|
||||
/// A body's tolerance over the largest gap of a vertex from its faces: SMLib checks that gap
|
||||
/// against the body's intersection tolerance, its record tolerance, whatever the vertex's own.
|
||||
const BODY_VERTEX_GAP_MARGIN: f64 = 2.0;
|
||||
/// A vertex's tolerance over its largest gap from its curves and surfaces: a record tolerance is
|
||||
/// SMLib's intersection tolerance, twice the zone (the bridge halves it), and SMLib checks a
|
||||
/// vertex's gaps against half its zone, so a vertex covers its gap with margin at twice an edge's.
|
||||
const VERTEX_GAP_MARGIN: f64 = 2.0 * pcurve::SURFACE_GAP_MARGIN;
|
||||
/// The least margin a vertex is kept with: one whose gap [`VERTEX_GAP_MARGIN`] would carry past
|
||||
/// [`MAX_TOLERANCE`] takes that limit instead, and is refused only past this one, as before the
|
||||
/// margin was raised (a 0.19 mm slip in a file of 0.01 mm, #3352).
|
||||
const VERTEX_GAP_LEAST_MARGIN: f64 = 1.5;
|
||||
|
||||
/// The tolerance a vertex `gap` mm off its curves and surfaces takes: [`VERTEX_GAP_MARGIN`] times
|
||||
/// it, at most SMLib's [`MAX_TOLERANCE`]; `None` when even [`VERTEX_GAP_LEAST_MARGIN`] would pass it.
|
||||
fn vertex_tolerance(gap: f64) -> Option<f64> {
|
||||
(VERTEX_GAP_LEAST_MARGIN * gap <= MAX_TOLERANCE).then(|| (VERTEX_GAP_MARGIN * gap).min(MAX_TOLERANCE))
|
||||
}
|
||||
/// Intervals an edge's curve is measured over against each face's surface it bounds, at the
|
||||
/// least: each knot span of the edge's curve and pcurve is measured too ([`widest_gap`]).
|
||||
const EDGE_GAP_SAMPLES: usize = 16;
|
||||
/// The least span of a face's parameter box along a direction of its surface that is not
|
||||
/// closed on itself, where its natural range has room ([`face_domain`]): 50 times the 2e-5 mm
|
||||
/// within which SMLib takes two of a surface's sides for one (#12757: a planar sliver face
|
||||
/// 1e-5 mm wide).
|
||||
const OPEN_DOMAIN_SPAN: f64 = 1e-3;
|
||||
/// 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;
|
||||
/// Points measured inside each knot span between two breaks ([`widest_gap`]).
|
||||
const EDGE_GAP_SPAN_SAMPLES: usize = 4;
|
||||
/// Breaks past which an edge's spans are measured at evenly spaced points instead ([`widest_gap`]).
|
||||
const EDGE_GAP_MAX_BREAKS: usize = 1024;
|
||||
/// Local maxima of the measured gaps refined to the gap's peak ([`widest_gap`]).
|
||||
const EDGE_GAP_REFINED: usize = 4;
|
||||
/// Golden-section steps refining a local maximum: its bracket shrinks to 0.618^24 = 1e-5 of
|
||||
/// the span around it.
|
||||
const EDGE_GAP_REFINE_STEPS: usize = 24;
|
||||
/// An edge's tolerance over the largest gap measured between its pcurves and its curve: SMLib's
|
||||
/// zone (half the record's) covers the gap with margin, as a surface's.
|
||||
const EDGE_GAP_MARGIN: f64 = pcurve::SURFACE_GAP_MARGIN;
|
||||
/// The least margin an edge is kept with: one whose gap its margin would carry past
|
||||
/// [`MAX_TOLERANCE`] takes that limit instead, and is refused only past this one, as before the
|
||||
/// margin was raised (a NIST FTC 07 edge 0.64 mm off its face, covered at 0.96 mm before).
|
||||
const EDGE_GAP_LEAST_MARGIN: f64 = 1.5;
|
||||
/// A solid (or sheet) read from the file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Solid {
|
||||
@@ -605,8 +643,11 @@ struct Builder<'b, 'a> {
|
||||
lines_between_vertices: usize,
|
||||
/// Inner loops written running the way the outer loop runs, run the other way.
|
||||
inner_loops_turned: usize,
|
||||
/// Edges whose tolerance was raised to the gap their curve has to a face's surface.
|
||||
/// Edges whose tolerance was raised to a gap of the file's own, or past its uncertainty,
|
||||
/// that their curve has to a face's surface.
|
||||
raised_edges: HashSet<u32>,
|
||||
/// The largest gap of a raised edge.
|
||||
widest_gap: f64,
|
||||
/// The line edges of POLY_LOOPs, by their two CARTESIAN_POINTs (lower id first): faces that
|
||||
/// share a side share its edge, as a faceted B-rep's do.
|
||||
poly_edges: HashMap<(u64, u64), u32>,
|
||||
@@ -630,6 +671,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
lines_between_vertices: 0,
|
||||
inner_loops_turned: 0,
|
||||
raised_edges: HashSet::new(),
|
||||
widest_gap: 0.0,
|
||||
poly_edges: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -656,11 +698,11 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
/// A vertex `gap` mm from where its edge's curve puts it: refused if far off, else its
|
||||
/// tolerance covers the gap.
|
||||
fn raise_vertex(&mut self, v: u32, gap: f64, edge: u64) -> R<()> {
|
||||
if !(VERTEX_GAP_MARGIN * gap <= MAX_TOLERANCE) {
|
||||
let Some(tolerance) = vertex_tolerance(gap) else {
|
||||
return Err(format!("edge #{edge}: a vertex lies {gap} mm off its curve"));
|
||||
}
|
||||
};
|
||||
if let Some(x) = self.vertices.get_mut(v as usize) {
|
||||
x.tolerance = x.tolerance.max(VERTEX_GAP_MARGIN * gap);
|
||||
x.tolerance = x.tolerance.max(tolerance);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -699,33 +741,43 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
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 = if k == EDGE_GAP_SAMPLES { t1 } else { t0 + (t1 - t0) * (k as f64 / EDGE_GAP_SAMPLES as f64) };
|
||||
let Some(p) = eval.point(t) else { continue };
|
||||
let gap = widest_gap([t0, t1], &[curve_breaks(curve)], |t| {
|
||||
let p = eval.point(t)?;
|
||||
// A point with no nearest point is the pcurves' to refuse, with why.
|
||||
let Some(s) = on.project_with(&starts, p, None, f64::INFINITY).and_then(|q| on.point(q)) else { continue };
|
||||
gap = gap.max(s.dist(p));
|
||||
}
|
||||
self.cover_edge(e, gap, pcurve::SURFACE_GAP_MARGIN)?;
|
||||
let s = on.project_with(&starts, p, None, f64::INFINITY).and_then(|q| on.point(q))?;
|
||||
Some(s.dist(p))
|
||||
});
|
||||
self.cover_edge(e, gap, pcurve::SURFACE_GAP_MARGIN, true)?;
|
||||
}
|
||||
}
|
||||
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<()> {
|
||||
/// Edge `e`'s tolerance covers `gap` with `margin`: refused if far off. The solid's notes
|
||||
/// count it when the gap is the file's own (`own`: its curve off its face's surface) or past
|
||||
/// the file's uncertainty; a gap within it to a pcurve the reader made is covered unnoted.
|
||||
fn cover_edge(&mut self, e: u32, gap: f64, margin: f64, own: bool) -> R<()> {
|
||||
let Some(x) = self.edges.get_mut(e as usize) else { return Err("edge reference".into()) };
|
||||
if margin * gap <= x.tolerance {
|
||||
return Ok(());
|
||||
}
|
||||
if !(margin * gap <= MAX_TOLERANCE) {
|
||||
if !(EDGE_GAP_LEAST_MARGIN * gap <= MAX_TOLERANCE) {
|
||||
return Err(format!("an edge lies {gap} mm off the face's surface"));
|
||||
}
|
||||
x.tolerance = margin * gap;
|
||||
let (tolerance, ends) = (x.tolerance, x.vertices);
|
||||
self.raised_edges.insert(e);
|
||||
// A vertex's tolerance covers every edge that meets it, as in any tolerant record.
|
||||
let wanted = (margin * gap).min(MAX_TOLERANCE);
|
||||
if wanted <= x.tolerance {
|
||||
return Ok(());
|
||||
}
|
||||
x.tolerance = wanted;
|
||||
let ends = x.vertices;
|
||||
// A gap within the file's uncertainty to a pcurve this reader made is that pcurve
|
||||
// wandering between its knots, not the file's: covered, but not the user's to hear
|
||||
// about (#12757).
|
||||
if own || gap > self.tol {
|
||||
self.raised_edges.insert(e);
|
||||
self.widest_gap = self.widest_gap.max(gap);
|
||||
}
|
||||
// A vertex's tolerance covers every edge that meets it, as in any tolerant record: its
|
||||
// gap from the face at the edge's end is the edge's there, checked as a vertex's (an edge
|
||||
// kept at its margin is within a vertex's least one, so this is never refused).
|
||||
let tolerance = vertex_tolerance(gap).unwrap_or(MAX_TOLERANCE);
|
||||
for v in ends {
|
||||
if let Some(vx) = self.vertices.get_mut(v as usize) {
|
||||
vx.tolerance = vx.tolerance.max(tolerance);
|
||||
@@ -749,16 +801,16 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
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 = if k == EDGE_GAP_SAMPLES { t1 } else { t0 + (t1 - t0) * (k as f64 / EDGE_GAP_SAMPLES as f64) };
|
||||
let pc_knots = match c {
|
||||
Curve2::Spline { knots, .. } | Curve2::RationalSpline { knots, .. } => knots.as_slice(),
|
||||
};
|
||||
let gap = widest_gap([t0, t1], &[curve_breaks(curve), pc_knots], |t| {
|
||||
// A point a pcurve or surface lacks is the record check's to name.
|
||||
let (Some(q), Some(p)) = (solidcore_ops::pcurve::eval(c, t), eval.point(t)) else { continue };
|
||||
if let Some(s) = on.point(q) {
|
||||
gap = gap.max(s.dist(p));
|
||||
}
|
||||
}
|
||||
self.cover_edge(e, gap, EDGE_GAP_MARGIN)?;
|
||||
let q = solidcore_ops::pcurve::eval(c, t)?;
|
||||
let p = eval.point(t)?;
|
||||
Some(on.point(q)?.dist(p))
|
||||
});
|
||||
self.cover_edge(e, gap, EDGE_GAP_MARGIN, false)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1271,11 +1323,11 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
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)",
|
||||
"{} edges lie off their faces' surfaces farther than {} mm covers: their tolerances cover the gaps they have (up to {:.3e} mm)",
|
||||
self.raised_edges.len(),
|
||||
self.tol
|
||||
self.tol,
|
||||
self.widest_gap
|
||||
));
|
||||
}
|
||||
let flipped = turned.iter().filter(|t| **t).count();
|
||||
@@ -1458,14 +1510,49 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
{
|
||||
let p = v3(x.point);
|
||||
let gap = surface.project(p, None, f64::INFINITY).and_then(|q| surface.point(q)).map_or(f64::INFINITY, |s| s.dist(p));
|
||||
if !(VERTEX_GAP_MARGIN * gap <= MAX_TOLERANCE) {
|
||||
let Some(tolerance) = vertex_tolerance(gap) else {
|
||||
return Err(format!("face #{}: a vertex loop's vertex lies {gap} mm off its surface", face.step));
|
||||
}
|
||||
x.tolerance = x.tolerance.max(VERTEX_GAP_MARGIN * gap);
|
||||
};
|
||||
x.tolerance = x.tolerance.max(tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The body's tolerance covers its raised vertices' gaps from their faces and keeps its
|
||||
// edges' and vertices' tolerances within SMLib's ratio (#11997).
|
||||
let mut vertex_gap = 0.0f64;
|
||||
for &f in &face_order {
|
||||
let Some(face) = self.faces.get(f as usize) else { continue };
|
||||
let mut raised = Vec::new();
|
||||
for l in &face.loops {
|
||||
match l {
|
||||
LoopB::Vertex(v) => raised.push(*v),
|
||||
LoopB::Edges(es) => raised.extend(es.iter().filter_map(|&(e, _)| self.edges.get(e as usize)).flat_map(|x| x.vertices)),
|
||||
}
|
||||
}
|
||||
raised.retain(|&v| self.vertices.get(v as usize).is_some_and(|x| x.tolerance > self.tol));
|
||||
if raised.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let surface = SurfaceEval::new(&face.surface).map_err(|m| m.to_string())?;
|
||||
for v in raised {
|
||||
let Some(x) = self.vertices.get(v as usize) else { continue };
|
||||
let p = v3(x.point);
|
||||
// A vertex the surface does not take is the record check's to name.
|
||||
if let Some(s) = surface.project(p, None, f64::INFINITY).and_then(|q| surface.point(q)) {
|
||||
vertex_gap = vertex_gap.max(s.dist(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
let widest = self.edges.iter().map(|e| e.tolerance).chain(self.vertices.iter().map(|v| v.tolerance)).fold(0.0, f64::max);
|
||||
let tolerance = self.tol.max(widest / BODY_TOLERANCE_RATIO).max(BODY_VERTEX_GAP_MARGIN * vertex_gap);
|
||||
if tolerance > self.tol {
|
||||
notes.push(format!(
|
||||
"the file states its geometry within {} mm, but its edges and vertices need tolerances up to {widest:.3e} mm and its vertices lie up to {vertex_gap:.3e} mm off their faces: the body's tolerance is {tolerance:.3e} mm",
|
||||
self.tol
|
||||
));
|
||||
}
|
||||
|
||||
let mut vertices = Vec::with_capacity(vertex_order.len());
|
||||
for &v in &vertex_order {
|
||||
vertices.push(self.vertices.get(v as usize).cloned().ok_or("vertex reference")?);
|
||||
@@ -1531,7 +1618,7 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
}
|
||||
out_shells.push(Shell::Faces { faces, wires: Vec::new() });
|
||||
}
|
||||
let brep = Brep { version: brep::VERSION, tolerance: self.tol, regions, shells: out_shells, faces, edges, vertices };
|
||||
let brep = Brep { version: brep::VERSION, tolerance, regions, shells: out_shells, faces, edges, vertices };
|
||||
solidcore_ops::record_check::validate(&brep).map_err(|e| format!("the record is invalid: {e}"))?;
|
||||
solidcore_ops::record_check::agreement(&brep).map_err(|e| format!("the record's geometry disagrees: {e}"))?;
|
||||
Ok(Solid { item, name, product: None, brep, face_origin, sheet, notes, turned: turned_faces })
|
||||
@@ -1882,6 +1969,77 @@ fn loop_area(l: &[(u32, bool)], pcs: &[Curve2], periodic: [bool; 2]) -> Option<f
|
||||
(area.abs() > 1e-9 * magnitude && magnitude > 0.0).then_some(0.5 * area)
|
||||
}
|
||||
|
||||
/// The knots of a spline edge curve, where its shape can change; none for a curve in closed form.
|
||||
fn curve_breaks(c: &BCurve) -> &[f64] {
|
||||
match c {
|
||||
BCurve::Spline { knots, .. } | BCurve::RationalSpline { knots, .. } => knots,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// The widest of `gap(t)` over `[t0, t1]`, as SMLib's check finds it: measured at
|
||||
/// [`EDGE_GAP_SAMPLES`] even intervals and at [`EDGE_GAP_SPAN_SAMPLES`] points inside each span
|
||||
/// between the `breaks` (knots of the edge's curve and pcurve, where a gap can rise and fall
|
||||
/// between two even samples: #12757's edges lay up to 1.7 times farther off than 16 samples
|
||||
/// found), then its [`EDGE_GAP_REFINED`] largest local maxima refined by golden section. A `t`
|
||||
/// where `gap` has no value is skipped.
|
||||
fn widest_gap(interval: [f64; 2], breaks: &[&[f64]], gap: impl Fn(f64) -> Option<f64>) -> f64 {
|
||||
let [t0, t1] = interval;
|
||||
if !(t0 < t1) {
|
||||
return gap(t0).unwrap_or(0.0);
|
||||
}
|
||||
let mut cuts: Vec<f64> = (0..=EDGE_GAP_SAMPLES).map(|k| t0 + (t1 - t0) * (k as f64 / EDGE_GAP_SAMPLES as f64)).collect();
|
||||
let inner: Vec<f64> = breaks.iter().flat_map(|b| b.iter().copied()).filter(|t| *t > t0 && *t < t1).collect();
|
||||
if inner.len() <= EDGE_GAP_MAX_BREAKS {
|
||||
cuts.extend(inner);
|
||||
} else {
|
||||
cuts.extend((1..EDGE_GAP_MAX_BREAKS).map(|k| t0 + (t1 - t0) * (k as f64 / EDGE_GAP_MAX_BREAKS as f64)));
|
||||
}
|
||||
cuts.sort_by(f64::total_cmp);
|
||||
cuts.dedup();
|
||||
let mut ts = Vec::new();
|
||||
for w in cuts.windows(2) {
|
||||
let [a, b] = [w.first().copied().unwrap_or(t0), w.get(1).copied().unwrap_or(t1)];
|
||||
ts.extend((0..=EDGE_GAP_SPAN_SAMPLES).map(|k| a + (b - a) * (k as f64 / (EDGE_GAP_SPAN_SAMPLES + 1) as f64)));
|
||||
}
|
||||
ts.push(t1);
|
||||
let samples: Vec<(f64, f64)> = ts.iter().filter_map(|&t| gap(t).map(|g| (t, g))).collect();
|
||||
let mut widest = samples.iter().fold(0.0f64, |m, s| m.max(s.1));
|
||||
// Local maxima, largest first, each refined between its neighbours.
|
||||
let g_at = |i: usize| samples.get(i).map_or(0.0, |s| s.1);
|
||||
let t_at = |i: usize| samples.get(i).map_or(t0, |s| s.0);
|
||||
let mut peaks: Vec<usize> = (0..samples.len())
|
||||
.filter(|&i| {
|
||||
let g = g_at(i);
|
||||
g > 0.0 && (i == 0 || g_at(i - 1) <= g) && samples.get(i + 1).is_none_or(|n| n.1 <= g)
|
||||
})
|
||||
.collect();
|
||||
peaks.sort_by(|a, b| g_at(*b).total_cmp(&g_at(*a)));
|
||||
const PHI: f64 = 0.618_033_988_749_894_9;
|
||||
for &i in peaks.iter().take(EDGE_GAP_REFINED) {
|
||||
let mut a = if i == 0 { t_at(i) } else { t_at(i - 1) };
|
||||
let mut b = samples.get(i + 1).map_or(t_at(i), |n| n.0);
|
||||
let at = |t: f64| gap(t).unwrap_or(0.0);
|
||||
let (mut c, mut d) = (b - PHI * (b - a), a + PHI * (b - a));
|
||||
let (mut gc, mut gd) = (at(c), at(d));
|
||||
for _ in 0..EDGE_GAP_REFINE_STEPS {
|
||||
if gc >= gd {
|
||||
b = d;
|
||||
(d, gd) = (c, gc);
|
||||
c = b - PHI * (b - a);
|
||||
gc = at(c);
|
||||
} else {
|
||||
a = c;
|
||||
(c, gc) = (d, gd);
|
||||
d = a + PHI * (b - a);
|
||||
gd = at(d);
|
||||
}
|
||||
}
|
||||
widest = widest.max(gc).max(gd);
|
||||
}
|
||||
widest
|
||||
}
|
||||
|
||||
/// The parameter box around every pcurve's control points, padded a little, within the
|
||||
/// surface's natural domain (at most one turn where periodic).
|
||||
fn face_domain(surface: &BSurface, pcs: &[Vec<Curve2>]) -> R<[[f64; 2]; 2]> {
|
||||
|
||||
Reference in New Issue
Block a user