Booleans
Boolean operations combine two solids: union (fuse), subtracting (cut), and intersection (common). In OCCTSwift they’re methods on Shape, wrapping OCCT’s BRepAlgoAPI_Fuse / _Cut / _Common. Each is fallible (returns Shape?), a degenerate or failed boolean yields nil.
OCCT C++ reference: Boolean Operations user guide (/open-cascade-sas/occt on context7).
The three operations
OCCT’s one-liner TopoDS_Shape S = BRepAlgoAPI_Fuse(A, B); becomes, here a cube and a cylinder passing through it:
guard let box = Shape.box(width: 10, height: 10, depth: 10),
let cyl = Shape.cylinder(at: SIMD3(0, 0, -8), direction: SIMD3(0, 0, 1),
radius: 3, height: 16) else { return }
let fused = box.union(cyl) // BRepAlgoAPI_Fuse . A ∪ B (box + protruding rod)
let cut = box.subtracting(cyl) // BRepAlgoAPI_Cut . A − B (box with a through-hole)
let common = box.intersection(cyl) // BRepAlgoAPI_Common. A ∩ B (the rod stub inside the box)
union (A ∪ B) | subtracting (A − B) | intersection (A ∩ B) |
🖱️ Drag to orbit · scroll to zoom · auto-rotating. The static render shows until the 3D model loads. (Models exported straight from these snippets via Exporter.writeGLTF.)
Volumes confirm the result (note volume is Double?, nil for non-solids / failures):
guard let a = Shape.box(origin: .zero, width: 10, height: 10, depth: 10),
let b = Shape.box(origin: SIMD3(5, 0, 0), width: 10, height: 10, depth: 10) else { return }
a.union(b)?.volume // 1500 (1000 + 1000 − 500 overlap)
a.intersection(b)?.volume // 500
a.subtracting(b)?.volume // 500 (1000 − 500)
Fuzzy value, tolerance for near-tangent faces
When operands share near-coincident or near-tangent faces, the exact boolean can produce spurious slivers or fail. OCCT’s fuzzy value (SetFuzzyValue) widens the intersection tolerance. OCCT’s C++ example sets aFuzzyValue = 2.1e-5; in OCCTSwift it’s a parameter (default 0 = OCCT’s own default tolerance, negatives ignored):
// Two solids whose walls nearly coincide, a small fuzzy value lets them fuse cleanly.
let merged = outer.union(inner, fuzzyValue: 2.1e-5)
Glue, coincident-face arguments
When you know the arguments share coincident faces (e.g. stacked blocks, consecutive loft chunks sharing an end section), glue tells OCCT those faces touch instead of intersecting them, a large robustness and speed win. Use it only when the faces really are coincident; gluing genuinely interpenetrating solids gives a wrong result.
// Two unit cubes stacked along Z, sharing the face at z = 10.
guard let lower = Shape.box(origin: .zero, width: 10, height: 10, depth: 10),
let upper = Shape.box(origin: SIMD3(0, 0, 10), width: 10, height: 10, depth: 10) else { return }
let stacked = lower.union(upper, glue: .shift) // .off (default) / .shift / .full
// stacked.volume == 2000, a single shell
BooleanGlue: .off (default), .shift (BOPAlgo_GlueShift, shared faces, otherwise disjoint), .full (BOPAlgo_GlueFull, all arguments coincident; fastest, strictest).
Timeout, never hang on a pathological operand
A self-intersecting / inside-out operand can make a boolean spin indefinitely. Every boolean is wall-clock bounded and returns nil at the deadline instead of hanging (default Shape.defaultBooleanTimeout = 120 s; pass 0 to disable):
// Returns nil within ~5 s if the cut can't complete, rather than blocking forever.
let result = blank.subtracting(toolThatMightBeBad, timeout: 5)
// Opt out (unbounded, prior behaviour) for a known-heavy but valid boolean:
let heavy = assembly.union(part, timeout: 0)
The parameters compose: a.subtracting(b, fuzzyValue: 1e-4, glue: .shift, timeout: 30).
Tell a timeout apart from a failure
That nil is the same nil a failed boolean returns, and the two mean opposite things: a failure is a property of the geometry, a timeout is a property of the deadline and of whatever else the machine was doing. Reporting the second as the first sends whoever reads the error to inspect a shape that is fine. Each boolean has an *Outcome sibling that separates them (#1067):
switch blank.subtractionOutcome(tools) {
case .success(let cut):
print("volume \(cut.volume as Any)")
case .timedOut:
// Not the geometry. Give it longer, or run it somewhere less busy.
print(blank.subtractionOutcome(tools, timeout: 600).shape as Any)
case .failed:
// The geometry. A larger timeout will not help; screen the operands instead.
print("the boolean declined these operands")
}
unionOutcome and intersectionOutcome are the same for + and &. Each takes exactly the parameters its named sibling takes, and union / subtracting / intersection are now thin wrappers returning BooleanOutcome.shape, so nothing changes for a caller who does not want the distinction:
// These two lines are the same call.
let a = blank.subtracting(tools, timeout: 30)
let b = blank.subtractionOutcome(tools, timeout: 30).shape
.timedOut is indeterminate, not a negative result: the boolean might well have succeeded given longer. That is the reading isSelfIntersecting(timeout:) gives its nil, and it inherits the same caveat, that timeout: is cooperative rather than a hard deadline (#293). With timeout: 0 there is no watchdog at all, so .timedOut cannot occur.
The two signals are not interchangeable, though. .timedOut is the watchdog and only the watchdog; isSelfIntersecting’s nil is wider, covering a refused argument and an errored analysis as well (#1054), which is why it has no timeout: 0 escape.
Validate an operand before a boolean
isValidSolid is a topology check, it does not catch global self-intersection (overlapping faces of one solid), which is exactly what poisons a boolean. Screen operands with isSelfIntersecting(timeout:) (returns true / false / nil = indeterminate). It’s accurate but expensive (seconds on B-spline solids), so it’s opt-in:
switch solid.isSelfIntersecting(timeout: 30) {
case false: break // clean, safe to use
case true: return // reject, would poison the boolean
case nil: break // indeterminate, treat as unknown, decide per use case
}
nil covers three things, not just the timeout: the analysis ran out of timeout, the analyzer refused the shape as an argument Boolean Operations cannot use, or the check errored. All three mean “unknown”, never “clean” (#1054).
Recipe: trust a loft result before cutting with it
A loft(ruled: false) can return a self-intersecting solid that still reports isValidSolid == true (see issue #206). Validate at the source, fix a reversed orientation, then reject self-intersection:
guard let raw = Shape.loft(profiles: sections, solid: true, ruled: false),
let solid = raw.orientedForward(), // fix inward-facing (negative-volume) result
solid.isSelfIntersecting() == false // reject self-intersecting overshoot
else {
// fall back (e.g. ruled: true, or a simpler profile correspondence)
return
}
let part = stock.subtracting(solid) // now safe
See also
- API mapping:
../../API_REFERENCE.md - Concepts (B-Rep topology, handles):
occt-concepts.md - History recording across booleans:
unionWithFullHistory/subtractedWithFullHistory(CHANGELOG v1.0.2).