Fast and Exact Mesh Intersection Curves in C++
Compute the exact curves where two or more triangle meshes intersect in C++. Polylines out, with tagged transformations, precomputed structures and N-mesh input. 3.5 ms on 2×500K polygons. Header-only, C++17.
An intersection curve is the line where two surfaces cross. You need it to inspect a fit, to trim a part, to mark a seam, or to check a boolean before you run it. trueform computes these curves exactly, between two meshes or between many, and returns them as polylines. It is header-only C++17. This tutorial covers two meshes, moved meshes, reused structures, N meshes, and how to use the curves.

Install
Header-only. It needs C++17 and oneTBB. Add it to your project via CMake FetchContent:
FetchContent_Declare(
trueform
GIT_REPOSITORY https://github.com/polydera/trueform
)
FetchContent_MakeAvailable(trueform)
target_link_libraries(my_app PRIVATE tf::trueform)
Also available via conan, nuget, and pip. See the installation guide for all options.
Loading the meshes
#include <trueform/trueform.hpp>
auto sphere_buffer = tf::read_stl("sphere.stl");
auto box_buffer = tf::read_stl("box.stl");
auto sphere = sphere_buffer.polygons();
auto box = box_buffer.polygons();
tf::read_stl returns a tf::polygons_buffer that owns the data. .polygons() is a view on it. tf::read_obj works the same way. If your faces and points already live in your own arrays, wrap them with tf::make_polygons(tf::make_faces<3>(faces), tf::make_points<3>(points)). No copy is made.
To follow along without files, make the two shapes. The box is larger than the sphere's inner cube and smaller than its outer one, so each box face cuts the sphere:
auto sphere_buffer = tf::make_sphere_mesh(1.f, 40, 60);
auto box_buffer = tf::make_box_mesh(1.5f, 1.5f, 1.5f, 6, 6, 6);
Curves between two meshes
tf::make_intersection_curves takes two meshes and returns a tf::curves_buffer:
auto curves = tf::make_intersection_curves(sphere, box);
curves.paths().size(); // 6

Six closed curves, one per box face. If the meshes do not touch, paths() is empty.
The computation is exact. Coordinates go onto an integer lattice and every predicate runs in exact integer arithmetic. Shared edges, shared vertices and coplanar faces are classified, not guessed. Open meshes, non-manifold edges and self-intersecting inputs are accepted. For meshes over a very large coordinate range, use the wider lattice:
auto fine = tf::make_intersection_curves<tf::exact::int64>(sphere, box);
Curves between moved meshes
Most real cases have one part in a different pose. Tag a transformation onto the form. The points are not copied or rewritten.
auto T = tf::make_transformation_from_translation(
tf::make_vector(1.f, 0.f, 0.f));
auto moved = sphere | tf::tag(T);
auto circle = tf::make_intersection_curves(sphere, moved);
// one closed circle in the plane x = 0.5

moved keeps the full API of sphere through the policy system. The same mesh data serves both sides.
Reusing structures
The call needs a spatial tree, the face membership and the manifold edge link for each mesh. It builds them when they are missing. When one mesh moves and you recompute many times, build them once and tag them on:
tf::aabb_tree<int, float, 3> sphere_tree(sphere, tf::config_tree(4, 4));
tf::aabb_tree<int, float, 3> box_tree(box, tf::config_tree(4, 4));
auto sphere_fm = tf::make_face_membership(sphere);
auto box_fm = tf::make_face_membership(box);
auto sphere_mel = tf::make_manifold_edge_link(sphere | tf::tag(sphere_fm));
auto box_mel = tf::make_manifold_edge_link(box | tf::tag(box_fm));
auto sphere_form = sphere | tf::tag(sphere_tree)
| tf::tag(sphere_fm) | tf::tag(sphere_mel);
auto box_form = box | tf::tag(box_tree)
| tf::tag(box_fm) | tf::tag(box_mel);
for (float x = 0.f; x <= 1.f; x += 0.1f) {
auto Tx = tf::make_transformation_from_translation(
tf::make_vector(x, 0.f, 0.f));
auto c = tf::make_intersection_curves(sphere_form, box_form | tf::tag(Tx));
}
The trees stay valid under the transformation. Moving the box does not rebuild anything.
Curves between N meshes
Pass a range of forms to get all pairwise curves in one call. The forms must share one type, so give each one a transformation, even the one that stays put:
auto f0 = sphere | tf::tag(tf::make_transformation_from_translation(
tf::make_vector(0.f, 0.f, 0.f)));
auto f1 = sphere | tf::tag(tf::make_transformation_from_translation(
tf::make_vector(1.f, 0.f, 0.f)));
auto f2 = sphere | tf::tag(tf::make_transformation_from_translation(
tf::make_vector(0.5f, 0.8f, 0.f)));
decltype(f0) forms[] = {f0, f1, f2};
auto arcs = tf::make_intersection_curves(tf::make_range(forms, forms + 3));
arcs.paths().size(); // 6

Three spheres make three pairwise circles. The circles cross each other on the surfaces. trueform splits the curves at every crossing point, so here you get six open arcs instead of three circles.
Working with results
Each path is one curve, as indices into the points. A closed curve repeats its first index at the end.
for (auto path : curves.paths()) {
bool closed = path.front() == path.back();
float length = 0.f;
for (std::size_t i = 1; i < path.size(); ++i)
length += tf::distance(curves.points()[path[i - 1]],
curves.points()[path[i]]);
}
The curves lie on both surfaces. You can check against the tagged tree:
float worst = 0.f; // close to 0
for (auto pt : curves.points())
worst = std::max(worst, tf::distance(sphere_form, pt));
If you also want the boolean, ask for the curves on the same call:
auto [result, labels, face_labels, boolean_curves] = tf::make_boolean(
sphere, box, tf::boolean_op::merge, tf::return_curves);
To split faces along the curves instead, so the curves become edges, use tf::make_mesh_arrangements:
auto [split, tag_labels, source_faces] =
tf::make_mesh_arrangements(sphere, box);
// tag_labels: which mesh each face came from; source_faces: its source face
To cut a mesh by a plane or a scalar field instead of another mesh, see mesh slicing and cross-sections.
Related tutorials
- Fast Mesh Booleans in C++ — exact union, intersection and difference of two meshes.
- Fast Variadic Mesh Booleans in C++ — one arrangement, many boolean expressions over N meshes.
- Fast and Robust Mesh Collision Detection in C++ — do two meshes touch, how far apart, and where.
- Fast and Robust Mesh Slicing and Cross-Sections in C++ — plane sections, layers and slabs of a mesh.
- Fast and Robust Isocontours on a Triangle Mesh in C++ — curves and bands of equal value on a scalar field.
Performance
Apple M4 Max 16 threads
The curve and boolean timings are on two 500K-polygon meshes. The arrangement benchmark splits all faces along all intersection curves, against libigl and CGAL:
Building the spatial tree is the up-front cost of every query here. On 1,025,766 polygons it takes 24.9 ms, against 76.8 ms for MeshLib and 248.5 ms for CGAL. A tagged transformation does not rebuild it.

Full benchmarks · Documentation · How the algorithm works
@article{polydera:mesh-intersection-curves-in-cpp,
title={Fast and Exact Mesh Intersection Curves in C++},
author={Sajovic, {\v{Z}}iga, Polydera},
year={2026},
url={https://polydera.com/tutorials/mesh-intersection-curves-in-cpp},
organization={Polydera}
}