Examples | PY

Booleans and Domains from One Build

One CsgGraph build answering boolean meshes, expression-selected domains, and hand-masked selections -- cut by a sheet.

One scene — a sphere straddling a knife plane, plus two floaters that touch nothing — queried three ways from one tf.CsgGraph build: the sides as boolean meshes, the volumes individually via expression-selected domains, and the same selection by hand from the inclusion matrix.

Source: arrangements.py

What the pipeline does

Inputsstraddleclosed · op 0floatersclosed · ops 1,2knifesheet · op 3tf.CsgGraph([...], sheets=[3])CSG graph — built once, queried three waysgraph — arrangement + domain classificationevery query below reuses this buildgraph.mesh(solids - op(3))Path 1 — boolean meshabove_mesh / below_mesh — one closed, capped mesh per sidewhen the sides are all you needgraph.domains(solids - op(3))Path 2 — domains by expressioncells — the selected volumes, individually watertightids stable across queries on one graphgraph.domains(return_index_map=True)Path 3 — domains by handcells, ids, imap.inclusion — (n_cells, n_ops) boolevery cell classified against every operandsheet column = behind the sheet's normalbelow = imap.inclusion[:, 3]Selection is a maskabove = ~below — any boolean combination, no new querysame cells as the path-2 expressions, by stable ids

The scene

import numpy as np
import trueform as tf

def sphere(center, radius):
    sf, sp = tf.make_sphere_mesh(radius, 32, 32)
    return tf.Mesh(sf, np.asarray(sp) + np.asarray(center, sp.dtype))

plane_faces, plane_points = tf.make_plane_mesh(4.0, 4.0)
meshes = [sphere((0, 0, 0), 1.0),      # op 0: straddles the knife
          sphere((0, 0, 2), 0.5),      # op 1: floats above, touches nothing
          sphere((0, 0, -2), 0.5),     # op 2: floats below, touches nothing
          tf.Mesh(plane_faces, plane_points)]  # op 3: the knife

Note the floaters never touch the knife — their side will come from winding alone; no cut geometry is needed to classify them.

One build

graph = tf.CsgGraph(meshes, sheets=[3])
solids = tf.op(0) | tf.op(1) | tf.op(2)

Declaring the plane a sheet makes op(3) an oriented separator: its operand bit means "behind the sheet's normal" (−Z here), so the knife cuts volumes through the same boolean algebra without enclosing one. Every query below reuses this build.

Path 1: boolean meshes

When you just need the two sides as meshes, one expression each:

above_mesh = graph.mesh(solids - tf.op(3))
below_mesh = graph.mesh(solids & tf.op(3))
=== Boolean meshes ===
  solids - knife: vol=2.5949 closed=True
  solids & knife: vol=2.5949 closed=True

Each side is a single closed mesh containing two disjoint pieces — the straddler's half capped by the knife, and the floater, whole.

Path 2: domains by expression

The same volumes, individually — one watertight mesh per cell:

above_cells, above_ids = graph.domains(solids - tf.op(3))
below_cells, below_ids = graph.domains(solids & tf.op(3))
=== Domains by expression ===
  above: 2 cells, vols [2.076, 0.519]
  below: 2 cells, vols [2.076, 0.519]

The two cells are exactly the pieces of the path-1 mesh (their volumes sum to it), now separately addressable.

Path 3: domains by hand

Extract everything once, then any selection is a mask over the inclusion matrix — the knife's column is 3, and behind its +Z normal means below:

cells, ids, imap = graph.domains(return_index_map=True)
below = imap.inclusion[:, 3]
above = ~below
=== Domains by hand ===
  4 cells; above 2, below 2

The masks select the same cells the path-2 expressions return — ids are stable across queries on one graph, so the example asserts it:

assert sorted(np.asarray(ids)[above].tolist()) == sorted(np.asarray(above_ids).tolist())
assert sorted(np.asarray(ids)[below].tolist()) == sorted(np.asarray(below_ids).tolist())

Writing and verifying

def write_side(mask, prefix):
    k = 0
    for (faces, points), keep in zip(cells, mask):
        if not keep:
            continue
        m = tf.Mesh(faces, points)
        tf.write_stl(m, f"{prefix}_{k}.stl")
        k += 1

write_side(above, "above")
write_side(below, "below")
  wrote above_0.stl (faces=1088, closed=True, manifold=True)
  wrote above_1.stl (faces=1984, closed=True, manifold=True)
  wrote below_0.stl (faces=1152, closed=True, manifold=True)
  wrote below_1.stl (faces=1984, closed=True, manifold=True)

Summary

PathAPIWhat you get
Buildtf.CsgGraph(meshes, sheets=[3])Arrangement + domain classification, once
1 — boolean meshgraph.mesh(solids - tf.op(3))One closed mesh per side
2 — domains by expressiongraph.domains(expr)The same volumes, individually
3 — domains by handgraph.domains(return_index_map=True) + imap.inclusion maskSame cells, stable ids
See CSG for CsgGraph, sheets, expressions, and the inclusion matrix; Topology for is_closed / is_manifold.