Skip to main content

NurbCurve

Struct NurbCurve 

Source
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>

Source

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

pub fn size(&self) -> GeopResult<S>

Chord length of the curve’s convex hull, used as a convergence measure for subdivision algorithms.

Source§

impl<S: Scalar> NurbCurve<S, 3>

Source

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

pub fn size(&self) -> GeopResult<S>

Chord length of the pcurve’s convex hull, used as a convergence measure for subdivision algorithms.

Source§

impl<S: Scalar, const D: usize> NurbCurve<S, D>

Source

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>

Source

pub fn evaluate(&self, t: S) -> GeopResult<Vector3<S>>

Evaluate the 3-D NURBS curve at t, returning a Cartesian Vector3.

Source§

impl<S: Scalar> NurbCurve<S, 3>

Source

pub fn evaluate(&self, t: S) -> GeopResult<Vector2<S>>

Evaluate the 2-D pcurve at t, returning a Cartesian Vector2.

Source§

impl<S: Scalar> NurbCurve<S, 4>

Source

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.

Source

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>

Source

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.

Source

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>

Source

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>

Source

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>

Source

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> NurbCurve<S, 3>

Source

pub fn swap_xy(&self) -> Self

This 2-D curve mirrored across the diagonal x = y: every point (x, y) becomes (y, x), with the same parametrization. Mirroring flips orientation, so a counter-clockwise loop becomes clockwise.

Source§

impl<S: Scalar, const D: usize> NurbCurve<S, D>

Source

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.

Source

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

pub fn split_mid(&self) -> GeopResult<(NurbCurve<S, D>, NurbCurve<S, D>)>

Split at the midpoint of the parameter domain.

Source§

impl<S: Scalar> NurbCurve<S, 4>

Source

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>

Source

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.

Source

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>

Source

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> NurbCurve<S, 4>

Source

pub fn translate(&self, offset: Vector3<S>) -> Self

This 3-D curve shifted by offset — same shape and parametrization, every point moved by offset. Translates each (homogeneous) control point by offset * weight, leaving weights and the knot vector untouched.

Source§

impl<S: Scalar, const D: usize> NurbCurve<S, D>

Source

pub fn try_new( degree: usize, control_points: Vec<Vector<S, D>>, knot_vector: Vec<S>, ) -> GeopResult<Self>

Source

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.

Source

pub fn domain(&self) -> (S, S)

Valid parameter range (start_t, end_t) of this curve.

Source

pub fn domain_as_scalar(&self) -> S

Source

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.

Source§

impl<S: Scalar> NurbCurve<S, 4>

Source

pub fn as_line(&self) -> GeopResult<Option<Axis<S>>>

The line the curve runs along, if it is straight: every control point on the line from the first to the last, which is then the curve’s direction. None for a curve that bends, or whose ends coincide.

Source

pub fn as_arc(&self) -> GeopResult<Option<Arc<S>>>

The arc the curve traces, if it is a circular one: rational quadratic pieces, each an exact arc, all of one circle — how the kernel builds every arc and circle, and what splitting one leaves. None for any other curve.

Trait Implementations§

Source§

impl<S: Clone + Scalar, const D: usize> Clone for NurbCurve<S, D>

Source§

fn clone(&self) -> NurbCurve<S, D>

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<S: Debug + Scalar, const D: usize> Debug for NurbCurve<S, D>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<S: Scalar> HasConvexHull<S, 2> for NurbCurve<S, 3>

Source§

fn convex_hull(&self) -> GeopResult<ConvexHull<S, 2>>

Source§

fn size(&self) -> GeopResult<S>

Source§

impl<S: Scalar> HasConvexHull<S, 3> for NurbCurve<S, 4>

Source§

fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>>

Source§

fn size(&self) -> GeopResult<S>

Source§

impl<S: Scalar> ParameterRefinable<S, 2> for NurbCurve<S, 3>

Source§

fn evaluate_cartesian(&self, t: S) -> GeopResult<Vector<S, 2>>

Source§

fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 2>>

The Cartesian tangent, via each dimension’s own 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>

Source§

fn evaluate_cartesian(&self, t: S) -> GeopResult<Vector<S, 3>>

Source§

fn tangent_cartesian(&self, t: S) -> GeopResult<Vector<S, 3>>

The Cartesian tangent, via each dimension’s own 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> RasterizableCurve<S> for NurbCurve<S, 4>

Source§

fn eval_at(&self, t: S) -> GeopResult<Vector3<S>>

Evaluate the curve at parameter t and return a 3D point.

Auto Trait Implementations§

§

impl<S, const D: usize> Freeze for NurbCurve<S, D>
where S: Freeze,

§

impl<S, const D: usize> RefUnwindSafe for NurbCurve<S, D>
where S: RefUnwindSafe,

§

impl<S, const D: usize> Send for NurbCurve<S, D>

§

impl<S, const D: usize> Sync for NurbCurve<S, D>

§

impl<S, const D: usize> Unpin for NurbCurve<S, D>
where S: Unpin,

§

impl<S, const D: usize> UnsafeUnpin for NurbCurve<S, D>
where S: UnsafeUnpin,

§

impl<S, const D: usize> UnwindSafe for NurbCurve<S, D>
where S: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.