pub struct NurbSurface<const D: usize> {
pub degree_u: usize,
pub degree_v: usize,
pub num_u: usize,
pub num_v: usize,
pub control_points: Vec<Vector<D>>,
pub knot_vector_u: Vec<Scalar>,
pub knot_vector_v: Vec<Scalar>,
/* 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<D>>§knot_vector_u: Vec<Scalar>§knot_vector_v: Vec<Scalar>Implementations§
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn covering(&self, points: &[Vector3]) -> GeopResult<Self>
pub fn covering(&self, points: &[Vector3]) -> GeopResult<Self>
This surface carried on past its sides (NurbSurface3D::extended)
until the foot point of each of points on it lies inside its
domain, not on a side: a surface something is to lie on has to reach
it. Carried on past a side each time by as much as the domain is
long that way — how much is a free choice — and refused, naming the
points, where it cannot be carried on far enough.
Sourcepub fn carrying(&self, other: &Self) -> GeopResult<Option<(Self, UvMap)>>
pub fn carrying(&self, other: &Self) -> GeopResult<Option<(Self, UvMap)>>
Where the patch other lies on this surface, if it does: this
surface carried on until it covers other (Self::covering), and
the map from other’s (u, v) to its own — the very same surface,
each parameter of other one of its own shifted and scaled, the way
patches of one surface cut apart, mirrored or built again are, its
normal the same way. None where other is another surface, faces
the other way, or is parametrized otherwise.
The map comes from where other‘s corners are on this surface. That
it holds everywhere is a polynomial identity: on each cell between
the two patches’ knots, both are rational of the same degree p in
each parameter, and two such that agree at 2p + 1 parameters in
each direction agree everywhere — their difference, cleared of
denominators, is a polynomial of degree 2p with more roots than
its degree. So they are compared there, and only there; within the
enclosures’ widths, as everything is.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn closest_point(
&self,
target: &Vector3,
) -> GeopResult<(Scalar, Scalar, Vector3)>
pub fn closest_point( &self, target: &Vector3, ) -> GeopResult<(Scalar, Scalar, Vector3)>
The point of the (untrimmed) surface nearest target, with its
parameters.
Subdivision isolates, Newton refines: least_distance bounds the
distance over every patch of the surface, so no dip of it is missed,
and each patch left at the handoff is refined by damped Newton (see
NurbSurface::refine_closest). The parameters are a free choice
of which point of the surface is returned, so every iterate is
sharp: the point returned lies on the surface, and its distance to
target is attained.
Sourcepub fn refine_closest(
&self,
target: &Vector3,
u: Scalar,
v: Scalar,
) -> GeopResult<(Scalar, Scalar)>
pub fn refine_closest( &self, target: &Vector3, u: Scalar, v: Scalar, ) -> GeopResult<(Scalar, Scalar)>
Damped Newton on the gradient of half the squared distance to
target, from (u, v). The step solves the full Hessian where it
is definitely positive definite — near the nearest point, where it
converges quadratically — and the first fundamental form where not
(Gauss–Newton, a descent direction wherever the parametrization is
regular); at a pole, where that is singular too, it moves along the
one parameter that still moves the point. A step that does not
bring the point nearer is halved, and the search stops once halving
does not help. Every iterate is clamped into the domain.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn curvature_radius(
&self,
u: Scalar,
v: Scalar,
) -> GeopResult<Option<Scalar>>
pub fn curvature_radius( &self, u: Scalar, v: Scalar, ) -> GeopResult<Option<Scalar>>
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 (see Self::mean_curvature). 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.
Sourcepub fn curvature_radius_along(
&self,
u: Scalar,
v: Scalar,
along: &Vector3,
) -> GeopResult<Option<Scalar>>
pub fn curvature_radius_along( &self, u: Scalar, v: Scalar, along: &Vector3, ) -> GeopResult<Option<Scalar>>
A radius of curvature at (u, v) along the tangent direction
along — a unit vector in the tangent plane, the way a curve on the
surface runs — or None if the surface runs straight that way: the
normal curvature κ = (L a² + 2 M a b + N b²), where along = a Su + b Sv and L, M, N are the second fundamental form (Suu·n,
Suv·n, Svv·n). Exact for any parametrization.
A curve on the surface turns, as far as the surface makes it, by this
— where Self::curvature_radius, the tightest bend in any
direction, says how a curve circling there would. At a cone’s apex the
tightest bend has the radius of the circle round the axis, which goes
to nothing; a generator running into the apex does not bend at all.
An error where Su × Sv vanishes (a pole), and where along leaves
the tangent plane’s reach.
Sourcepub fn normal_curvature(
&self,
u: Scalar,
v: Scalar,
along: &Vector3,
) -> GeopResult<Scalar>
pub fn normal_curvature( &self, u: Scalar, v: Scalar, along: &Vector3, ) -> GeopResult<Scalar>
The normal curvature at (u, v) along the unit tangent along,
signed: positive where the surface bends towards its normal
normalize(Su × Sv) (see Self::normal) that way, the inverse of
the radius of the circle it bends round there, on that side (see
Self::curvature_radius_along).
Sourcepub fn normal_turning(
&self,
u: Scalar,
v: Scalar,
along: &Vector3,
) -> GeopResult<(Vector3, Vector3)>
pub fn normal_turning( &self, u: Scalar, v: Scalar, along: &Vector3, ) -> GeopResult<(Vector3, Vector3)>
The unit normal normalize(Su × Sv) at (u, v) and how fast it
turns as the point moves on the surface at the velocity along, a
tangent vector there: dn = (dN − n (n·dN)) / |N| for N = Su × Sv, dN = (Suu a + Suv b) × Sv + Su × (Suv a + Svv b), (a, b)
the parameters’ rates that move the point at along (its
components in Su, Sv, by the first fundamental form). Over a
box of (u, v) an enclosure of both over the box.
Sourcepub fn mean_curvature(&self, u: Scalar, v: Scalar) -> GeopResult<Scalar>
pub fn mean_curvature(&self, u: Scalar, v: Scalar) -> GeopResult<Scalar>
The mean curvature at (u, v), signed against the normal
normalize(Su × Sv): half the trace of the second fundamental form
relative to the first,
H = (L G − 2 M F + N E) / (2 (E G − F²)), with E, F, G the
first fundamental form and L, M, N the second (Suu·n, Suv·n,
Svv·n). Unlike Self::curvature_radius it is exact for any
parametrization, orthogonal or not.
An error where Su × Sv vanishes (a pole): the surface may well have
a curvature there, but not one these partials can express.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn evaluate(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3>
pub fn evaluate(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3>
Evaluate the surface at (u, v), returning a 3-D Cartesian point.
Relative to one of the control points acting there ([centered]),
so an interval (u, v) gives a point as wide as the patch is, not as
far from the origin as it lies.
Over a box of (u, v) that is also the mean value form, the point
at the box’s middle plus the partials over the box times the half
widths, and the two intersected: both enclose the same points. Taken
directly, A / w over a box divides two enclosures that cannot be
correlated, and the quotient widens with how far the point lies from
the span’s control point times how much the weight changes: a
cylinder evaluated at a foot point a micron wide came out ten
microns wide along its axis, and a shell’s copy of an edge each time
eight times as wide as the edge.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn fill(
sides: &[NurbCurve3D],
neighbours: &[Option<Neighbour<'_>>],
) -> GeopResult<Fill>
pub fn fill( sides: &[NurbCurve3D], neighbours: &[Option<Neighbour<'_>>], ) -> GeopResult<Fill>
The closed loop sides, three or more curves each starting where
the one before ends, filled smoothly (see the module docs): patches
that run exactly along every side, meet each other and the flat
inner polygon tangentially, and meet the face neighbours[k] names
along side k, if any, tangentially too — exactly where that face
is flat, leaving it at TILT towards its normal where it is
curved, as NurbSurface3D::tangent_to does.
Refused, naming the corner: a loop that turns back on itself, sides at a corner that do not lie in the tangent plane of a face given along either, and faces along two sides meeting there at an angle.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn fit_pcurve(
&self,
curve: &NurbCurve<4>,
pin_start: Option<Vector2>,
pin_end: Option<Vector2>,
max_nodes: usize,
min_subdivision_size: Scalar,
) -> GeopResult<NurbCurve2D>
pub fn fit_pcurve( &self, curve: &NurbCurve<4>, pin_start: Option<Vector2>, pin_end: Option<Vector2>, max_nodes: usize, min_subdivision_size: Scalar, ) -> GeopResult<NurbCurve2D>
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 pcurve is fitted piece by piece, the curve broken at each knot
where it is less than twice continuously differentiable — the joints
of the kernel’s rational arcs and helices, which are only C1 — and
the pieces joined. The fit is a cubic spline, C2 at its own knots:
across a jump in the trace’s curvature it cannot follow, and its
drift there shrinks only as h^2 with the spacing of the samples. A
helix of pitch 1 fitted across its joints drifted past the accuracy
limit. Within a piece, the samples are doubled while the pcurve is
wider than the target (see [DRIFT_PER_RESOLUTION]).
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 located by Self::locate (max_nodes /
min_subdivision_size bound its search). 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.
Sourcepub fn affine_pcurve(
&self,
curve: &NurbCurve<4>,
) -> GeopResult<Option<NurbCurve2D>>
pub fn affine_pcurve( &self, curve: &NurbCurve<4>, ) -> GeopResult<Option<NurbCurve2D>>
The pcurve of curve, which lies on this surface, where the
surface is affine in (u, v) — degree one both ways, its four
control points a parallelogram of one weight: then (u, v) are an
affine function of the point, and the pcurve is curve itself with
each control point mapped to them, of the same weights and knots.
Exact, where a fit only encloses. None for any other surface.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn joined(&self, next: &Self, along_u: bool) -> GeopResult<Self>
pub fn joined(&self, next: &Self, along_u: bool) -> GeopResult<Self>
This surface and next, which starts along u — along v, if not
along_u — where this one ends, as one surface: next’s knots
carried on from where this one’s end, its homogeneous control points
scaled so that its first row’s weights are this one’s last row’s —
the same rational surface — and the two joined by a knot of full
multiplicity, so the joined surface is exactly the two pieces,
however they meet. The row where they meet is what both had there:
two enclosures of one row of points, so what they share. What the
pieces of a wall swept along a profile are, one face per piece of
the profile, joined into the wall.
Fails, saying why, for surfaces of other degrees or other knots across the join, knot vectors not clamped where they meet, and rows that do not meet.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn gordon(
u_curves: &[NurbCurve3D],
v_curves: &[NurbCurve3D],
u_crossings: &[Vec<Scalar>],
v_crossings: &[Vec<Scalar>],
) -> GeopResult<Self>
pub fn gordon( u_curves: &[NurbCurve3D], v_curves: &[NurbCurve3D], u_crossings: &[Vec<Scalar>], v_crossings: &[Vec<Scalar>], ) -> GeopResult<Self>
The Gordon surface through a network of curves on [0, 1]²: the
curves u_curves along u, in order of increasing v, and
v_curves along v, in order of increasing u, each u curve
crossing each v curve once. u_crossings[j][i] is the parameter of
u_curves[j] where it crosses v_curves[i], v_crossings[i][j]
that of v_curves[i] there; both increase along each curve. Two
curves at least in each direction.
Each curve is reparametrized (see NurbCurve::reparametrized) so
that the curves cross at common parameters, chosen as
common_parameters says, and cut to the stretch between its first
and last crossing: the surface’s sides are the first and last curves
of each family. The crossing points are each the union of where the
two curves have them — an error naming both if they could not be
one point. The surface is then the sum of the two skinned surfaces
less the tensor-product one, each of degree three across the curves,
or lower for fewer than four. Its border rows are the outer curves’
control points, as NurbSurface3D::coons’ are, so it interpolates
them exactly, and with two curves each way it is their Coons patch.
Across a crossed curve the surface is as smooth as the curves’ reparametrizations: where they cross every curve at the same fraction of its length, smooth; otherwise each curve’s speed changes there, and the surface may bend across it.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn partials(
&self,
u: Scalar,
v: Scalar,
order: usize,
) -> GeopResult<Vec<Vec<Vector3>>>
pub fn partials( &self, u: Scalar, v: Scalar, order: usize, ) -> GeopResult<Vec<Vec<Vector3>>>
Every partial derivative up to order at (u, v): S[k][l] is
∂^(k+l) S / ∂u^k ∂v^l for k + l <= order, S[0][0] the point.
Each homogeneous partial is the span’s own net differenced k times
along u and l times along v ([hodograph]), then evaluated;
the Cartesian ones follow by the rational quotient rule (Piegl &
Tiller algorithm A4.4). Relative to a control point of the span, as
everything here is (see [centered]).
Differenced first, then evaluated, so that an interval (u, v) box
gives an enclosure as wide as the box times the next derivative.
Evaluated first along v and differenced along u afterwards, as
Self::second_derivatives does for its mixed partial, the rows’
values over the box are independent intervals, and their third
differences, divided by the span’s width cubed, came out a hundred
times wider than the partial itself.
A box reaching over a knot is taken span by span, and the partials
over the pieces united (see [find_spans]): one span’s polynomial
carried on over the next encloses nothing of the surface there, and
a proof over the box (NurbSurface3D::enclose_foot) would prove
nothing.
Sourcepub fn derivatives(
&self,
u: Scalar,
v: Scalar,
) -> GeopResult<(Vector3, Vector3)>
pub fn derivatives( &self, u: Scalar, v: Scalar, ) -> GeopResult<(Vector3, Vector3)>
Partial derivatives (∂Scalar/∂u, ∂Scalar/∂v) at (u, v).
Sourcepub fn second_derivatives(
&self,
u: Scalar,
v: Scalar,
) -> GeopResult<[Vector3; 3]>
pub fn second_derivatives( &self, u: Scalar, v: Scalar, ) -> GeopResult<[Vector3; 3]>
Second partial derivatives [∂²S/∂u², ∂²S/∂u∂v, ∂²S/∂v²] at
(u, v). The pure ones by the rational quotient rule along their own
direction ([rational_derivatives]); the mixed one by differentiating
A = w Scalar once along each: A_uv = w_uv Scalar + w_u S_v + w_v S_u + w S_uv.
Sourcepub fn pole_at(&self, u: Scalar, v: Scalar) -> Option<Vector3>
pub fn pole_at(&self, u: Scalar, v: Scalar) -> Option<Vector3>
The pole (u, v) lies on: the point a boundary row it touches
collapses to. None if it touches no pole.
Sourcepub fn normal(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3>
pub fn normal(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3>
Unit surface normal at (u, v), normalize(∂Scalar/∂u × ∂Scalar/∂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.
At a pole — (u, v) touching a boundary row that collapses to a
point — S_u × S_v vanishes, and the normal is that of the pole
itself (Self::pole_normal), the same for every u (v) along
the row. At a corner whose boundary curves leave it along one
line, it is the corner’s (Self::corner_normal). Anything else
where the cross product vanishes has no normal and is an error.
Sourcepub fn normal_at(
&self,
point: &Vector3,
max_nodes: usize,
min_subdivision_size: Scalar,
) -> GeopResult<Vector3>
pub fn normal_at( &self, point: &Vector3, max_nodes: usize, min_subdivision_size: Scalar, ) -> GeopResult<Vector3>
The unit normal of this surface at point, which lies on it: at
its (u, v) there (Self::locate). An error if the point is not
on the surface.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn offset(&self, distance: Scalar) -> GeopResult<Option<Self>>
pub fn offset(&self, distance: Scalar) -> GeopResult<Option<Self>>
The surface distance along its normal — against it for a negative
distance — with the same parametrization: the offset of the point at
(u, v) is at (u, v) of the result, so a trim loop’s (u, v) stay
meaningful on it. An apex is the exception (see the module docs):
it goes to the offset’s apex, at the same (u, v), and along the
lines from it the points move along those lines too.
None where the offset collapses: every point of the surface passes
its centre of curvature, as a cylinder shrunk past its axis, a
sphere or a torus’s tube past its centre, so the offset has no
area — a blend narrower than the walls a shell builds are thick.
Fails where only part of it would, where the offset folds, and at an
apex whose offset does not close in a point.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn has_normal_everywhere(&self) -> GeopResult<bool>
pub fn has_normal_everywhere(&self) -> GeopResult<bool>
Whether the surface has a normal everywhere: its normal field, a
pole’s factor divided out, definitely not zero over every span, each
split in four up to [REGULAR_SPLITS] times. false where it
vanishes, or could: a patch carried on so far that it folds back on
itself, its offset no surface.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn ruled(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self>
pub fn ruled(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self>
The ruled surface between a, at v = 0, and b, at v = 1: the
straight lines joining their points of equal parameter, both brought
onto [0, 1], which is the surface’s u. Its sides along u are
the two curves exactly.
Sourcepub fn ruled_compatible(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self>
pub fn ruled_compatible(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self>
The ruled surface between a, at v = 0, and b, at v = 1, which
are already alike — of one degree, as many control points and knots —
taken as they are: its u runs over a‘s knots, not brought onto
[0, 1]. Exact, since its sides along u are the curves’ own control
points.
Sourcepub fn bilinear(
p00: Vector3,
p10: Vector3,
p11: Vector3,
p01: Vector3,
) -> GeopResult<Self>
pub fn bilinear( p00: Vector3, p10: Vector3, p11: Vector3, p01: Vector3, ) -> GeopResult<Self>
The bilinear patch with corners p00 at (u, v) = (0, 0), p10 at
(1, 0), p11 at (1, 1) and p01 at (0, 1), over [0, 1]² —
a plane’s, where the four lie in one.
Sourcepub fn coons(sides: [&NurbCurve3D; 4]) -> GeopResult<Self>
pub fn coons(sides: [&NurbCurve3D; 4]) -> GeopResult<Self>
The bilinearly blended Coons patch bounded by sides, which run
round it end to end: from the corner at (0, 0) along v = 0 to
(1, 0), up u = 1 to (1, 1), back along v = 1 to (0, 1) and
down u = 0 to the start — counter-clockwise in (u, v), so the
patch’s normal is the one the loop winds around.
The patch is the sum of the ruled surfaces between opposite sides, less the bilinear patch of the corners, each laid on one control net (the Greville abscissae turn a linear blend into control points exactly): opposite sides are made compatible, and each blend is taken of the homogeneous control points, so rational sides are fine. Its border rows are the sides’ control points themselves — that is what the blend gives there exactly; taking them as they are only leaves out the rounding of adding and taking away the same corners — so the patch interpolates its sides exactly, and its corners are the union of the two sides meeting there.
The sides must end where the next one starts, with their end weights one (or a single span, which can be brought to it): two sides meeting at a corner must agree on its weight for the blend to meet them both.
Sourcepub fn elevate_degree(&self, along_u: bool) -> GeopResult<Self>
pub fn elevate_degree(&self, along_u: bool) -> GeopResult<Self>
The same surface one degree higher in u (along_u) or in v, for
knots clamped at both ends: each row of control points along that
direction raised as a curve (see NurbCurve::elevate_degree).
Sourcepub fn extended(
&self,
along_u: bool,
at_end: bool,
by: Scalar,
) -> GeopResult<Self>
pub fn extended( &self, along_u: bool, at_end: bool, by: Scalar, ) -> GeopResult<Self>
The same surface carried on by by of its u (along_u) or v
parameter past the end (at_end) or before the start of that
domain: each row of control points along that direction continued
as a curve (see NurbCurve::extended), so the surface goes on as
the polynomial it is there, as smooth as it was.
Sourcepub fn tangent_to(&self, sides: &[Option<Neighbour<'_>>; 4]) -> GeopResult<Self>
pub fn tangent_to(&self, sides: &[Option<Neighbour<'_>>; 4]) -> GeopResult<Self>
The same patch made tangent along each side k (numbered as
NurbSurface3D::coons numbers them) that has a sides[k], to the
face it names — a face ending at the side, the patch continuing it
smoothly.
The patch is first raised in degree until each such side has a row
of its own next to it, not next to the opposite side. Each control
point of that row is then moved, along the face’s normal, into the
face’s tangent plane where the side is at that point’s Greville
abscissa: the derivative across a side is a combination of that row
less the side, so a row in the tangent plane makes the patch tangent
there — along a curved face, that plane leant by TILT (see
below). Where two such rows cross, the point goes onto the line the
two planes meet in.
Along a flat face the tangent plane is one plane, and the patch is
tangent to it exactly — rational or not. Along a curved one it
turns, and no NURBS row follows it exactly: the row is set so that
the patch leaves the face at TILT towards the face’s normal at
each of those abscissae — where an approximate tangency would dip
through the face as often as not, a crease of a millionth of a
radian keeps the patch on one side of it — and proven over the whole
side to stay within twice that, on that side (NurbSurface3D::strays),
the patch refined along the side while it does not.
The ends of that row are the second control points of the sides meeting this one: they are curves the patch must keep, so they are not moved, and must lie in the tangent plane at the corner — a side leaving it there contradicts the tangency, and is refused. So is a row that does not head away from the face: the patch would fold back over the face it is to continue.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn project(
&self,
target: Vector3,
u0: Scalar,
v0: Scalar,
iterations: usize,
) -> GeopResult<(Scalar, Scalar)>
pub fn project( &self, target: Vector3, u0: Scalar, v0: Scalar, iterations: usize, ) -> GeopResult<(Scalar, Scalar)>
Fixed-iteration-count Newton foot-point projection of target onto
this surface, starting from (u0, v0).
Each step is Newton’s on the squared distance: it solves the 2x2
system H·Δ = r where r = (residual·Su, residual·Sv) and H its
Hessian, [[Su·Su − residual·Suu, Su·Sv − residual·Suv], [·, Sv·Sv − residual·Svv]], then clamps (u, v) into domain_u() /
domain_v() before the next iteration. Where H is not definitely
positive definite — far from the foot point, or by a pole — the step
is Gauss-Newton’s, H without the second derivatives (the first
fundamental form). Without them always, the step over a curved
surface overshoots by the target’s distance over the surface’s
radius of curvature: a target twice the radius off a cylinder, a
ball rounding a fill of its own radius, swung from side to side of
its foot point for ever. 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 repeats one before it exactly — a fixed point, or a cycle in
the last bit (iterate).
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). There only the
collapsed parameter stops moving the point; the other still does, so
that iteration takes the Newton step along it alone — down the
meridian, off the pole, after which the full step takes over. A seed
on the pole would otherwise never leave it, whatever the target. For
a target at the pole that step is zero, and (u, v) stays. Where
neither derivative is definitely nonzero, no step is taken, and
(u, v) is the seed the previous iteration left.
Sourcepub fn locate(
&self,
point: &Vector3,
max_nodes: usize,
min_subdivision_size: Scalar,
) -> GeopResult<Option<(Scalar, Scalar)>>
pub fn locate( &self, point: &Vector3, max_nodes: usize, min_subdivision_size: Scalar, ) -> GeopResult<Option<(Scalar, Scalar)>>
Where point, a point of this surface, lies on it: its (u, v), or
None if it is not on the surface.
Subdivide to isolate, then Newton to refine (see AGENTS.md): the
containment search (surface_could_contain, with max_nodes and
min_subdivision_size bounding its effort) proves the point lies in
a box only min_subdivision_size narrow, and Self::project from
the box’s midpoint polishes that to what the point’s own width
allows. Both enclose the same foot point, so the answer is their
intersection. The refinement only tightens: where Newton fails or
leaves the box, the box is the answer.
A point that could be a pole lies on the row collapsing to it (see
AGENTS.md, “A pole is one vertex at many (u, v)”), whatever Newton
settled on beside it — where the Jacobian is singular, it stops
where it can step no further, a hair off the row: the row’s
parameter is kept as one of the point’s too.
Sourcepub fn enclose_foot(
&self,
target: &Vector3,
near: (Scalar, Scalar),
) -> GeopResult<Option<(Scalar, Scalar)>>
pub fn enclose_foot( &self, target: &Vector3, near: (Scalar, Scalar), ) -> GeopResult<Option<(Scalar, Scalar)>>
The (u, v) of the foot point of every point of the box target
near near, proven: a box in which each of those points has one
foot point and no other, None where that cannot be shown.
A foot point is where the gradient of half the squared distance,
g = (Su·(S − P), Sv·(S − P)), vanishes. Krawczyk’s operator
K(X) = c − Y g(c) + (I − Y J(X)) (X − c), with J the Hessian of
the distance over the box X (and every P of target), c its
middle and Y the inverse of the Hessian at c — a preconditioner,
a free choice — holds every zero of g in X; where it lies within
X, there is exactly one, and it is in K(X) too. X starts as
twice the Newton step from near and grows where K reaches past
it, a few times: how hard the search tries. It must keep within the
domain, so a target whose foot is on the domain’s edge needs the
surface carried on past it (Self::extended).
Over a box of targets along a curve on the surface, the box found
holds the curve’s (u, v) over that stretch: what the normal and
its turn are enclosed over (see Self::normal_turning).
Sourcepub fn foot_moving(
&self,
target: &Vector3,
velocity: &Vector3,
near: (Scalar, Scalar),
) -> GeopResult<Option<MovingFoot>>
pub fn foot_moving( &self, target: &Vector3, velocity: &Vector3, near: (Scalar, Scalar), ) -> GeopResult<Option<MovingFoot>>
The foot point of a point moving through the box target at
velocity, near near, proven as Self::enclose_foot proves it,
and how it moves: None where that cannot be shown.
The foot keeps the gradient g of half the squared distance zero,
so H (u', v') = (Su·P', Sv·P') for the Hessian H of that
distance: the foot moves at Su u' + Sv v', and the normal there
turns as Self::normal_turning says for that motion. Over the box
of feet, every one an enclosure over the whole box of targets.
Sourcepub fn project_in_domain(
&self,
target: Vector3,
u0: Scalar,
v0: Scalar,
iterations: usize,
) -> GeopResult<(Scalar, Scalar)>
pub fn project_in_domain( &self, target: Vector3, u0: Scalar, v0: Scalar, iterations: usize, ) -> GeopResult<(Scalar, Scalar)>
The foot point of target as Self::project finds it, within the
surface’s domain: the foot point lies on the patch, so its (u, v)
are in the domain as much as where the projection says, and both
enclosures hold at once.
Source§impl<const D: usize> NurbSurface<D>
impl<const D: usize> NurbSurface<D>
Sourcepub fn reverse_u(&self) -> Self
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 NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn revolve(
profile: &NurbCurve3D,
axis: &Axis,
from: f64,
to: f64,
) -> GeopResult<Self>
pub fn revolve( profile: &NurbCurve3D, axis: &Axis, from: f64, to: f64, ) -> GeopResult<Self>
The surface profile sweeps turning about axis from the angle
from to the angle to, in radians, right-handed about
axis.direction: the profile as it is at angle 0, turned.
u is the turn, on [0, 1]: rational quadratic pieces of equal
angle, each at most a quarter turn, the way crate::shape::Arc
builds an arc. v is the profile’s own parameter. So Su x Sv
points away from the axis where the profile runs along
axis.direction.
A profile control point on the axis stays put at every angle: its row collapses to one point, a pole of the parametrization.
The angles of the joints between pieces are free choices, taken in
f64 as crate::shape::Arc::point_at does: any angle serves
equally well, and each joint lies honestly on the circle it is built
from. Fails unless to is more than from, by at most a full turn.
Source§impl NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn spherical_triangle(
center: &Vector3,
radius: Scalar,
corners: [Vector3; 3],
) -> GeopResult<Self>
pub fn spherical_triangle( center: &Vector3, radius: Scalar, corners: [Vector3; 3], ) -> GeopResult<Self>
The piece of the sphere of radius around center bounded by the
great arcs between its points corners, each less than a half turn
from the others: exactly, a rational patch of degree 4 along u
and 2 along v.
v = 0 is the great arc from the first corner (u = 0) to the
second (u = 1), in its usual form — the rational quadratic whose
middle control point is where the arc’s end tangents meet, weighted
by the cosine of half its angle (degree-raised, which leaves it the
same curve). Every u-line is the great arc from that arc’s point to
the third corner, which v = 1 collapses to: a pole. So u = 0 and
u = 1 are the great arcs from the first and the second corner to
the third, again each in its usual form. The normal faces out of the
sphere where the corners run counter-clockwise, seen from outside.
Why it is exact: the great arc from a point B to P, with b = B - C and p = P - C, has weights (1, cos(t/2), 1), t the angle
between them — or any (a, m, c) with m^2 / (a c) = (1 + cos t) / 2. Along the base arc B(u) = X(u) / beta(u), X and beta
quadratic, 1 + cos t = gamma / (beta r^2) with gamma = beta r^2 + (X - beta C) · p, also quadratic. Then the row (X, beta), tau (C delta + r^2 (X + beta p), gamma) (delta = gamma - beta r^2, the
middle point where the tangents meet, weighted tau gamma), 2 r^2 tau^2 gamma (P, 1) has that ratio for every u and any tau(u);
tau linear, chosen so that both side arcs come out in their usual
form, makes the row polynomial of degree at most 4.
The rows multiply the center and the corners by weights and by
each other, so it is built about the center’s middle, a free choice
of origin, and moved back by one rounding: built where it lies, a
corner of a block at (1e4, 1e4, 0) came out with control points
8e-4 wide, and at 1e5 with no normal left at all.
Source§impl<const D: usize> NurbSurface<D>
impl<const D: usize> NurbSurface<D>
Sourcepub fn split_u(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
pub fn split_u(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<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.
Sourcepub fn split_v(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
pub fn split_v(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<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.
Sourcepub fn iso_curve(&self, u_fixed: bool, at: Scalar) -> GeopResult<NurbCurve<D>>
pub fn iso_curve(&self, u_fixed: bool, at: Scalar) -> GeopResult<NurbCurve<D>>
The iso-line of the surface at u = at (u_fixed), or at v = at,
as a curve along the other parameter, on its knots: a row of the
control net, exactly, where at is a clamped end of the domain or a
knot of full multiplicity, and otherwise the row knot insertion
makes there. at is used exactly as given, as by Self::split_u.
Fails for an at outside the domain, or at an end that is not
clamped.
Sourcepub fn split_u_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
pub fn split_u_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
Split at the midpoint of the u domain.
Sourcepub fn split_v_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
pub fn split_v_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)>
Split at the midpoint of the v domain.
Sourcepub fn sub_surface(
&self,
(u0, u1): (Scalar, Scalar),
(v0, v1): (Scalar, Scalar),
) -> GeopResult<Self>
pub fn sub_surface( &self, (u0, u1): (Scalar, Scalar), (v0, v1): (Scalar, Scalar), ) -> 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 NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn translate(&self, offset: Vector3) -> Self
pub fn translate(&self, offset: Vector3) -> 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.
Sourcepub fn transform(&self, motion: &Motion) -> Self
pub fn transform(&self, motion: &Motion) -> Self
This surface moved by motion — a rigid motion, a translation or a
mirror — with the same parametrization, so every pcurve drawn on it
still traces the same patch: each control point moved (see
[crate::nurb_curve::transform_control_point]), weights and knots
untouched.
A mirror turns Su x Sv against the mirrored surface’s material
side: the normal of the result is the mirrored normal, negated. What
the surface bounds has to be turned around for that (see
Model::transform_body).
Source§impl<const D: usize> NurbSurface<D>
impl<const D: usize> NurbSurface<D>
pub fn try_new( degree_u: usize, degree_v: usize, control_points: Vec<Vector<D>>, knot_vector_u: Vec<Scalar>, knot_vector_v: Vec<Scalar>, ) -> GeopResult<Self>
Sourcepub fn recompute_aabb(&mut self)
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).
Sourcepub fn widen(&mut self, pad: Scalar)
pub fn widen(&mut self, pad: Scalar)
This surface widened by ±pad in every coordinate, everywhere: every
control point by that much (times its weight, in homogeneous form),
as crate::nurb_curve::NurbCurve::widen widens a curve — the
basis is non-negative and sums to one, so the widened patch holds
every point within pad of the patch along each axis.
Sourcepub fn aabb(&self) -> &[Scalar; 3]
pub fn aabb(&self) -> &[Scalar; 3]
The box around the control points, and so around the patch (see
crate::nurb_curve::NurbCurve::aabb).
Sourcepub fn could_overlap(&self, other: &Self) -> bool
pub fn could_overlap(&self, other: &Self) -> bool
Whether the two patches could share a point: false proves they do
not, as the boxes around their control points — which hold each
whole patch, by the convex hull property — are apart.
Sourcepub fn size(&self) -> GeopResult<Scalar>
pub fn size(&self) -> GeopResult<Scalar>
The diagonal of the box around the control points: by the convex hull property, a length the whole patch fits within — the size of the feature it is part of, for choices that should scale with it.
Sourcepub fn domain_u(&self) -> (Scalar, Scalar)
pub fn domain_u(&self) -> (Scalar, Scalar)
Valid parameter range in the u direction: (u_min, u_max).
Sourcepub fn domain_v(&self) -> (Scalar, Scalar)
pub fn domain_v(&self) -> (Scalar, Scalar)
Valid parameter range in the v direction: (v_min, v_max).
Sourcepub fn breakpoints_u(&self) -> Vec<Scalar>
pub fn breakpoints_u(&self) -> Vec<Scalar>
Where the surface stops being one polynomial piece along u: the
ends of its domain and every distinct knot between them.
Sourcepub fn breakpoints_v(&self) -> Vec<Scalar>
pub fn breakpoints_v(&self) -> Vec<Scalar>
Like Self::breakpoints_u, along v.
Sourcepub fn could_be_equal(&self, other: &Self) -> bool
pub fn could_be_equal(&self, other: &Self) -> bool
Whether other could be the very same patch: equal degrees, and
knots and homogeneous control points that could_be_equal pairwise.
A test of the representation, so it is exact where it answers true
— two patches built the same way, e.g. two copies of one solid — and
says nothing about the same surface parametrized differently.
Sourcepub fn is_everything(&self) -> bool
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.
Sourcepub fn everything() -> Self
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 NurbSurface<4>
impl NurbSurface<4>
Sourcepub fn as_plane(&self) -> GeopResult<Option<Plane>>
pub fn as_plane(&self) -> GeopResult<Option<Plane>>
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.
Sourcepub fn axis_of_revolution(&self) -> GeopResult<Option<Axis>>
pub fn axis_of_revolution(&self) -> GeopResult<Option<Axis>>
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.
Sourcepub fn as_round_wall(&self) -> GeopResult<Option<RoundWall>>
pub fn as_round_wall(&self) -> GeopResult<Option<RoundWall>>
The round wall the surface lies on, if it is a piece of one: a
surface of revolution (see NurbSurface3D::axis_of_revolution)
whose rows of control points are arcs whose centres along the axis
and radii lie on one straight line — the revolution of a straight
profile, with no row collapsed to a pole. Every point of the surface
is a blend of the rows with the same weights for its height and its
radius, so it lies on that line too. A cylinder has no taper; None
for a sphere, a disc, a torus or any other surface.
Trait Implementations§
Source§impl<const D: usize> Clone for NurbSurface<D>
impl<const D: usize> Clone for NurbSurface<D>
Source§fn clone(&self) -> NurbSurface<D>
fn clone(&self) -> NurbSurface<D>
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more