Modules | C++

Cut

Mesh booleans and arrangements.

The cut module provides high-level operations for splitting meshes along curves and performing boolean operations. It builds on the intersect module to compute intersection curves, then embeds these curves into the mesh topology by splitting faces and creating new connectivity. It additionally provides facilities for planar arrangements.

Include the module with:

#include <trueform/cut.hpp>

Overview

Cut operations take intersection curves and produce new meshes where:

  • Original faces are split along the curves
  • The curves become edges in the new mesh topology
  • Each resulting face region receives a label indicating its classification

All cut operations support an optional tf::return_curves parameter that returns both the cut mesh and the explicit curve geometry.

Embedded Isocurves

Isocurves are level sets of a scalar field. Embedding them creates a mesh where the 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};

// Returns: {mesh, per-face labels}
auto [result_mesh, labels] = tf::embedded_isocurves(
    mesh.polygons(),
    scalar_field,
    tf::make_range(cut_values));

// With curves: returns {mesh, labels, curves_buffer}
auto [result_mesh, labels, curves] = tf::embedded_isocurves(
    mesh.polygons(),
    scalar_field,
    tf::make_range(cut_values),
    tf::return_curves);

The labels buffer contains one integer per face in the result mesh, indicating which isoband (region between consecutive cut values) the face belongs to. Faces are labeled with indices 0, 1, 2, ... corresponding to regions (-∞, cut_values[0]), [cut_values[0], cut_values[1]), etc.

Technical Details

  • Cut values are automatically sorted internally
  • Scalar values are vertex-based (one per point)
  • Multiple cut values are processed efficiently in a single pass

Isobands

While embedded_isocurves creates a mesh with all regions between cut values, make_isobands extracts only the selected bands:

std::array<float, 4> cut_values = {0.0f, 0.25f, 0.5f, 0.75f};
std::array<int, 2> selected_bands = {1, 3}; // Extract 2nd and 4th bands

auto [result_mesh, labels] = tf::make_isobands(
    mesh.polygons(),
    scalar_field,
    tf::make_range(cut_values),
    tf::make_range(selected_bands));

Isobands are useful for:

  • Extracting regions within specific value ranges
  • Creating multi-level terrain representations
  • Segmenting meshes by property values

Boolean Operations

Boolean operations combine two meshes using set operations. Functions can be called directly on polygons without precomputed structures, or with precomputed and tagged structures for interactive applications where the same mesh is reused across multiple operations.

Input Mesh Requirements

Boolean operations accept PWN (piecewise winding number) meshes—plus the non-manifold flaps and geometric inconsistencies that real-world pipelines accumulate. These defects are modeled as independent noise: locally across polygon regions, globally across manifold edge-connected components.

Intersections collapse to canonical form via reduction diagrams in the ε-topology. The goal is commutative correctness: operations that commute with mesh idealization.

See our Research for the conceptual model and formal definitions.

Basic Boolean: make_boolean

Computes a single boolean result from two input meshes:

// Union operation
auto [result_mesh, labels] = tf::make_boolean(
    mesh1.polygons(), mesh2.polygons(), tf::boolean_op::merge);

// With curves
auto [result_mesh, 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.

With precomputed structures:

// Build structures once, reuse across operations
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);

auto [result, labels] = tf::make_boolean(
    tagged, other.polygons(), tf::boolean_op::merge);

Boolean Pair: make_boolean_pair

Returns the two halves of the boolean result, split along the intersection curve. Each half contains the faces from one input mesh that survive the operation:

auto [left_result, right_result] = tf::make_boolean_pair(
    mesh1.polygons(), mesh2.polygons(), tf::boolean_op::left_difference);

