Modules | TS

Intersect

Exact mesh intersections and self-intersections.

The Intersect module computes geometric intersections between meshes. All intersection computations are geometrically and topologically exact, using exact arithmetic.

import * as tf from "@polydera/trueform";

Overview

The Intersect module computes intersection geometry without modifying the input meshes:

  • Mesh-mesh intersections: Curves where two or more meshes intersect
  • Self-intersections: Curves where a mesh intersects itself

Curves are returned as Curves objects with paths (an OffsetBlockedBuffer of index sequences) and points (an NDArrayFloat32 of coordinates).

To embed intersection curves into mesh topology (splitting faces along curves), use the Arrangement module. For scalar field crossings and their curves, see the Iso module.

Supported Input

Intersection computation supports a wide range of input geometry:

  • Open and closed meshes — boundaries are handled correctly
  • Non-manifold edges — edges shared by 3 or more faces
  • Coplanar faces — overlapping faces from the same or different meshes
  • Self-intersecting geometry — meshes that intersect themselves are detected and curves are extracted
  • Crossing intersection curves — where curves from different mesh pairs meet on a face, crossings can be resolved. Configured via the IntersectOpts interface — see Intersection Configuration.
For detecting where a single mesh intersects itself, use tf.selfIntersectionCurves; for whether it does at all, tf.hasSelfIntersections.

Intersection Configuration

The IntersectOpts interface controls intersection computation. It maps to the C++ tf::intersect_config {mode, tolerance}; the within option is composed into the mode bitmask as primitives | within.

interface IntersectOpts {
  mode?: "sos" | "primitives";
  tolerance?: number;
  within?: boolean;
}
FieldDescription
modeThe classifier ("primitives" or "sos"); within composes with whichever is selected into the one mode that crosses. "primitives" (default) handles shared edges/vertices/coplanar faces; "sos" (Simulation of Simplicity) is fast with no degenerate cases, but no contact is ever coplanar — coplanar walls then do not pool and the regions they would have separated stay joined.
toleranceWorld-coordinate distance an input vertex may move to reach the lattice (default 0, exact).
withinAlso intersect each mesh with itself — its self records and the resolution of crossings within one contour class are one request, never separable. Required when a mesh can self-overlap, e.g. meshes concatenated into one input. Honored by every entry taking these options, intersectionCurves included; a one-mesh entry implies it. Default false.

Contour crossings are resolved unconditionally, not on request: between contour classes whenever a third mesh exists to make such a pair, and within one class whenever the run is within.

Tolerance

A non-zero tolerance is a statement about the INPUT, not about a predicate, and it promises exactly two things. ALMOST-COPLANAR IS COPLANAR: faces whose directions agree pool onto one committed plane, and a face that another form's vertex stands within the tolerance of commits a plane together with that vertex's own faces lying within the tolerance of it. ALMOST-ON-PLANE IS ON-PLANE: a vertex within the tolerance of a committed plane is dropped exactly onto it — where the plane settled, not where its face began. First the planes pull: a face commits to a plane only if its own corners move within the tolerance onto it. Then the vertices drop, bounded by the same tolerance. Everywhere the tolerance does not act it is zero: a vertex nothing pulled is not moved, and the output states it exactly as you gave it. What the pipeline arranges is that moved mesh, EXACTLY: every predicate below the placement runs at zero, so the tolerance only ever adds contacts the input states within it, and never removes one it states exactly. A plane its corners cannot follow, and a vertex no committed plane can hold within the tolerance, keep their own positions — the tolerance never invents one.

const { mesh, tagLabels, faceLabels } = tf.meshArrangements(
  [mesh0, mesh1], { tolerance: 1e-6 });

A tolerance of 0 is the identity: nothing is pooled, nothing moves, no placement table is built, and the result is the exact arrangement of the input as given.

Each function sets appropriate defaults — see individual function documentation below and in Arrangement.

Self-Intersection Test

Whether a mesh meets itself — true exactly when the self-intersection build would state a record.

if (tf.hasSelfIntersections(mesh)) {
  const curves = tf.selfIntersectionCurves(mesh);
}

The discovery runs in exact arithmetic and stops at the first contact, so a mesh that meets itself is answered without the rest of it being seen. A shared vertex or edge is not itself a contact; any other touching — between neighbouring faces too — is one.

The tree, faceMembership and manifoldEdgeLink the call needs are completed into the mesh's cache, so a later query — a second test, a curve extraction, a boolean — pays for none of them. 3D meshes only.

Intersection Curves

Between Two Meshes

const curves = tf.intersectionCurves(mesh0, mesh1);
const curves = tf.intersectionCurves(mesh0, mesh1, { mode: "primitives" });

Default: mode: "primitives", tolerance: 0 (exact). 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 when the run is within, which the arrangement entries take.

N-Mesh Intersection Curves

Compute all pairwise intersection curves from an array of meshes:

const curves = tf.intersectionCurves([mesh0, mesh1, mesh2]);
const curves = tf.intersectionCurves([mesh0, mesh1, mesh2], { mode: "sos" });

Default: mode: "primitives", tolerance: 0 (exact). With 3+ meshes the arity makes a cross-class contour pair possible, so those crossings are resolved.

Self-Intersection Curves

Find where a mesh intersects itself:

const curves = tf.selfIntersectionCurves(mesh);
const curves = tf.selfIntersectionCurves(mesh, { mode: "primitives" });

Default: mode: "primitives", tolerance: 0 (exact). A one-mesh entry implies within, so the mesh's own self records and its self-crossing contours are both in.

To embed self-intersection curves into mesh topology, use tf.polygonArrangements from the Arrangement module.

With Transformations

mesh1.transformation = tf.makeTranslation([5, 0, 0]);
const curves = tf.intersectionCurves(mesh0, mesh1);

Using Curves

for (let i = 0; i < curves.length; i++) {
  const pathIds = curves.paths.at(i);
  // Access points: curves.points
}

Async

All intersect functions are available as async variants via tf.async for off-main-thread execution:

const curves = await tf.async.intersectionCurves(mesh0, mesh1);
const self   = await tf.async.selfIntersectionCurves(mesh);
const meets  = await tf.async.hasSelfIntersections(mesh);
For implementation details, exact arithmetic, and low-level intersection access, see the C++ Intersect documentation.