Examples | C++

Booleans and Domains from One Build

One csg_graph 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::make_csg_graph 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.cpp

What the pipeline does

Inputsstraddleclosed · op 0floatersclosed · ops 1,2knifesheet · op 3tf::make_csg_graph(forms, sheets)CSG graph — built once, queried three waysgraph — arrangement + domain classificationevery query below reuses this buildtf::make_csg_mesh(graph, difference(solids, 3))Path 1 — boolean meshabove_mesh / below_mesh — one closed, capped mesh per sidewhen the sides are all you needtf::make_csg_domains(graph, e)Path 2 — domains by expressioncells — the selected volumes, individually watertightids stable across queries on one graphtf::make_csg_domains(graph, tf::return_index_map)Path 3 — domains by handcells, ids, imap.inclusion — true iff cell k inside op ievery cell classified against every operandsheet column = behind the sheet's normalbelow[k] = imap.inclusion[k][3]Selection is a maskabove = !below — any boolean combination, no new querysame cells as the path-2 expressions, by stable ids

The scene

auto straddle = tf::make_sphere_mesh<Index>(Real(1), 32, 32);   // op 0
auto above_s = tf::make_sphere_mesh<Index>(Real(0.5), 32, 32);  // op 1: floats above
auto below_s = tf::make_sphere_mesh<Index>(Real(0.5), 32, 32);  // op 2: floats below
auto plane = tf::make_plane_mesh<Index>(Real(4), Real(4));      // op 3: the knife

auto p0 = straddle.polygons() | tf::tag(fid);
auto p1 = above_s.polygons() | tf::tag(fa);     // frame: +2 in z
auto p2 = below_s.polygons() | tf::tag(fb);     // frame: -2 in z
auto p_knife = plane.polygons() | tf::tag(fid);
std::vector<decltype(p0)> forms{p0, p1, p2, p_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

std::array<int, 1> sheets{3};
auto graph =
    tf::make_csg_graph(tf::make_range(forms), tf::make_range(sheets));
auto solids = tf::csg::merge(tf::csg::merge(0, 1), 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:

auto above_mesh = tf::make_csg_mesh(graph, tf::csg::difference(solids, 3));
auto below_mesh = tf::make_csg_mesh(graph, tf::csg::intersection(solids, 3));
=== Boolean meshes ===
  solids - knife: vol=2.59494 closed=1
  solids & knife: vol=2.59494 closed=1

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:

auto [above_cells, above_ids] =
    tf::make_csg_domains(graph, tf::csg::difference(solids, 3));
auto [below_cells, below_ids] =
    tf::make_csg_domains(graph, tf::csg::intersection(solids, 3));
=== Domains by expression ===
  above: 2 cells
  below: 2 cells

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:

auto [cells, ids, imap] =
    tf::make_csg_domains(graph, tf::return_index_map);
tf::buffer<char> below;
below.allocate(cells.size());
for (std::size_t k = 0; k < cells.size(); ++k)
  below[k] = imap.inclusion[k][3] ? char(1) : char(0);
=== 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.

Writing and verifying

auto write_side = [&, &cs = cells](bool want_below, const char *prefix) {
  std::size_t k = 0;
  for (std::size_t i = 0; i < cs.size(); ++i) {
    if (bool(below[i]) != want_below)
      continue;
    tf::write_stl(cs[i].polygons(),
                  std::string(prefix) + "_" + std::to_string(k++) + ".stl");
  }
};
write_side(false, "above");
write_side(true, "below");
  wrote above_0.stl (faces=1088, closed=1, manifold=1)
  wrote above_1.stl (faces=1984, closed=1, manifold=1)
  wrote below_0.stl (faces=1152, closed=1, manifold=1)
  wrote below_1.stl (faces=1984, closed=1, manifold=1)

Summary

PathAPIWhat you get
Buildtf::make_csg_graph(forms, sheets)Arrangement + domain classification, once
1 — boolean meshtf::make_csg_mesh(graph, e)One closed mesh per side
2 — domains by expressiontf::make_csg_domains(graph, e)The same volumes, individually
3 — domains by handtf::make_csg_domains(graph, tf::return_index_map) + imap.inclusionSame cells, stable ids
See CSG for CsgGraph, sheets, expressions, and the inclusion matrix; Topology for is_closed / is_manifold.