RasmahRasmah

BRep Booleans, Intersections, and Editing

println("Julia ", VERSION)
Julia 1.12.6

Overview

The exact BRep (boundary representation) represents a solid by its boundary (vertices, edges, faces) and performs exact boolean operations on analytic surfaces. This tutorial tours the boolean kernel: surface–surface intersection, point classification, exact cavities, and interactive topology editing.

The worked parts are standard machined geometry — a drilled base plate, a manifold block with an internal passage, a ball-joint socket, and a dimpled plate — each built by a single exact subtraction, each watertight, each with a closed-form volume.

Surface–surface intersection

ssi(a, b) returns the intersection curve(s) of two analytic surfaces, represented as CircleCurve/CircleArc (circles) or LineCurve (lines).

using Rasmah

# plane z = 0
zplane = PlaneSurface([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0])

# plane × cylinder (radius 10 mm, axis z) → a circle
c = ssi(zplane, CylinderSurface(0.010, 0.020))[1]
println("plane×cylinder: circle radius = ", round(c.radius * 1000; digits = 1), " mm at z = ", c.z)

# plane × sphere (radius 15 mm) → a circle
sc = ssi(zplane, SphereSurface(0.015))[1]
println("plane×sphere:   circle radius = ", round(sc.radius * 1000; digits = 1), " mm")

# plane × plane → a line
l = ssi(zplane, PlaneSurface([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]))[1]
println("plane×plane: line from ", round.(curve_point(l, 0.0); digits = 3), " to ", round.(curve_point(l, 1.0); digits = 3))
plane×cylinder: circle radius = 10.0 mm at z = 0.0
plane×sphere:   circle radius = 15.0 mm
plane×plane: line from [0.0, -1000.0, 0.0] to [0.0, 1000.0, 0.0]

The ssi above is analytic: it returns exact curves for known surface pairs. For two arbitrary implicit fields there is a numerical counterpart — trace_intersection marches along the curve where both fields vanish (returning an ordered polyline), and ssi_nurbs fits a least-squares NURBS curve to that polyline:

f = to_sdf(plane([0.0, 0.0, 1.0], 0.0))   # z = 0
g = to_sdf(sphere(0.015))
lo, hi = [-0.03, -0.03, -0.03], [0.03, 0.03, 0.03]

pts = trace_intersection(f, g, lo, hi, 24)
println("traced points: ", length(pts))

nurbs = ssi_nurbs(f, g, lo, hi, 24)
println("fitted curve: degree ", nurbs.degree, ", ", length(nurbs.control_points), " control points")
traced points: 7
fitted curve: degree 3, 7 control points

trace_intersection seeds the curve by minimizing |f| + |g| on a grid, then marches along the tangent ∇f × ∇g, re-projecting onto f = g = 0 at each step. ssi_nurbs wraps the traced polyline as a NurbsCurve (see the NURBS tutorial).

Point-in-solid classification

point_in_solid classifies a point against an SDF (signed distance field) — :inside, :outside, or :boundary:

sdf = to_sdf(sphere(0.015))
println(point_in_solid(sdf, [0.0, 0.0, 0.0]))   # inside
println(point_in_solid(sdf, [0.03, 0.0, 0.0]))  # outside
println(point_in_solid(sdf, [0.015, 0.0, 0.0])) # boundary
inside
outside
boundary

classify_face classifies a BRep face the same way, but using the face’s corner vertices:

b = brep(box(0.060, 0.040, 0.010))
small = to_sdf(box(0.030, 0.020, 0.005))   # fits inside b
big = to_sdf(box(0.120, 0.080, 0.020))     # contains b

println(classify_face(b, 1, small))   # face 1 lies outside the small box
println(classify_face(b, 1, big))     # face 1 lies inside the big box
outside
inside

Exact booleans: four machined parts

The exact BRep supports boolean features over analytic surfaces. Four standard parts, each a single subtraction, each a valid multi-face solid:

1. The drilled base plate — a 60 × 40 × 10 mm plate with a Ø20 mm through-hole, the classic bracket blank. The BRep must report the box’s 8 vertices and 12 edges plus the hole’s 2 circles: 8 vertices, 14 edges, 7 faces (six flats plus the hole wall):

