NURBS: curves & surfaces

Pure-Python port of the NURBS evaluation API from BOSL2’s nurbs.scad, as two classes: NurbsCurve (evaluate a curve, sample it into a path, raise its degree) and NurbsPatch (sample a surface, mesh it into a VNF). All three flavours – CLAMPED, OPEN and CLOSED – are supported, with weights (rational NURBS), knot multiplicities, and explicit knot vectors.

Each object owns its whole definition, so operations chain off it instead of threading six arguments through free functions:

NurbsCurve(ctrl, 3).curve(splinesteps=12).stroke(width=3)
NurbsCurve(ctrl, 3).elevate_degree().point(0.5)
NurbsPatch(patch, (3, 3)).vnf(splinesteps=(8, 8)).polyhedron()

NurbsCurve.curve() returns a Path2D (2-D control points) or a Path3D (3-D), so the result carries the full path/extrude/stroke API, and NurbsPatch.vnf() returns a VNF. The classic rational-NURBS sphere is rendered and checked for real in tests/test_stl_render.py.

Every per-direction setting on a patch is a (u, v) pair: degree=(3, 3), splinesteps=(16, 16), knots=(u_knots, v_knots), and so on. The curve/patch definition is read-only once constructed – build a new object to change it.

Coverage of BOSL2 nurbs.scad

BOSL2 function

Status

Notes

nurbs_curve

ported

NurbsCurve – clamped/open/closed, weights, mult, explicit knots. curve(splinesteps) samples the whole curve into a path; point(u) / points(u) evaluate chosen parameters.

nurbs_patch_points

ported

NurbsPatchsurface(splinesteps) samples a uniform grid, points(u, v) a chosen one, point(u, v) a single point; per-direction degree/type/mult/knots.

nurbs_vnf

ported

NurbsPatch.vnf() – mesh a patch (built on vnf_vertex_array), with style / reverse / caps.

nurbs_elevate_degree

ported

NurbsCurve.elevate_degree() – raise a clamped/open curve’s degree (collocation at Greville points); returns a new NurbsCurve.

is_nurbs_patch

ported

NurbsPatch.is_patch().

nurbs_interp / nurbs_interp_surface

not ported

the constrained least-squares interpolation solvers (fit a NURBS through given points with derivative/curvature/corner constraints) – thousands of lines of custom linear algebra; a large follow-up.

debug_nurbs / debug_nurbs_interp

not ported

preview/annotation display modules.

Examples

A cubic clamped NURBS curve, swept into a tube:

from pybosl2 import NurbsCurve

ctrl = [[0, 0, 0], [10, 20, 5], [30, -10, 10], [50, 20, 0], [60, 0, 15]]
NurbsCurve(ctrl, 3).curve(splinesteps=12).stroke(width=3).show()
Loading 3-D preview…

⬇ Download STL mesh

A cubic B-spline surface patch meshed into a sheet:

from pybosl2 import NurbsPatch

patch = [
    [[-50, 50, 0], [-16, 50, 20], [16, 50, 20], [50, 50, 0]],
    [[-50, 16, 20], [-16, 16, 40], [16, 16, 40], [50, 16, 20]],
    [[-50, -16, 20], [-16, -16, 40], [16, -16, 40], [50, -16, 20]],
    [[-50, -50, 0], [-16, -50, 20], [16, -50, 20], [50, -50, 0]],
]
NurbsPatch(patch, (3, 3)).vnf(splinesteps=(10, 10)).polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh

A sphere as a rational NURBS surface (weights + repeated knots):

from pybosl2 import NurbsPatch

patch = [[[0, 0, 1]] * 7,
         [[2, 0, 1], [2, 4, 1], [-2, 4, 1], [-2, 0, 1], [-2, -4, 1], [2, -4, 1], [2, 0, 1]],
         [[2, 0, -1], [2, 4, -1], [-2, 4, -1], [-2, 0, -1], [-2, -4, -1], [2, -4, -1], [2, 0, -1]],
         [[0, 0, -1]] * 7]
weights = [[w / 9 for w in row] for row in
           [[9, 3, 3, 9, 3, 3, 9], [3, 1, 1, 3, 1, 1, 3], [3, 1, 1, 3, 1, 1, 3], [9, 3, 3, 9, 3, 3, 9]]]
NurbsPatch(patch, (3, 3), weights=weights,
           knots=(None, [0, 0.5, 0.5, 0.5, 1])).vnf(splinesteps=(12, 12)).polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh

API reference

NURBS curve/surface evaluation and meshing (de Boor).

class pybosl2.nurbs.NurbsType(*values)[source]

Bases: Enum

NURBS curve/surface boundary condition.

Determines how the knot vector is built and whether the curve/surface wraps.

