Skip to main content

NurbSurface

Struct NurbSurface 

Source
pub struct NurbSurface<S: Scalar, const D: usize> {
    pub degree_u: usize,
    pub degree_v: usize,
    pub num_u: usize,
    pub num_v: usize,
    pub control_points: Vec<Vector<S, D>>,
    pub knot_vector_u: Vec<S>,
    pub knot_vector_v: Vec<S>,
    /* private fields */
}
Expand description

A NURBS surface patch whose control points live in D-dimensional homogeneous space.

In practice D = 4 for all 3-D surfaces ((wx, wy, wz, w) control points).

Control points are stored row-major: control_points[i * num_v + j] is the point at u-index i (0 ≤ i < num_u) and v-index j (0 ≤ j < num_v).

Fields§

§degree_u: usize§degree_v: usize§num_u: usize§num_v: usize§control_points: Vec<Vector<S, D>>§knot_vector_u: Vec<S>§knot_vector_v: Vec<S>

Implementations§

Source§

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

Source

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

Convex hull of the surface patch’s Cartesian (dehomogenized) control points, stored row-major (matching Self::control_points).

By the convex-hull property of the NURBS basis, every point on the patch lies within this hull.

Source

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

max(u_size, v_size), measured via the convex hull’s control-net edges (see Self::extents), used as a convergence measure for subdivision algorithms.

Source

pub fn split_mid(&self) -> GeopResult<(NurbSurface<S, 4>, NurbSurface<S, 4>)>

Split along the longer of the u/v dimensions (measured via Self::extents) at that dimension’s domain midpoint.

Source§

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

Source

pub fn curvature_radius(&self, u: S, v: S) -> GeopResult<Option<S>>

A conservative radius of curvature at (u, v), or None if the surface is (locally) flat in both parametric directions.

Computed straight from the surface’s own second partial derivatives — no history, no finite differences — via the normal curvature along each parametric direction, κ_a = (S_aa · n) / |S_a|² for a ∈ {u, v} (the diagonal terms of the second fundamental form divided by the diagonal terms of the first). This is the exact normal curvature in direction a only when Su ⊥ Sv; the general formula also needs the mixed partial Suv and the off-diagonal metric term F = Su·Sv to handle an arbitrary direction. Every surface this crate actually constructs has orthogonal parametric directions by construction — flat bilinear box/cap faces (Su, Sv are the patch’s two edge directions) and revolve’s ruled patches (axial u is always perpendicular to the circular v) — so the approximation is exact for our surfaces, not just a rough heuristic.

The returned radius is 1 / max(|κ_u|, |κ_v|): the tighter of the two bends, so a caller sizing steps off of it stays conservative.

Source§

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

Source

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

Evaluate the surface at (u, v), returning a 3-D Cartesian point.

Source§

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

Source

pub fn fit_pcurve( &self, curve: &NurbCurve<S, 4>, pin_start: Option<Vector2<S>>, pin_end: Option<Vector2<S>>, max_nodes: usize, min_subdivision_size: S, ) -> GeopResult<NurbCurve2D<S>>

The (u, v) trace of curve across this surface: sample the curve, Newton-project each sample onto the surface (each projection seeded from the previous one’s result, so the walk stays continuous), and fit a pcurve through the results.

The walk’s first seed has to be found globally, because Newton only polishes a foot point it is already near: seeded from anywhere else on a curved patch it settles on whichever local foot point is closest — including one clamped against a domain bound, where the residual is merely orthogonal to the boundary — and every later sample, seeded from its predecessor, follows it there. So the start is pin_start when there is one (the face’s own authoritative (u, v)), and is otherwise isolated by surface_could_contain (max_nodes / min_subdivision_size bound that search) — subdivide to isolate, then Newton to refine. A curve that does not start on this surface is an error: there is no trace of it to fit.