plate = brep(difference(box(0.060, 0.040, 0.010), cylinder(0.010, 0.012)))
println(
    "drilled plate: ",
    length(plate.vertices), " vertices, ",
    length(plate.edges), " edges, ",
    length(plate.faces), " faces, ",
    "watertight ", is_watertight(plate),
)
drilled plate: 8 vertices, 14 edges, 7 faces, watertight true
V_plate = 0.060 * 0.040 * 0.010 - π * 0.010^2 * 0.010
println("closed-form volume = ", round(V_plate * 1e9; digits = 0), " mm³")
println("mass (steel)       = ", round(V_plate * 7850 * 1000; digits = 0), " g")
closed-form volume = 20858.0 mm³
mass (steel)       = 164.0 g

2. The manifold block — a 60 × 40 × 20 mm block with a closed Ø20 mm cylindrical passage along its length, the internal gallery of a hydraulic manifold. box_with_cylinder_cavity stitches the reversed bore wall into the block; the cavity is hidden from the outside, which is why the vertex count is the box’s 8 plus the 2 seam points of the bore:

manifold = box_with_cylinder_cavity(0.060, 0.040, 0.020, 0.010, 0.020, 0.0, 0.0, 0.0)
println(
    "manifold block: ",
    length(manifold.faces), " faces, ",
    length(manifold.edges), " edges, ",
    "watertight ", is_watertight(manifold),
)
manifold block: 9 faces, 15 edges, watertight true
V_man = 0.060 * 0.040 * 0.020 - π * 0.010^2 * 0.020
println("block volume  = ", round(V_man * 1e9; digits = 0), " mm³")
println("mass (steel)  = ", round(V_man * 7850 * 1000; digits = 0), " g")
block volume  = 41717.0 mm³
mass (steel)  = 327.0 g

3. The ball-joint socket — the same block with a Ø24 mm spherical cavity scooped out of its centre:

socket = box_with_spherical_cavity(0.060, 0.040, 0.020, 0.012)
println(
    "socket block: ",
    length(socket.faces), " faces, ",
    length(socket.edges), " edges, ",
    "watertight ", is_watertight(socket),
)
socket block: 8 faces, 14 edges, watertight true

4. The dimpled plate — a Ø40 mm sphere crossing both top and bottom faces, leaving a spherical band through the middle of the block:

dimples = box_with_dimples(0.060, 0.040, 0.020, 0.020)
println(
    "dimpled plate: ",
    length(dimples.faces), " faces, ",
    length(dimples.edges), " edges, ",
    "watertight ", is_watertight(dimples),
)
dimpled plate: 7 faces, 14 edges, watertight true

The cavity helpers stitch trimmed and reversed analytic surfaces internally — TrimmedSurface cuts a sphere to an equatorial band and ReversedSurface flips a surface’s orientation for stitching — and box_with_dimples builds the related “dimple” solid (a sphere crossing two opposite faces, here the top and bottom).

The exact multi-shell solids render directly:

vtk(plate; n=16, opacity=0.7)
vtk(manifold; n=16, opacity=0.7)
vtk(socket; n=16, opacity=0.7)
vtk(dimples; n=16, opacity=0.7)

Every face of these parts carries an exact analytic surface, sampled with surface_point — the manifold’s hole wall (face 7 of 9 in this build) is a true cylinder, not a faceted tube:

cyl = manifold.faces[7]
println("hole-wall point at u=0, v=0: ", round.(surface_point(cyl.surface, 0.0, 0.0); digits = 4))
hole-wall point at u=0, v=0: [0.01, 0.0, 0.0]

Orientation

A solid’s faces are oriented: each loop winds so the surface normal points outward. reverse_orientation flips every loop, turning the solid “inside-out”. It is the primitive the boolean kernel uses to make a cavity’s wall point inward (a subtracted feature must face the other way):

b = brep(box(0.060, 0.040, 0.010))
flipped = reverse_orientation(b)