CLAMPED = 'clamped'

Clamped (end-point-interpolating) — the default.

OPEN = 'open'

Open (non-interpolating) B-spline.

CLOSED = 'closed'

Closed (periodic) — start and end connect.

class pybosl2.nurbs.NurbsCurve(control, degree, nurbs_type=NurbsType.CLAMPED, knots=None, mult=None, weights=None)[source]

Bases: object

A NURBS curve: control points plus their knot structure, with every operation as a method.

The object owns its whole definition – degree, boundary condition, knot vector, knot multiplicities and rational weights – so operations chain off it instead of repeating six arguments at every call (BOSL2’s nurbs_curve() / nurbs_elevate_degree()):

NurbsCurve(ctrl, 3).curve(splinesteps=12).stroke(width=3)
NurbsCurve(ctrl, 3).elevate_degree(2).point(0.5)

Evaluate at chosen parameters with point() / points(), sample the whole curve into a path with curve(), and raise the degree (keeping the shape) with elevate_degree(). Indexing, iteration and len() walk the control points.

Parameters:
control : Path | Sequence[Sequence[float]] | np.ndarray

The control points – a sequence of [x,y] or [x,y,z] points.

degree : int

The curve degree.

nurbs_type : NurbsType

The boundary condition – NurbsType.CLAMPED (the default), NurbsType.OPEN or NurbsType.CLOSED.

knots : Sequence[float] | None

An explicit knot vector, or None for a uniform one.

mult : Sequence[int] | None

Knot multiplicities, or None.

weights : Sequence[float] | None

Weights for a rational NURBS curve, or None.

Examples

A cubic clamped NURBS curve through five control points, swept into a tube:

from pybosl2 import NurbsCurve

ctrl = [[0, 0, 0], [10, 20, 5], [30, -10, 10], [50, 20, 0], [60, 0, 15]]
NurbsCurve(ctrl, 3).curve(splinesteps=12).stroke(width=3).show()
Loading 3-D preview…

⬇ Download STL mesh

property array : ndarray

The control points as an (N, dim) numpy array.

property to_list : list[list[float]]

The control points as a plain list.

property degree : int

The curve degree.

property nurbs_type : NurbsType

The boundary condition of the curve.

property knots : list[float] | None

The explicit knot vector, or None when the curve uses a uniform one.

property weights : list[float] | None

The rational weights, or None for a non-rational curve.

points(u)[source]

Evaluate the curve at each parameter in u.

Parameters:
u : Sequence[float]

Parameter values, each in [0, 1].

Returns:

An (len(u), dim) ndarray of points.

Return type:

ndarray

point(u)[source]

Evaluate the curve at a single parameter value.

Parameters:
u : float

The parameter value in [0, 1].

Returns:

A length-dim ndarray for the point at u.

Return type:

ndarray

curve(splinesteps=16)[source]

Sample the whole curve into a path.

Takes splinesteps uniform samples between every pair of knots, plus a sample at every knot, which is BOSL2’s nurbs_curve(..., splinesteps=) behaviour. Closed curves come back as closed paths.

Parameters:
splinesteps : int

Number of samples per knot span (default 16).

Returns:

A Path2D for 2-D control points, or a Path3D for 3-D ones.

Return type:

Path

Examples

Sampling a cubic curve and sweeping it into a tube:

from pybosl2 import NurbsCurve

ctrl = [[0, 0, 0], [10, 20, 5], [30, -10, 10], [50, 20, 0], [60, 0, 15]]
NurbsCurve(ctrl, 3).curve(splinesteps=12).stroke(width=3).show()
Loading 3-D preview…

⬇ Download STL mesh

elevate_degree(times=1)[source]

Raise the curve’s degree, keeping its shape.

Only NurbsType.CLAMPED and NurbsType.OPEN curves can be elevated (as in BOSL2). The result carries the knot vector the elevated curve needs, so it evaluates to the same points as this one.

Parameters:
times : int

How many times to elevate (default 1); 0 returns an equivalent curve.

Returns:

A new NurbsCurve of degree self.degree + times.

Raises:

AssertionError – If the curve is NurbsType.CLOSED, or times is negative.

Return type:

NurbsCurve

class pybosl2.nurbs.NurbsPatch(control, degree=(3, 3), nurbs_type=(NurbsType.CLAMPED, NurbsType.CLAMPED), knots=(None, None), mult=(None, None), weights=None)[source]

Bases: object

A NURBS surface patch: a rectangular grid of control points, with its knot structure.

The surface counterpart of NurbsCurve (BOSL2’s nurbs_patch_points() / nurbs_vnf()). Every per-direction setting is a (u, v) pair – degree, boundary condition, knot multiplicities, knot vectors and splinesteps:

