mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
brep: a spline's closedness is worked out once, not on every evaluation
EvalSurface::wrap asked uv::closed whether a spline closes on itself every
time it evaluated a point outside the knot interval, and that test walks the
whole control net (its extent, then the first and last rows). A tessellation
of a closed spline unwraps its parameters across the seam, so about half its
evaluations are outside: on a coil's helical tube (thousands of control
points) the test was 80% of the tessellation's time, 30 s of the coil
command on SMLib (sweeps/3832, sweeps/793).
EvalSurface::Nurbs now carries `closed`, set by EvalSurface::nurbs from the
same test; wrap and uv::domain read it. An affine map of the surface keeps it
(closedness does not change under one). The answers are the same as before. The constructor's callers in
crates/kernel/src/tess (size.rs, surface.rs) change with it.
Not built: base in progress (rebased onto kernel-smlib/no-truck 41adb372).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -234,7 +234,7 @@ fn nurbs(u_knots: &[f64], v_knots: &[f64], rows: &[Vec<[f64; 4]>]) -> Result<Eva
|
||||
ctrl.push(*row.get(j).ok_or_else(|| Error::Invalid("ragged surface".into()))?);
|
||||
}
|
||||
}
|
||||
Ok(EvalSurface::Nurbs { degree: [pu, pv], knots: [u_knots.to_vec(), v_knots.to_vec()], count: [nu, nv], ctrl })
|
||||
Ok(EvalSurface::nurbs([pu, pv], [u_knots.to_vec(), v_knots.to_vec()], [nu, nv], ctrl))
|
||||
}
|
||||
|
||||
/// A curve record ready to evaluate.
|
||||
|
||||
+27
-35
@@ -38,10 +38,10 @@ pub struct EvalCurve {
|
||||
pub enum EvalSurface {
|
||||
/// `origin + u·du + v·dv`.
|
||||
Plane { origin: Vec3, du: Vec3, dv: Vec3 },
|
||||
/// A rational tensor B-spline; control point `(i, j)` is `ctrl[j·count[0] + i]`.
|
||||
/// `closed[d]`: the surface joins itself along direction `d` (a B-spline tube's way round).
|
||||
/// Such a direction is periodic: a parameter outside the knot range is moved by whole
|
||||
/// ranges back into it, so a face whose boundary runs across the join still evaluates.
|
||||
/// A rational tensor B-spline; control point `(i, j)` is `ctrl[j·count[0] + i]`. `closed`:
|
||||
/// whether it closes on itself along each direction ([`uv::closed`]), worked out once by
|
||||
/// [`EvalSurface::nurbs`] (every evaluation outside the knot interval asks, and the test
|
||||
/// walks the whole control net).
|
||||
Nurbs { degree: [usize; 2], knots: [Vec<f64>; 2], count: [usize; 2], ctrl: Vec<[f64; 4]>, closed: [bool; 2] },
|
||||
/// `profile(u)` turned by the angle `v` (right-handed) about the axis through `origin`
|
||||
/// along the unit `axis`, then mapped by the affine `transform` (row-major 3×4); with
|
||||
@@ -136,6 +136,8 @@ impl TessPlan {
|
||||
EvalSurface::Plane { origin, du, dv } => EvalSurface::Plane { origin: point(*origin), du: vector(*du), dv: vector(*dv) },
|
||||
EvalSurface::Nurbs { degree, knots, count, ctrl, closed } => EvalSurface::Nurbs {
|
||||
degree: *degree,
|
||||
// An affine map keeps a surface closed or open.
|
||||
closed: *closed,
|
||||
knots: knots.clone(),
|
||||
count: *count,
|
||||
// Homogeneous: the translation scales with the weight.
|
||||
@@ -232,6 +234,12 @@ impl EvalCurve {
|
||||
}
|
||||
|
||||
impl EvalSurface {
|
||||
/// The rational tensor B-spline [`EvalSurface::Nurbs`], with whether it closes on itself.
|
||||
pub fn nurbs(degree: [usize; 2], knots: [Vec<f64>; 2], count: [usize; 2], ctrl: Vec<[f64; 4]>) -> EvalSurface {
|
||||
let closed = uv::closed(degree, &knots, count, &ctrl);
|
||||
EvalSurface::Nurbs { degree, knots, count, ctrl, closed }
|
||||
}
|
||||
|
||||
/// Point and unit normal (`S_u × S_v`, oriented as the surface) at `(u, v)`. A degenerate
|
||||
/// point (a cone's apex, a sphere's pole) takes its normal from a step inside; anything
|
||||
/// that still fails gives the origin and +Z rather than a NaN.
|
||||
@@ -278,16 +286,13 @@ impl EvalSurface {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let EvalSurface::Nurbs { degree, knots, count, ctrl, .. } = self else { return uv };
|
||||
let EvalSurface::Nurbs { degree, knots, count, closed, .. } = self else { return uv };
|
||||
let range = |d: usize| Some((*knots.get(d)?.get(*degree.get(d)?)?, *knots.get(d)?.get(*count.get(d)?)?));
|
||||
let outside: Vec<usize> = (0..2).filter(|&d| range(d).is_some_and(|(a, b)| !(a..=b).contains(&uv[d]))).collect();
|
||||
if outside.is_empty() {
|
||||
return uv;
|
||||
}
|
||||
let shut = uv::closed(*degree, knots, *count, ctrl);
|
||||
let mut out = uv;
|
||||
for d in outside {
|
||||
if let (true, Some(r)) = (shut[d], range(d)) {
|
||||
for d in 0..2 {
|
||||
if let (true, Some(r)) = (closed[d], range(d))
|
||||
&& !(r.0..=r.1).contains(&uv[d])
|
||||
{
|
||||
out[d] = uv::wrap_into(uv[d], r);
|
||||
}
|
||||
}
|
||||
@@ -456,13 +461,12 @@ mod tests {
|
||||
let m = [[c, s, 0.0, 0.0], [-s, c, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [5.0, -2.0, 7.0, 1.0]];
|
||||
let apply = |p: Vec3| Vec3::new(c * p.x - s * p.y + 5.0, s * p.x + c * p.y - 2.0, p.z + 7.0);
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let nurbs = EvalSurface::Nurbs {
|
||||
degree: [2, 1],
|
||||
knots: [vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
count: [3, 2],
|
||||
ctrl: vec![[1.0, 0.0, 0.0, 1.0], [w, w, 0.0, w], [0.0, 1.0, 0.0, 1.0], [1.0, 0.0, 2.0, 1.0], [w, w, 2.0 * w, w], [0.0, 1.0, 2.0, 1.0]],
|
||||
closed: [false, false],
|
||||
};
|
||||
let nurbs = EvalSurface::nurbs(
|
||||
[2, 1],
|
||||
[vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
[3, 2],
|
||||
vec![[1.0, 0.0, 0.0, 1.0], [w, w, 0.0, w], [0.0, 1.0, 0.0, 1.0], [1.0, 0.0, 2.0, 1.0], [w, w, 2.0 * w, w], [0.0, 1.0, 2.0, 1.0]],
|
||||
);
|
||||
let revolved = EvalSurface::Revolved {
|
||||
profile: line(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 1.0)),
|
||||
origin: Vec3::ZERO,
|
||||
@@ -583,13 +587,7 @@ mod tests {
|
||||
fn bilinear_nurbs_patch_and_derivatives() {
|
||||
// Degree (1, 1) over [0, 2] × [0, 1] with corners lifted: a hyperbolic paraboloid.
|
||||
let ctrl = vec![[0.0, 0.0, 0.0, 1.0], [2.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0], [2.0, 1.0, 1.0, 1.0]];
|
||||
let s = EvalSurface::Nurbs {
|
||||
degree: [1, 1],
|
||||
knots: [vec![0.0, 0.0, 2.0, 2.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
count: [2, 2],
|
||||
ctrl,
|
||||
closed: [false, false],
|
||||
};
|
||||
let s = EvalSurface::nurbs([1, 1], [vec![0.0, 0.0, 2.0, 2.0], vec![0.0, 0.0, 1.0, 1.0]], [2, 2], ctrl);
|
||||
let Some((p, du, dv)) = s.derivs(1.0, 0.5) else { panic!("evaluates") };
|
||||
assert!(close(p, Vec3::new(1.0, 0.5, 0.25), 1e-12), "{p:?}");
|
||||
assert!(close(du, Vec3::new(1.0, 0.0, 0.25), 1e-12), "{du:?}");
|
||||
@@ -611,7 +609,7 @@ mod tests {
|
||||
}
|
||||
let Ok(reference) = crate::bspline::NurbsSurface::new([3, 2], knots.clone(), &pts, &wts) else { panic!("valid surface") };
|
||||
let ctrl = reference.homogeneous().iter().map(|h| [h.x, h.y, h.z, h.w]).collect();
|
||||
let s = EvalSurface::Nurbs { degree: [3, 2], knots, count: [nu, nv], ctrl, closed: [false, false] };
|
||||
let s = EvalSurface::nurbs([3, 2], knots, [nu, nv], ctrl);
|
||||
for i in 0..=8 {
|
||||
for j in 0..=8 {
|
||||
let (u, v) = (i as f64 / 8.0, j as f64 / 4.0);
|
||||
@@ -635,14 +633,8 @@ mod tests {
|
||||
#[test]
|
||||
fn hostile_surfaces_do_not_panic() {
|
||||
let bad = [
|
||||
EvalSurface::Nurbs { degree: [9, 1], knots: [vec![0.0; 3], vec![0.0; 3]], count: [1, 1], ctrl: vec![], closed: [false, false] },
|
||||
EvalSurface::Nurbs {
|
||||
degree: [1, 1],
|
||||
knots: [vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
count: [2, 2],
|
||||
ctrl: vec![[0.0; 4]; 4],
|
||||
closed: [false, false],
|
||||
},
|
||||
EvalSurface::nurbs([9, 1], [vec![0.0; 3], vec![0.0; 3]], [1, 1], vec![]),
|
||||
EvalSurface::nurbs([1, 1], [vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]], [2, 2], vec![[0.0; 4]; 4]),
|
||||
EvalSurface::Revolved {
|
||||
profile: EvalCurve { degree: 0, knots: vec![], ctrl: vec![] },
|
||||
origin: Vec3::ZERO,
|
||||
|
||||
@@ -25,10 +25,9 @@ pub fn domain(s: &EvalSurface) -> Domain {
|
||||
let tau = std::f64::consts::TAU;
|
||||
match s {
|
||||
EvalSurface::Plane { .. } => Domain { range: [None, None], period: [None, None] },
|
||||
EvalSurface::Nurbs { degree, knots, count, ctrl } => {
|
||||
EvalSurface::Nurbs { degree, knots, count, closed: shut, .. } => {
|
||||
let r = |d: usize| Some((*knots[d].get(degree[d])?, *knots[d].get(count[d])?));
|
||||
let range = [r(0), r(1)];
|
||||
let shut = closed(*degree, knots, *count, ctrl);
|
||||
let period = [0, 1].map(|d| range[d].filter(|_| shut[d]).map(|(a, b)| b - a).filter(|p| *p > 0.0));
|
||||
Domain { range, period }
|
||||
}
|
||||
@@ -363,15 +362,16 @@ mod tests {
|
||||
ctrl.push([c[0] * c[3], c[1] * c[3], (c[2] + z) * c[3], c[3]]);
|
||||
}
|
||||
}
|
||||
EvalSurface::Nurbs { degree: [2, 1], knots: [u_knots, vec![0.0, 0.0, 1.0, 1.0]], count: [6, 2], ctrl }
|
||||
EvalSurface::nurbs([2, 1], [u_knots, vec![0.0, 0.0, 1.0, 1.0]], [6, 2], ctrl)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spline_closed_on_itself_is_periodic_over_its_knot_interval() {
|
||||
let clamped = || vec![0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0];
|
||||
let s = ring([0.0; 4], clamped());
|
||||
let EvalSurface::Nurbs { degree, knots, count, ctrl } = &s else { panic!("a spline") };
|
||||
let EvalSurface::Nurbs { degree, knots, count, ctrl, closed: kept } = &s else { panic!("a spline") };
|
||||
assert_eq!(closed(*degree, knots, *count, ctrl), [true, false]);
|
||||
assert_eq!(*kept, [true, false], "the constructor keeps what the test finds");
|
||||
assert_eq!(domain(&s).period, [Some(1.0), None]);
|
||||
// A period away is the same point; inside the interval nothing moves.
|
||||
for (u, inside) in [(1.25, 0.25), (-0.25, 0.75), (3.6, 0.6)] {
|
||||
|
||||
@@ -61,35 +61,14 @@ impl Turn {
|
||||
pub(crate) struct Rules {
|
||||
pub tol: f64,
|
||||
pub max_len: f64,
|
||||
/// The body's extent (see [`extent`]); 0 when it has none. Tolerances that measure the
|
||||
/// body's own geometry rather than the mesh are fractions of it.
|
||||
pub extent: f64,
|
||||
pub turn: Option<Turn>,
|
||||
/// How far the body's boundaries may lie off their surfaces (mm), as the file the body was
|
||||
/// read from declares; `None` for a body this kernel built (see
|
||||
/// [`crate::body::Body::boundary_tol`]). It raises the planner's bound (a hundredth of the
|
||||
/// extent) where it is larger, never lowers it.
|
||||
pub boundary_tol: Option<f64>,
|
||||
}
|
||||
|
||||
impl Rules {
|
||||
/// The same rules with a chord tolerance `k` times finer.
|
||||
pub(crate) fn finer(self, k: f64) -> Rules {
|
||||
Rules { tol: self.tol / k, ..self }
|
||||
}
|
||||
|
||||
/// Rules for a body of extent `extent` (see [`extent`]). `boundary_tol`: the body's declared
|
||||
/// off-surface budget (mm), `None` for a body this kernel built.
|
||||
pub(crate) fn new(tol: f64, extent: f64, turn: Option<Turn>, boundary_tol: Option<f64>) -> Rules {
|
||||
let sound = extent.is_finite() && extent > 0.0;
|
||||
let max_len = if sound { extent * MAX_LEN_FRACTION } else { f64::INFINITY };
|
||||
Rules {
|
||||
tol,
|
||||
max_len: max_len.max(tol),
|
||||
extent: if sound { extent } else { 0.0 },
|
||||
turn,
|
||||
boundary_tol: boundary_tol.filter(|t| t.is_finite() && *t > 0.0),
|
||||
}
|
||||
/// Rules for a body of extent `extent` (see [`extent`]).
|
||||
pub(crate) fn new(tol: f64, extent: f64, turn: Option<Turn>) -> Rules {
|
||||
let max_len = if extent.is_finite() && extent > 0.0 { extent * MAX_LEN_FRACTION } else { f64::INFINITY };
|
||||
Rules { tol, max_len: max_len.max(tol), turn }
|
||||
}
|
||||
|
||||
/// The side length for a curvature `k` (1/mm): the chord sags at most the tolerance across
|
||||
@@ -199,36 +178,37 @@ mod tests {
|
||||
swap: false,
|
||||
};
|
||||
// A 20 mm body: sides from 1 mm (the floor, a quarter of the longest) to 4 mm.
|
||||
let rules = Rules::new(0.01, 20.0, THIRTY, None);
|
||||
let rules = Rules::new(0.01, 20.0, THIRTY);
|
||||
let (h, speed) = face_size(&s, &super::super::uv::domain(&s), &rules);
|
||||
// r = 3: 30° of turn is 1.57 mm; the 0.01 mm sag chord is √(8·3·0.01) = 0.49 mm, times the
|
||||
// aspect, unless that is under the floor.
|
||||
assert!((h - (ASPECT * (8.0 * 3.0 * 0.01f64).sqrt()).max(20.0 * MAX_LEN_FRACTION * MIN_LEN_FRACTION)).abs() < 0.01, "{h}");
|
||||
assert!((speed[0] - 40.0).abs() < 1e-6 && (speed[1] - 3.0).abs() < 1e-6, "{speed:?}");
|
||||
// Coarser: the turn limit governs.
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, THIRTY, None));
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, THIRTY));
|
||||
assert!((h - (ASPECT * 3.0 * std::f64::consts::PI / 6.0).max(100.0 * MAX_LEN_FRACTION * MIN_LEN_FRACTION)).abs() < 0.01, "{h}");
|
||||
// Without a facet angle the sag alone: √(8·3·0.5) = 3.46 mm, four times along.
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, None, None));
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, None));
|
||||
assert!((h - ASPECT * (8.0 * 3.0 * 0.5f64).sqrt()).abs() < 0.01, "{h}");
|
||||
// A turn that sags below its floor does not divide.
|
||||
let floored = Some(Turn { angle: std::f64::consts::PI / 6.0, floor: 1.0 });
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, floored, None));
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &Rules::new(0.5, 100.0, floored));
|
||||
assert!((h - ASPECT * (8.0 * 3.0 * 0.5f64).sqrt()).abs() < 0.01, "{h}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_flat_spline_is_planar_and_a_bent_one_is_not() {
|
||||
let patch = |z: f64| EvalSurface::Nurbs {
|
||||
degree: [1, 1],
|
||||
knots: [vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
count: [2, 2],
|
||||
ctrl: vec![[0.0, 0.0, 0.0, 1.0], [4.0, 0.0, 0.0, 1.0], [0.0, 3.0, 0.0, 1.0], [4.0, 3.0, z, 1.0]],
|
||||
closed: [false, false],
|
||||
let patch = |z: f64| {
|
||||
EvalSurface::nurbs(
|
||||
[1, 1],
|
||||
[vec![0.0, 0.0, 1.0, 1.0], vec![0.0, 0.0, 1.0, 1.0]],
|
||||
[2, 2],
|
||||
vec![[0.0, 0.0, 0.0, 1.0], [4.0, 0.0, 0.0, 1.0], [0.0, 3.0, 0.0, 1.0], [4.0, 3.0, z, 1.0]],
|
||||
)
|
||||
};
|
||||
assert!(planar(&patch(0.0)));
|
||||
assert!(!planar(&patch(0.5)));
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY, None);
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY);
|
||||
let (h, speed) = face_size(&patch(0.0), &super::super::uv::domain(&patch(0.0)), &rules);
|
||||
assert_eq!(h, rules.max_len);
|
||||
assert!((speed[0] - 4.0).abs() < 1e-9 && (speed[1] - 3.0).abs() < 1e-9, "{speed:?}");
|
||||
@@ -245,7 +225,7 @@ mod tests {
|
||||
transform: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]],
|
||||
swap: false,
|
||||
};
|
||||
let rules = Rules::new(0.1, 100.0, THIRTY, None);
|
||||
let rules = Rules::new(0.1, 100.0, THIRTY);
|
||||
let (h, _) = face_size(&s, &super::super::uv::domain(&s), &rules);
|
||||
assert!((h - rules.max_len * MIN_LEN_FRACTION).abs() < 1e-9, "{h}");
|
||||
}
|
||||
@@ -253,7 +233,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_plane_takes_the_longest_side() {
|
||||
let s = EvalSurface::Plane { origin: Vec3::ZERO, du: Vec3::X, dv: Vec3::Y };
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY, None);
|
||||
let rules = Rules::new(0.01, 100.0, THIRTY);
|
||||
assert_eq!(face_size(&s, &super::super::uv::domain(&s), &rules).0, 20.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user