Boundary Representation (BRep)
println("Julia ", VERSION)Julia 1.12.6
Overview
Where an SDF (signed distance field) describes geometry implicitly (as a function), a boundary representation (BRep) describes it explicitly: the exact vertices, edges, loops, faces, shells, and solids that make up the shape. BRep is the data model behind most engineering CAD (computer-aided design) kernels.
The same shape serves as a sampled field or as an exact topological model. In this tutorial we build shapes and inspect their exact BRep topology.
The BRep data model
A BRep is a hierarchy:
Solid → Shell → Face → Loop → Edge → Vertex
- Vertex — a point in space.
- Edge — a curve bounded by two vertices.
- Loop — a closed cycle of edges on a face.
- Face — a surface bounded by loops.
- Shell — a connected set of faces.
- Solid — a bounded region enclosed by shells.
The brep function turns a shape into its exact BRep:
using Rasmah
b = brep(box(2.0, 1.0, 0.5))
println("vertices: ", length(b.vertices))
println("edges: ", length(b.edges))
println("loops: ", length(b.loops))
println("faces: ", length(b.faces))
println("shells: ", length(b.shells))
println("solids: ", length(b.solids))vertices: 8
edges: 12
loops: 6
faces: 6
shells: 1
solids: 1
A box has 8 vertices, 12 edges, and 6 faces — exactly the numbers of a cube’s corners, edges, and sides.
A real part’s topology
The same counters read off the topology of a part you would actually make: a 60 × 40 × 10 mm mounting plate with a Ø9 mm through-hole — the classic drilled bracket. The BRep must say: 6 flat faces plus 1 cylindrical hole wall, 8 vertices, and 14 edges (the box’s 12 plus the 2 circles that cap the hole wall):
plate = brep(difference(box(0.060, 0.040, 0.010), cylinder(0.0045, 0.010)))
println("vertices: ", length(plate.vertices), " (box 8, no new corners)")
println("edges: ", length(plate.edges), " (12 box + 2 hole circles)")
println("faces: ", length(plate.faces), " (6 box + 1 hole wall)")
println("shells: ", length(plate.shells))vertices: 8 (box 8, no new corners)
edges: 14 (12 box + 2 hole circles)
faces: 7 (6 box + 1 hole wall)
shells: 1
The face list itself names every surface — six PlaneSurfaces and the one CylinderSurface:
for (i, f) in enumerate(plate.faces)
println("Face", i, ": ", typeof(f.surface))
endFace1: PlaneSurface{Float64}
Face2: PlaneSurface{Float64}
Face3: PlaneSurface{Float64}
Face4: PlaneSurface{Float64}
Face5: PlaneSurface{Float64}
Face6: PlaneSurface{Float64}
Face7: CylinderSurface{Float64}
A drilled hole turns a cube into a torus-topology solid (genus 1), which is exactly the kind of part whose validity matters downstream. is_watertight verifies the shell is closed and consistently oriented before it ever reaches meshing or STEP export:
println("watertight: ", is_watertight(plate))watertight: true
vtk(plate; n = 32, opacity = 0.7)And the face names are the hooks a boundary condition attaches to later — the hole wall keeps one stable name, the plate top another, through a rebuild:
println("face names: ", face_names(plate))face names: ["Face1", "Face2", "Face3", "Face4", "Face5", "Face6", "Face7"]
Curve and surface evaluation
Each edge carries a curve and each face carries a surface. curve_point and surface_point sample them at parametric coordinates.
The first edge of the box is a straight line segment:
edge = b.edges[1]
println("curve type: ", typeof(edge.curve))
println("start (t=0): ", curve_point(edge.curve, 0.0))
println("mid (t=½): ", curve_point(edge.curve, 0.5))
println("end (t=1): ", curve_point(edge.curve, 1.0))curve type: LineCurve{Float64}
start (t=0): [-1.0, -0.5, -0.25]
mid (t=½): [0.0, -0.5, -0.25]
end (t=1): [1.0, -0.5, -0.25]
A LineCurve linearly interpolates its two endpoints.
Curved primitives
Primitives like spheres and cylinders expose exact curved geometry in their BRep — no faceting:
s = brep(sphere(1.0))
println("sphere faces: ", length(s.faces))
println("surface type: ", typeof(s.faces[1].surface))
# the north pole (u = 0, v = 0)
println("north pole: ", surface_point(s.faces[1].surface, 0.0, 0.0))
# a point on the equator (u = 0, v = π/2)
println("equator: ", surface_point(s.faces[1].surface, 0.0, π / 2))sphere faces: 2
surface type: SphereSurface{Float64}
north pole: [0.0, 0.0, 1.0]
equator: [1.0, 0.0, 6.123233995736766e-17]
A cylinder’s BRep has three faces: the curved side plus two flat caps, each with its own surface type:
c = brep(cylinder(1.0, 2.0))
for f in c.faces
println(typeof(f.surface))
endCylinderSurface{Float64}
PlaneSurface{Float64}
PlaneSurface{Float64}
A cone’s curved face is a ConeSurface:
co = brep(cone(1.0, 2.0))
for f in co.faces
println(typeof(f.surface))
endConeSurface{Float64}
PlaneSurface{Float64}
Procedural surfaces
The STEP reader models surface_of_linear_extrusion, surface_of_revolution, and toroidal_surface as procedural surfaces, distinct from the analytic primitives: an ExtrusionSurface sweeps a curve along an axis, a RevolutionSurface revolves a curve about an axis, and a ToroidalSurface is the closed torus (which the torus primitive produces directly). All three are sampled with surface_point, just like the analytic surfaces:
# surface_of_linear_extrusion: a circle swept along +z by 2.0
ext = ExtrusionSurface(CircleCurve([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 1.0), [0.0, 0.0, 2.0])
println("extrusion surface : ", typeof(ext))
println(" at u = 0, v = 0 : ", round.(surface_point(ext, 0.0, 0.0); digits = 3))
# surface_of_revolution: a circle revolved about the +z axis
rev = RevolutionSurface(CircleCurve([2.0, 0.0, 0.0], [0.0, 0.0, 1.0], 1.0), [0.0, 0.0, 0.0], [0.0, 0.0, 1.0])
println("revolution surface: ", typeof(rev))
println(" at u = 0, v = 0 : ", round.(surface_point(rev, 0.0, 0.0); digits = 3))
# a torus is a single closed ToroidalSurface (see the torus/wedge tutorial)
tr = brep(torus(2.0, 0.5))
println("torus surface : ", typeof(tr.faces[1].surface))extrusion surface : ExtrusionSurface{CircleCurve{Float64}, Float64}
at u = 0, v = 0 : [1.0, 0.0, 0.0]
revolution surface: RevolutionSurface{CircleCurve{Float64}, Float64}
at u = 0, v = 0 : [3.0, 0.0, 0.0]
torus surface : ToroidalSurface{Float64}
FlippedSurface(s) wraps a surface with its orientation reversed — the STEP reader applies it when a face’s same_sense flag disagrees with the loop winding, and the boolean driver uses it to mark an inward cavity face:
flip = FlippedSurface(brep(sphere(1.0)).faces[1].surface)
println("flipped surface : ", typeof(flip))flipped surface : FlippedSurface{SphereSurface{Float64}}
Visualizing a BRep
vtk tessellates any BRep to a surface mesh on demand (n controls the facet count per curved edge):
vtk(b)vtk(c; n=48) # the cylinder, smoothly facetedvtk(s; n=48) # the spheresurface_mesh_faces returns the tessellation and the per-triangle source face index, so the mesh can be traced back to the exact face it came from:
mesh, face_ids = surface_mesh_faces(b; n = 24)
println("triangles: ", size(mesh.faces, 2), ", unique source faces: ", length(unique(face_ids)))triangles: 12, unique source faces: 6
The same shape, two views
The key design point: the same shape serves as a field (for meshing) or as a BRep (exact topology):
shape = cylinder(1.0, 2.0)
exact = brep(shape) # topological model
println("SDF at the centre : ", field(shape, [0.0, 0.0, 0.0]))
println("BRep faces : ", length(exact.faces))SDF at the centre : -1.0
BRep faces : 3
(The exact BRep is available for the primitives — box, sphere, cylinder, cone, and extrude/revolve of simple sketches — plus transforms (translate, rotate) and exact booleans: difference (box minus cylinder/sphere/box) and box-only union/intersection. See the BRep booleans tutorial.)
Topological naming
Every face, edge, and vertex of a BRep has a local name (Face1, Edge1, Vertex1, …) and a global name that qualifies it with the body name, so a reference like Box_1.Face3 survives a rebuild as long as the topology is unchanged:
tn = TopologicalNames("Box_1", brep(box(2.0, 3.0, 4.0)))
println("local faces : ", tn.faces)
println("global faces : ", global_faces(tn))
println("global edges : ", global_edges(tn))
println("global vertices : ", global_vertices(tn))local faces : ["Face1", "Face2", "Face3", "Face4", "Face5", "Face6"]
global faces : ["Box_1.Face1", "Box_1.Face2", "Box_1.Face3", "Box_1.Face4", "Box_1.Face5", "Box_1.Face6"]
global edges : ["Box_1.Edge1", "Box_1.Edge2", "Box_1.Edge3", "Box_1.Edge4", "Box_1.Edge5", "Box_1.Edge6", "Box_1.Edge7", "Box_1.Edge8", "Box_1.Edge9", "Box_1.Edge10", "Box_1.Edge11", "Box_1.Edge12"]
global vertices : ["Box_1.Vertex1", "Box_1.Vertex2", "Box_1.Vertex3", "Box_1.Vertex4", "Box_1.Vertex5", "Box_1.Vertex6", "Box_1.Vertex7", "Box_1.Vertex8"]
The per-entity accessors give a single local name, and qualified_name combines it with the body name:
bx = brep(box(2.0, 3.0, 4.0))
println("face 1 name : ", face_name(bx, 1))
println("edge 3 name : ", edge_name(bx, 3))
println("vertex 2 name : ", vertex_name(bx, 2))
println("qualified : ", qualified_name("Box_1", face_name(bx, 3)))
println("all faces : ", face_names(bx))
println("all edges : ", edge_names(bx))
println("all vertices : ", vertex_names(bx))face 1 name : Face1
edge 3 name : Edge3
vertex 2 name : Vertex2
qualified : Box_1.Face3
all faces : ["Face1", "Face2", "Face3", "Face4", "Face5", "Face6"]
all edges : ["Edge1", "Edge2", "Edge3", "Edge4", "Edge5", "Edge6", "Edge7", "Edge8", "Edge9", "Edge10", "Edge11", "Edge12"]
all vertices : ["Vertex1", "Vertex2", "Vertex3", "Vertex4", "Vertex5", "Vertex6", "Vertex7", "Vertex8"]
The names are index-based, so they are stable for deterministically-built BReps. Pair them with topology_changed to detect when a parameter change actually reorders the topology:
a = brep(box(2.0, 2.0, 2.0))
b = brep(cylinder(1.0, 2.0))
println("topology changed? ", topology_changed(a, b)) # true: 6 faces vs 3
sig = topology_signature(a)
println("signature type : ", typeof(sig)) # TopologySignaturetopology changed? true
signature type : TopologySignature
Next steps
- Linear elasticity — turn a volume mesh into a finite-element simulation.
- Differentiable optimization — take derivatives through the pipeline.