println("faces: ", length(b.faces), " (unchanged: ", length(flipped.faces), ")")
println("loop 1 before: ", b.loops[1].edges)
println("loop 1 after : ", flipped.loops[1].edges)
faces: 6 (unchanged: 6)
loop 1 before: [-1, -4, -3, -2]
loop 1 after : [2, 3, 4, 1]

Each signed edge index is negated and reversed, so the same loop now traverses the boundary in the opposite direction.

Topology editing

The BRep supports interactive edits — splitting edges and faces (including split_face_by_line, which cuts a planar face along a line crossing its boundary), and punching holes — each returning a new, consistent BRep:

bx = brep(box(0.060, 0.040, 0.010))

# split edge 1 in half (adds a vertex)
b1 = split_edge(bx, 1, 0.5)
println("split_edge: ", length(b1.vertices), " vertices, ", length(b1.edges), " edges")

# add a circular hole loop to the bottom face (face 1, z = -5 mm)
b2 = add_hole(bx, 1, CircleCurve(0.010, -0.005))
println("add_hole: face 1 now has ", length(b2.faces[1].loops), " loops")

# split a face between two of its boundary vertices
b3 = split_face(bx, 1, 1, 3)
println("split_face: ", length(b3.faces), " faces")

# split a planar face along a line crossing its boundary in two points
b4 = split_face_by_line(bx, 1, LineCurve([0.0, -0.020, -0.005], [0.0, 0.020, -0.005]))
println("split_face_by_line: ", length(b4.faces), " faces")
split_edge: 9 vertices, 13 edges
add_hole: face 1 now has 2 loops
split_face: 7 faces
split_face_by_line: 7 faces

split_face_by_line finds where the line crosses the face boundary (using split_edge at each crossing), then calls split_face between the two new vertices — a convenient shorthand for cutting a face in half.

For a closed cut (a hole) there is split_face_by_curve, which splits a planar face along a closed curve lying inside it. Instead of a straight cut it builds two TrimmedSurfaces — an annulus (the face boundary minus the curve) and a disk (the curve) — the generic loop-trimmed replacement for the hand-built add_hole loop:

b5 = split_face_by_curve(bx, 1, CircleCurve(0.010, -0.005))
println("split_face_by_curve: ", length(b5.faces), " faces")
split_face_by_curve: 7 faces

The annulus face uses the face boundary as its outer loop and the curve as an inner loop; the disk face uses the curve as its outer loop. Both are TrimmedSurfaces whose ParamLoops are polygons in the surface’s (u, v) parameter space, and point_in_loop tests membership in such a loop by even–odd ray casting:

loop = ParamLoop([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
println("inside  (0.5, 0.5): ", point_in_loop((0.5, 0.5), loop))
println("outside (2.0, 0.5): ", point_in_loop((2.0, 0.5), loop))
inside  (0.5, 0.5): true
outside (2.0, 0.5): false

BRep-level sets

The BRepVertexSet/BRepEdgeSet/BRepFaceSet/BRepSolidSet wrappers tag topology entities by id, mirroring the mesh-level sets. And boundary_sets(b, g) classifies a BRep’s faces by the SDF labels of a representative point on each face — the exact-geometry counterpart of boundary_sets(mesh, g):

bx = brep(box(0.060, 0.040, 0.010))
g = to_sdf(box(0.060, 0.040, 0.010))

vs = BRepVertexSet([1, 2])
fs = BRepFaceSet([1, 3])
println("vertex set ids: ", vs.ids, ", face set ids: ", fs.ids)

sets = boundary_sets(bx, g)
for (name, s) in sort(collect(pairs(sets)); by=p -> String(p[1]))
    println("  ", name, " -> faces ", s.ids)
end
vertex set ids: [1, 2], face set ids: [1, 3]
  xmax -> faces [6]
  xmin -> faces [5]
  ymax -> faces [4]
  ymin -> faces [3]
  zmax -> faces [2]
  zmin -> faces [1]

The same feature can thus produce consistent named face sets across the SDF, mesh, and BRep representations.

Summary

ssi + point_in_solid + classify_face are the analytic boolean-kernel primitives; difference/cavity helpers build exact multi-face solids; and split_edge/split_face/add_hole edit the topology in place — all while staying a valid, watertight BRep.