diff --git a/crates/geom/src/brep/pcurve.rs b/crates/geom/src/brep/pcurve.rs index ec03c7154c..8685656f32 100644 --- a/crates/geom/src/brep/pcurve.rs +++ b/crates/geom/src/brep/pcurve.rs @@ -38,13 +38,15 @@ pub enum Error { type Result = std::result::Result; /// 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>], tolerance: f64) -> Result>> { if !(tolerance.is_finite() && tolerance > 0.0 && tolerance <= 1e3) { @@ -87,7 +89,10 @@ pub fn pcurves(surface_record: &Surface, loops: &[Vec>], 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 { diff --git a/crates/geom/src/brep/pcurve_tests.rs b/crates/geom/src/brep/pcurve_tests.rs index c696bdf709..b00e93fe1f 100644 --- a/crates/geom/src/brep/pcurve_tests.rs +++ b/crates/geom/src/brep/pcurve_tests.rs @@ -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![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 = order.iter().map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward }).collect(); + let l: Vec = + 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); +} diff --git a/crates/geom/src/brep/tests.rs b/crates/geom/src/brep/tests.rs index b4c71fc897..2f9390f582 100644 --- a/crates/geom/src/brep/tests.rs +++ b/crates/geom/src/brep/tests.rs @@ -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![], diff --git a/crates/kernel/src/blend_smlib.rs b/crates/kernel/src/blend_smlib.rs index 94e140d466..d0711769b6 100644 --- a/crates/kernel/src/blend_smlib.rs +++ b/crates/kernel/src/blend_smlib.rs @@ -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")); diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index eac51497b7..8d4614adf5 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -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>, +} + +/// 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, + pub(crate) vertices: Vec, } /// 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 { + 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) -> 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 { self.tessellate_with(tol, false) diff --git a/crates/kernel/src/meshbody.rs b/crates/kernel/src/meshbody.rs index 1b859f6a6e..34729b927e 100644 --- a/crates/kernel/src/meshbody.rs +++ b/crates/kernel/src/meshbody.rs @@ -127,6 +127,7 @@ pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result { 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, }) } diff --git a/crates/kernel/src/native.rs b/crates/kernel/src/native.rs index c87ec635ea..339ca2943a 100644 --- a/crates/kernel/src/native.rs +++ b/crates/kernel/src/native.rs @@ -148,12 +148,12 @@ pub fn decode_native_body(bytes: &[u8]) -> Result { 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 { 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))) }) } diff --git a/crates/kernel/src/native/adapter.rs b/crates/kernel/src/native/adapter.rs index f0a8a7cad1..0553427d26 100644 --- a/crates/kernel/src/native/adapter.rs +++ b/crates/kernel/src/native/adapter.rs @@ -100,19 +100,23 @@ pub(super) fn encode(body: &Body) -> Result { 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> { 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::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 { 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 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, - edges: &[(mt::Edge, b::Curve, [f64; 2])], + edges: &[(mt::Edge, b::Curve, [f64; 2], f64)], tolerance: f64, ) -> Result> { 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::>>()?; uses.push(l); @@ -222,7 +226,7 @@ pub(super) fn face_alone(face: &mt::Face, tolerance: f64) -> Result { 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 { decode_from(body, Producer::Own) } +#[cfg(test)] pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result { decode_numbered(body, producer).map(|(solid, _)| solid) } @@ -352,6 +358,9 @@ pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result)>, pub edges: Vec, + /// 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 { + 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 = numbering.edge_tolerances.iter().copied().collect(); + let vertices: HashMap = 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, HashMap) { + 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 = vertices.iter().map(|v| v.tolerance).collect(); let vertices: Vec = 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 = 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::>>().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, diff --git a/crates/kernel/src/outline.rs b/crates/kernel/src/outline.rs index 456e88c10b..2615769852 100644 --- a/crates/kernel/src/outline.rs +++ b/crates/kernel/src/outline.rs @@ -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))) } diff --git a/crates/kernel/src/splitface.rs b/crates/kernel/src/splitface.rs index b6551a5346..e511edfc1f 100644 --- a/crates/kernel/src/splitface.rs +++ b/crates/kernel/src/splitface.rs @@ -648,6 +648,7 @@ pub(crate) fn split_edges_at(body: &Body, points: &[Vec3]) -> Result { 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)) diff --git a/crates/kernel/src/surfaces.rs b/crates/kernel/src/surfaces.rs index 49ae2fb0c7..51da727815 100644 --- a/crates/kernel/src/surfaces.rs +++ b/crates/kernel/src/surfaces.rs @@ -45,6 +45,7 @@ fn surface_of(shell: mt::Shell) -> Body { display_of: None, meshes: None, phased: false, + tolerances: None, regularization_protected: false, } } diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index 8928e54f7e..06f76d6818 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -d041e7f9679436a4ad3a9620861965dd41105a27 +ca2559f785a662c9ec589aa2cdcfaf3217597db8 diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index 41b14cf615..9308cd73da 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -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>> { + 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> = 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, edges: Vec, @@ -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>, + vertices: Option>, +} + +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> = 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::>>()?; 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); } diff --git a/crates/smlib/src/flat.rs b/crates/smlib/src/flat.rs index a8f3907b7b..91b8de6b45 100644 --- a/crates/smlib/src/flat.rs +++ b/crates/smlib/src/flat.rs @@ -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; diff --git a/crates/smlib/src/import.rs b/crates/smlib/src/import.rs index 9f137ae611..bf74a07193 100644 --- a/crates/smlib/src/import.rs +++ b/crates/smlib/src/import.rs @@ -203,12 +203,15 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R { 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]); diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index 9328a55499..95c5c7293e 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -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); }