Modules | C++

Cut

Exact mesh arrangements, booleans, and curve embedding.

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.

For repeated computation on the same or moving geometry, build structures once and tag them, or use tagged transformations. See With Precomputed Structures and With Transformations.

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. Controlled via tf::intersect_mode flags — 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.

To detect where a single mesh's own polygons intersect each other, use tf::embedded_self_intersection_curves or tf::make_polygon_arrangements.

Coordinate Precision

All cut operations use exact integer arithmetic internally, defaulting to int32 coordinates. For increased precision, all functions accept an optional Int template parameter:

// Default: int32
auto [result, labels, face_labels] = tf::make_boolean(
    mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge);

// Higher precision: int64
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 details on the precision chain.

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:

embedded_curves.cpp
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 only one contour exists, so crossing resolution flags have no effect. 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:

embedded_self_curves.cpp
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

mesh_arrangements.cpp
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 (0 or 1)
  • face_labels: Index of the original face in its source mesh that each output face came from

With curves:

mesh_arrangements_curves.cpp
auto [mesh, tag_labels, face_labels, curves] = tf::make_mesh_arrangements(
    mesh1.polygons(), mesh2.polygons(), tf::return_curves);

Default mode: primitives. With two meshes only one contour exists, so crossing resolution flags have no effect.

auto [mesh, tag_labels, face_labels] = tf::make_mesh_arrangements(
    mesh1.polygons(), mesh2.polygons(), tf::intersect_mode::primitives);

N-Mesh Arrangements

Decompose a range of meshes into classified regions:

n_mesh_arrangements.cpp
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:

self_arrangements.cpp
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. Both cross-contour and self-crossing resolution are enabled.

auto [mesh, face_labels] = tf::make_polygon_arrangements(
    merged.polygons(),
    tf::intersect_mode::primitives | tf::intersect_mode::resolve_crossing_contours);

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

boolean.cpp
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:

OperationDescription
tf::boolean_op::mergeUnion: A ∪ B
tf::boolean_op::intersectionIntersection: A ∩ B
tf::boolean_op::left_differenceDifference: A \ B
tf::boolean_op::right_differenceDifference: 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 only one contour exists, so crossings cannot occur.

Configuration

boolean_config.cpp
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);
ParameterDefaultDescription
support_multi_nestingtrueWhen 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:

boolean_pair.cpp
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:

embedded_isocurves.cpp
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:

isobands.cpp
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:

segment_arrangements.cpp
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.

segment_arrangements_3d.cpp
// 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:

segment_planar_embedding.cpp
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:

cut_precomputed.cpp
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:

cut_transformed.cpp
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));