pin_start / pin_end override the projected (u, v) of the first and last sample. Pass them whenever the curve’s endpoint is a place this surface’s face already has a coedge for: that coedge’s own pcurve endpoint is the authoritative (u, v) there, and an independently re-projected one lands a hair away from it — enough to break the exact could_be_equal continuity a face’s boundary loop requires between one coedge’s pcurve end and the next one’s start. The endpoints are free to pin without disturbing the rest of the curve because interpolate produces a clamped B-spline, which passes exactly through each sample.

Source§

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

Source

pub fn derivatives(&self, u: S, v: S) -> GeopResult<(Vector3<S>, Vector3<S>)>

Partial derivatives (∂S/∂u, ∂S/∂v) at (u, v).

Source

pub fn normal(&self, u: S, v: S) -> GeopResult<Vector3<S>>

Unit surface normal at (u, v), normalize(∂S/∂u × ∂S/∂v).

Whether this points outward or inward for a given face depends on that surface’s own u/v parametrization convention — it is each surface constructor’s responsibility to pick the matching Face::sense (Forward if this normal is already outward, Reversed if it needs negating) so that callers can treat Face::sense-corrected normal() as reliably outward. See box_solid’s FACE_DEFS ((P10 − P00) × (P01 − P00), Forward) and revolve/sphere’s patch constructors (natural normal is inward, hence Reversed) for both cases.

Source§

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

Source

pub fn project( &self, target: Vector3<S>, u0: S, v0: S, iterations: usize, ) -> GeopResult<(S, S)>

Fixed-iteration-count Newton foot-point projection of target onto this surface, starting from (u0, v0).

Each step solves the exact 2x2 system J·Δ = r where r = (residual·Su, residual·Sv) and J = [[Su·Su, Su·Sv], [Su·Sv, Sv·Sv]] (the first fundamental form — exact for the degree-1 bilinear/low-degree patches used in this crate’s tests, since second derivatives are dropped), then clamps (u, v) into domain_u() / domain_v() before the next iteration. Runs iterations times with no convergence tolerance — interval scalars can’t judge “close enough” — but stops early, with the identical result, once the sharpened iterate reaches an exact fixed point (see the loop).

J is singular exactly where the surface’s own parametrization is — a coordinate-singular pole (e.g. the apex of a revolved disk cap, where every v collapses to one point and Sv = 0). Rather than erroring there (div by an exactly- or interval-possibly-zero determinant), that iteration’s update is simply skipped, leaving (u, v) exactly where the previous, non-singular iteration left it. For a target genuinely at or very near such a pole, earlier iterations still pull (u, v) right up to the singular edge_loop before this kicks in, so the frozen result is still a meaningful (if imprecise right at the pole) answer — good enough for a caller to then recognize “this converged onto a known singular vertex” and handle it explicitly, instead of the whole projection just failing.

Source§

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

Source

pub fn reverse_u(&self) -> Self

Mirror the u parametrization: the point at u moves to u_lo + u_hi - u, leaving v alone.

The surface traces exactly the same set of points, but Su reverses, so Su x Sv — the normal — flips. That is the only way to turn a face’s material side around in this kernel, since orientation lives in the parametrization rather than in a flag on the face.

Mirroring rather than merely reordering matters: the domain is unchanged, so every pcurve drawn on this surface stays in range and only needs the same mirror applied to its own u coordinate (see Model::reverse_face).

Source§

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

Source

pub fn split_u( &self, t: S, ) -> GeopResult<(NurbSurface<S, D>, NurbSurface<S, D>)>

Split at parameter t in the u direction.

t must lie strictly inside the u domain. Returns (left, right).

t is used exactly as given — not sharpened here, see NurbCurve::split’s own doc comment for why (this is the same Boehm-insertion construction, one dimension up: an unsharpened t carried into the new knot vector lets alpha = (t - e) / (s - e) blow up over repeated splits as s - e shrinks while t’s own width doesn’t). Every current caller therefore sharpens its own midpoint before calling; a caller splitting at a located parameter must validate the sharpened value it is about to use, not the wide one it started from.

Source

