Intersect
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.
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.
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:
| Mode | Description |
|---|---|
"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
| Parameter | Description |
|---|---|
within | Also 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 does exactly two things. ALMOST-COPLANAR IS COPLANAR: every exact plane is quantized — its direction onto a fixed chart whose pitch keeps two planes of one cell within a quarter band over the whole lattice, its intercept onto the band — and faces whose planes quantize to one key, across all forms, pool onto one committed plane the lattice is dense on, and their vertices ride it, each standing on it exactly, moved by at most the tolerance. The rounding is the whole relation: pooling is transitive and never walks from a face to its neighbour, and a key held by one plane pools nothing. A vertex that cannot stand on every pool around it stays put, and every pool at it releases its faces there, to a fixed point. A vertex moves, never a face, so the mesh stays cohesive. A RIM CLOSES: a vertex on a boundary edge — an edge only one face holds — within the tolerance of another mesh's face lands exactly on the nearest feature of that face. Near a corner it takes the corner's point; near an edge it takes the lattice point nearest its projection onto the edge and is stated on that edge as a vertex-on-edge contact; inside the face it takes a lattice point on the face's committed plane. A meeting the input already states is kept as given: a rim vertex exactly on the face, or one of whose boundary edges already crosses the other mesh's edges, stays where it is. Interior vertices never move, because only a rim can leave a result open. A landing that cannot stand within the tolerance is refused, and the vertex keeps its own position. What the pipeline arranges is that moved mesh, EXACTLY: every predicate below the placement runs at zero.
(faces, points), tag_labels, face_labels = tf.mesh_arrangements(
[mesh0, mesh1], tolerance=1e-6)
A tolerance below one lattice unit acts as one unit — a lattice resolves nothing finer — and only 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.
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
