kernel: path segments hold any rational Bézier span, and edges as paths

PathSeg::Bezier(SpaceBezier) replaces PathSeg::Cubic: a rational Bézier span in
space of degree 1 to 9 (homogeneous control points), so a drawn spline's cubic
spans and an edge's own curve are one kind of segment, swept by SMLib as the
NURBS it is. SpaceBezier evaluates, differentiates (quotient rule on the
hodograph), measures and walks by length (Gauss-Legendre), reverses and maps.

edge_path gives a body edge's record curve as path segments exactly: a line,
an arc, an ellipse as rational quadratic spans, a polyline's lines, a B-spline
as its Bézier spans between knots (bspline::segment); an intersection curve
(no exact curve in the record) is refused. PathPiece::Segs carries such exact
curves in a point path; PathSeg gains reversed, mapped (a placement) and
of_sketch (a sketch segment on its plane). The cubic_* helpers are deleted.
surfaces.rs: its one PathSeg::Cubic arm follows the rename.

lib.rs (coordinator): export SpaceBezier and edge_path from point_path.
(not built: base in progress; cargo check shows no error in these files)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 00:53:56 -04:00
parent 0a216d6925
commit 33cfa34d41
4 changed files with 327 additions and 69 deletions
+40 -12
View File
@@ -1288,7 +1288,7 @@ pub(crate) fn smooth_loft_faces(wires: &[mt::Wire], numbers: &[Vec<Vec<usize>>],
}
/// A 3D path segment for sweeps.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathSeg {
Line {
a: Vec3,
@@ -1306,11 +1306,9 @@ pub enum PathSeg {
plane: Plane,
seg: Seg2,
},
/// A cubic Bézier in space with control points `p`: a span of a drawn 3D sketch spline
/// ([`crate::PathPiece::Spline`]).
Cubic {
p: [Vec3; 4],
},
/// A rational Bézier span in space: a span of a drawn 3D sketch spline
/// ([`crate::PathPiece::Spline`]) or of an edge's curve ([`crate::edge_path`]).
Bezier(crate::point_path::SpaceBezier),
}
impl PathSeg {
@@ -1319,7 +1317,7 @@ impl PathSeg {
match *self {
PathSeg::Line { a, .. } | PathSeg::Arc { a, .. } => a,
PathSeg::Curve { plane, seg } => plane.to_world(seg.start()),
PathSeg::Cubic { p } => p[0],
PathSeg::Bezier(b) => b.start(),
}
}
@@ -1329,7 +1327,7 @@ impl PathSeg {
PathSeg::Line { b, .. } => b,
PathSeg::Arc { a, center, axis, angle } => axis.normalized().map_or(a, |k| rotate_about(a, center, k, angle)),
PathSeg::Curve { plane, seg } => plane.to_world(seg.end()),
PathSeg::Cubic { p } => p[3],
PathSeg::Bezier(b) => b.end(),
}
}
@@ -1348,9 +1346,39 @@ impl PathSeg {
PathSeg::Curve { plane, seg } => {
Some((plane.dir_to_world(seg.start_tangent()).normalized()?, plane.dir_to_world(seg.end_tangent()).normalized()?))
}
PathSeg::Cubic { p } => {
Some((crate::point_path::cubic_derivative(&p, 0.0).normalized()?, crate::point_path::cubic_derivative(&p, 1.0).normalized()?))
}
PathSeg::Bezier(b) => Some((b.derivative(0.0)?.normalized()?, b.derivative(1.0)?.normalized()?)),
}
}
/// The segment the other way round.
pub fn reversed(&self) -> PathSeg {
match *self {
PathSeg::Line { a, b } => PathSeg::Line { a: b, b: a },
PathSeg::Arc { center, axis, angle, .. } => PathSeg::Arc { a: self.end(), center, axis: axis * -1.0, angle },
PathSeg::Curve { plane, seg } => PathSeg::Curve { plane, seg: seg.reversed() },
PathSeg::Bezier(b) => PathSeg::Bezier(b.reversed()),
}
}
/// The segment carried by the rigid motion `point` of points and `vector` of directions
/// (a placement); `None` where a carried span no longer evaluates.
pub fn mapped(&self, point: impl Fn(Vec3) -> Vec3, vector: impl Fn(Vec3) -> Vec3) -> Option<PathSeg> {
Some(match *self {
PathSeg::Line { a, b } => PathSeg::Line { a: point(a), b: point(b) },
PathSeg::Arc { a, center, axis, angle } => PathSeg::Arc { a: point(a), center: point(center), axis: vector(axis), angle },
PathSeg::Curve { plane, seg } => PathSeg::Curve { plane: Plane::new(point(plane.origin), vector(plane.x), vector(plane.y))?, seg },
PathSeg::Bezier(b) => PathSeg::Bezier(b.mapped(point)?),
})
}
/// A sketch segment on `plane` as the path segment that is the same curve: a line, an arc
/// about the plane's normal, or the free curve on its plane.
pub fn of_sketch(plane: &Plane, s: &Seg2) -> PathSeg {
let w = |p: Vec2| plane.to_world(p);
match *s {
Seg2::Line { a, b } => PathSeg::Line { a: w(a), b: w(b) },
Seg2::Arc { center, sweep, .. } => PathSeg::Arc { a: w(s.start()), center: w(center), axis: plane.normal() * sweep.signum(), angle: sweep.abs() },
Seg2::Cubic { .. } | Seg2::Conic { .. } | Seg2::Bezier { .. } => PathSeg::Curve { plane: *plane, seg: *s },
}
}
@@ -1372,7 +1400,7 @@ impl PathSeg {
match *self {
PathSeg::Line { b, .. } => vec![b],
PathSeg::Curve { plane, seg } => seg.polyline(tol).into_iter().skip(1).map(|q| plane.to_world(q)).collect(),
PathSeg::Cubic { p } => crate::point_path::cubic_polyline(&p, tol),
PathSeg::Bezier(b) => b.polyline(tol),
PathSeg::Arc { a, center, axis, angle } => {
let Some(k) = axis.normalized() else { return vec![a] };
let r = (a - center).len();
+275 -45
View File
@@ -1,12 +1,15 @@
//! Sweep paths through 3D sketch curves ([`PointPath`]): polylines (linked, projected and
//! included curves, which reach the kernel as their points) and drawn 3D splines, which it gets
//! as their fit points and so exactly ([`solidcraft_geom::catmull_rom`]). SMLib follows the
//! path's segments ([`PointPath::segs`]), each spline span exactly as a [`PathSeg::Cubic`];
//! [`PointPath::points`] is its polyline, for what takes points (rails, curve networks).
//! Sweep paths through 3D sketch curves ([`PointPath`]): drawn 3D splines, which the kernel gets
//! as their fit points and so exactly ([`solidcraft_geom::catmull_rom`]); curves it holds
//! exactly as path segments (sketch curves, body edges: [`edge_path`]); and polylines (curves
//! that reach it only as points: projected onto a face, intersection and iso curves). SMLib
//! follows the path's segments ([`PointPath::segs`]), each free-form span exactly as a
//! [`PathSeg::Bezier`] ([`SpaceBezier`]); [`PointPath::points`] is its polyline, for what takes
//! points (rails, curve networks).
use solidcraft_geom::bspline::{ArcLength, CurveN};
use solidcraft_geom::brep;
use solidcraft_geom::bspline::{HPoint, bezier_point, segment};
use solidcraft_geom::catmull_rom;
use solidcraft_geom::{Plane, Region2, Vec3};
use solidcraft_geom::{MAX_BEZIER_CTRL, MAX_BEZIER_DEGREE, Plane, Region2, Vec3};
use crate::body::Body;
use crate::{KernelError, PathSeg, Result, SweepControl};
@@ -19,6 +22,9 @@ pub enum PathPiece {
/// A drawn 3D spline: the centripetal Catmull–Rom spline through `fit`, from its last fit
/// point back to its first when `reversed`.
Spline { fit: Vec<Vec3>, reversed: bool },
/// A curve the kernel holds exactly as path segments end to end: a sketch curve
/// ([`PathSeg::of_sketch`]) or a body edge ([`edge_path`]).
Segs(Vec<PathSeg>),
}
impl PathPiece {
@@ -34,6 +40,13 @@ impl PathPiece {
}
pts
}
PathPiece::Segs(segs) => {
let mut pts: Vec<Vec3> = segs.first().map(PathSeg::start).into_iter().collect();
for s in segs {
pts.extend(s.points_after_start(1e-2));
}
pts
}
}
}
@@ -45,23 +58,28 @@ impl PathPiece {
PathPiece::Polyline(pts)
}
PathPiece::Spline { fit, reversed } => PathPiece::Spline { fit, reversed: !reversed },
PathPiece::Segs(segs) => PathPiece::Segs(segs.iter().rev().map(PathSeg::reversed).collect()),
}
}
/// A spline of three or more fit points as its exact spans ([`PathSeg::Cubic`]), in the
/// direction of travel; `None` for a polyline (and a spline of one or two points, a point or
/// a line).
/// The piece's exact segments in the direction of travel: a spline of three or more fit
/// points as its spans ([`PathSeg::Bezier`]), a curve held exactly as its segments; `None`
/// for a polyline (and a spline of one or two points, a point or a line).
pub(crate) fn spans(&self) -> Result<Option<Vec<PathSeg>>> {
let PathPiece::Spline { fit, reversed } = self else { return Ok(None) };
let (fit, reversed) = match self {
PathPiece::Spline { fit, reversed } => (fit, reversed),
PathPiece::Segs(segs) => return Ok(Some(segs.clone())),
PathPiece::Polyline(_) => return Ok(None),
};
let spans = catmull_rom::beziers(fit).ok_or_else(|| KernelError::Invalid("the 3D sketch spline is not finite".into()))?;
if spans.is_empty() {
return Ok(None);
}
let seg = |p: [Vec3; 4]| PathSeg::Cubic { p };
let seg = |p: [Vec3; 4]| SpaceBezier::cubic(p).map(PathSeg::Bezier).ok_or_else(|| KernelError::Invalid("the 3D sketch spline is not finite".into()));
Ok(Some(if *reversed {
spans.into_iter().rev().map(|[a, b, c, d]| seg([d, c, b, a])).collect()
spans.into_iter().rev().map(|[a, b, c, d]| seg([d, c, b, a])).collect::<Result<_>>()?
} else {
spans.into_iter().map(seg).collect()
spans.into_iter().map(seg).collect::<Result<_>>()?
}))
}
}
@@ -94,7 +112,7 @@ impl PointPath {
chain
}
/// The path as segments: each spline span exactly ([`PathSeg::Cubic`]), each polyline as the
/// The path as segments: each spline span exactly ([`PathSeg::Bezier`]), each polyline as the
/// lines between its points, starting where the piece before it ended (as [`Self::points`]
/// chains them).
pub(crate) fn segs(&self) -> Result<Vec<PathSeg>> {
@@ -121,43 +139,255 @@ pub fn sweep_point_path_with(plane: &Plane, region: &Region2, path: &PointPath,
crate::sweep_path::smlib_sweep_points(plane, region, path, ctl)
}
/// A cubic Bézier in space (a [`PathSeg::Cubic`]): its point at `x` (0…1).
pub(crate) fn cubic_at(p: &[Vec3; 4], x: f64) -> Vec3 {
let y = 1.0 - x;
p[0] * (y * y * y) + p[1] * (3.0 * x * y * y) + p[2] * (3.0 * x * x * y) + p[3] * (x * x * x)
/// Most segments one edge's curve may give a path.
const MAX_EDGE_SEGS: usize = 1000;
/// Edge `edge` of `body` (its index in the body's edge order) as the path segments that are its
/// curve exactly, from its start vertex to its end: a line, an arc, rational quadratic spans of
/// an ellipse, the lines of a polyline, the Bézier spans of a B-spline between its knots. An
/// intersection curve, which the record holds only as its surfaces and a leader near it, is
/// refused.
pub fn edge_path(body: &Body, edge: usize) -> Result<Vec<PathSeg>> {
let record = crate::native::brep_record(body)?;
let e = record.edges.get(edge).ok_or_else(|| KernelError::Invalid(format!("the body has no edge {edge}")))?;
curve_path(&e.curve, e.interval)
}
/// Its derivative at `x`.
pub(crate) fn cubic_derivative(p: &[Vec3; 4], x: f64) -> Vec3 {
let y = 1.0 - x;
(p[1] - p[0]) * (3.0 * y * y) + (p[2] - p[1]) * (6.0 * x * y) + (p[3] - p[2]) * (3.0 * x * x)
/// The record curve `c` on `[t0, t1]` as path segments ([`edge_path`]).
pub(crate) fn curve_path(c: &brep::Curve, [t0, t1]: [f64; 2]) -> Result<Vec<PathSeg>> {
let bad = |what: &str| KernelError::Invalid(format!("the edge's {what} does not evaluate"));
if !(t0.is_finite() && t1.is_finite() && t0 < t1) {
return Err(bad("interval"));
}
let v = |q: [f64; 3]| Vec3::new(q[0], q[1], q[2]);
let out = match c {
brep::Curve::Line { start, end } => {
let (s, e) = (v(*start), v(*end));
vec![PathSeg::Line { a: s + (e - s) * t0, b: s + (e - s) * t1 }]
}
brep::Curve::Circle { frame, radius } => {
let (o, x, y) = (v(frame.origin), v(frame.x_axis), v(frame.y_axis));
vec![PathSeg::Arc { a: o + (x * t0.cos() + y * t0.sin()) * *radius, center: o, axis: x.cross(y), angle: t1 - t0 }]
}
brep::Curve::Ellipse { frame, radii } => {
// Each span of at most a quarter turn the affine image of a circular arc's rational
// quadratic: ends on the ellipse, the middle point at the tangents' meeting, weight
// cos(half the span).
let (o, x, y) = (v(frame.origin), v(frame.x_axis) * radii[0], v(frame.y_axis) * radii[1]);
let at = |t: f64, r: f64| o + (x * t.cos() + y * t.sin()) * r;
let m = ((t1 - t0) / std::f64::consts::FRAC_PI_2 - 1e-9).ceil().max(1.0) as usize;
if m > MAX_EDGE_SEGS {
return Err(bad("ellipse"));
}
let d = (t1 - t0) / m as f64;
(0..m)
.map(|i| {
let a = t0 + d * i as f64;
let w = (d / 2.0).cos();
let ctrl = [HPoint::weighted(at(a, 1.0), 1.0), HPoint::weighted(at(a + d / 2.0, 1.0 / w), w), HPoint::weighted(at(a + d, 1.0), 1.0)];
SpaceBezier::new(&ctrl).map(PathSeg::Bezier).ok_or_else(|| bad("ellipse"))
})
.collect::<Result<_>>()?
}
brep::Curve::Polyline { points } => {
let at = |t: f64| -> Option<Vec3> {
let i = (t.floor().max(0.0) as usize).min(points.len().saturating_sub(2));
let (a, b) = (v(*points.get(i)?), v(*points.get(i + 1)?));
Some(a + (b - a) * (t - i as f64))
};
let mut ts = vec![t0];
ts.extend((t0.floor() as i64 + 1..=t1.ceil() as i64 - 1).map(|k| k as f64).filter(|k| *k > t0 && *k < t1));
ts.push(t1);
if ts.len() > MAX_EDGE_SEGS {
return Err(bad("polyline"));
}
ts.windows(2).map(|w| Some(PathSeg::Line { a: at(w[0])?, b: at(w[1])? })).collect::<Option<_>>().ok_or_else(|| bad("polyline"))?
}
brep::Curve::Spline { knots, points } => {
let ctrl: Vec<HPoint> = points.iter().map(|q| HPoint::weighted(v(*q), 1.0)).collect();
spline_spans(knots, &ctrl, t0, t1)?
}
brep::Curve::RationalSpline { knots, homogeneous } => {
let ctrl: Vec<HPoint> = homogeneous.iter().map(|q| HPoint { x: q[0], y: q[1], z: q[2], w: q[3] }).collect();
spline_spans(knots, &ctrl, t0, t1)?
}
brep::Curve::SurfaceIntersection { .. } => {
return Err(KernelError::Failed("not supported yet: a path along an intersection curve (the record holds no exact curve for it)".into()));
}
};
Ok(out)
}
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
fn cubic_curve(p: &[Vec3; 4]) -> CurveN<3> {
CurveN { degree: 3, knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], ctrl: p.iter().map(|q| [q.x, q.y, q.z]).collect() }
/// The B-spline (homogeneous `ctrl` on `knots`) on `[t0, t1]` as its Bézier spans, one per knot
/// span: each the piece of the spline between two neighbouring knots ([`segment`]), exactly.
fn spline_spans(knots: &[f64], ctrl: &[HPoint], t0: f64, t1: f64) -> Result<Vec<PathSeg>> {
let bad = || KernelError::Invalid("the edge's B-spline does not evaluate".into());
let p = knots.len().checked_sub(ctrl.len() + 1).ok_or_else(bad)?;
if !(1..=MAX_BEZIER_DEGREE).contains(&p) {
return Err(KernelError::Failed(format!("not supported yet: a path along a B-spline of degree {p}")));
}
let mut cuts = vec![t0];
cuts.extend(knots.iter().copied().filter(|k| *k > t0 && *k < t1));
cuts.push(t1);
cuts.dedup();
if cuts.len() > MAX_EDGE_SEGS {
return Err(KernelError::Failed("not supported yet: a path along an edge of more than 1000 spans".into()));
}
cuts.windows(2)
.map(|w| {
let (_, piece) = segment(p, knots, ctrl, w[0], w[1]).map_err(|e| KernelError::Invalid(format!("the edge's B-spline: {e}")))?;
SpaceBezier::new(&piece).map(PathSeg::Bezier).ok_or_else(bad)
})
.collect()
}
/// Its length (`None` for a cubic that does not evaluate).
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
pub(crate) fn cubic_length(p: &[Vec3; 4]) -> Option<f64> {
ArcLength::of(&cubic_curve(p)).map(|a| a.total())
/// A rational Bézier span in space (a [`PathSeg::Bezier`]): degree 1…[`MAX_BEZIER_DEGREE`],
/// homogeneous control points `(x·w, y·w, z·w, w)` with positive weights, on 0…1. A drawn 3D
/// spline's spans are cubics of weight one; an edge's NURBS curve and an ellipse are spans of
/// their degree, exactly.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpaceBezier {
n: u8,
p: [HPoint; MAX_BEZIER_CTRL],
}
/// The point `f` (0…1) of the way along the cubic by length, and the unit direction there.
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
pub(crate) fn cubic_along(p: &[Vec3; 4], f: f64) -> Option<(Vec3, Vec3)> {
let c = cubic_curve(p);
let arc = ArcLength::of(&c)?;
let x = arc.param_at(&c, f * arc.total())?;
Some((cubic_at(p, x), cubic_derivative(p, x).normalized()?))
}
/// Pieces a span's length is integrated over (8-point Gauss–Legendre on each).
const LENGTH_PIECES: usize = 16;
/// Points along the cubic, without its start, the chords within `tol` of it (by the bound on
/// its second derivative, 6 max |P(i) - 2 P(i+1) + P(i+2)|: a chord of a piece `h` long in the
/// parameter is within h^2/8 of that of the curve).
pub(crate) fn cubic_polyline(p: &[Vec3; 4], tol: f64) -> Vec<Vec3> {
let bend = (p[0] - p[1] * 2.0 + p[2]).len().max((p[1] - p[2] * 2.0 + p[3]).len()) * 6.0;
let n = ((bend / (8.0 * tol.max(1e-9))).sqrt().ceil() as usize).clamp(1, 512);
(1..=n).map(|i| cubic_at(p, i as f64 / n as f64)).collect()
/// Nodes and weights of the 8-point Gauss–Legendre rule on [-1, 1].
const GAUSS_8: [(f64, f64); 8] = [
(-0.960_289_856_497_536_2, 0.101_228_536_290_376_26),
(-0.796_666_477_413_626_7, 0.222_381_034_453_374_47),
(-0.525_532_409_916_329, 0.313_706_645_877_887_3),
(-0.183_434_642_495_649_8, 0.362_683_783_378_362),
(0.183_434_642_495_649_8, 0.362_683_783_378_362),
(0.525_532_409_916_329, 0.313_706_645_877_887_3),
(0.796_666_477_413_626_7, 0.222_381_034_453_374_47),
(0.960_289_856_497_536_2, 0.101_228_536_290_376_26),
];
impl SpaceBezier {
/// The span over `ctrl` (2…[`MAX_BEZIER_CTRL`] finite homogeneous points, positive weights).
pub fn new(ctrl: &[HPoint]) -> Option<SpaceBezier> {
let n = u8::try_from(ctrl.len().checked_sub(1)?).ok()?;
if !(1..=MAX_BEZIER_DEGREE as u8).contains(&n) || ctrl.iter().any(|q| !q.is_finite() || !(q.w > 0.0)) {
return None;
}
let mut p = [HPoint::ZERO; MAX_BEZIER_CTRL];
p.get_mut(..ctrl.len())?.copy_from_slice(ctrl);
Some(SpaceBezier { n, p })
}
/// The cubic over `p` (weights one).
pub fn cubic(p: [Vec3; 4]) -> Option<SpaceBezier> {
SpaceBezier::new(&p.map(|q| HPoint::weighted(q, 1.0)))
}
pub fn degree(&self) -> usize {
usize::from(self.n)
}
/// The homogeneous control points.
pub fn ctrl(&self) -> &[HPoint] {
self.p.get(..=self.degree()).unwrap_or(&[])
}
/// The point at `x` (0…1).
pub fn point(&self, x: f64) -> Option<Vec3> {
bezier_point(self.ctrl(), x).project()
}
pub fn start(&self) -> Vec3 {
self.ctrl().first().and_then(|q| q.project()).unwrap_or_default()
}
pub fn end(&self) -> Vec3 {
self.ctrl().last().and_then(|q| q.project()).unwrap_or_default()
}
/// The derivative at `x`: the quotient rule on the homogeneous curve and its hodograph.
pub fn derivative(&self, x: f64) -> Option<Vec3> {
let c = self.ctrl();
let n = self.degree() as f64;
let hodograph: Vec<HPoint> = c.windows(2).map(|w| (w[1] - w[0]) * n).collect();
let (h, d) = (bezier_point(c, x), bezier_point(&hodograph, x));
(h.w.abs() > f64::MIN_POSITIVE).then(|| (d.xyz() * h.w - h.xyz() * d.w) * (1.0 / (h.w * h.w))).filter(|v| v.is_finite())
}
/// The span the other way round.
pub fn reversed(&self) -> SpaceBezier {
let mut r = *self;
if let Some(c) = r.p.get_mut(..=self.degree()) {
c.reverse();
}
r
}
/// The span carried by the affine map `f` of points (weights kept: an affine image of a
/// rational Bézier is the Bézier of the images of its control points).
pub fn mapped(&self, f: impl Fn(Vec3) -> Vec3) -> Option<SpaceBezier> {
let ctrl: Vec<HPoint> = self.ctrl().iter().map(|q| q.project().map(|p| HPoint::weighted(f(p), q.w))).collect::<Option<_>>()?;
SpaceBezier::new(&ctrl)
}
/// The control points as SMLib takes them: (x, y, z, weight).
pub(crate) fn weighted_points(&self) -> Option<Vec<[f64; 4]>> {
self.ctrl().iter().map(|q| q.project().map(|p| [p.x, p.y, p.z, q.w])).collect()
}
fn speed(&self, x: f64) -> Option<f64> {
self.derivative(x).map(Vec3::len)
}
/// The length from `a` to `b` (8-point Gauss–Legendre).
fn length_between(&self, a: f64, b: f64) -> Option<f64> {
let (mid, half) = (0.5 * (a + b), 0.5 * (b - a));
GAUSS_8.iter().try_fold(0.0, |sum, (x, w)| Some(sum + w * half * self.speed(mid + half * x)?))
}
/// The length before each of the [`LENGTH_PIECES`] pieces, and the whole length last.
fn lengths(&self) -> Option<Vec<f64>> {
let mut out = vec![0.0];
for i in 0..LENGTH_PIECES {
let (a, b) = (i as f64 / LENGTH_PIECES as f64, (i + 1) as f64 / LENGTH_PIECES as f64);
out.push(out.last().copied().unwrap_or(0.0) + self.length_between(a, b)?);
}
out.last().is_some_and(|t| t.is_finite()).then_some(out)
}
/// Its length (`None` for a span that does not evaluate).
pub fn length(&self) -> Option<f64> {
self.lengths()?.last().copied()
}
/// The point `f` (0…1) of the way along the span by length, and the unit direction there.
pub fn along(&self, f: f64) -> Option<(Vec3, Vec3)> {
let table = self.lengths()?;
let want = table.last()? * f.clamp(0.0, 1.0);
let k = table.partition_point(|l| *l <= want).saturating_sub(1).min(LENGTH_PIECES - 1);
let (a, before) = (k as f64 / LENGTH_PIECES as f64, *table.get(k)?);
let (mut lo, mut hi) = (a, (k + 1) as f64 / LENGTH_PIECES as f64);
for _ in 0..60 {
let x = 0.5 * (lo + hi);
if before + self.length_between(a, x)? < want {
lo = x;
} else {
hi = x;
}
}
let x = 0.5 * (lo + hi);
Some((self.point(x)?, self.derivative(x)?.normalized()?))
}
/// Points along the span, without its start, every chord within `tol` of it (by the bound
/// on the homogeneous second differences, as for a polynomial span of weight one; a rational
/// span's homogeneous bound over its smallest weight).
pub fn polyline(&self, tol: f64) -> Vec<Vec3> {
let c = self.ctrl();
let n = self.degree() as f64;
let wmin = c.iter().map(|q| q.w).fold(f64::INFINITY, f64::min).max(1e-12);
let bend = c.windows(3).map(|w| (w[0] - w[1] * 2.0 + w[2]).xyz().len()).fold(0.0, f64::max) * n * (n - 1.0).max(1.0) / wmin;
let k = ((bend / (8.0 * tol.max(1e-9))).sqrt().ceil() as usize).clamp(1, 512);
(1..=k).filter_map(|i| self.point(i as f64 / k as f64)).collect()
}
}
+11 -11
View File
@@ -483,11 +483,11 @@ mod imp {
Curve::Arc { center: p(center), axis: p(k), reference: p(reference), radius, start: 0.0, end: turn }
}
crate::PathSeg::Curve { plane, seg } => curve(&plane, &seg)?,
crate::PathSeg::Cubic { p: ctrl } => Curve::Nurbs {
degree: 3,
points: ctrl.iter().map(|q| [q.x, q.y, q.z, 1.0]).collect(),
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
},
crate::PathSeg::Bezier(b) => {
let degree = b.degree();
let points = b.weighted_points().ok_or_else(|| KernelError::Invalid("a path span does not evaluate".into()))?;
Curve::Nurbs { degree, points, knots: std::iter::repeat_n(0.0, degree + 1).chain(std::iter::repeat_n(1.0, degree + 1)).collect() }
}
})
}
@@ -564,7 +564,7 @@ mod imp {
(a, crate::build::rotate_about(a, center, k, angle))
}
crate::PathSeg::Curve { plane, seg } => (plane.to_world(seg.start()), plane.to_world(seg.end())),
crate::PathSeg::Cubic { p: ctrl } => (ctrl[0], ctrl[3]),
crate::PathSeg::Bezier(b) => (b.start(), b.end()),
})
}
@@ -608,7 +608,7 @@ mod imp {
length += a.dist(b).max(match *s {
crate::PathSeg::Arc { a, center, angle, .. } => (a - center).len() * angle.abs(),
crate::PathSeg::Curve { seg, .. } => seg.length(),
crate::PathSeg::Cubic { .. } => seg_len(s),
crate::PathSeg::Bezier(_) => seg_len(s),
crate::PathSeg::Line { .. } => 0.0,
});
}
@@ -712,7 +712,7 @@ mod imp {
crate::PathSeg::Line { a, b } => a.dist(b),
crate::PathSeg::Arc { a, center, angle, .. } => angle.abs() * a.dist(center),
crate::PathSeg::Curve { seg, .. } => seg.length(),
crate::PathSeg::Cubic { p: ctrl } => crate::point_path::cubic_length(&ctrl).unwrap_or(f64::NAN),
crate::PathSeg::Bezier(b) => b.length().unwrap_or(f64::NAN),
}
}
@@ -740,7 +740,7 @@ mod imp {
let t = 0.5 * (lo + hi);
Some((plane.to_world(seg.point_at(t)), plane.dir_to_world(seg.derivative(t)).normalized()?))
}
crate::PathSeg::Cubic { p: ctrl } => crate::point_path::cubic_along(&ctrl, f),
crate::PathSeg::Bezier(b) => b.along(f),
}
}
@@ -782,8 +782,8 @@ mod imp {
.map(|(u, v)| u.cross(v).atan2(u.dot(v)).abs())
.sum()
}
crate::PathSeg::Cubic { p: ctrl } => {
let pts: Vec<Vec3> = std::iter::once(ctrl[0]).chain(crate::point_path::cubic_polyline(&ctrl, 1e-3)).collect();
crate::PathSeg::Bezier(b) => {
let pts: Vec<Vec3> = std::iter::once(b.start()).chain(b.polyline(1e-3)).collect();
pts.windows(3)
.filter_map(|w| Some(((w[1] - w[0]).normalized()?, (w[2] - w[1]).normalized()?)))
.map(|(u, v)| u.cross(v).len().atan2(u.dot(v)))
+1 -1
View File
@@ -237,7 +237,7 @@ pub fn sweep_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[crate::PathSeg])
(part, Box::new(move |p| crate::build::rotate_about(p, center, k, angle)))
}
// The path was flattened: no curves are left.
PathSeg::Curve { .. } | PathSeg::Cubic { .. } => return Err(KernelError::Invalid("a free-form path curve".into())),
PathSeg::Curve { .. } | PathSeg::Bezier(_) => return Err(KernelError::Invalid("a free-form path curve".into())),
};
let next = far_wire(&part, &cur, map.as_ref())?;
sides.extend(swept_sides(&part, &seeds, &[]));