Link Search Menu Expand Document

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