diff --git a/crates/geom/src/brep/pcurve.rs b/crates/geom/src/brep/pcurve.rs index 67fc845af6..4e80223218 100644 --- a/crates/geom/src/brep/pcurve.rs +++ b/crates/geom/src/brep/pcurve.rs @@ -38,8 +38,12 @@ 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, 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). +/// twice, is fitted once), its curve record and interval, whether the loop runs along it, the +/// edge record's own tolerance (its pcurve is fitted to that, never closer than the face's), and +/// the use's exact pcurve where the producer holds one (`given`, on the use's interval in its +/// curve's direction: a kernel's own UV trim mapped exactly into the record's parameters). A +/// given pcurve is taken as it is, only moved by whole periods with its loop; it is never +/// checked against the edge or replaced here (`Brep::check_geometry` checks it). #[derive(Clone, Copy, Debug)] pub struct EdgeUse<'a> { pub edge: u32, @@ -47,6 +51,7 @@ pub struct EdgeUse<'a> { pub interval: [f64; 2], pub forward: bool, pub tolerance: f64, + pub given: Option<&'a Curve2>, } /// Knot spans a fitted pcurve may have (the schema takes 4096 control points). @@ -97,6 +102,10 @@ pub fn pcurves_within( for (w, l) in loops.iter().enumerate() { let mut fitted = Vec::with_capacity(l.len()); for (i, u) in l.iter().enumerate() { + if let Some(given) = u.given { + fitted.push((own_pcurve(given, u.interval, &mut budget).map_err(|e| Error::Invalid(format!("loop {w} use {i}: {e}")))?, u.forward)); + continue; + } let pc = match done.iter().find(|(e, _)| *e == u.edge) { Some((_, pc)) => pc.clone(), None => { @@ -127,6 +136,18 @@ pub fn pcurves_within( Ok(out) } +/// A producer's own pcurve, checked as a record: a valid 2D B-spline on the use's interval +/// (within 10⁻⁹ of its magnitude, the schema's slack for a linear remap's rounding). +fn own_pcurve(c: &Curve2, [t0, t1]: [f64; 2], budget: &mut Budget) -> Result { + super::curve2::curve2(c, budget).map_err(|e| Error::Invalid(format!("given pcurve: {e}")))?; + let [a, b] = c.interval().ok_or_else(|| Error::Invalid("given pcurve: no interval".into()))?; + let slack = 1e-9 * t0.abs().max(t1.abs()).max(1.0); + if (a - t0).abs() > slack || (b - t1).abs() > slack { + return Err(Error::Invalid(format!("given pcurve on [{a}, {b}], its edge on [{t0}, {t1}]"))); + } + Ok(c.clone()) +} + /// A face's surface: its evaluator and parameter box. struct On { surface: SurfaceEval, diff --git a/crates/geom/src/brep/pcurve_tests.rs b/crates/geom/src/brep/pcurve_tests.rs index e0dca631b6..93d314dd77 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, tolerance: 0.0 }]] + vec![vec![EdgeUse { edge: 0, curve, interval, forward: true, tolerance: 0.0, given: None }]] } /// 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, 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 }, + EdgeUse { edge: 0, curve: &low, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 2, curve: &high, interval: full, forward: false, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &seam, interval: [0., 1.], forward: false, tolerance: 0.0, given: None }, ]; 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, tolerance: 0.0 }, - EdgeUse { edge: 0, curve: &meridian, interval: half, forward: true, tolerance: 0.0 }, + EdgeUse { edge: 0, curve: &meridian, interval: half, forward: false, tolerance: 0.0, given: None }, + EdgeUse { edge: 0, curve: &meridian, interval: half, forward: true, tolerance: 0.0, given: None }, ]; for tol in [1e-3, 1e-6] { let pcs = &pcurves(&sphere, std::slice::from_ref(&l), tol).unwrap()[0]; @@ -246,8 +246,8 @@ fn meridians_at_a_pole_span_the_face_box() { for (box_u, span) in [([0., 1.5 * PI], 1.5 * PI), ([-FRAC_PI_2, 0.], FRAC_PI_2)] { for flip in [false, true] { let l = vec![ - EdgeUse { edge: 0, curve: &first, interval: half, forward: !flip, tolerance: 0.0 }, - EdgeUse { edge: 1, curve: &last, interval: half, forward: flip, tolerance: 0.0 }, + EdgeUse { edge: 0, curve: &first, interval: half, forward: !flip, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &last, interval: half, forward: flip, tolerance: 0.0, given: None }, ]; let pcs = &pcurves_within(&sphere, std::slice::from_ref(&l), 1e-7, Some([box_u, [-FRAC_PI_2, FRAC_PI_2]])).unwrap()[0]; let (a, b) = (pcs[0].eval(0.).unwrap()[0], pcs[1].eval(0.).unwrap()[0]); @@ -290,8 +290,10 @@ 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, tolerance: 0.0 }).collect(); + let l: Vec = order + .iter() + .map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward, tolerance: 0.0, given: None }) + .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 { @@ -329,10 +331,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, 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 }, + EdgeUse { edge: 0, curve: &around, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &tube, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 0, curve: &around, interval: full, forward: false, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &tube, interval: full, forward: false, tolerance: 0.0, given: None }, ]; let pcs = &pcurves(&torus, std::slice::from_ref(&l), 1e-9).unwrap()[0]; let (ends, area) = walk(&l, pcs); @@ -415,10 +417,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, 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 }, + EdgeUse { edge: 0, curve: &outer, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 2, curve: &top, interval: full, forward: false, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &quarter, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0, given: None }, ]; for tol in [1e-4, 1e-7] { closes_around_the_box(&band, &l, [1, 3], None, tol, &format!("torus band, tol {tol}")); @@ -429,10 +431,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, 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 }, + EdgeUse { edge: 0, curve: &low, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 2, curve: &high, interval: full, forward: false, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &line, interval: [0., 1.], forward: false, tolerance: 0.0, given: None }, ]; 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. @@ -440,9 +442,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, 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 }, + EdgeUse { edge: 0, curve: &equator, interval: full, forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: true, tolerance: 0.0, given: None }, + EdgeUse { edge: 1, curve: &meridian, interval: [0., FRAC_PI_2], forward: false, tolerance: 0.0, given: None }, ]; 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 @@ -603,7 +605,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, tolerance: 0.0 }; MAX_USES + 1]]; + let many = vec![vec![EdgeUse { edge: 0, curve: &line, interval: [0., 1.], forward: true, tolerance: 0.0, given: None }; 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. }; @@ -636,7 +638,7 @@ fn an_edge_is_fitted_to_its_own_tolerance() { 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 own = vec![vec![EdgeUse { edge: 0, curve: &off, interval: [0., 1.], forward: true, tolerance: 2e-4, given: None }]]; 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 2f9390f582..3e1647c5ab 100644 --- a/crates/geom/src/brep/tests.rs +++ b/crates/geom/src/brep/tests.rs @@ -36,7 +36,14 @@ 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, tolerance: e.tolerance } + pcurve::EdgeUse { + edge: u.edge, + curve: &e.curve, + interval: e.interval, + forward: u.forward, + tolerance: e.tolerance, + given: None, + } }) .collect(), Loop::Vertex { .. } => vec![], diff --git a/crates/kernel/src/native/adapter.rs b/crates/kernel/src/native/adapter.rs index 0553427d26..ca9c9380ae 100644 --- a/crates/kernel/src/native/adapter.rs +++ b/crates/kernel/src/native/adapter.rs @@ -196,7 +196,7 @@ fn face_record( .map(|e| { let i = *edge_index.get(&e.id()).ok_or_else(|| invalid("edge index"))?; 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 }) + Ok(b::pcurve::EdgeUse { edge: index(i)?, curve, interval: *interval, forward: e.orientation(), tolerance: *tolerance, given: None }) }) .collect::>>()?; uses.push(l); diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index 3bff85f43d..947584ccd3 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -25,9 +25,15 @@ //! two faces. //! - **Order:** faces, edges and vertices renumbered in the canonical walk; each edge's uses //! from that walk (`upper = !forward`), never SMLib's radial entry flags. -//! - **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. +//! - **Pcurves:** on a B-spline face, SMLib's own UV trims, exactly: the face's parameters are +//! SMLib's, and each trim's knots are moved onto its edge record's parameter where that is an +//! affine image of SMLib's (a spline edge's is SMLib's own; a line's the line's STEP +//! parameter, which the C side maps the trims onto). SMLib keeps an analytic surface's trims +//! on the surface's internal rational B-spline, whose angular parameter is not the angle +//! (SmCylinder.h: "the map from one to the other is not linear"), and a circle's or ellipse's +//! on its NURBS parameter, not its angle; those pcurves are computed from the edge record on +//! the face's surface record (`solidcraft_geom::brep::pcurve`). Every loop is moved by whole +//! periods into the face's domain. //! - **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 @@ -166,6 +172,8 @@ const I64_IDS: &[i32] = &[ EDGEUSE_ORIENTATION, EDGEUSE_NEXT_RADIAL, EDGEUSE_RADIAL_ENTRY, + EDGEUSE_NURBS_VERTEX_COUNT, + EDGEUSE_NURBS_ORDER, EDGE_CURVE_TYPE, EDGE_VERTICES, EDGE_NURBS_VERTEX_COUNT, @@ -197,6 +205,9 @@ const F64_IDS: &[i32] = &[ EDGE_ELLIPSE_REF_DIRECTION, EDGE_ELLIPSE_X_RADIUS, EDGE_ELLIPSE_Y_RADIUS, + EDGEUSE_NURBS_CVS, + EDGEUSE_NURBS_KNOTS, + EDGEUSE_NURBS_WEIGHTS, FACE_NURBS_CVS, FACE_NURBS_U_KNOTS, FACE_NURBS_V_KNOTS, @@ -476,9 +487,14 @@ fn as_circle(n: &Nurbs, range: [f64; 2]) -> Option<(b::Curve, [f64; 2])> { Some((b::Curve::Circle { frame: b::Frame { origin: arr(center), x_axis: arr(x), y_axis: arr(y) }, radius }, [a0, total])) } -/// Each edge's record and interval (in SMLib's direction along it), and how many NURBS edges -/// were recognised as lines or circles and how many stayed splines. -fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, usize)> { +/// How a UV trim's parameter (SMLib's edge parameter, as the arrays carry it) becomes the edge +/// record's: t ↦ scale · t + offset, scale > 0. None where the record's parameter is not an +/// affine image of SMLib's (a circle or ellipse, whose angle is not its NURBS parameter). +type TrimMap = Option<[f64; 2]>; + +/// Each edge's record and interval (in SMLib's direction along it) and its [`TrimMap`], and how +/// many NURBS edges were recognised as lines or circles and how many stayed splines. +fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2], TrimMap)>, usize, usize)> { let kinds = flat.i(EDGE_CURVE_TYPE); let range = flat.f(EDGE_RANGE); if kinds.len() != n_edges || range.len() != 2 * n_edges { @@ -494,7 +510,8 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, CURVE_LINE => { let (o, d) = (v3(flat.f(EDGE_LINE_ORIGIN), lines, "line")?, v3(flat.f(EDGE_LINE_DIRECTION), lines, "line")?); lines += 1; - (b::Curve::Line { start: o, end: [o[0] + d[0], o[1] + d[1], o[2] + d[2]] }, r) + // The C side maps a line's trims onto the line's STEP parameter, the record's. + (b::Curve::Line { start: o, end: [o[0] + d[0], o[1] + d[1], o[2] + d[2]] }, r, Some([1.0, 0.0])) } CURVE_CIRCLE => { let f = frame( @@ -504,7 +521,7 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, ); let radius = at(flat.f(EDGE_CIRCLE_RADIUS), circles, "circle")?; circles += 1; - (b::Curve::Circle { frame: f, radius }, r) + (b::Curve::Circle { frame: f, radius }, r, None) } CURVE_ELLIPSE => { let f = frame( @@ -514,7 +531,7 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, ); let radii = [at(flat.f(EDGE_ELLIPSE_X_RADIUS), ellipses, "ellipse")?, at(flat.f(EDGE_ELLIPSE_Y_RADIUS), ellipses, "ellipse")?]; ellipses += 1; - (b::Curve::Ellipse { frame: f, radii }, r) + (b::Curve::Ellipse { frame: f, radii }, r, None) } CURVE_NURBS => { let n = count(at(flat.i(EDGE_NURBS_VERTEX_COUNT), nurbs, "NURBS edge")?, "NURBS edge")?; @@ -530,15 +547,17 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, kn += n + o; let c = Nurbs { knots, points: cvs.as_chunks::<3>().0.to_vec(), weights }; // A fallback: SMLib exports lines, circles and ellipses as their own kinds. - match as_line(&c, r).or_else(|| as_circle(&c, r)) { - Some(x) => { - recognised += 1; - x - } - None => { - kept += 1; - (c.record(), r) - } + if let Some((line, s)) = as_line(&c, r) { + recognised += 1; + // as_line's t ↦ (t − k₁)/(k₂ − k₁). + let (k1, k2) = (c.knots.get(1).copied().unwrap_or(0.0), c.knots.get(2).copied().unwrap_or(1.0)); + (line, s, Some([1.0 / (k2 - k1), -k1 / (k2 - k1)])) + } else if let Some((circle, s)) = as_circle(&c, r) { + recognised += 1; + (circle, s, None) + } else { + kept += 1; + (c.record(), r, Some([1.0, 0.0])) } } other => return Err(unsupported(format!("SMLib curve kind {other}"))), @@ -547,6 +566,89 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, [f64; 2])>, usize, Ok((out, recognised, kept)) } +/// Each edgeuse record's UV trim as SMLib holds it (`EDGEUSE_NURBS_*`), along its edgeuse (the +/// C side writes a trim whose edgeuse runs against its edge reversed, on the reversed parameter +/// a + b − t of the edge's interval [a, b]); none where the arrays carry none for it (an analytic +/// face's, or an analytic edge's). +fn trims(flat: View, n_edgeuses: usize) -> Res>> { + let (counts, orders) = (flat.i(EDGEUSE_NURBS_VERTEX_COUNT), flat.i(EDGEUSE_NURBS_ORDER)); + if counts.is_empty() && orders.is_empty() { + return Ok(vec![None; n_edgeuses]); + } + if counts.len() != n_edgeuses || orders.len() != n_edgeuses { + return Err(invalid("UV trim arrays")); + } + let (cvs, knots, weights) = (flat.f(EDGEUSE_NURBS_CVS), flat.f(EDGEUSE_NURBS_KNOTS), flat.f(EDGEUSE_NURBS_WEIGHTS)); + let (mut cv, mut kn) = (0usize, 0usize); + let mut out = Vec::with_capacity(n_edgeuses); + for (&n, &o) in counts.iter().zip(orders) { + let (n, o) = (count(n, "UV trim")?, count(o, "UV trim")?); + if n == 0 || o == 0 { + out.push(None); + continue; + } + if o < 2 || n < o { + return Err(invalid("UV trim order")); + } + let pts = cvs.get(2 * cv..2 * (cv + n)).ok_or_else(|| invalid("UV trim points"))?; + let k = knots.get(kn..kn + n + o).ok_or_else(|| invalid("UV trim knots"))?.to_vec(); + let w = match weights { + [] => vec![1.0; n], + w => w.get(cv..cv + n).ok_or_else(|| invalid("UV trim weights"))?.to_vec(), + }; + cv += n; + kn += n + o; + let pts = pts.as_chunks::<2>().0; + out.push(Some(if w.iter().all(|x| *x == 1.0) { + b::Curve2::Spline { knots: k, points: pts.to_vec() } + } else { + b::Curve2::RationalSpline { knots: k, homogeneous: pts.iter().zip(&w).map(|(p, w)| [p[0] * w, p[1] * w, *w]).collect() } + })); + } + if 2 * cv != cvs.len() || kn != knots.len() { + return Err(invalid("UV trim arrays: blocks do not cover them")); + } + Ok(out) +} + +/// A trim along its edge, its parameter moved onto the edge record's ([`TrimMap`]): one whose +/// edgeuse runs against the edge (`same` false) reversed exactly (control points in reverse +/// order, knots t ↦ a + b − t), then the knots mapped. SMLib keeps a trim on its edge's +/// interval; where the two ends differ by the rounding of SMLib's own or this remap (within the +/// schema's 10⁻⁹ of the interval's magnitude, at least 1), the knots at the trim's ends are the +/// edge record's interval ends, so the pcurve is defined there; farther apart is an error. +fn mapped(trim: &b::Curve2, same: bool, [scale, offset]: [f64; 2], [t0, t1]: [f64; 2]) -> Res { + fn along(knots: &[f64], points: &[T], same: bool) -> (Vec, Vec) { + if same { + return (knots.to_vec(), points.to_vec()); + } + let (a, b) = (knots.first().copied().unwrap_or(0.0), knots.last().copied().unwrap_or(0.0)); + (knots.iter().rev().map(|t| a + b - t).collect(), points.iter().rev().cloned().collect()) + } + let map = |k: Vec, n: usize| -> Res> { + let mut k: Vec = k.into_iter().map(|t| scale * t + offset).collect(); + let degree = k.len().checked_sub(n + 1).ok_or_else(|| invalid("UV trim knots"))?; + let (a, z) = (at(&k, degree, "UV trim knots")?, at(&k, n, "UV trim knots")?); + let slack = 1e-9 * t0.abs().max(t1.abs()).max(1.0); + if (a - t0).abs() > slack || (z - t1).abs() > slack { + return Err(invalid(format!("a UV trim on [{a}, {z}], its edge on [{t0}, {t1}]"))); + } + k.iter_mut().filter(|x| **x == a).for_each(|x| *x = t0); + k.iter_mut().filter(|x| **x == z).for_each(|x| *x = t1); + Ok(k) + }; + Ok(match trim { + b::Curve2::Spline { knots, points } => { + let (k, points) = along(knots, points, same); + b::Curve2::Spline { knots: map(k, points.len())?, points } + } + b::Curve2::RationalSpline { knots, homogeneous } => { + let (k, homogeneous) = along(knots, homogeneous, same); + b::Curve2::RationalSpline { knots: map(k, homogeneous.len())?, homogeneous } + } + }) +} + /// A domain side moved onto the natural range where it overshoots it by rounding (a sphere's /// latitudes, a cone's apex). fn onto(r: [f64; 2], natural: b::Natural) -> [f64; 2] { @@ -580,6 +682,10 @@ struct Face { loops: Vec, /// The array edgeuse record of each use, loop by loop as `loops` (none for a vertex loop). records: Vec>, + /// Each use's pcurve, SMLib's own UV trim on the record's parameters, loop by loop as + /// `loops`: on a B-spline face, for an edge whose record's parameter is an affine image of + /// SMLib's; none elsewhere. + given: Vec>>, } #[derive(Clone)] @@ -596,6 +702,8 @@ struct Edge { vertices: [u32; 2], /// The record's parameter runs against SMLib's edge direction. reversed: bool, + /// How SMLib's UV trims of this edge map onto the record's parameter. + trim_map: TrimMap, } /// The schema 4 B-rep of SMLib's flat arrays: mapped, renumbered canonically and validated. @@ -677,7 +785,7 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { report.recognised_edges = recognised; report.nurbs_edges = kept; let mut edges = Vec::with_capacity(n_edges); - for (e, (curve, [a, z])) in records.into_iter().enumerate() { + for (e, (curve, [a, z], trim_map)) in records.into_iter().enumerate() { if !(a.is_finite() && z.is_finite()) || a == z { return Err(invalid("edge interval")); } @@ -689,11 +797,13 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { interval: if reversed { [z, a] } else { [a, z] }, vertices: if reversed { [end, start] } else { [start, end] }, reversed, + trim_map, }); } // Faces: loops as SMLib keeps them, then turned where SMLib's normal is not the record's. let surfaces = surfaces(flat, n_faces)?; + let trims = trims(flat, n_edgeuses)?; let range = flat.f(FACE_RANGE); if range.len() != 4 * n_faces { return Err(invalid("face ranges")); @@ -703,27 +813,37 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { let r = range.get(4 * f..4 * f + 4).ok_or_else(|| invalid("face range"))?; let natural = surface.natural(); let domain = [onto([r[0], r[2]], natural[0]), onto([r[1], r[3]], natural[1])]; + let spline = matches!(surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. }); let mut out = Vec::with_capacity(loops.len()); let mut records = Vec::with_capacity(loops.len()); + let mut given = Vec::with_capacity(loops.len()); for l in loops.clone() { let uses = loop_edgeuses.get(l).ok_or_else(|| invalid("loop"))?; if uses.is_empty() { let v = idx(at(flat.i(LOOP_VERTEX), l, "loop vertex")?, n_vertices, "loop vertex")?; out.push(Loop::Vertex(index(v)?)); records.push(Vec::new()); + given.push(Vec::new()); continue; } let mut l = Vec::with_capacity(uses.len()); + let mut g = Vec::with_capacity(uses.len()); records.push(uses.clone().collect()); for u in uses.clone() { let e = idx(at(flat.i(EDGEUSE_EDGE), u, "edgeuse")?, n_edges, "edgeuse edge")?; let same = at(flat.i(EDGEUSE_ORIENTATION), u, "edgeuse")? == 1; let edge = edges.get(e).ok_or_else(|| invalid("edge"))?; l.push((index(e)?, same != edge.reversed)); + let trim = trims.get(u).ok_or_else(|| invalid("edgeuse"))?.as_ref(); + g.push(match (spline, edge.trim_map, trim) { + (true, Some(map), Some(trim)) => Some(mapped(trim, same, map, edge.interval)?), + _ => None, + }); } out.push(Loop::Edges(l)); + given.push(g); } - faces.push(Face { surface, domain, agrees: true, loops: out, records }); + faces.push(Face { surface, domain, agrees: true, loops: out, records, given }); } for face in &mut faces { if outer_area(face, &edges, largest)? < 0.0 { @@ -735,6 +855,7 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> { } } face.records.iter_mut().for_each(|r| r.reverse()); + face.given.iter_mut().for_each(|g| g.reverse()); report.reversed_faces += 1; } } @@ -1033,7 +1154,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance Loop::Vertex(v) => Ok(Loop::Vertex(*v)), }) .collect::>()?; - let mut curves = pcurves(&face.surface, face.domain, &renumbered, &edge_records, tolerance)?.into_iter(); + let mut curves = pcurves(&face.surface, face.domain, &renumbered, &face.given, &edge_records, tolerance)?.into_iter(); let mut loops = Vec::with_capacity(renumbered.len()); for (li, l) in renumbered.iter().enumerate() { loops.push(match l { @@ -1294,16 +1415,37 @@ fn split_touching(faces: &mut [Face], edges: &mut Vec, edge_source: &mut V Ok(()) } -/// A face's pcurves, edge loop by edge loop, from the plain-record routine on the face's -/// records, each loop moved by whole periods into the face's domain. -fn pcurves(surface: &b::Surface, domain: [[f64; 2]; 2], loops: &[Loop], edges: &[b::Edge], tolerance: f64) -> Res>> { +/// A face's pcurves, edge loop by edge loop: SMLib's own UV trims where `given` (loop by loop as +/// `loops`) has them, the plain-record routine on the face's records for the rest; each loop moved +/// by whole periods into the face's domain. +fn pcurves( + surface: &b::Surface, + domain: [[f64; 2]; 2], + loops: &[Loop], + given: &[Vec>], + edges: &[b::Edge], + tolerance: f64, +) -> Res>> { + if given.len() != loops.len() { + return Err(invalid("UV trims of a face")); + } let mut uses = Vec::new(); - for l in loops { + for (l, g) in loops.iter().zip(given) { let Loop::Edges(l) = l else { continue }; + if g.len() != l.len() { + return Err(invalid("UV trims of a loop")); + } let mut loop_uses = Vec::with_capacity(l.len()); - for &(e, forward) in l { + for (&(e, forward), given) in l.iter().zip(g) { 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, tolerance: edge.tolerance }); + loop_uses.push(b::pcurve::EdgeUse { + edge: e, + curve: &edge.curve, + interval: edge.interval, + forward, + tolerance: edge.tolerance, + given: given.as_ref(), + }); } uses.push(loop_uses); } @@ -1318,7 +1460,8 @@ fn outer_area(face: &Face, edges: &[Edge], tolerance: f64) -> Res { let Some(Loop::Edges(outer)) = face.loops.first() else { return Ok(0.0) }; let records: Vec = edges.iter().map(|e| b::Edge { curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance, uses: Vec::new() }).collect(); - let curves = pcurves(&face.surface, face.domain, &[Loop::Edges(outer.clone())], &records, tolerance)?; + let given = face.given.first().map(std::slice::from_ref).unwrap_or(&[]); + let curves = pcurves(&face.surface, face.domain, &[Loop::Edges(outer.clone())], given, &records, tolerance)?; let mut pts: Vec<[f64; 2]> = Vec::new(); for (pc, &(e, forward)) in curves.first().map(Vec::as_slice).unwrap_or(&[]).iter().zip(outer) { let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?; diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index 01e1266e41..fe1c2c55dc 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -936,7 +936,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, tolerance: 0.0 }); + u.push(EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward, tolerance: 0.0, given: None }); } uses.push(u); } diff --git a/docs/native-brep.md b/docs/native-brep.md index 911e8130d7..511e6430a8 100644 --- a/docs/native-brep.md +++ b/docs/native-brep.md @@ -168,9 +168,16 @@ pcurves and tessellation, one active knot interval per period: a seam on it has its two uses an interval apart. The record is unchanged; a parameter outside the interval is evaluated a whole number of periods back inside it. -Pcurves are SolidCraft's own, never a kernel's internal parameter space: -`brep::pcurve::pcurves` computes them from a face's surface record and each -use's curve record and interval, for every backend. It is exact in closed form +Where a kernel holds a use's pcurve exactly in the record's parameters, the +record keeps the kernel's own: the SMLib export takes SMLib's UV trims on +B-spline faces for edges recorded as splines or lines (the face's parameters +are SMLib's, the edge's parameter SMLib's or an affine image of it). SMLib keeps +an analytic face's trims on its internal rational B-spline, whose angle +parameter is not the record's angle, and a circle's or ellipse's on its NURBS +parameter, not its angle; neither is an exact B-spline in the record's +parameters. The rest come from `brep::pcurve::pcurves`, which computes them from +a face's surface record and each use's curve record and interval, for every +backend (a use's `given` pcurve is taken as it is). It is exact in closed form for lines, polylines and whole splines on planes, circles about the axis of a cylinder, cone, sphere or torus (v fixed, u the circle's angle plus a constant) and lines along a cylinder's or cone's ruling (u fixed, v linear), each checked