CSG
The CSG module builds an arrangement of N forms once and evaluates any boolean expression over them — no need to chain pairwise booleans.
Include the module with:
#include <trueform/csg.hpp>
Overview
A CSG computation has three pieces:
- A
tf::csg_graph— the implicit arrangement of N forms, built once. - A
tf::csg::expr— a runtime boolean expression over operand ids. Built fromtf::csg::merge,tf::csg::intersection,tf::csg::difference,tf::csg::complement, plustf::csg::any_of/tf::csg::all_offor ranges. - An extraction call:
tf::make_csg_mesh(graph, expr).
std::vector forms{ mesh0.polygons(), mesh1.polygons(), mesh2.polygons() };
auto graph = tf::make_csg_graph(tf::make_range(forms));
auto expr = tf::csg::difference(0, tf::csg::merge(1, 2)); // a \ (b ∪ c)
auto out = tf::make_csg_mesh(graph, expr);
Operand ids are positions in the forms range. Integers are auto-promoted to leaves, so tf::csg::merge(0, 1, 2) reads exactly like the algebra.
Configuration & Supported Input
tf::make_csg_graph takes an optional tf::intersect_config — the same config used by make_boolean and the arrangement constructors. The config's mode flags decide what kinds of input are accepted and how degeneracies are resolved. What works in CSG is exactly what works under that mode:
| Mode flag | What CSG accepts when set |
|---|---|
tf::intersect_mode::primitives | Full 5-type intersection classification — handles shared edges, shared vertices, coplanar faces (aligned vs opposing boundary), and non-manifold edges shared by 3+ faces. |
tf::intersect_mode::sos | Symbolic-perturbation path: all intersections are edge-face, no degenerate cases. Faster, but cannot represent coplanar overlap or shared-vertex contact. |
tf::intersect_mode::resolve_crossing_contours | Resolves crossings between intersection curves from different form pairs that meet on the same face — required when more than two forms can pairwise intersect along the same face. |
The default for tf::make_csg_graph is primitives | resolve_crossing_contours, matching make_boolean. Override only when you have a reason to:
tf::intersect_config cfg{tf::intersect_mode::primitives
| tf::intersect_mode::resolve_crossing_contours};
auto graph = tf::make_csg_graph(tf::make_range(forms), cfg);
Full flag semantics, the tolerance field, and the mode-vs-config equivalence are documented in Intersect: Intersection Configuration.
Regardless of mode, each form should be PWN (piecewise winding number) — locally consistent orientation. The per-domain inclusion lattice that backs every boolean expression only carries meaning when each form draws a clean inside/outside boundary.
Coordinate Precision
The arrangement uses exact integer arithmetic internally. The lattice resolution is selected by the Int template parameter on make_csg_graph and resolves automatically from the input coordinate type:
| Input scalar | Resolved Int |
|---|---|
float | tf::exact::int32 |
double | tf::exact::int64 |
| any other | tf::exact::int32 (fallback) |
// Auto-resolved
auto graph = tf::make_csg_graph(tf::make_range(forms));
// Explicit override
auto graph = tf::make_csg_graph<tf::exact::int64>(tf::make_range(forms));
See Intersect: Exact Arithmetic for the full precision chain.
Output Coordinate Type
The output mesh's scalar type is controlled by the OutputCoordinateType template parameter on tf::make_csg_mesh. It defaults to the input forms' real type; any floating-point type may be supplied.
// Output matches input
auto out = tf::make_csg_mesh(graph, expr);
// Explicit override (independent of the graph's Int)
auto out = tf::make_csg_mesh<double>(graph, expr);
Internally the pipeline carries intersection points at full precision regardless of OutputCoordinateType; the parameter only governs the coordinate type at output emission. The Int chosen at graph-build time is unchanged.
Many Operations, One Graph
The arrangement is the cost. Once graph is built, every additional boolean expression evaluates without re-running the geometric pipeline:
auto graph = tf::make_csg_graph(tf::make_range(forms));
auto m_union = tf::make_csg_mesh(graph, tf::csg::merge(0, 1, 2));
auto m_inter = tf::make_csg_mesh(graph, tf::csg::intersection(0, 1, 2));
auto m_diff = tf::make_csg_mesh(graph,
tf::csg::difference(0,
tf::csg::any_of({1, 2})));
With Precomputed Structures
tf::make_csg_graph auto-tags any form that's missing tf::face_membership, tf::manifold_edge_link, or tf::tree. For repeated work — for example, instancing the same canonical mesh many times — pre-tag once and share the structures:
tf::face_membership<int> fm;
fm.build(bunny.polygons());
tf::manifold_edge_link<int, 3> mel;
mel.build(bunny.polygons().faces(), fm);
tf::aabb_tree<int, float, 3> tree(bunny.polygons(), tf::config_tree(4, 4));
auto bunny_tagged = bunny.polygons() | tf::tag(fm) | tf::tag(mel) | tf::tag(tree);
std::vector forms{ sphere_tagged, bunny_tagged, bunny_tagged, bunny_tagged };
auto graph = tf::make_csg_graph(tf::make_range(forms));
All copies of bunny_tagged share the same FM / MEL / tree — the arrangement build re-uses them across instances.
With Transformations
Combine tagged structures with tagged transformations to instance the same canonical mesh at different positions without copying geometry:
auto T1 = tf::make_transformation_from_translation(tf::make_vector(1.0f, 0, 0));
auto T2 = tf::make_transformation_from_translation(tf::make_vector(0, 1.0f, 0));
std::vector forms{
sphere_tagged | tf::tag(tf::make_frame(tf::transformation<float, 3>{})),
bunny_tagged | tf::tag(tf::make_frame(T1)),
bunny_tagged | tf::tag(tf::make_frame(T2)),
};
auto graph = tf::make_csg_graph(tf::make_range(forms));
auto out = tf::make_csg_mesh(graph,
tf::csg::difference(0, tf::csg::merge(1, 2)));
One tagged canonical mesh + N frames + one graph is the cheapest way to fold many instances of the same shape into a single boolean.
Relationship to Cut
| Cut | CSG | |
|---|---|---|
| Operands | 1 or 2 | N |
| Ops per call | 1 | unbounded (one graph, many make_csg_mesh) |
| Output | materialized mesh + face labels | materialized mesh per expression |
Anything binary fits cleanly in tf::make_boolean. N-ary or repeated boolean over the same forms is what the CSG module is for.