// With curves
auto [left_result, right_result, 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. Concatenating and cleaning them produces the same watertight mesh as make_boolean.

With precomputed structures:

auto tagged1 = mesh1.polygons() | tf::tag(tree1) | tf::tag(fm1) | tf::tag(mel1);
auto tagged2 = mesh2.polygons() | tf::tag(tree2) | tf::tag(fm2) | tf::tag(mel2);

auto [left_result, right_result] = tf::make_boolean_pair(
    tagged1, tagged2, tf::boolean_op::left_difference);

Mesh Arrangements: make_mesh_arrangements

While boolean operations select specific regions to combine into a result, make_mesh_arrangements returns all subdivided regions created by the intersection of two meshes, each classified by origin and spatial relationship.

This is the complete decomposition of the intersection problem—every region is returned as a separate mesh with full classification:

auto [meshes, labels, classes] = tf::make_mesh_arrangements(
    mesh1.polygons(), mesh2.polygons());

// meshes: std::vector<polygons_buffer> - one mesh per region
// labels: buffer<int8_t> - which input mesh (0 or 1) each region came from
// classes: buffer<arrangement_class> - spatial classification

Classification details:

  • Labels: Each region is marked with 0 (came from first mesh) or 1 (came from second mesh)
  • Classes: Each region is classified as one of:
    • tf::arrangement_class::inside - The region is inside the other mesh
    • tf::arrangement_class::outside - The region is outside the other mesh
    • tf::arrangement_class::aligned_boundary - Coplanar faces with same normal direction
    • tf::arrangement_class::opposing_boundary - Coplanar faces with opposite normal direction

Example usage:

auto [meshes, labels, classes] = tf::make_mesh_arrangements(
    mesh1.polygons(), mesh2.polygons());

for (const auto& [mesh, label, cls] : tf::zip(meshes, labels, classes)) {
    bool from_first = (label == 0);

    if (cls & tf::arrangement_class::inside) {
        // Region is inside the other mesh
    } else if (cls & tf::arrangement_class::outside) {
        // Region is outside the other mesh
    } else if (cls & tf::arrangement_class::aligned_boundary) {
        // Coplanar region, normals aligned
    } else if (cls & tf::arrangement_class::opposing_boundary) {
        // Coplanar region, normals opposing
    }
}

Relationship to boolean operations:

Mesh arrangements provide the complete decomposition from which any boolean operation can be reconstructed:

  • Union: Outside regions from both meshes + aligned boundary regions
  • Intersection: Inside regions from both meshes + aligned boundary regions
  • A \ B: Outside regions from A + opposing boundary regions from B

The arrangements approach is useful when you need:

  • Complete control over region selection
  • Multiple different boolean results from the same intersection
  • Per-region analysis with spatial classification
  • Visualization of all subdivided regions

With precomputed structures:

auto tagged1 = mesh1.polygons() | tf::tag(tree1) | tf::tag(fm1) | tf::tag(mel1);
auto tagged2 = mesh2.polygons() | tf::tag(tree2) | tf::tag(fm2) | tf::tag(mel2);

auto [meshes, labels, classes] = tf::make_mesh_arrangements(tagged1, tagged2);

Boolean with Transformations

Forms can include transformations, enabling boolean operations between transformed instances:

auto transform = tf::make_transformation_from_translation(
    tf::make_vector(5.0f, 0.0f, 0.0f));
// Computes boolean between original and translated mesh
auto [res_polygons, labels] = tf::make_boolean(
    polygons, polygons | tf::tag(transform), tf::boolean_op::merge);

This pattern is used in the benchmarks to create random overlapping configurations.

Embedded Self-Intersection Curves

To resolve self-intersections within a single mesh:

auto mesh = load_mesh("self_intersecting.obj");

// Returns: new mesh where self-intersection curves are embedded as edges
auto result_mesh = tf::embedded_self_intersection_curves(mesh.polygons());

// With curves: returns {polygons_buffer, curves_buffer}
auto [result_mesh, curves] =
    tf::embedded_self_intersection_curves(mesh.polygons(), tf::return_curves);

This operation:

  1. Computes all self-intersection curves using tf::intersections_within_polygons
  2. Splits faces along these curves
  3. Returns a new mesh where the intersection curves are edges

The input mesh remains unchanged. The output mesh has faces split such that no face contains a self-intersection curve in its interior.

With precomputed structures:

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);

auto result_mesh = tf::embedded_self_intersection_curves(tagged);

Planar Arrangements

Planar arrangements compute the subdivision of a 2D plane induced by a set of line segments:

tf::buffer<float> segment_coords; // x0,y0, x1,y1, x2,y2, ...
tf::buffer<int> segment_edges;    // 0,1, 2,3, 4,5, ...

auto segments = tf::make_segments(
    tf::make_blocked_range<2>(segment_edges),
    tf::make_points<2>(segment_coords));

// Build planar arrangement
tf::planar_arrangements<int, float> pa;
pa.build(segments);

for (auto [face, hole_ids] :
    tf::zip(pa.faces(), pa.holes_for_faces())){
    auto polygon = tf::make_polygon(face, pa.points());
    for (auto hole : tf::make_indirect_range(hole_ids, pa.holes())) {
      auto hole_polygon = tf::make_polygon(hole, pa.points());
    }
}