Tutorials
October 31, 2026
javascripttypescriptintersectionstutorialwebassembly

Fast and Exact Mesh Intersection Curves in JavaScript

Compute the exact curves where two or more triangle meshes intersect in JavaScript and TypeScript. Polylines out, with transformations and N-mesh input. WebAssembly, runs in Node.js and the browser.

Žiga Sajovic, Polydera

Also available in Python and C++.

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. One npm install. It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers two meshes, moved meshes, N meshes, and how to use the curves.

trueform GitHub Documentation

Install

npm install @polydera/trueform

The WebAssembly binary loads on first use. In the browser, multithreading needs SharedArrayBuffer headers. See the installation guide.

Loading the meshes

import * as tf from "@polydera/trueform";
import { readFileSync } from "node:fs";

const mesh0 = tf.readStl(readFileSync("part.stl"));
const mesh1 = tf.readObj(readFileSync("tool.obj"));

In the browser, pass the bytes from fetch instead. To follow along without files, build a sphere and a box:

const sphere = tf.sphereMesh(1.0, 40, 40);
const box = tf.boxMesh(1.5, 1.5, 1.5);

The box is smaller than the sphere, but its corners poke out. Each of its six faces cuts the sphere in a circle.

Curves between two meshes

tf.intersectionCurves takes two meshes and returns a Curves object:

const curves = tf.intersectionCurves(sphere, box);
curves.length;        // 6
curves.points.shape;  // [562, 3]

Six closed curves, one per box face. If the meshes do not touch, curves.length is 0 and curves.points has shape [0, 3].

The computation is exact. Shared edges, shared vertices and coplanar faces are classified, not guessed. Open meshes, non-manifold edges and self-intersecting inputs are accepted.

Curves between moved meshes

Most real cases have one part in a different pose. Set a 4×4 transformation on the mesh. The points are not copied or rewritten.

const moved = tf.sphereMesh(1.0, 40, 40);
moved.transformation = tf.makeTranslation(1.0, 0, 0);

const curves = tf.intersectionCurves(sphere, moved);
// one closed circle in the plane x = 0.5

Matrices are row-major. A Three.js Matrix4 is column-major, so transpose it: tf.ndarray(m.elements, [4, 4]).T.

To intersect a mesh with a moved copy of itself, use a shallow copy. It shares the points and faces. Only the transformation differs:

const view = sphere.shallowCopy();
view.transformation = tf.makeTranslation(1.0, 0, 0);

const curves = tf.intersectionCurves(sphere, view);

The spatial tree is built once per mesh. Changing a transformation does not rebuild it.

Curves between N meshes

Pass an array to get all pairwise curves in one call:

const m0 = tf.sphereMesh(1.0, 40, 40);
const m1 = tf.sphereMesh(1.0, 40, 40);
const m2 = tf.sphereMesh(1.0, 40, 40);
m1.transformation = tf.makeTranslation(1.0, 0, 0);
m2.transformation = tf.makeTranslation(0.5, 0.8, 0);

const curves = tf.intersectionCurves([m0, m1, m2]);
curves.length;  // 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

curves.paths is an OffsetBlockedBuffer. Each block is one curve, as indices into curves.points. A closed curve repeats its first index at the end.

const p = curves.points.data;

for (const pathIds of curves.paths) {
  const ids = pathIds.data;
  const closed = ids[0] === ids[ids.length - 1];
  let length = 0;
  for (let k = 1; k < ids.length; k++) {
    const a = 3 * ids[k - 1], b = 3 * ids[k];
    length += Math.hypot(p[b] - p[a], p[b + 1] - p[a + 1], p[b + 2] - p[a + 2]);
  }
}

The curves lie on both surfaces. Check it with a batch distance query:

tf.distance(sphere, tf.point(curves.points)).max();  // close to 0

Put the mesh first. In 0.10.8, tf.distance(points, mesh) with the batch first throws.

To draw the curves, build an index buffer of segments for THREE.LineSegments, or sweep them into a tube mesh:

const tubes = tf.tubeMesh(curves, 0.01, 12);  // radius, radial segments

If you also want the boolean, ask for the curves on the same call:

const { mesh, labels, faceLabels, curves: seam } =
  tf.booleanUnion(sphere, moved, { returnCurves: true });

To split faces along the curves instead, use tf.meshArrangements:

const { mesh, tagLabels, faceLabels } = tf.meshArrangements([sphere, moved]);

In the browser, tf.async.intersectionCurves runs the same call off the main thread.

Performance

15.8msSpatial tree build1,025,766 polygons, vs 259.9 ms three-mesh-bvh
16×Faster tree buildthan three-mesh-bvh
467.9msPolygon arrangements2,051,532 polygons

Apple M4 Max 16 threads Node.js WebAssembly

Every intersection query starts from a spatial tree. On a 1,025,766-polygon mesh, trueform builds it in 15.8 ms in Node.js, against 259.9 ms for three-mesh-bvh. Build it ahead of time with mesh.buildTree(). Polygon arrangements, which find every crossing among the faces of a mesh and split the faces along the curves, take 467.9 ms on 2,051,532 polygons. That benchmark has no JavaScript competitor.

Full benchmarks · Documentation · How the algorithm works

Cite as
@article{polydera:mesh-intersection-curves-in-javascript,
  title={Fast and Exact Mesh Intersection Curves in JavaScript},
  author={Sajovic, {\v{Z}}iga, Polydera},
  year={2026},
  url={https://polydera.com/tutorials/mesh-intersection-curves-in-javascript},
  organization={Polydera}
}