pub fn split_v( &self, t: S, ) -> GeopResult<(NurbSurface<S, D>, NurbSurface<S, D>)>

Split at parameter t in the v direction.

t must lie strictly inside the v domain. Returns (left, right). t is used exactly as given — not sharpened here, same as Self::split_u.

Source

pub fn split_u_mid(&self) -> GeopResult<(NurbSurface<S, D>, NurbSurface<S, D>)>

Split at the midpoint of the u domain.

Source

pub fn split_v_mid(&self) -> GeopResult<(NurbSurface<S, D>, NurbSurface<S, D>)>

Split at the midpoint of the v domain.

Source

pub fn sub_surface( &self, (u0, u1): (S, S), (v0, v1): (S, S), ) -> GeopResult<Self>

This surface restricted to u ∈ [u0, u1], v ∈ [v0, v1], with the same cut rule as NurbCurve::sub_curve: only bounds strictly inside the domain are cut at, so the result covers at least the requested box ∩ domain. Bounds are used exactly as given and must be narrow.

Source§

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

Source

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

This surface shifted by offset — same shape and parametrization, every point moved by offset. Translates each (homogeneous) control point by offset * weight, leaving weights and knot vectors untouched.

Source§

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

Source

pub fn try_new( degree_u: usize, degree_v: usize, control_points: Vec<Vector<S, D>>, knot_vector_u: Vec<S>, knot_vector_v: Vec<S>, ) -> GeopResult<Self>

Source

pub fn recompute_aabb(&mut self)

Refresh the cached Self::aabb from the current control_points — see crate::nurb_curve::NurbCurve::recompute_aabb’s identical doc comment for why this is needed at all (control_points is pub, and code outside this crate does mutate it in place).

Source

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

Valid parameter range in the u direction: (u_min, u_max).

Source

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

Valid parameter range in the v direction: (v_min, v_max).

Source

pub fn num_u(&self) -> usize

Number of control points in the u direction.

Source

pub fn num_v(&self) -> usize

Number of control points in the v direction.

Source

pub fn is_everything(&self) -> bool

Whether this is the NurbSurface::everything placeholder — the unsharp stand-in a face carries before it is given real geometry.

A finished solid must have none: a placeholder face has no position, so nothing can be classified against it, and it will silently swallow any containment or intersection query it is handed (every comparison against ENTIRE succeeds). Construction code that splits faces off a starting placeholder has to consume the last one rather than leave it behind, and this is how a test says so.

Source

pub fn everything() -> Self

A degenerate, maximally-unsharp surface: a single 1×1 control point whose every coordinate is [Scalar::ENTIRE], over a domain that accepts any (u, v). evaluate() anywhere 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> NurbSurface<S, 4>

Source

pub fn as_plane(&self) -> GeopResult<Option<Plane<S>>>

The plane the surface lies in, if it is flat: every control point on the plane through its middle, normal to it there — the normal pointing the way the surface’s own does. None for a surface that bends.

Source

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

The axis the surface turns around, if it is a surface of revolution — a cylinder, a cone, a sphere, a torus, a disc: one of its parameter directions sweeps circular arcs around a common axis. None for any other surface.

Checked on the control net: every row of control points along that direction is an arc (see NurbCurve3D::as_arc) around the one axis, starting at the same angle, with weights proportional to every other row’s — or a single point on the axis, as at a sphere’s pole. Then every row turns through the same angles at the same parameters, and a blend of them across the other direction is the blended profile, turned: a surface of revolution.

Trait Implementations§

Source§

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

Source§

fn clone(&self) -> NurbSurface<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 NurbSurface<S, D>

Source§

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

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

impl<S: Scalar> RasterizableSurface<S> for NurbSurface<S, 4>

Source§

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

Evaluate the surface at (u, v) and return a 3D point.

Auto Trait Implementations§

§

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

§

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

§

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

§

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

§

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

§

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

§

impl<S, const D: usize> UnwindSafe for NurbSurface<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.