Cut
The Cut module splits meshes and segments along intersection curves, performs boolean operations, and classifies regions. It builds on the Intersect module for computing intersections, then embeds these curves into mesh topology by splitting faces and creating new connectivity. All operations are geometrically and topologically exact.
Include the module with:
#include <trueform/cut.hpp>
Overview
The Cut module provides operations at several levels:
- Embedded intersection curves: Split a mesh along intersection curves without classifying regions
- Mesh arrangements: Decompose two or more meshes into classified regions — the complete intersection problem
- Boolean operations: Select regions from arrangements to produce union, intersection, or difference
- Isocurve embedding: Split meshes along scalar field level sets
- Segment arrangements: Split 2D or 3D segments at all intersection points
All cut operations return a face_labels buffer that maps each output face back to the index of the original face it came from in the source mesh. This enables attribute transfer and provenance tracking. Multi-mesh operations (arrangements and booleans) additionally return tag_labels — which input mesh each face belongs to.
All cut operations support an optional tf::return_curves parameter that additionally returns the explicit curve geometry as a tf::curves_buffer.
The arrangement operations (tf::make_mesh_arrangements, tf::make_polygon_arrangements) additionally support an optional tf::return_index_map parameter. In place of the loose tag_labels / face_labels buffers it returns a single index-map struct that relates both the points and faces of the output back to the input, plus a forward map from each input point to its output index. See Index Maps.
Supported Input
Embedding and arrangement operations inherit the same robustness as the Intersect module:
- Open and closed meshes — boundaries are handled correctly
- Non-manifold edges — edges shared by 3 or more faces
- Coplanar faces — overlapping faces are classified (aligned/opposing boundary)
- Self-intersecting geometry — detected and resolved
- Crossing intersection curves — where curves from different mesh pairs meet on a face, crossings can be resolved by splitting curves at the crossing point. Configured via
tf::intersect_config; each function sets appropriate defaults.
Boolean operations additionally require that intersection curves split the meshes into separate inside/outside regions. Input meshes should be PWN (piecewise winding number) — locally consistent orientation.
tf::embedded_self_intersection_curves or tf::make_polygon_arrangements.Coordinate Precision
All cut operations use exact integer arithmetic internally. The lattice resolution is selected by the Int template parameter 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 [result, labels, face_labels] = tf::make_boolean(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge);
// Explicit override
auto [result, labels, face_labels] = tf::make_boolean<tf::exact::int64>(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge);
This applies to all cut operations: make_boolean, make_boolean_pair, make_mesh_arrangements, make_polygon_arrangements, make_segment_arrangements, embedded_intersection_curves, and embedded_self_intersection_curves.
See Intersect: Exact Arithmetic for the full precision chain.
Output Coordinate Type
The output mesh's scalar type is controlled by an optional second template parameter, OutputCoordinateType. It defaults to the input mesh's real type. Any floating-point type may be supplied.
The two template parameters can be set independently:
// Both auto: Int from input scalar, output matches input
auto [result, labels, face_labels] = tf::make_boolean(
float_mesh1.polygons(), float_mesh2.polygons(), tf::boolean_op::merge);
// Explicit Int, output auto (matches input)
auto [result, labels, face_labels] =
tf::make_boolean<tf::exact::int64>(
float_mesh1.polygons(), float_mesh2.polygons(), tf::boolean_op::merge);
// Auto Int, explicit output type
auto [result, labels, face_labels] =
tf::make_boolean<tf::none_t, double>(
float_mesh1.polygons(), float_mesh2.polygons(), tf::boolean_op::merge);
// Both explicit
auto [result, labels, face_labels] =
tf::make_boolean<tf::exact::int64, double>(
float_mesh1.polygons(), float_mesh2.polygons(), tf::boolean_op::merge);
The first template slot is Int (use tf::none_t to keep auto-resolve), and the second is OutputCoordinateType (use tf::none_t to default to the input scalar).
Internally the pipeline carries intersection points at full precision regardless of OutputCoordinateType; the parameter only governs the coordinate type at output emission.
Embedded Intersection Curves
Embed intersection curves into mesh topology without performing boolean selection or region classification. The result is a mesh where intersection curves become edges.
Between Two Meshes
Embeds curves from the intersection of mesh A and mesh B into mesh A. All faces from mesh A are preserved (split where intersecting), with no faces from mesh B:
auto [result, face_labels] = tf::embedded_intersection_curves(
mesh1.polygons(), mesh2.polygons());
// With curves
auto [result, face_labels, curves] = tf::embedded_intersection_curves(
mesh1.polygons(), mesh2.polygons(), tf::return_curves);
This is useful for projecting cutting guides onto a surface or visualizing contact regions.
Default mode: primitives. With two meshes every contour is of the one class (A,B); crossings among them — e.g. from disjoint components of one mesh — are resolved by resolve_self_crossing_contours or within. To pass a custom mode:
auto [result, face_labels] = tf::embedded_intersection_curves(
mesh1.polygons(), mesh2.polygons(),
tf::intersect_mode::primitives | tf::intersect_mode::resolve_self_crossing_contours);
Self-Intersection
Embed curves where a mesh intersects itself:
auto [result, face_labels] = tf::embedded_self_intersection_curves(mesh.polygons());
// With curves
auto [result, face_labels, curves] = tf::embedded_self_intersection_curves(
mesh.polygons(), tf::return_curves);
The output mesh has faces split such that no face contains a self-intersection curve in its interior. The input mesh remains unchanged.
Default mode: primitives | resolve_contours. Both cross-contour and self-crossing resolution are enabled.
auto [result, face_labels] = tf::embedded_self_intersection_curves(
mesh.polygons(),
tf::intersect_mode::primitives | tf::intersect_mode::resolve_crossing_contours);
Mesh Arrangements
Mesh arrangements decompose intersecting meshes into classified regions. This is the complete solution to the intersection problem — every region is returned with labels indicating origin and spatial classification.
Two-Mesh Arrangements
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons());
Returns a single merged mesh with per-face labels:
tag_labels: Which input mesh each face came from (0or1)face_labels: Index of the original face in its source mesh that each output face came from
With curves:
auto [mesh, tag_labels, face_labels, curves] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons(), tf::return_curves);
Default mode: primitives. With two meshes every contour is of the one class (A,B); crossings among them — e.g. from disjoint components of one mesh — are resolved by resolve_self_crossing_contours or within.
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons(), tf::intersect_mode::primitives);
With tolerance: to recover the intended topology on inputs that carry float-precision drift, pass an intersect_config with a non-zero tolerance. See Intersection Configuration.
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons(),
tf::intersect_config{tf::intersect_mode::primitives, 1e-6});
Arrangement Configuration
Every arrangement entry point takes a tf::arrangement_config — the intersection run plus the triangulation built over the cut surfaces:
struct arrangement_config {
intersect_config intersect; // mode + tolerance
triangulation_type triangulation; // cdt (default) or refined_cdt
};
It is implicitly constructible from an intersect_config, an intersect_mode, or a triangulation_type alone, so a call site spells only the part it cares about:
// intersect settings only — default cdt triangulation
auto r0 = tf::make_mesh_arrangements(
a.polygons(), b.polygons(),
tf::intersect_config{tf::intersect_mode::primitives, 1e-6});
// quality-refined triangulation, default intersect settings
auto r1 = tf::make_mesh_arrangements(
a.polygons(), b.polygons(), tf::triangulation_type::refined_cdt);
// both
auto r2 = tf::make_mesh_arrangements(
a.polygons(), b.polygons(),
{tf::intersect_config{tf::intersect_mode::primitives, 1e-6},
tf::triangulation_type::refined_cdt});
triangulation_type::refined_cdt quality-refines the cut surfaces (Ruppert circumcenter insertion with negotiated boundary splits), so shared boundaries stay watertight by construction. Refinement adds Steiner points, so the output carries more vertices and triangles than the plain cdt.
N-Mesh Arrangements
Decompose a range of meshes into classified regions:
decltype(mesh0.polygons()) forms[] = {
mesh0.polygons() | tf::tag(f0),
mesh1.polygons() | tf::tag(f1),
mesh2.polygons() | tf::tag(f2)};
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
tf::make_range(forms, forms + 3));
// With curves
auto [mesh, tag_labels, face_labels, curves] = tf::make_mesh_arrangements(
tf::make_range(forms, forms + 3), tf::return_curves);
The tag_labels values range from 0 to N-1, indicating which input mesh each face originated from.
Default mode: primitives | resolve_crossing_contours. With 3+ meshes, contours from different mesh pairs can cross on a shared face — crossings are resolved by default.
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
tf::make_range(forms, forms + 3),
tf::intersect_mode::primitives | tf::intersect_mode::resolve_contours);
Self-Intersection Arrangements
Decompose a single mesh at its self-intersection curves:
auto [mesh, face_labels, curves] = tf::make_polygon_arrangements(
merged.polygons(), tf::return_curves);
Returns the split mesh with per-face labels identifying connected regions.
Default mode: primitives | resolve_contours | within — the self path always
runs within, and the default spells it so the boundary is visible.
auto [mesh, face_labels] = tf::make_polygon_arrangements(
merged.polygons(),
tf::intersect_mode::primitives | tf::intersect_mode::resolve_crossing_contours);
Index Maps
Passing tf::return_index_map to an arrangement returns a single index-map struct in place of the loose tag_labels / face_labels buffers. It relates both the points and faces of the output back to the input, and adds a forward map from each input point to its output index — everything the operation already computes internally, surfaced instead of discarded.
Two-mesh and N-mesh arrangements return a tf::mesh_arrangement_index_map:
auto [mesh, imap] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons(), tf::return_index_map);
// N-mesh: identical struct, tags range 0..N-1
auto [nmesh, nimap] = tf::make_mesh_arrangements(
tf::make_range(forms, forms + 3), tf::return_index_map);
| Member | Indexed by | Maps to |
|---|---|---|
point_tag_labels | output point | input mesh tag (0..N-1); created → n_tags |
point_labels | output point | input point id within its mesh; created → n_output_points |
face_tag_labels | output face | input mesh tag |
face_labels | output face | input face id within its mesh |
point_f | [tag][input point id] | output point index (forward); unmapped → n_output_points |
n_original_points | — | outputs ≥ this are created points |
n_original_faces | — | boundary between original and cut faces |
n_tags | — | number of input meshes; the tag axis end sentinel |
n_output_points | — | total output points; the point-id end sentinel |
Single-mesh arrangements return a tf::polygon_arrangement_index_map — the same, minus the tag axis (one mesh): members point_labels, face_labels, point_f (indexed point_f[input point id]), and the same three boundary fields.
auto [mesh, imap] = tf::make_polygon_arrangements(
merged.polygons(), tf::return_index_map);
Created points and the end sentinel. Output points split into kept originals [0, n_original_points) and created intersection points [n_original_points, n_output_points). A created point has no input origin, so each inverse axis carries an end sentinel one past its own range — the tag ends at n_tags, the point id ends at n_output_points — the same idiom tf::index_map uses for unmapped entries:
bool is_created = o >= imap.n_original_points;
// equivalently (N-mesh): imap.point_tag_labels[o] == imap.n_tags
The forward point_f emits n_output_points for any input point with no output. Faces never carry a sentinel — a cut face is a piece of an input face, so it always keeps a real origin.
Carrying attributes across an arrangement. Because the inverse labels are output-indexed and the forward map is input-indexed, per-vertex attributes transfer in a single pass:
auto [mesh, imap] = tf::make_mesh_arrangements(
mesh1.polygons(), mesh2.polygons(), tf::return_index_map);
for (std::size_t o = 0; o < mesh.polygons().points().size(); ++o) {
if (int(o) >= imap.n_original_points)
continue; // created point — interpolate from neighbours instead
int tag = imap.point_tag_labels[o];
int in = imap.point_labels[o];
out_attr[o] = input_attr[tag][in];
}
Float input, integer output. As with tf::return_curves, when the input is floating-point and OutputCoordinateType is integral, the converter is appended as a trailing tuple element:
auto [mesh, imap, converter] =
tf::make_mesh_arrangements<tf::none_t, std::int32_t>(
mesh1.polygons(), mesh2.polygons(), tf::return_index_map);
auto p = converter.deconvert(mesh.polygons().points()[0]); // integer point back to float
Relationship to Boolean Operations
Mesh arrangements provide the complete decomposition from which any boolean operation can be reconstructed:
- Union (A ∪ B): Outside regions from both meshes + aligned boundary
- Intersection (A ∩ B): Inside regions from both meshes + aligned boundary
- Difference (A \ B): Outside regions from A + inside regions from B with opposing boundary
Use arrangements when you need complete control over region selection, multiple boolean results from the same intersection, or per-region analysis.
Boolean Operations
Boolean operations select specific regions from a mesh arrangement to produce a single result mesh.
Basic Boolean: make_boolean
auto [result, labels, face_labels] = tf::make_boolean(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge);
// With curves
auto [result, labels, face_labels, curves] = tf::make_boolean(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge, tf::return_curves);
Available operations:
| Operation | Description |
|---|---|
tf::boolean_op::merge | Union: A ∪ B |
tf::boolean_op::intersection | Intersection: A ∩ B |
tf::boolean_op::left_difference | Difference: A \ B |
tf::boolean_op::right_difference | Difference: B \ A |
The labels buffer contains one integer per face, indicating which input mesh (0 or 1) the face originated from. The face_labels buffer maps each output face to its original face index in the source mesh.
Booleans use primitives mode internally with no contour crossing resolution — with two meshes every contour is of the one class (A,B), and same-class crossings are the within flag's territory.
Configuration
tf::boolean_config config;
config.support_multi_nesting = true; // default
auto [result, labels, face_labels] = tf::make_boolean(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge, config);
| Parameter | Default | Description |
|---|---|---|
support_multi_nesting | true | When true, uses ray-based containment to correctly classify multi-nested geometry (e.g., a shell inside another shell). Open mesh components are treated as having no interior. When false, uses signed distance to the nearest surface — faster but only sees the closest component. |
Boolean Pair: make_boolean_pair
Returns the two halves of the boolean result, split along the intersection curve:
auto [left, right, fl_left, fl_right] = tf::make_boolean_pair(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::left_difference);
// With curves
auto [left, right, fl_left, fl_right, curves] = tf::make_boolean_pair(
mesh1.polygons(), mesh2.polygons(), tf::boolean_op::left_difference,
tf::return_curves);
Both results have open boundaries at the intersection curve.
Embedded Isocurves
Isocurves are level sets of a scalar field. Embedding them creates a mesh where contour lines become edges:
tf::buffer<float> scalar_field;
scalar_field.allocate(mesh.points().size());
// Assign scalar values to vertices...
std::array<float, 3> cut_values = {0.0f, 0.5f, 1.0f};
auto [result, labels, face_labels] = tf::embedded_isocurves(
mesh.polygons(),
tf::make_range(scalar_field),
tf::make_range(cut_values));
// With curves
auto [result, labels, face_labels, curves] = tf::embedded_isocurves(
mesh.polygons(),
tf::make_range(scalar_field),
tf::make_range(cut_values),
tf::return_curves);
The labels buffer contains one integer per face, indicating which isoband the face belongs to. Labels 0, 1, 2, ... correspond to regions (-∞, cut_values[0]), [cut_values[0], cut_values[1]), etc.
Isobands
While embedded_isocurves creates a mesh with all regions, make_isobands extracts only selected bands:
std::array<float, 4> cut_values = {0.0f, 0.25f, 0.5f, 0.75f};
std::array<int, 2> selected_bands = {1, 3};
auto [result, labels, face_labels] = tf::make_isobands(
mesh.polygons(),
tf::make_range(scalar_field),
tf::make_range(cut_values),
tf::make_range(selected_bands));
Segment Arrangements
Split 2D or 3D segments at all intersection points. Returns the subdivided segments with labels mapping each sub-edge to its original edge:
auto [result, edge_labels] = tf::make_segment_arrangements(segments);
The input segments can be plain or pre-tagged with tree and edge_membership structures. The output is a tf::segments_buffer with all intersections resolved and edges split.
// Also works for 3D segments
tf::segments_buffer<int, float, 3> segments_3d;
// ... fill with data ...
auto [result, edge_labels] = tf::make_segment_arrangements(segments_3d.segments());
// edge_labels[i] = index of the original edge that output edge i came from
Planar Embedding
To compute the faces (regions) induced by the split segments, use tf::planar_embedding from the Topology module:
auto [result, edge_labels] = tf::make_segment_arrangements(segments);
tf::planar_embedding<int> pe;
pe.build(result.segments());
for (auto [face, hole_ids] : tf::zip(pe.faces(), pe.holes_for_faces())) {
auto polygon = tf::make_polygon(face, result.points());
for (auto hole : tf::make_indirect_range(hole_ids, pe.holes())) {
auto hole_polygon = tf::make_polygon(hole, result.points());
}
}
With Precomputed Structures
All cut functions accept plain polygons or forms with precomputed spatial and topological structures. When structures are pre-tagged, the function skips building them — useful for repeated operations on the same mesh:
tf::aabb_tree<int, float, 3> tree;
tree.build(mesh.polygons(), tf::config_tree(4, 4));
tf::face_membership<int> fm;
fm.build(mesh.polygons());
tf::manifold_edge_link<int, 3> mel;
mel.build(mesh.polygons().faces(), fm);
auto tagged = mesh.polygons() | tf::tag(tree) | tf::tag(fm) | tf::tag(mel);
// Use tagged form in any cut operation
auto [result, face_labels] = tf::embedded_intersection_curves(tagged, other_tagged);
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(tagged, other_tagged);
auto [result, labels, face_labels] = tf::make_boolean(tagged, other_tagged, tf::boolean_op::merge);
With Transformations
Tagged transformations enable operations in transformed space without copying geometry. Build structures once, then apply different transformations per operation:
auto T = tf::make_transformation_from_translation(
tf::make_vector(5.0f, 0.0f, 0.0f));
// Boolean between original and translated mesh
auto [result, labels, face_labels] = tf::make_boolean(
mesh.polygons(),
mesh.polygons() | tf::tag(T),
tf::boolean_op::merge);
// Arrangements with per-mesh transforms
auto T0 = tf::make_transformation_from_translation(
tf::make_vector(0.5f, 0.0f, 0.0f));
auto T1 = tf::make_transformation_from_translation(
tf::make_vector(-0.5f, 0.0f, 0.0f));
auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
mesh1.polygons() | tf::tag(T0),
mesh2.polygons() | tf::tag(T1));
