pub struct NurbCurve<S: Scalar, const D: usize> {
pub degree: usize,
pub control_points: Vec<Vector<S, D>>,
pub knot_vector: Vec<S>,
/* private fields */
}Expand description
A NURBS curve whose control points live in D-dimensional homogeneous space.
D = 4: 3-D curve — control points are(wx, wy, wz, w).D = 3: 2-D curve (pcurve) — control points are(wu, wv, w).
Fields§
§degree: usize§control_points: Vec<Vector<S, D>>§knot_vector: Vec<S>Implementations§
Source§impl<S: Scalar> NurbCurve<S, 4>
impl<S: Scalar> NurbCurve<S, 4>
Sourcepub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>>
pub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>>
Convex hull of the curve’s Cartesian (dehomogenized) control points.
By the convex-hull property of the NURBS basis, every point on the curve lies within this hull.
Source§impl<S: Scalar> NurbCurve<S, 3>
impl<S: Scalar> NurbCurve<S, 3>
Sourcepub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 2>>
pub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 2>>
Convex hull of the pcurve’s Cartesian (dehomogenized) control points.
By the convex-hull property of the NURBS basis, every point on the curve lies within this hull.
Source§impl<S: Scalar, const D: usize> NurbCurve<S, D>
impl<S: Scalar, const D: usize> NurbCurve<S, D>
Sourcepub fn derivative(&self) -> GeopResult<NurbCurve<S, D>>
pub fn derivative(&self) -> GeopResult<NurbCurve<S, D>>
Return the derivative of this curve as a new NurbCurve of degree p − 1.
The returned curve is the derivative of the homogeneous B-spline.
Evaluating it at t yields the homogeneous tangent vector. To obtain
the Cartesian tangent C′(t), apply the quotient rule:
C′(t) = (A′(t) − w′(t)·C(t)) / w(t)Errors if self.degree == 0.
Source§impl<S: Scalar> NurbCurve<S, 4>
impl<S: Scalar> NurbCurve<S, 4>
Sourcepub fn interpolate(points: &[Vector3<S>], degree: usize) -> GeopResult<Self>
pub fn interpolate(points: &[Vector3<S>], degree: usize) -> GeopResult<Self>
Fit a 3-D NURBS curve exactly through points — see interpolate
above. Between the points it is only an approximation of whatever
curve they were sampled from; use Self::interpolate_enclosing
when the result has to enclose that curve.
Sourcepub fn interpolate_enclosing(
points: &[Vector3<S>],
between: &[Vec<Vector3<S>>],
degree: usize,
) -> GeopResult<Self>
pub fn interpolate_enclosing( points: &[Vector3<S>], between: &[Vec<Vector3<S>>], degree: usize, ) -> GeopResult<Self>
Fit a 3-D NURBS curve through samples points of some true curve
(through their centres; their full width is enclosed), widened so it
also encloses that curve between them: between[i] are
points of the true curve strictly between points[i] and
points[i + 1], in order and roughly evenly spaced (see
true_point_fractions for the usual choice of where).
An interpolant drifts from the curve it was sampled from between the samples. That drift is a genuine uncertainty about where the curve is, so it belongs in the curve’s interval width: otherwise a point on the true curve tests as not on its interpolant, and every containment or intersection question asked of it is answered for the wrong curve. A true point per interval measures it where it is largest; the enclosure is exact there and as good as that measurement in between.
Source§impl<S: Scalar> NurbCurve<S, 3>
impl<S: Scalar> NurbCurve<S, 3>
Sourcepub fn interpolate(points: &[Vector2<S>], degree: usize) -> GeopResult<Self>
pub fn interpolate(points: &[Vector2<S>], degree: usize) -> GeopResult<Self>
Fit a 2-D NURBS curve exactly through points — see the 3-D
NurbCurve::interpolate.
Sourcepub fn interpolate_enclosing(
points: &[Vector2<S>],
between: &[Vec<Vector2<S>>],
degree: usize,
) -> GeopResult<Self>
pub fn interpolate_enclosing( points: &[Vector2<S>], between: &[Vec<Vector2<S>>], degree: usize, ) -> GeopResult<Self>
The 2-D counterpart of the 3-D NurbCurve::interpolate_enclosing.
Source§impl<S: Scalar> NurbCurve<S, 4>
impl<S: Scalar> NurbCurve<S, 4>
Sourcepub fn project(
&self,
target: Vector3<S>,
max_nodes: usize,
min_subdivision_size: S,
) -> GeopResult<Vec<S>>
pub fn project( &self, target: Vector3<S>, max_nodes: usize, min_subdivision_size: S, ) -> GeopResult<Vec<S>>
Every parameter t at which this curve passes through target
(plural since a self-intersecting curve can pass through the same
point at more than one, genuinely distinct, parameter).
Recursively subdivides the curve, discarding any segment whose
convex hull could not contain target. A surviving segment
converges once its hull’s chord length is no longer definitely
greater than min_subdivision_size, contributing the
[Scalar::union] of its own [t0, t1] domain as a candidate,
folded into a [DisjointSet] so no two returned solutions ever
describe the same physical parameter — for a genuine interval
scalar each is a real “the true parameter is provably within this
span” guarantee, not an arbitrarily narrowed single point.
Source§impl<S: Scalar, const D: usize> NurbCurve<S, D>
impl<S: Scalar, const D: usize> NurbCurve<S, D>
Sourcepub fn refine_parameter_at_point<const C: usize>(
&self,
t: S,
point: &Vector<S, C>,
) -> GeopResult<S>where
NurbCurve<S, D>: ParameterRefinable<S, C>,
pub fn refine_parameter_at_point<const C: usize>(
&self,
t: S,
point: &Vector<S, C>,
) -> GeopResult<S>where
NurbCurve<S, D>: ParameterRefinable<S, C>,
Refine t — a parameter enclosure produced by a subdivision search —
into the tightest enclosure of the parameter at which this curve
passes through point.
§Why this exists
Subdivision is a global method: it reliably finds and separates every solution, and (via the leaf-count signal the intersection searches rely on) recognizes coincidence even for a partial overlap. What it is bad at is the last few digits — it converges one bit per split, so squeezing a parameter down to machine accuracy would take ~50 levels, which is exponentially more work than the ~7 needed to isolate the solution in the first place.
Newton is the opposite: useless for finding solutions, unbeatable for polishing one that is already isolated, converging quadratically. So the two compose — subdivide to isolate, then refine here.
This is what lets NurbCurve::split be called without sharpening.
A min_subdivision_size-wide t cannot be fed to Boehm insertion:
its width flows into alpha = (t - e) / (s - e), whose denominator
shrinks with every successive split while the width does not, so the
sub-curves’ control points widen without bound. The old answer was to
sharpen the parameter at each call site, which moved the split to
the interval’s midpoint rather than the point actually located — a
silent geometric error of |t_mid - t*| x |C'(t)|, and the reason an
edge endpoint could land ~1e-8 from the vertex it is anchored to. A
refined parameter is narrow and still an honest enclosure, so it
needs no sharpening and introduces no such error.
§Honesty of the result
Every iterate except the last is sharpened, which is legitimate: it is
only a seed for the next step, and any value inside it is an equally
good one. The final step is left unsharpened, so the returned width is
the honest statement of how precisely point pins down a parameter
(see “Sharpen only where the value is a free choice” in AGENTS.md —
this is exactly the rule NurbSurface::project follows).
The result is then intersected with the incoming t: both are valid
enclosures of the same parameter, so their intersection is too, and is
tighter than either. If they turn out to be disjoint, Newton has
wandered out of the box the search proved the solution lies in — the
incoming enclosure is returned unchanged rather than trusting the
refinement. The same fallback covers a vanishing tangent (the
Gauss-Newton denominator could be zero), so this never turns a usable
answer into a failure.
Source§impl<S: Scalar, const D: usize> NurbCurve<S, D>
impl<S: Scalar, const D: usize> NurbCurve<S, D>
Sourcepub fn reverse(&self) -> Self
pub fn reverse(&self) -> Self
This curve traversed in the opposite direction: same domain, same
shape, but reverse().evaluate(t) == evaluate(t_min + t_max - t).
Reverses the control points and complements the knot vector
(u -> t_min + t_max - u, then reversed) — the standard B-spline
reversal construction.
Source§impl<S: Scalar, const D: usize> NurbCurve<S, D>
impl<S: Scalar, const D: usize> NurbCurve<S, D>
Sourcepub fn split(&self, t: S) -> GeopResult<(NurbCurve<S, D>, NurbCurve<S, D>)>
pub fn split(&self, t: S) -> GeopResult<(NurbCurve<S, D>, NurbCurve<S, D>)>
Split at parameter t (must be strictly inside the domain).
Returns (left, right) sharing the junction value.
t is used exactly as given — it is not sharpened here. Whether
t’s width may be discarded belongs to whoever produced it, since
only they know what it means, so this function neither assumes nor
imposes an answer.
What t must be is narrow, not sharp. Boehm insertion cannot absorb
a wide parameter: its width flows into alpha = (t - e) / (s - e) — s - e shrinks with every successive
split while an unsharpened width does not, so their ratio widens
without bound — and on into the sub-curves’ control points, until
downstream subdivision searches stop converging.
Callers used to meet that by sharpening, which was a real geometric
error: it moved the cut to the interval’s midpoint rather than the
point actually located, off by |t_mid - t*| x |C'(t)|, which is how
an edge endpoint ended up ~1e-8 from the vertex it was anchored to.
They now Newton-refine instead — see
NurbCurve::refine_parameter_at_point and
intersection::curve_surface::refine_crossing — which yields a
parameter that is narrow and still an honest enclosure. Subdivision
isolates the solution, Newton polishes it; neither does the other’s
job. No caller on the split path sharpens any more.
See “Sharpen only where the value is a free choice” in AGENTS.md.
Sourcepub fn sub_curve(&self, t0: S, t1: S) -> GeopResult<NurbCurve<S, D>>
pub fn sub_curve(&self, t0: S, t1: S) -> GeopResult<NurbCurve<S, D>>
This curve restricted to [t0, t1], cut in one pass: each bound that
lies strictly inside the domain (definitely_greater the start /
definitely_less the end) is inserted to full multiplicity and the
outside is dropped; a bound that doesn’t is not cut at, so the result
always covers at least [t0, t1] ∩ domain. Cheaper than two
Self::splits: no discarded piece or intermediate curve is built.
Like split, the bounds are used exactly as given and must be narrow.
Source§impl<S: Scalar> NurbCurve<S, 4>
impl<S: Scalar> NurbCurve<S, 4>
Sourcepub fn sweep(&self, offset: Vector3<S>) -> NurbSurface3D<S>
pub fn sweep(&self, offset: Vector3<S>) -> NurbSurface3D<S>
The ruled surface swept out by translating this curve along
offset: S(t, s) = self.evaluate(t) + s * offset. Degree
(self.degree, 1) — same knot vector as self in t, s ∈ [0, 1] — control points [cp_i, cp_i + offset] row-major per original
control point cp_i.
Source§impl<S: Scalar> NurbCurve<S, 4>
impl<S: Scalar> NurbCurve<S, 4>
Sourcepub fn tangent(&self, t: S) -> GeopResult<Vector3<S>>
pub fn tangent(&self, t: S) -> GeopResult<Vector3<S>>
Cartesian tangent vector C'(t) (not normalized).
Evaluates the homogeneous derivative curve from NurbCurve::derivative
and applies the quotient rule C'(t) = (A'(t) − w'(t)·C(t)) / w(t),
where (A(t), w(t)) is self’s own homogeneous point at t.
Sourcepub fn second_derivative(&self, t: S) -> GeopResult<Vector3<S>>
pub fn second_derivative(&self, t: S) -> GeopResult<Vector3<S>>
Cartesian second derivative C''(t) (not normalized).
Applies the quotient rule twice: C''(t) = (A''(t) − 2·w'(t)·C'(t) − w''(t)·C(t)) / w(t), where (A(t), w(t)) is self’s own homogeneous
point at t and A''(t), w''(t) come from differentiating the
homogeneous curve twice via NurbCurve::derivative.
A curve of degree < 2 has no well-defined second derivative of its
homogeneous representation (differentiating twice underflows the
degree) — but geometrically a degree-0/1 curve is a straight
line, whose Cartesian second derivative is genuinely zero, so that’s
what’s returned instead of an error.
Source§impl<S: Scalar> NurbCurve<S, 3>
impl<S: Scalar> NurbCurve<S, 3>
Sourcepub fn tangent(&self, t: S) -> GeopResult<Vector2<S>>
pub fn tangent(&self, t: S) -> GeopResult<Vector2<S>>
Cartesian tangent vector C'(t) (not normalized), for a 2-D pcurve.
Same quotient-rule derivation as the 3-D NurbCurve::<S, 4>::tangent.
Source§impl<S: Scalar, const D: usize> NurbCurve<S, D>
impl<S: Scalar, const D: usize> NurbCurve<S, D>
pub fn try_new( degree: usize, control_points: Vec<Vector<S, D>>, knot_vector: Vec<S>, ) -> GeopResult<Self>
Sourcepub fn recompute_aabb(&mut self)
pub fn recompute_aabb(&mut self)
Refresh the cached Self::aabb from the current control_points.
Every constructor in this module keeps aabb in sync automatically,
but control_points is a pub field and at least one caller outside
this crate (Model::reverse_face, mirroring a pcurve’s control
points in place to flip a face) legitimately mutates it directly
rather than building a new curve — call this afterwards or the
cached box silently goes stale and the intersection search’s
aabb_could_overlap prefilter starts pruning real overlaps.
pub fn domain_as_scalar(&self) -> S
Sourcepub fn everything() -> Self
pub fn everything() -> Self
A degenerate, maximally-unsharp curve: a single control point whose
every coordinate is [Scalar::ENTIRE], over a domain that accepts
any parameter. evaluate() at any t returns ENTIRE in every
coordinate, so it could_be_equals any point — a placeholder for
geometry that is not yet known.
Trait Implementations§
Source§impl<S: Scalar> HasConvexHull<S, 2> for NurbCurve<S, 3>
impl<S: Scalar> HasConvexHull<S, 2> for NurbCurve<S, 3>
Source§impl<S: Scalar> HasConvexHull<S, 3> for NurbCurve<S, 4>
impl<S: Scalar> HasConvexHull<S, 3> for NurbCurve<S, 4>
Source§impl<S: Scalar> ParameterRefinable<S, 2> for NurbCurve<S, 3>
impl<S: Scalar> ParameterRefinable<S, 2> for NurbCurve<S, 3>
fn evaluate_cartesian(&self, t: S) -> GeopResult<Vector<S, 2>>
Source§fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 2>>
fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 2>>
tangent. Not the
derivative() curve: that is the homogeneous derivative, whose
weight component is zero for a non-rational curve, so evaluating it
as a rational curve fails outright.Source§impl<S: Scalar> ParameterRefinable<S, 3> for NurbCurve<S, 4>
impl<S: Scalar> ParameterRefinable<S, 3> for NurbCurve<S, 4>
fn evaluate_cartesian(&self, t: S) -> GeopResult<Vector<S, 3>>
Source§fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 3>>
fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 3>>
tangent. Not the
derivative() curve: that is the homogeneous derivative, whose
weight component is zero for a non-rational curve, so evaluating it
as a rational curve fails outright.