Modules | PY

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 trueform as tf

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 (paths, points) tuples where paths is an OffsetBlockedArray of index sequences and points is a numpy array 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 are resolved by splitting the curves at the crossing point. This is not a request: it happens whenever the operands can make such a pair — see Intersection Configuration.
For detecting where a single mesh intersects itself, use tf.self_intersection_curves; for whether it does at all, tf.has_self_intersections.

Intersection Configuration

All intersection and arrangement functions accept the same configuration as keyword-only arguments. They map to the C++ tf::intersect_config {mode, tolerance}; within is composed into the mode bitmask as primitives | within (the language surface keeps it as a natural boolean option).

Mode

The mode parameter names the classifier; the optional within request composes with it as the primitives | within mode bit:

ModeDescription
"primitives"Full 5-type classification. Handles shared edges, shared vertices, and coplanar faces. The default everywhere.
"sos"SoS (Simulation of Simplicity) perturbation. Fast, no degenerate cases — but no contact is ever coplanar, so coplanar walls do not pool and the regions they would have separated stay joined.

Within

ParameterDescription
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. Taken by the arrangement and boolean builds; intersection_curves does not take it, and 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.

(faces, points), tag_labels, face_labels = tf.mesh_arrangements(
    [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 the 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:

mesh = tf.Mesh(*tf.read_stl("mesh.stl"))

if tf.has_self_intersections(mesh):
    paths, points = tf.self_intersection_curves(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, and a mesh that does not is the whole discovery and nothing above it. A shared vertex or edge is not itself a contact — a fan or a bowtie does not make the verdict true on its own — but any other touching, between neighbouring faces too, is one.

Takes a tf.Mesh (3D, triangle or dynamic). The tree, face_membership and manifold_edge_link it needs are built on the mesh and kept, so a later query — a second test, a curve extraction, a boolean — pays for none of them. The verdict is computed in the mesh's own coordinates: this entry does not apply the Mesh transformation.

Intersection Curves

Between Two Meshes

mesh0 = tf.Mesh(*tf.read_stl("mesh0.stl"))
mesh1 = tf.Mesh(*tf.read_stl("mesh1.stl"))

paths, points = tf.intersection_curves(mesh0, mesh1)
paths, points = tf.intersection_curves(mesh0, mesh1, mode="primitives")

Default: mode="primitives", tolerance=0.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 a list of meshes:

paths, points = tf.intersection_curves([mesh0, mesh1, mesh2])
paths, points = tf.intersection_curves([mesh0, mesh1, mesh2], mode="sos")

Default: mode="primitives", tolerance=0.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:

paths, points = tf.self_intersection_curves(mesh)
paths, points = tf.self_intersection_curves(mesh, mode="primitives")

Default: mode="primitives", tolerance=0.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.polygon_arrangements from the Arrangement module.

With Transformations

import numpy as np

mesh1.transformation = np.eye(4, dtype=np.float32)
mesh1.transformation[:3, 3] = [5, 0, 0]

paths, points = tf.intersection_curves(mesh0, mesh1)

Using Curves

for path_ids in paths:
    curve_points = points[path_ids]
    # Process curve
For implementation details, exact arithmetic, and low-level intersection access, see the C++ Intersect documentation.