RasmahRasmah

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.brepFunction
brep(m) -> BRep

Evaluate 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.

source

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_nameFunction
face_name(b, i) -> String

Local (index-based) name "Face$i" of the i-th face of a BRep.

source
Rasmah.edge_nameFunction
edge_name(b, i) -> String

Local (index-based) name "Edge$i" of the i-th edge of a BRep.

source
Rasmah.vertex_nameFunction
vertex_name(b, i) -> String

Local (index-based) name "Vertex$i" of the i-th vertex of a BRep.

source
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.NurbsCurveType
NurbsCurve

A 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.
source

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_nurbsFunction
to_nurbs(c::LineCurve) -> NurbsCurve
to_nurbs(c::CircleArc) -> NurbsCurve
to_nurbs(c::CircleCurve) -> NurbsCurve

Convert a line, a circular arc (< 180°), or a full circle to an equivalent NurbsCurve.

source
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_pointFunction
snap_point(cursor; points = [], segments = [], faces = [], grid = nothing, radius = 0.1) -> SnapResult

Snap 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, or nothing to disable grid snapping.
  • radius: the snap (magnet) radius.
source

The result is a SnapResult: the kind of target found (snap_kind) and the snapped position (snapped):

Rasmah.SnapResultType
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.
source

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.