kernel: an SMLib body keeps each edge's and vertex's tolerance through its record

SolidCraft's own record of a body SMLib made (the rig's record check, the saved body, the next
SMLib import) re-fitted every pcurve at the kernel's working tolerance, 1e-5 mm. SMLib's fillet
and chamfer surfaces lie some 2e-5 to 2e-4 mm off their faces, so 15 fillet and chamfer cases
failed that check although SMLib's own passed. And SMLib reported one tolerance for a whole body,
the largest any part of it needed, so one loose edge (a pocket cut left one at 0.23 mm) made the
whole body loose on its way back into SMLib.

Now the tolerances go per edge and per vertex, both ways:
- solidcraft-smlib reads and writes the fork's new SMLIB_FLAT_EDGE_TOLERANCE and
  VERTEX_TOLERANCE arrays. Export gives each edge and vertex record its own (never below the
  floor, which becomes the body's). Where those understate what an approximated edge needs on
  its face (SMLib holds a blend's end curve 2.3e-5 mm off its plane at 2e-5), the export falls
  back to SMLib's largest everywhere, as before. Import writes each record edge's and vertex's
  tolerance.
- `geom::brep::pcurve::EdgeUse` carries its edge record's tolerance, and a pcurve is fitted to
  it where it is larger than the face's.
- The kernel's decoded body keeps its record's edge and vertex tolerances
  (`Body::tolerances`, in its edge and vertex order) after an SMLib operation and a saved body's
  decode, and keeps them through a rigid placement. Its record writes them again; anything that
  builds a new solid drops them.

SMLIB_REV moves to the fork commit that adds the arrays (smlib-blends-abi2 4a67471, ABI 2).

Rig and ladder fillet and chamfer cases on SMLib: 43 pass before, 60 now, none lost. Tests: a
pcurve fitted to its use's own tolerance, and the ladder's 08/006 cylinder rim chamfer whose
record, saved body and placed copy all stay valid and SMLib's check takes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 10:45:10 -04:00
parent f73d403830
commit 1d8299aedf
16 changed files with 251 additions and 62 deletions
+9 -4
View File
@@ -38,13 +38,15 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
/// One oriented edge of a loop, as [`pcurves`] reads it: the edge's index (a seam edge, used
/// twice, is fitted once), its curve record and interval, and whether the loop runs along it.
/// twice, is fitted once), its curve record and interval, whether the loop runs along it, and the
/// edge record's own tolerance (its pcurve is fitted to that, never closer than the face's).
#[derive(Clone, Copy, Debug)]
pub struct EdgeUse<'a> {
pub edge: u32,
pub curve: &'a Curve,
pub interval: [f64; 2],
pub forward: bool,
pub tolerance: f64,
}
/// Knot spans a fitted pcurve may have (the schema takes 4096 control points).
@@ -61,8 +63,8 @@ const MAX_USES: usize = 100_000;
const CHECKS: usize = 16;
/// For each loop of a face on `surface`, the pcurve of each of its edge uses, in order, within
/// `tolerance` mm of the edge when lifted onto the surface. Loops are as the record keeps them
/// (relative to the surface's natural normal: outer counter-clockwise, inner clockwise); each
/// `tolerance` mm (or the use's own edge tolerance where larger) of the edge when lifted onto
/// the surface. Loops are as the record keeps them (relative to the surface's natural normal: outer counter-clockwise, inner clockwise); each
/// pcurve runs on its use's interval in its curve's direction, whatever `forward` says.
pub fn pcurves(surface_record: &Surface, loops: &[Vec<EdgeUse<'_>>], tolerance: f64) -> Result<Vec<Vec<Curve2>>> {
if !(tolerance.is_finite() && tolerance > 0.0 && tolerance <= 1e3) {
@@ -87,7 +89,10 @@ pub fn pcurves(surface_record: &Surface, loops: &[Vec<EdgeUse<'_>>], tolerance:
curve(u.curve, 0, &mut budget).map_err(|e| Error::Invalid(format!("loop {w} use {i}: {e}")))?;
interval(&u.interval, curve_natural(u.curve)).map_err(|e| Error::Invalid(format!("loop {w} use {i}: {e}")))?;
let c = CurveEval::new(u.curve)?;
let pc = on.pcurve(u.curve, &c, u.interval, tolerance).map_err(|e| {
// An edge record that holds a looser tolerance than the face's (another
// kernel's approximated edge) is fitted to its own, up to the same cap.
let own = if u.tolerance.is_finite() && u.tolerance <= 1e3 { tolerance.max(u.tolerance) } else { tolerance };
let pc = on.pcurve(u.curve, &c, u.interval, own).map_err(|e| {
let at = |t: f64| c.point(t).map(|p| [p.x, p.y, p.z]);
let message = format!("pcurve of loop {w} use {i} ({:?} to {:?}): {e}", at(u.interval[0]), at(u.interval[1]));
match e {
+39 -24
View File
@@ -11,7 +11,7 @@ fn about_z(origin: [f64; 3], down: bool) -> Frame {
frame(origin, [1., 0., 0.], if down { [0., -1., 0.] } else { [0., 1., 0.] })
}
fn one(curve: &Curve, interval: [f64; 2]) -> Vec<Vec<EdgeUse<'_>>> {
vec![vec![EdgeUse { edge: 0, curve, interval, forward: true }]]
vec![vec![EdgeUse { edge: 0, curve, interval, forward: true, tolerance: 0.0 }]]
}
/// The largest distance between the lifted pcurve and the curve, at 65 points and each span's
/// middle.
@@ -185,10 +185,10 @@ fn a_seam_used_twice_gets_pcurves_a_period_apart() {
let high = Curve::Circle { frame: about_z([0., 0., 4.], false), radius: 3. };
let full = [0., TAU];
let l = vec![
EdgeUse { edge: 0, curve: &low, interval: full, forward: true },
EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: true },
EdgeUse { edge: 2, curve: &high, interval: full, forward: false },
EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: false },
EdgeUse { edge: 0, curve: &low, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: true, tolerance: 0.0 },
EdgeUse { edge: 2, curve: &high, interval: full, forward: false, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: false, tolerance: 0.0 },
];
let pcs = &pcurves(&cylinder, std::slice::from_ref(&l), 1e-9).unwrap()[0];
let (ends, area) = walk(&l, pcs);
@@ -214,8 +214,8 @@ fn meridians_run_straight_into_the_poles() {
let meridian = Curve::Circle { frame: frame([0.; 3], [1., 0., 0.], [0., 0., 1.]), radius: 6. };
let half = [-FRAC_PI_2, FRAC_PI_2];
let l = vec![
EdgeUse { edge: 0, curve: &meridian, interval: half, forward: false },
EdgeUse { edge: 0, curve: &meridian, interval: half, forward: true },
EdgeUse { edge: 0, curve: &meridian, interval: half, forward: false, tolerance: 0.0 },
EdgeUse { edge: 0, curve: &meridian, interval: half, forward: true, tolerance: 0.0 },
];
for tol in [1e-3, 1e-6] {
let pcs = &pcurves(&sphere, std::slice::from_ref(&l), tol).unwrap()[0];
@@ -268,7 +268,8 @@ fn a_seam_from_pole_to_pole_encloses_the_whole_sphere() {
};
for (seam, iv) in [(&circle, [-FRAC_PI_2, FRAC_PI_2]), (&nurbs, [0., 1.])] {
for order in [[true, false], [false, true]] {
let l: Vec<EdgeUse> = order.iter().map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward }).collect();
let l: Vec<EdgeUse> =
order.iter().map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward, tolerance: 0.0 }).collect();
let pcs = &pcurves(&sphere, std::slice::from_ref(&l), tol).unwrap()[0];
let what = format!("{sf:?} {turn} {off} {seam:?} {order:?}");
for pc in pcs {
@@ -306,10 +307,10 @@ fn a_torus_bounded_by_its_seams_spans_both_periods() {
let tube = Curve::Circle { frame: Frame { origin: p(ax.o + ax.x * 10.), x_axis: p(ax.x), y_axis: p(ax.z) }, radius: 2. };
let full = [0., TAU];
let l = vec![
EdgeUse { edge: 0, curve: &around, interval: full, forward: true },
EdgeUse { edge: 1, curve: &tube, interval: full, forward: true },
EdgeUse { edge: 0, curve: &around, interval: full, forward: false },
EdgeUse { edge: 1, curve: &tube, interval: full, forward: false },
EdgeUse { edge: 0, curve: &around, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &tube, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 0, curve: &around, interval: full, forward: false, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &tube, interval: full, forward: false, tolerance: 0.0 },
];
let pcs = &pcurves(&torus, std::slice::from_ref(&l), 1e-9).unwrap()[0];
let (ends, area) = walk(&l, pcs);
@@ -392,10 +393,10 @@ fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
let top = Curve::Circle { frame: about_z([0., 0., small], false), radius: big };
let quarter = Curve::Circle { frame: frame([big, 0., 0.], [1., 0., 0.], [0., 0., 1.]), radius: small };
let l = vec![
EdgeUse { edge: 0, curve: &outer, interval: full, forward: true },
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: true },
EdgeUse { edge: 2, curve: &top, interval: full, forward: false },
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: false },
EdgeUse { edge: 0, curve: &outer, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0 },
EdgeUse { edge: 2, curve: &top, interval: full, forward: false, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0 },
];
for tol in [1e-4, 1e-7] {
closes_around_the_box(&band, &l, [1, 3], None, tol, &format!("torus band, tol {tol}"));
@@ -406,10 +407,10 @@ fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
(Curve::Circle { frame: about_z([0.; 3], false), radius: 3. }, Curve::Circle { frame: about_z([0., 0., 4.], false), radius: 3. });
let line = Curve::Line { start: [3., 0., 0.], end: [3., 0., 4.] };
let l = vec![
EdgeUse { edge: 0, curve: &low, interval: full, forward: true },
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: true },
EdgeUse { edge: 2, curve: &high, interval: full, forward: false },
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: false },
EdgeUse { edge: 0, curve: &low, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0 },
EdgeUse { edge: 2, curve: &high, interval: full, forward: false, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: false, tolerance: 0.0 },
];
closes_around_the_box(&cylinder, &l, [1, 3], None, 1e-7, "cylinder");
// The dome: radius 6 from its equator (v = 0) to its pole (v = 1), the seam a meridian.
@@ -417,9 +418,9 @@ fn a_seam_on_a_spline_closed_in_u_gets_pcurves_a_period_apart() {
let equator = Curve::Circle { frame: about_z([0.; 3], false), radius: 6. };
let meridian = Curve::Circle { frame: frame([0.; 3], [1., 0., 0.], [0., 0., 1.]), radius: 6. };
let l = vec![
EdgeUse { edge: 0, curve: &equator, interval: full, forward: true },
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: true },
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: false },
EdgeUse { edge: 0, curve: &equator, interval: full, forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0 },
EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0 },
];
closes_around_the_box(&dome, &l, [1, 2], Some(2), 1e-7, "dome");
// A spline whose ends merely come near each other stays open: no period, so the seam's
@@ -483,7 +484,7 @@ fn hostile_records_give_errors() {
let collapsed = Curve::Circle { frame: about_z([0.; 3], false), radius: 0. };
assert!(matches!(pcurves(&plane, &one(&collapsed, [0., 1.]), 1e-6), Err(Error::Invalid(_))));
// Too many uses, refused before any work.
let many = vec![vec![EdgeUse { edge: 0, curve: &line, interval: [0., 1.], forward: true }; MAX_USES + 1]];
let many = vec![vec![EdgeUse { edge: 0, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0 }; MAX_USES + 1]];
assert!(matches!(pcurves(&plane, &many, 1e-6), Err(Error::TooLarge(_))));
// A polyline of many points on a curved surface: too many spans for one pcurve.
let cylinder = Surface::Cylinder { frame: about_z([0.; 3], false), radius: 1. };
@@ -506,3 +507,17 @@ fn hostile_records_give_errors() {
let far = Curve::Circle { frame: about_z([0.; 3], false), radius: 1e12 };
let _ = pcurves(&torus, &one(&far, [0., 1.]), 1e-6);
}
/// An edge that lies up to 7.5e-5 mm off its plane (another kernel's approximation) gets no
/// pcurve at the face's 1e-5 mm, and one within its own record tolerance of 2e-4 mm when the
/// use carries it.
#[test]
fn an_edge_is_fitted_to_its_own_tolerance() {
let plane = Surface::PlaneThrough { origin: [1., 2., 3.], u_point: [3., 2., 3.], v_point: [1., 2., 7.] };
let off =
Curve::Spline { knots: vec![0., 0., 0., 0., 1., 1., 1., 1.], points: vec![[1., 2., 3.], [2., 2.0001, 4.], [3., 2.0001, 5.], [4., 2., 6.]] };
assert!(pcurves(&plane, &one(&off, [0., 1.]), 1e-5).is_err());
let own = vec![vec![EdgeUse { edge: 0, curve: &off, interval: [0., 1.], forward: true, tolerance: 2e-4 }]];
let pc = &pcurves(&plane, &own, 1e-5).unwrap()[0][0];
assert!(lift_error(&plane, &off, [0., 1.], pc) <= 2e-4);
}
+1 -1
View File
@@ -36,7 +36,7 @@ fn fit_pcurves(b: &mut Brep) {
.iter()
.map(|u| {
let e = &b.edges[u.edge as usize];
pcurve::EdgeUse { edge: u.edge, curve: &e.curve, interval: e.interval, forward: u.forward }
pcurve::EdgeUse { edge: u.edge, curve: &e.curve, interval: e.interval, forward: u.forward, tolerance: e.tolerance }
})
.collect(),
Loop::Vertex { .. } => vec![],
+20
View File
@@ -342,6 +342,26 @@ mod tests {
assert!((volume(&back) - volume(&r)).abs() < 1e-6 * 8000.0);
}
/// The ladder's 08/006: a 2 mm chamfer of a cylinder's top rim. SMLib's cone and rail lie some
/// 2e-5 mm off the faces they bound, more than the kernel's working tolerance; the body keeps
/// the tolerances SMLib's record gave them, so its own record (and the saved body's) is valid
/// and SMLib's check takes it back.
#[test]
fn an_smlib_blend_keeps_its_record_valid() {
let c = crate::cylinder(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 10.0, 20.0).unwrap();
let r = imp::blend(&c, "chamfer", Kind::Chamfer, &[Vec3::new(10.0, 0.0, 20.0)], &[2.0]).unwrap();
let v = crate::validity(&r);
assert!(v.record.is_ok(), "{:?}", v.record);
assert!(matches!(v.smlib, Some(crate::SmlibCheck::Valid)), "{:?}", v.smlib);
let saved = crate::decode_native_body(&crate::encode_native_body(&r).unwrap()).unwrap();
assert!(crate::validity(&saved).record.is_ok());
assert!(
crate::validity(&saved.placed(&truck_modeling::Matrix4::from_translation(truck_modeling::Vector3::new(5.0, 0.0, 0.0))).unwrap())
.record
.is_ok()
);
}
#[test]
fn options_smlib_cannot_build_are_refused() {
assert!(unsupported("x").to_string().contains("not supported yet"));
+43 -6
View File
@@ -68,6 +68,19 @@ pub struct Body {
/// placed copy, which keeps them where they are, need not turn them again: searching every
/// revolved face of every placed occurrence was most of the time moving an assembly took.
pub(crate) phased: bool,
/// The tolerances the record this solid was decoded from held for its edges and vertices
/// (another kernel's approximations, which lie farther from their faces than this kernel's
/// working tolerance), so that its own record of the solid holds them again. `None`: the
/// working tolerance everywhere. Dropped by anything that makes a new solid.
pub(crate) tolerances: Option<std::sync::Arc<Tolerances>>,
}
/// Per edge and per vertex, in [`Body::unique_edges`] and [`Body::unique_vertices`] order, the
/// tolerance a decoded record gave it (see `Body::tolerances`).
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Tolerances {
pub(crate) edges: Vec<f64>,
pub(crate) vertices: Vec<f64>,
}
/// The meshes made of one solid (see [`Body::keeping_meshes`]), by tolerance, angle and
@@ -684,6 +697,7 @@ impl Body {
display_of: None,
meshes: None,
phased: true,
tolerances: None,
})
}
@@ -736,6 +750,7 @@ impl Body {
display_of: None,
meshes: None,
phased: true,
tolerances: None,
})
}
@@ -753,12 +768,16 @@ impl Body {
return Err(KernelError::Invalid("degenerate placement".into()));
}
let rigid = crate::ops::rigid_placement(&a);
let out = Body::placed_as_is(mt::builder::transformed(&*self.solid, *m), det < 0.0, rigid.then_some(self.coarse_missing).flatten())?;
// A placement that does not change distances leaves the boundaries exactly as far off
// their surfaces as they were, so the file's own figure still describes them (see
// [`Body::boundary_tol`]); one that scales the part does not, and the copy declares
// nothing. The same rule as the mesh count above.
Ok(if rigid { out.with_boundary_tol(self.boundary_tol) } else { out })
let mut placed = Body::placed_as_is(mt::builder::transformed(&*self.solid, *m), det < 0.0, rigid.then_some(self.coarse_missing).flatten())?;
// A rigid placement moves every edge and vertex as it is, in the same order, and leaves
// the boundaries exactly as far off their surfaces as they were, so the file's own figure
// still describes them (see [`Body::boundary_tol`]); one that scales the part does not,
// and the copy declares nothing. The same rule as the mesh count above.
if rigid {
placed.tolerances = self.tolerances.clone();
placed = placed.with_boundary_tol(self.boundary_tol);
}
Ok(placed)
}
pub(crate) fn inherit_regularization_protection(mut self, input: &Body) -> Self {
@@ -1240,6 +1259,24 @@ impl Body {
solid.edge_iter().filter(|e| seen.insert(e.id())).collect()
}
/// The solid's vertices, each once, in the order [`Solid::vertex_iter`] first meets them.
pub(crate) fn unique_vertices(solid: &Solid) -> Vec<mt::Vertex> {
let mut seen = std::collections::HashSet::new();
solid.vertex_iter().filter(|v| seen.insert(v.id())).collect()
}
/// The record tolerances this body keeps for its own solid (`Body::tolerances`), when they
/// still number its edges and vertices.
pub(crate) fn kept_tolerances(&self) -> Option<&Tolerances> {
self.tolerances.as_deref().filter(|t| t.edges.len() == self.edge_count() && t.vertices.len() == Body::unique_vertices(&self.solid).len())
}
/// This body keeping `t` as its record tolerances (see `Body::tolerances`).
pub(crate) fn with_tolerances(mut self, t: Option<Tolerances>) -> Self {
self.tolerances = t.map(std::sync::Arc::new);
self
}
/// Triangulate with chord tolerance `tol` (mm). Triangles carry their face index.
pub fn tessellate(&self, tol: f64) -> Result<Mesh> {
self.tessellate_with(tol, false)
+2
View File
@@ -127,6 +127,7 @@ pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result<Body> {
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: false,
})
}
@@ -227,6 +228,7 @@ impl Body {
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: self.regularization_protected,
})
}
+10 -3
View File
@@ -148,12 +148,12 @@ pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
validate_value(&value, 0)?;
let saved: SavedBody = serde_json::from_value(value).map_err(invalid)?;
validate_topology(&saved)?;
let solid = adapter::decode(saved.solid)?;
let (solid, numbering) = adapter::decode_numbered(saved.solid, adapter::Producer::Own)?;
let Runtime { size, coarse_missing } = saved.runtime;
if !(size.is_finite() && size >= 0.0) || coarse_missing.is_some_and(|(tol, n)| !tol.is_finite() || n > MAX_ITEMS as u64) {
return Err(invalid("runtime values"));
}
let mut body = Body {
let body = Body {
solid: Arc::new(solid),
size,
volume: Some(std::sync::Arc::new(std::sync::Mutex::new(None))),
@@ -167,8 +167,12 @@ pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: false,
};
// A body made by another kernel keeps the tolerances its record saved for it.
let tolerances = adapter::decoded_tolerances(&numbering, &body);
let mut body = body.with_tolerances(tolerances);
metadata::apply(&mut body, saved.metadata)?;
Ok(body)
})
@@ -193,7 +197,10 @@ pub(crate) fn body_from_smlib_ordered(brep: &solidcraft_smlib::Brep) -> Result<(
let (solid, numbering) = adapter::decode_numbered(record, adapter::Producer::Foreign)?;
let body = Body::oriented(solid)?;
let kept = numbering.kept_by(&body.solid);
Ok((body, kept.then_some(order)))
// SMLib's approximated edges lie farther from their faces than the kernel's working
// tolerance: the body keeps the tolerances the record gave them, so its own record does.
let tolerances = adapter::decoded_tolerances(&numbering, &body);
Ok((body.with_tolerances(tolerances), kept.then_some(order)))
})
}
+55 -13
View File
@@ -100,19 +100,23 @@ pub(super) fn encode(body: &Body) -> Result<b::Brep> {
encode_within(body, working_tolerance(body))?
}
/// The record of `body`'s solid with `tolerance` for the body, its edges and vertices.
/// `Ok(Err(_))`: some edge lies farther than that from a face it bounds, so no pcurve fits it
/// within the tolerance.
/// The record of `body`'s solid with `tolerance` for the body, its edges and vertices, or for
/// an edge or vertex the larger tolerance the body keeps for it from the record it was decoded
/// from (`Body::tolerances`). `Ok(Err(_))`: some edge lies farther than its tolerance from a face
/// it bounds, so no pcurve fits it.
pub(super) fn encode_within(body: &Body, tolerance: f64) -> Result<Result<b::Brep>> {
let solid = &body.solid;
let shells = solid.boundaries();
let regions = regions(body)?;
let (edge_tol, vertex_tol) = kept_tolerances(body);
let edge_tolerance = |e: &mt::Edge| edge_tol.get(&e.id()).map_or(tolerance, |t| t.max(tolerance));
let vertex_tolerance = |v: &mt::Vertex| vertex_tol.get(&v.id()).map_or(tolerance, |t| t.max(tolerance));
// Faces in truck's order; edges and vertices in first appearance, each edge's curve record
// and interval written when it first appears.
let mut face_index = HashMap::new();
let mut edge_index = HashMap::new();
let mut edges: Vec<(mt::Edge, b::Curve, [f64; 2])> = Vec::new();
let mut edges: Vec<(mt::Edge, b::Curve, [f64; 2], f64)> = Vec::new();
let mut uses: Vec<Vec<b::EdgeUseRef>> = Vec::new();
let mut faces = Vec::new();
let mut shell_records = Vec::new();
@@ -136,7 +140,7 @@ pub(super) fn encode_within(body: &Body, tolerance: f64) -> Result<Result<b::Bre
None => {
let curve = e.curve();
let (t0, t1) = curve.try_range_tuple().ok_or_else(|| invalid("unbounded edge curve"))?;
edges.push((e.absolute_clone(), save_curve(&curve, 0)?, [t0, t1]));
edges.push((e.absolute_clone(), save_curve(&curve, 0)?, [t0, t1], edge_tolerance(e)));
uses.push(Vec::new());
edge_index.insert(e.id(), edges.len() - 1);
edges.len() - 1
@@ -156,18 +160,18 @@ pub(super) fn encode_within(body: &Body, tolerance: f64) -> Result<Result<b::Bre
let mut vertex_index = HashMap::new();
let mut vertices = Vec::new();
let mut edge_records = Vec::with_capacity(edges.len());
for ((e, curve, interval), uses) in edges.into_iter().zip(uses) {
for ((e, curve, interval, edge_tolerance), uses) in edges.into_iter().zip(uses) {
if uses.len() > 2 {
return Err(unsupported("the radial order of an edge with more than two uses"));
}
let mut vertex = |x: &mt::Vertex| {
*vertex_index.entry(x.id()).or_insert_with(|| {
vertices.push(b::Vertex { point: point(x.point()), tolerance });
vertices.push(b::Vertex { point: point(x.point()), tolerance: vertex_tolerance(x) });
vertices.len() - 1
})
};
let ends = [vertex(e.absolute_front()), vertex(e.absolute_back())];
edge_records.push(b::Edge { curve, interval, vertices: [index(ends[0])?, index(ends[1])?], tolerance, uses });
edge_records.push(b::Edge { curve, interval, vertices: [index(ends[0])?, index(ends[1])?], tolerance: edge_tolerance, uses });
}
let result = b::Brep { version: b::VERSION, tolerance, regions, shells: shell_records, faces, edges: edge_records, vertices };
result.validate().map_err(invalid)?;
@@ -181,7 +185,7 @@ fn face_record(
face: &mt::Face,
wires: &[mt::Wire],
edge_index: &HashMap<mt::EdgeID, usize>,
edges: &[(mt::Edge, b::Curve, [f64; 2])],
edges: &[(mt::Edge, b::Curve, [f64; 2], f64)],
tolerance: f64,
) -> Result<Result<b::Face>> {
let surface = save_surface(&face.surface(), 0)?;
@@ -191,8 +195,8 @@ fn face_record(
.edge_iter()
.map(|e| {
let i = *edge_index.get(&e.id()).ok_or_else(|| invalid("edge index"))?;
let (_, curve, interval) = edges.get(i).ok_or_else(|| invalid("edge index"))?;
Ok(b::pcurve::EdgeUse { edge: index(i)?, curve, interval: *interval, forward: e.orientation() })
let (_, curve, interval, tolerance) = edges.get(i).ok_or_else(|| invalid("edge index"))?;
Ok(b::pcurve::EdgeUse { edge: index(i)?, curve, interval: *interval, forward: e.orientation(), tolerance: *tolerance })
})
.collect::<Result<Vec<_>>>()?;
uses.push(l);
@@ -222,7 +226,7 @@ pub(super) fn face_alone(face: &mt::Face, tolerance: f64) -> Result<b::Face> {
if let std::collections::hash_map::Entry::Vacant(slot) = edge_index.entry(e.id()) {
let curve = e.curve();
let (t0, t1) = curve.try_range_tuple().ok_or_else(|| invalid("unbounded edge curve"))?;
edges.push((e.absolute_clone(), save_curve(&curve, 0)?, [t0, t1]));
edges.push((e.absolute_clone(), save_curve(&curve, 0)?, [t0, t1], tolerance));
slot.insert(edges.len() - 1);
}
}
@@ -337,10 +341,12 @@ pub(super) enum Producer {
/// The solid of a record: validated, its geometry checked against its topology (truck builds
/// edges and faces from it unchecked), then built as the kernel built it.
#[cfg(test)]
pub(super) fn decode(body: b::Brep) -> Result<mt::Solid> {
decode_from(body, Producer::Own)
}
#[cfg(test)]
pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid> {
decode_numbered(body, producer).map(|(solid, _)| solid)
}
@@ -352,6 +358,9 @@ pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid
pub(super) struct Numbering {
pub faces: Vec<(mt::FaceID, std::collections::HashSet<mt::EdgeID>)>,
pub edges: Vec<mt::EdgeID>,
/// Each record edge's and vertex's tolerance, by its identity in the solid.
pub edge_tolerances: Vec<(mt::EdgeID, f64)>,
pub vertex_tolerances: Vec<(mt::VertexID, f64)>,
}
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
@@ -367,6 +376,32 @@ impl Numbering {
}
}
/// The tolerances a decoded record gave `body`'s edges and vertices, in the body's order, where
/// any is larger than the body's working tolerance (another kernel's approximated geometry);
/// `None` otherwise. An edge or vertex the record does not name (a face rebuilt on decode) takes
/// the largest the record gave.
pub(super) fn decoded_tolerances(numbering: &Numbering, body: &Body) -> Option<crate::body::Tolerances> {
let floor = working_tolerance(body);
let most = numbering.edge_tolerances.iter().map(|(_, t)| *t).chain(numbering.vertex_tolerances.iter().map(|(_, t)| *t)).fold(0.0, f64::max);
if !(most.is_finite() && most > floor) {
return None;
}
let edges: HashMap<mt::EdgeID, f64> = numbering.edge_tolerances.iter().copied().collect();
let vertices: HashMap<mt::VertexID, f64> = numbering.vertex_tolerances.iter().copied().collect();
Some(crate::body::Tolerances {
edges: Body::unique_edges(&body.solid).iter().map(|e| edges.get(&e.id()).copied().unwrap_or(most)).collect(),
vertices: Body::unique_vertices(&body.solid).iter().map(|v| vertices.get(&v.id()).copied().unwrap_or(most)).collect(),
})
}
/// The tolerances `body` keeps for its edges and vertices (`Body::tolerances`), by identity.
fn kept_tolerances(body: &Body) -> (HashMap<mt::EdgeID, f64>, HashMap<mt::VertexID, f64>) {
let Some(t) = body.kept_tolerances() else { return (HashMap::new(), HashMap::new()) };
let edges = Body::unique_edges(&body.solid).iter().map(mt::Edge::id).zip(t.edges.iter().copied()).collect();
let vertices = Body::unique_vertices(&body.solid).iter().map(mt::Vertex::id).zip(t.vertices.iter().copied()).collect();
(edges, vertices)
}
/// [`decode_from`], with each record face's and edge's identity in the solid: a caller holding
/// indices into the record (an SMLib history) checks with them that the solid's face and edge
/// order is the record's.
@@ -375,8 +410,10 @@ pub(super) fn decode_numbered(body: b::Brep, producer: Producer) -> Result<(mt::
body.check_geometry().map_err(invalid)?;
compatible(&body, producer)?;
let b::Brep { shells, faces, edges, vertices, .. } = body;
let vertex_tolerances_in: Vec<f64> = vertices.iter().map(|v| v.tolerance).collect();
let vertices: Vec<mt::Vertex> = vertices.into_iter().map(|v| mt::Vertex::new(p(v.point))).collect();
let vertex = |i: u32| vertices.get(i as usize).ok_or_else(|| invalid("edge vertices"));
let edge_tolerances_in: Vec<f64> = edges.iter().map(|e| e.tolerance).collect();
let edges = edges
.into_iter()
.map(|e| Ok(mt::Edge::new_unchecked(vertex(e.vertices[0])?, vertex(e.vertices[1])?, load_edge_curve(e.curve, e.interval)?)))
@@ -406,7 +443,12 @@ pub(super) fn decode_numbered(body: b::Brep, producer: Producer) -> Result<(mt::
return Err(invalid("non-manifold shell"));
}
let faces = face_ids.into_iter().collect::<Option<Vec<_>>>().ok_or_else(|| invalid("a face no shell uses"))?;
let numbering = Numbering { faces, edges: edges.iter().map(mt::Edge::id).collect() };
let numbering = Numbering {
faces,
edges: edges.iter().map(mt::Edge::id).collect(),
edge_tolerances: edges.iter().map(mt::Edge::id).zip(edge_tolerances_in).collect(),
vertex_tolerances: vertices.iter().map(mt::Vertex::id).zip(vertex_tolerances_in).collect(),
};
Ok((mt::Solid::new_unchecked(shells), numbering))
}
/// A face's topology as recorded, references already checked: built as the kernel built it,
+1
View File
@@ -516,6 +516,7 @@ fn divided(body: &Body, faces: &[usize], other: &[(mt::Edge, Piece)], fr: &Frame
display_of: None,
meshes: None,
phased: false,
tolerances: None,
};
Ok(Some(record::rebuilt_body(split, &[&body.solid], &rec)))
}
+2
View File
@@ -648,6 +648,7 @@ pub(crate) fn split_edges_at(body: &Body, points: &[Vec3]) -> Result<Body> {
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: true,
};
Ok(crate::record::rebuilt_body(split, &[&body.solid], &rec))
@@ -1186,6 +1187,7 @@ pub(crate) fn split_any(body: &Body, faces: &[usize], tool: &SplitTool) -> Resul
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: true,
};
Ok((crate::record::rebuilt_body(split, &[&body.solid], &imp.rec), imp))
+1
View File
@@ -45,6 +45,7 @@ fn surface_of(shell: mt::Shell) -> Body {
display_of: None,
meshes: None,
phased: false,
tolerances: None,
regularization_protected: false,
}
}
+1 -1
View File
@@ -1 +1 @@
d041e7f9679436a4ad3a9620861965dd41105a27
ca2559f785a662c9ec589aa2cdcfaf3217597db8
+61 -9
View File
@@ -24,8 +24,10 @@
//! - **Pcurves:** computed from each edge record on its face's surface record
//! (`solidcraft_geom::brep::pcurve`), never SMLib's UV trims, moved by whole periods into the
//! face's domain.
//! - **Tolerances:** SMLib's intersection tolerance, or 10⁻⁶ of the body's size (at least 10⁻⁵ mm)
//! when larger, for the body, every edge and every vertex.
//! - **Tolerances:** each edge's and vertex's own as SMLib reports it (`EDGE_TOLERANCE`,
//! `VERTEX_TOLERANCE`), never below a floor of 10⁻⁶ of the body's size (at least 10⁻⁵ mm),
//! which is the body's; from a library without them, SMLib's intersection tolerance (or the
//! floor where larger) for the body, every edge and every vertex.
use std::f64::consts::TAU;
use std::collections::HashMap;
@@ -214,6 +216,8 @@ const F64_IDS: &[i32] = &[
FACE_TORUS_REF_DIRECTION,
FACE_TORUS_MAJOR_RADIUS,
FACE_TORUS_MINOR_RADIUS,
EDGE_TOLERANCE,
VERTEX_TOLERANCE,
];
// The kind values as the arrays hold them (int64); a local item shadows the glob import.
@@ -561,6 +565,7 @@ fn onto(r: [f64; 2], natural: b::Natural) -> [f64; 2] {
// ---- The mapping ----------------------------------------------------------------------------
/// One face before renumbering: loops relative to the record's normal, as (edge, forward).
#[derive(Clone)]
struct Face {
surface: b::Surface,
domain: [[f64; 2]; 2],
@@ -607,11 +612,26 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
return Err(invalid("extent"));
}
let floor = (1e-6 * size).max(1e-5);
let tolerance = match flat.f(TOLERANCE) {
let largest = match flat.f(TOLERANCE) {
[t] if t.is_finite() && *t > floor => *t,
[_] => floor,
_ => return Err(invalid("tolerance")),
};
// Each edge's and vertex's own tolerance where SMLib gives them (never below the floor); the
// body's is then the floor, and only what needs more carries more. Without them, every
// tolerance is the largest SMLib reports for the body.
let own = |id: i32, n: usize| -> Res<Option<Vec<f64>>> {
match flat.f(id) {
[] => Ok(None),
t if t.len() == n && t.iter().all(|x| x.is_finite() && *x > 0.0) => Ok(Some(t.iter().map(|x| x.max(floor)).collect())),
_ => Err(invalid("edge or vertex tolerances")),
}
};
let n_vertex_points = flat.f(VERTEX_POSITION).len() / 3;
let edge_tolerances = own(EDGE_TOLERANCE, flat.i(EDGE_CURVE_TYPE).len())?;
let vertex_tolerances = own(VERTEX_TOLERANCE, n_vertex_points)?;
let tolerance = if edge_tolerances.is_some() && vertex_tolerances.is_some() { floor } else { largest };
let tolerances = Tolerances { body: tolerance, edges: edge_tolerances, vertices: vertex_tolerances };
// Topology blocks.
let n_faceuses = flat.i(FACEUSE_FACE).len();
@@ -695,7 +715,7 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
faces.push(Face { surface, domain, agrees: true, loops: out });
}
for face in &mut faces {
if outer_area(face, &edges, tolerance)? < 0.0 {
if outer_area(face, &edges, largest)? < 0.0 {
face.agrees = false;
for l in &mut face.loops {
if let Loop::Edges(uses) = l {
@@ -796,10 +816,22 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
}
let shells: Vec<Vec<(u32, bool)>> = order.iter().filter_map(|s| shells.get(*s as usize).cloned()).collect();
canonical(Parts { faces, edges, shells, regions }, n_vertices, flat, tolerance).map(|(brep, order)| (brep, report, order))
let parts = Parts { faces, edges, shells, regions };
let made = match canonical(parts.clone(), n_vertices, flat, &tolerances) {
// SMLib's own tolerance for an edge or vertex can understate what its approximated
// geometry needs here (a blend's end curve 2.3e-5 mm off its plane at 2e-5): then the
// largest SMLib reports for the body, everywhere, as without per-edge tolerances.
Err(e) if tolerances.edges.is_some() || tolerances.vertices.is_some() => {
let uniform = Tolerances { body: largest, edges: None, vertices: None };
canonical(parts, n_vertices, flat, &uniform).map_err(|_| e)
}
other => other,
};
made.map(|(brep, order)| (brep, report, order))
}
/// The mapped records before renumbering.
#[derive(Clone)]
struct Parts {
faces: Vec<Face>,
edges: Vec<Edge>,
@@ -811,9 +843,28 @@ struct Parts {
/// the faces (a face whose first oriented face is reversed walked backwards), vertices through
/// the edges and then the vertex loops, with each face's pcurves; validated. With it, the
/// canonical index of each array face and edge.
fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res<(b::Brep, Order)> {
/// The record's tolerances: the body's, and each array edge's and vertex's own where given.
struct Tolerances {
body: f64,
edges: Option<Vec<f64>>,
vertices: Option<Vec<f64>>,
}
impl Tolerances {
fn edge(&self, e: usize) -> f64 {
self.edges.as_ref().and_then(|t| t.get(e)).copied().unwrap_or(self.body)
}
fn vertex(&self, v: usize) -> f64 {
self.vertices.as_ref().and_then(|t| t.get(v)).copied().unwrap_or(self.body)
}
}
fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerances) -> Res<(b::Brep, Order)> {
let tolerance = tolerances.body;
let Parts { mut faces, mut edges, shells, regions } = parts;
let n_edges = edges.len();
// A copy made by the split keeps the tolerance of the array edge or vertex it copies.
let (edge_source, vertex_source) = split_shared(&mut faces, &mut edges, &shells, n_vertices)?;
let n_vertices = vertex_source.len();
let mut face_new: Vec<Option<u32>> = vec![None; faces.len()];
@@ -899,7 +950,8 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res
let mut edge_records = Vec::with_capacity(edge_order.len());
for ((&e, vertices), uses) in edge_order.iter().zip(edge_vertices).zip(edge_uses) {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
edge_records.push(b::Edge { curve: edge.curve.clone(), interval: edge.interval, vertices, tolerance, uses });
let source = edge_source.get(e as usize).copied().ok_or_else(|| invalid("edge"))?;
edge_records.push(b::Edge { curve: edge.curve.clone(), interval: edge.interval, vertices, tolerance: tolerances.edge(source), uses });
}
let mut face_records = Vec::with_capacity(face_order.len());
for (fi, &f) in face_order.iter().enumerate() {
@@ -935,7 +987,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res
.iter()
.map(|&v| {
let at = vertex_source.get(v as usize).copied().ok_or_else(|| invalid("vertex"))?;
Ok(b::Vertex { point: v3(positions, at, "vertex")?, tolerance })
Ok(b::Vertex { point: v3(positions, at, "vertex")?, tolerance: tolerances.vertex(at) })
})
.collect::<Res<Vec<_>>>()?;
let shells = shells
@@ -1069,7 +1121,7 @@ fn pcurves(surface: &b::Surface, domain: [[f64; 2]; 2], loops: &[Loop], edges: &
let mut loop_uses = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
loop_uses.push(b::pcurve::EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward });
loop_uses.push(b::pcurve::EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward, tolerance: edge.tolerance });
}
uses.push(loop_uses);
}
+2
View File
@@ -100,6 +100,8 @@ pub const FACE_TORUS_AXIS: i32 = 160;
pub const FACE_TORUS_REF_DIRECTION: i32 = 161;
pub const FACE_TORUS_MAJOR_RADIUS: i32 = 162;
pub const FACE_TORUS_MINOR_RADIUS: i32 = 163;
pub const EDGE_TOLERANCE: i32 = 164;
pub const VERTEX_TOLERANCE: i32 = 165;
/// `SMLIB_FLAT_MAX_LEN`: no array is longer.
pub const MAX_LEN: usize = 1 << 27;
pub const SURFACE_NURBS: u8 = 0;
+3
View File
@@ -203,12 +203,15 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
curve(&mut a, &e.curve)?;
a.i(flat::EDGE_VERTICES).extend([i64::from(e.vertices[0]), i64::from(e.vertices[1])]);
a.r(flat::EDGE_RANGE).extend(e.interval);
// Each edge's own tolerance, never below the body's.
a.r(flat::EDGE_TOLERANCE).push(e.tolerance.max(brep.tolerance));
}
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
for v in &brep.vertices {
a.i(flat::VERTEX_POINT_TYPE).push(1);
a.r3(flat::VERTEX_POSITION, v.point);
a.r(flat::VERTEX_TOLERANCE).push(v.tolerance.max(brep.tolerance));
for k in 0..3 {
lo[k] = lo[k].min(v.point[k]);
hi[k] = hi[k].max(v.point[k]);
+1 -1
View File
@@ -931,7 +931,7 @@ impl<'b, 'a> Builder<'b, 'a> {
let mut u = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
u.push(EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward });
u.push(EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward, tolerance: 0.0 });
}
uses.push(u);
}