pub struct NurbsCurve {
pub degree: usize,
pub knots: Vec<f64>,
pub control_points: Vec<Vec4>,
/* private fields */
}Expand description
A rational B-spline curve.
Invariant: an in-place edit of the public fields must keep the curve
valid. The curve caches that it passed validation (finite values,
positive weights, a clamped knot vector with a positive domain), and
Clone copies that flag; nothing re-checks a field edited in place. Prefer
building a new value with NurbsCurve::new.
Fields§
§degree: usize§knots: Vec<f64>§control_points: Vec<Vec4>Implementations§
Source§impl NurbsCurve
impl NurbsCurve
pub fn new( degree: usize, knots: Vec<f64>, control_points: Vec<Vec4>, ) -> Result<Self, String>
pub fn domain(&self) -> Result<[f64; 2], String>
pub fn evaluate_homogeneous(&self, parameter: f64) -> Result<Vec4, String>
pub fn evaluate(&self, parameter: f64) -> Result<Vec3, String>
Sourcepub fn evaluate_extended(&self, parameter: f64) -> Result<Vec3, String>
pub fn evaluate_extended(&self, parameter: f64) -> Result<Vec3, String>
Value beyond the domain (Golovanov §2.15): every curve must answer out-of-domain queries because intersection and projection algorithms probe there. Closed curves wrap the parameter cyclically; open curves extend linearly along the end tangent.
Sourcepub fn derivatives_extended(
&self,
parameter: f64,
derivative_count: usize,
) -> Result<Vec<Vec3>, String>
pub fn derivatives_extended( &self, parameter: f64, derivative_count: usize, ) -> Result<Vec<Vec3>, String>
Derivatives beyond the domain (§2.15). The open-end extension is linear: the first derivative is the boundary tangent and higher derivatives vanish.
pub fn derivatives( &self, parameter: f64, derivative_count: usize, ) -> Result<Vec<Vec3>, String>
Sourcepub fn unit_tangent(
&self,
parameter: f64,
range_start: f64,
range_end: f64,
) -> Result<Vec3, String>
pub fn unit_tangent( &self, parameter: f64, range_start: f64, range_end: f64, ) -> Result<Vec3, String>
The curve’s unit TANGENT at parameter, read as the one-sided limit
when the first derivative vanishes there.
This is what derivatives(t, 1)?[1].normalized()? should always have
been. That shape refuses on a curve class that is not exotic at all —
any Hermite segment whose end tangent is the zero vector, of which an
INVOLUTE flank is the standard source (speed r_base * t, so a profile
sampled from the base circle outwards carries an exactly zero tangent at
its start, one per flank per tooth). The direction of travel is still
defined there, as the limit from inside the range, and
Self::stationary_tangent_rescue reads it.
Pass the range the CALLER owns — an edge’s [t0, t1], not the curve’s
domain, where the two differ (an EdgeRecord keeps the whole curve and
records the sub-range it uses), so the probe never reads geometry
outside the caller’s own span.
The error names the parameter and the range it probed, because the only curve that reaches it is stationary EVERYWHERE the range allows, and the caller’s own identifier is the half this function cannot supply.
Escape hatch for tamper-verification: BREP_CUSP_TANGENT_RESCUE=0
restores the bare normalized() refusal, the same hatch the imprint
driver’s seed loop carries.
Sourcepub fn point_and_unit_tangent_extended(
&self,
parameter: f64,
range_start: f64,
range_end: f64,
) -> Result<(Vec3, Vec3), String>
pub fn point_and_unit_tangent_extended( &self, parameter: f64, range_start: f64, range_end: f64, ) -> Result<(Vec3, Vec3), String>
The point and unit tangent that derivatives_extended(parameter, 1)
reads, with the tangent taken as Self::unit_tangent takes it.
The blend march reads its section plane — the point and the edge’s
unit tangent — through the EXTENDED evaluation at every station,
overshoot stations past the edge’s ends included, and unit_tangent
cannot stand in for that read: it evaluates derivatives, which CLAMPS
a parameter outside the domain, where derivatives_extended WRAPS it on
a closed curve. A closed rim’s overshoot station would read a different
tangent. This is the same one evaluation the march always made, so a
non-zero derivative returns the vector derivatives[1].normalized()
returned, to the bit, at no extra cost.
Where the derivative vanishes, the one-sided limit is read at the parameter the extended evaluation actually took its derivative from — the parameter itself inside the domain, the wrapped one on a closed curve, the boundary past an open end, where the extension’s direction IS the boundary’s one-sided limit. The caller’s range is widened to contain that parameter, because an overshoot station reads outside the caller’s span on purpose; the ladder still probes from the side the range lies on.
Sourcepub fn stationary_tangent_rescue(
&self,
parameter: f64,
range_start: f64,
range_end: f64,
) -> Option<Vec3>
pub fn stationary_tangent_rescue( &self, parameter: f64, range_start: f64, range_end: f64, ) -> Option<Vec3>
The unit tangent just inside [range_start, range_end], for a
parameter where the first derivative VANISHES.
A curve can be stationary at a parameter where the curve itself is
perfectly well behaved, and the direction of travel is then still
defined as the one-sided limit. The standard case is a cubic Bezier
whose first two control points coincide — the shape a Hermite segment
takes when its end tangent is the zero vector — and an INVOLUTE is the
standard source of one: the involute’s speed is r_base * t, so a gear
flank sampled from the base circle outwards starts with an exactly zero
tangent and every tooth profile carries one per flank. The 2026-09-14
herringbone report is that shape: a hit landing exactly on the cusp
parameter made derivatives(t, 1)[1].normalized() fail and killed the
whole union.
The range bounds the probe — pass an edge’s own [t0, t1] and the
rescue never reads the curve outside the span the caller owns. The
ladder walks OUTWARDS, and it starts at 1e-6 of the span rather than at
the smallest representable step: the rescued direction is a ONE-SIDED
limit, so a nearer probe truncates less, but the derivative at a
stationary point is a CANCELLING sum that shrinks with the step while
its rounding error does not — at 1e-9 of the span the involute cusp
below reads its own limit direction only to 1e-6, worse than at 1e-6 of
the span. Later rungs exist for a higher-order stationary point, whose
derivative is still under the zero-length floor at the first. None
means the curve is stationary everywhere the range allows, where no
direction exists to report at all.
pub fn reversed(&self) -> Result<Self, String>
pub fn insert_knot( &self, parameter: f64, requested: usize, ) -> Result<Self, String>
pub fn split(&self, parameter: f64) -> Result<(Self, Self), String>
Source§impl NurbsCurve
impl NurbsCurve
Sourcepub fn extend_natural(
&self,
low: bool,
delta_param: f64,
) -> Result<NurbsCurve, ExtendRefusal>
pub fn extend_natural( &self, low: bool, delta_param: f64, ) -> Result<NurbsCurve, ExtendRefusal>
Continue this curve past its low (low) or high end by delta_param,
as the analytic continuation of its own terminal Bézier span: the curve
analogue of NurbsSurface::extend_natural, through the same knot
and control-point construction, so a surface’s isoline and the
continued isoline agree. Everything over the original domain survives.
A rational continuation whose appended weights are not positive is
refused; the caller samples the span where it needs more than that.