NURBS and the exact BRep
Overview
So far every shape was implicit — a signed distance field, a formula like $s(\mathbf{x}) = \|\mathbf{x}\| - r$ that answers how far is this point from the surface?. That is the right representation for booleans, fillets, and differentiation, but real CAD work also needs the exact boundary representation (BRep): the topology of a solid — its faces, edges, and vertices — with analytic geometry on each piece.
This chapter introduces the BRep, the NURBS curves that give its edges their analytic shape, and the two ideas that make the BRep usable interactively: topological naming (giving each face, edge, and vertex a stable name) and snapping (finding the exact geometry under the cursor).
The exact BRep
brep evaluates a feature into its boundary representation: a watertight set of vertices, edges, and faces, each carrying analytic geometry (an exact curve on an edge, an exact surface on a face) rather than triangles.
Rasmah.brep — Function
brep(m) -> BRepEvaluate feature m into its exact boundary representation (a watertight BRep of vertices, edges, loops, faces, shells, and solids). Equivalent to evaluate(m, BRepBackend()); the exact BRepBackend builds analytic geometry where a closed form exists and falls back to the select-and-stitch boolean driver otherwise.
A box has six faces, twelve edges, and eight vertices:
b = brep(box(1.0, 1.0, 1.0))
(length(b.faces), length(b.edges), length(b.vertices))(6, 12, 8)Topological naming
A BRep is more than a list of faces — each element has a name, so you can refer to "the face at $x = 1$" in a way that survives re-meshing. The naming is local (index-based): face_name, edge_name, and vertex_name give the name of the $i$-th element, and their face_names / edge_names / vertex_names counterparts return every name at once.
Rasmah.face_name — Function
face_name(b, i) -> StringLocal (index-based) name "Face$i" of the i-th face of a BRep.
Rasmah.edge_name — Function
edge_name(b, i) -> StringLocal (index-based) name "Edge$i" of the i-th edge of a BRep.
Rasmah.vertex_name — Function
vertex_name(b, i) -> StringLocal (index-based) name "Vertex$i" of the i-th vertex of a BRep.
face_names(b)6-element Vector{String}:
"Face1"
"Face2"
"Face3"
"Face4"
"Face5"
"Face6"edge_names(b)12-element Vector{String}:
"Edge1"
"Edge2"
"Edge3"
"Edge4"
"Edge5"
"Edge6"
"Edge7"
"Edge8"
"Edge9"
"Edge10"
"Edge11"
"Edge12"NURBS
A NURBS (non-uniform rational B-spline) curve of degree $p$ evaluates to
\[C(t) = \frac{\sum_i N_{i,p}(t)\, w_i\, P_i}{\sum_i N_{i,p}(t)\, w_i},\]
where $N_{i,p}$ are the B-spline basis functions over a knot vector, $P_i$ are the control points, and $w_i$ their weights. NURBS are the lingua franca of CAD: a straight line, a circle, a conic, and a free-form spline are all exact NURBS, so a single format can represent everything a boundary representation needs.
NurbsCurve holds a NURBS directly — its degree, knot vector, control points, and weights:
Rasmah.NurbsCurve — Type
NurbsCurveA rational B-spline (NURBS) curve of degree degree evaluating to C(t) = Σ N_{i,p}(t) w_i P_i / Σ N_{i,p}(t) w_i.
Fields
degree: polynomial degree.knots: knot vector.control_points: vector of control points.weights: control-point weights.
to_nurbs converts an analytic curve to an equivalent NURBS. A line is a degree-1 NURBS with two control points; a full circle is a degree-2 NURBS with nine (four rational arcs of $90°$):
Rasmah.to_nurbs — Function
to_nurbs(c::LineCurve) -> NurbsCurve
to_nurbs(c::CircleArc) -> NurbsCurve
to_nurbs(c::CircleCurve) -> NurbsCurveConvert a line, a circular arc (< 180°), or a full circle to an equivalent NurbsCurve.
to_nurbs(CircleCurve([0.0, 0, 0], [0, 0, 1], 1.0))NurbsCurve{Float64}(2, [0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0], [[1.0, 0.0, 0.0], [1.0, 0.9999999999999998, 0.0], [6.123233995736766e-17, 1.0, 0.0], [-0.9999999999999998, 1.0, 0.0], [-1.0, 1.2246467991473532e-16, 0.0], [-1.0000000000000002, -0.9999999999999998, 0.0], [-1.8369701987210297e-16, -1.0, 0.0], [0.9999999999999997, -1.0000000000000002, 0.0], [1.0, 0.0, 0.0]], [1.0, 0.7071067811865476, 1.0, 0.7071067811865476, 1.0, 0.7071067811865476, 1.0, 0.7071067811865476, 1.0])The circle becomes degree 2 with 9 control points, exactly as the four-arc construction predicts.
Snapping
When a user clicks on a model, the cursor is almost never exactly on the surface. Snapping finds the intended geometry: it searches the available points, segment midpoints, edges, and faces for the nearest target within a radius, preferring vertex over edge over midpoint over face.
Rasmah.snap_point — Function
snap_point(cursor; points = [], segments = [], faces = [], grid = nothing, radius = 0.1) -> SnapResultSnap the cursor to the nearest available target within radius, preferring vertex > edge > midpoint > face > grid. Returns a SnapResult with kind :none when nothing matches.
Keyword arguments
points: candidate vertices to snap to.segments: candidate(a, b)segments (edge or midpoint snap).faces: candidate(a, b, c)triangles (face snap).grid: grid spacing, ornothingto disable grid snapping.radius: the snap (magnet) radius.
The result is a SnapResult: the kind of target found (snap_kind) and the snapped position (snapped):
Rasmah.SnapResult — Type
SnapResult{P}A snap target and its kind, returned by snap_point.
Fields
point: the snapped point.kind::vertex,:edge,:midpoint,:face,:grid, or:none.
Rasmah.snap_kind — Function
snap_kind(r::SnapResult) -> SymbolThe kind (:vertex, :edge, :midpoint, :face, :grid, or :none) of a SnapResult.
Rasmah.snapped — Function
snapped(r::SnapResult)The snapped point of a SnapResult.
A click near the origin snaps onto the nearest vertex rather than the face:
r = snap_point([0.1, 0.05, 0.0]; points = [[0.0, 0, 0], [0.2, 0, 0]], radius = 0.2)
(snap_kind(r), snapped(r))(:vertex, [0.0, 0.0, 0.0])Next steps
With the exact BRep in hand, the next step is to turn a shape into a mesh for simulation: Meshing: from shapes to triangles.
