mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
solidcraft-smlib: pcurves on B-spline faces are SMLib's own UV trims
On a B-spline face the export now takes each edgeuse's UV trim exactly as
SMLib holds it (EDGEUSE_NURBS_*): the face's parameters are SMLib's, so the
trim is the pcurve, reversed exactly (knots t -> a + b - t) where the
edgeuse runs against its edge, its knots mapped onto the edge record's
parameter where that is an affine image of SMLib's (identity for spline
edges, as_line's map for NURBS lines, the C side's map for lines), and its
end knots set to the edge interval where they differ by rounding (within
the schema's 1e-9 slack, else an error). Changed: export.rs `curves`
(per-edge TrimMap), new `trims`, `mapped`, `Face::given`,
`Edge::trim_map`, the face loop in `native_ordered`, `pcurves`,
`outer_area`.
Analytic faces and circle/ellipse edges keep brep::pcurve: SMLib keeps an
analytic face's trims on its internal rational B-spline (SmCylinder.h:
"the map from one to the other is not linear") and a circle's on its NURBS
parameter, neither an exact B-spline in the record's angle parameters.
brep::pcurve::EdgeUse gains `given`, a producer's exact pcurve taken as it
is (structure and interval checked, moved by whole periods with its loop,
never refitted). Existing construction sites (STEP reader, tests, truck
adapter) pass `given: None`; the existing tests change only for the new
field.
Built, clippy-clean and tested on 4b2c0eaf before the rebase onto
kernel-smlib/no-truck (not built: base in progress).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -38,8 +38,12 @@ 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, 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<Curve2> {
|
||||
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,
|
||||
|
||||
@@ -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, 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<EdgeUse> =
|
||||
order.iter().map(|&forward| EdgeUse { edge: 0, curve: seam, interval: iv, forward, tolerance: 0.0 }).collect();
|
||||
let l: Vec<EdgeUse> = 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);
|
||||
}
|
||||
|
||||
@@ -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![],
|
||||
|
||||
@@ -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::<Result<Vec<_>>>()?;
|
||||
uses.push(l);
|
||||
|
||||
+171
-28
@@ -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<Vec<Option<b::Curve2>>> {
|
||||
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<b::Curve2> {
|
||||
fn along<T: Clone>(knots: &[f64], points: &[T], same: bool) -> (Vec<f64>, Vec<T>) {
|
||||
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<f64>, n: usize| -> Res<Vec<f64>> {
|
||||
let mut k: Vec<f64> = 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<Loop>,
|
||||
/// The array edgeuse record of each use, loop by loop as `loops` (none for a vertex loop).
|
||||
records: Vec<Vec<usize>>,
|
||||
/// 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<Vec<Option<b::Curve2>>>,
|
||||
}
|
||||
|
||||
#[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::<Res<_>>()?;
|
||||
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>, 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<Vec<Vec<b::Curve2>>> {
|
||||
/// 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<Option<b::Curve2>>],
|
||||
edges: &[b::Edge],
|
||||
tolerance: f64,
|
||||
) -> Res<Vec<Vec<b::Curve2>>> {
|
||||
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<f64> {
|
||||
let Some(Loop::Edges(outer)) = face.loops.first() else { return Ok(0.0) };
|
||||
let records: Vec<b::Edge> =
|
||||
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"))?;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+10
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user