NurbsPatch(patch, (3, 3)).vnf(splinesteps=(8, 8)).polyhedron()

Evaluate single points with point(), a grid of chosen parameters with points(), a uniformly sampled grid with surface(), and mesh it with vnf(). Indexing, iteration and len() walk the control-point rows.

Parameters:
control : Sequence[Sequence[Sequence[float]]] | np.ndarray

A rectangular grid (rows of [x,y,z] control points).

degree : tuple[int, int]

Per-direction degree (u_degree, v_degree) (default (3,3)).

nurbs_type : tuple[NurbsType, NurbsType]

Per-direction boundary condition (u_type, v_type).

knots : tuple[Sequence[float] | None, Sequence[float] | None]

Per-direction knot vectors (u_knots, v_knots).

mult : tuple[Sequence[int] | None, Sequence[int] | None]

Per-direction knot multiplicities (u_mult, v_mult).

weights : Sequence[Sequence[float]] | None

A weight matrix the same size as control for rational NURBS, or None.

Examples

A cubic B-spline surface patch meshed into a solid:

from pybosl2 import NurbsPatch

patch = [
    [[-50, 50, 0], [-16, 50, 20], [16, 50, 20], [50, 50, 0]],
    [[-50, 16, 20], [-16, 16, 40], [16, 16, 40], [50, 16, 20]],
    [[-50, -16, 20], [-16, -16, 40], [16, -16, 40], [50, -16, 20]],
    [[-50, -50, 0], [-16, -50, 20], [16, -50, 20], [50, -50, 0]],
]
NurbsPatch(patch, (3, 3)).vnf().polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh

static is_patch(x)[source]

Check if x looks like a NURBS patch (BOSL2 is_nurbs_patch()).

Parameters:
x : Any

The object to test.

Returns:

True if x is a rectangular 2-D array of point vectors with equal-length rows.

Return type:

bool

property array : ndarray

The control points as a (rows, cols, 3) numpy array.

property to_list : list[list[list[float]]]

The control-point grid as a plain list of rows.

property degree : tuple[int, int]

The per-direction degree (u_degree, v_degree).

property nurbs_type : tuple[NurbsType, NurbsType]

The per-direction boundary condition (u_type, v_type).

property weights : list[list[float]] | None

The rational weight matrix, or None for a non-rational patch.

point(u, v)[source]

Evaluate the surface at a single (u, v) parameter pair.

Parameters:
u : float

The parameter along U in [0, 1].

v : float

The parameter along V in [0, 1].

Returns:

A length-3 ndarray for the point at (u, v).

Return type:

ndarray

points(u, v)[source]

Evaluate the surface on the grid of parameters u x v.

Parameters:
u : Sequence[float]

Parameter values along U, each in [0, 1].

v : Sequence[float]

Parameter values along V, each in [0, 1].

Returns:

A (len(u), len(v), 3) ndarray of surface points.

Return type:

ndarray

surface(splinesteps=(16, 16))[source]

Sample the whole surface on a uniform grid.

Parameters:
splinesteps : tuple[int, int]

Per-direction samples per knot span (default (16,16)).

Returns:

A (rows, cols, 3) ndarray of surface points.

Return type:

ndarray

vnf(splinesteps=(16, 16), style=VNFStyle.DEFAULT, reverse=False, caps=None)[source]

Mesh the surface into a VNF.

Samples the patch with surface() and builds the mesh with vertex_array(). Wrapping follows the boundary condition – CLOSED directions produce a continuous tube or torus.

Parameters:
splinesteps : tuple[int, int]

Per-direction samples per knot span (default (16,16)).

style : VnfStyle

vertex_array() triangulation style.

reverse : bool

If True, flip every face normal.

caps : CapsSpec | None

A CapsSpec closing the open ends of a (CLAMPED, CLOSED) or (CLOSED, CLAMPED) surface; None for no caps.

Returns:

A VNF.

Raises:

AssertionError – If caps are requested on a patch that isn’t paired CLAMPED/CLOSED (or the reverse).

Return type:

VNF

Examples

Meshing a cubic B-spline patch into a solid:

from pybosl2 import NurbsPatch

patch = [
    [[-50, 50, 0], [-16, 50, 20], [16, 50, 20], [50, 50, 0]],
    [[-50, 16, 20], [-16, 16, 40], [16, 16, 40], [50, 16, 20]],
    [[-50, -16, 20], [-16, -16, 40], [16, -16, 40], [50, -16, 20]],
    [[-50, -50, 0], [-16, -50, 20], [16, -50, 20], [50, -50, 0]],
]
NurbsPatch(patch, (3, 3)).vnf(splinesteps=(10, 10)).polyhedron().show()
Loading 3-D preview…

⬇ Download STL mesh