Modules | TS

Spatial

Distance, intersection, neighbor search, and ray casting.

The Spatial module provides distance, intersection, closest-point, neighbor search, and ray casting queries. All queries work uniformly across primitives and forms, and all support batch broadcasting. The spatial tree is built automatically on first query — no configuration needed.

Every query also has an async variant via tf.async for off-main-thread execution.

The spatial tree is built automatically on first query. To control when the cost is paid, call mesh.buildTree() explicitly, or await tf.async.buildTree(mesh) to build off the main thread. Subsequent calls are no-ops if the tree is already up to date.

Transformations on Forms

Mesh and PointCloud can carry a 4x4 transformation matrix. Queries use the transformation on the fly — the underlying data and spatial tree stay unchanged.

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

const mesh = tf.mesh(faces, points);

// Set from a primitive transformation
mesh.transformation = tf.makeTranslation(5, 0, 0);

// Set from raw data
mesh.transformation = new Float32Array([
  1, 0, 0, 1,
  0, 1, 0, 2,
  0, 0, 1, 3,
  0, 0, 0, 1,
]);

// Read back
const t = mesh.transformation;  // NDArrayFloat32 [4,4] or null

// Clear
mesh.transformation = null;
PropertyTypeDescription
transformation (get)NDArrayFloat32 | nullCurrent 4x4 transformation matrix
transformation (set)NDArrayFloat32 | Float32Array | nullSet, replace, or clear (null) the transformation
Matrices are row-major (translation in [0,3], [1,3], [2,3]). Three.js and WebGL use column-major — convert with .T:
// Three.js → trueform
mesh.transformation = tf.ndarray(threeMatrix4.elements, [4, 4]).T;

// trueform → Three.js
const m4 = new THREE.Matrix4().fromArray(mesh.transformation.T.data);

Shallow Copies

shallowCopy() creates a new handle that shares the underlying buffers (points, faces) but has its own cache slots and a cleared transformation. This lets you pose the same geometry independently without duplicating the data.

const mesh = tf.mesh(faces, points);

// Create copies with different poses
const viewA = mesh.shallowCopy();
viewA.transformation = tf.makeTranslation(5, 0, 0);

const viewB = mesh.shallowCopy();
viewB.transformation = tf.makeTranslation(-5, 0, 0);

const d = tf.distance(viewA, viewB);
Each shallow copy builds its own spatial tree on first query. If you want all copies to share a prebuilt tree, run a query (or mesh.buildTree()) on the original and then pass it directly — copies are only needed when you want independent transformations.

Queries

All query functions accept any combination of primitives and forms.

Queries broadcast. A single primitive paired with a batch of N primitives produces N results — the single operand is reused for each element in the batch. Two batches of the same size are paired element-wise. A form paired with a batch queries each element against the spatial tree independently.

Dtype dispatch

Operations follow a fixed dtype policy:

PairBehaviorResult dtype
Form + FormMust match. Mixing float32 and float64 throws dtype mismatch.Form's dtype
Form + PrimitiveForm's dtype wins. Primitive is automatically upcast if needed.Form's dtype
Primitive + PrimitiveUpcast to the wider of the two (float64 wins over float32).Wider dtype
const m32 = tf.sphereMesh(1, 8, 8);
const m64 = tf.sphereMesh(1, 8, 8, { dtype: "float64" });

tf.distance2(m32, m32);                 // OK → float32
tf.distance2(m64, m64);                 // OK → float64
tf.distance2(m32, m64);                 // throws: dtype mismatch
tf.distance2(m64, tf.point(0, 0, 0));   // OK — point upcast to float64

Distance

tf.distance(a, b) and tf.distance2(a, b) compute the distance (or squared distance) between any two geometric objects. distance is signed for point-plane queries (positive on the normal side).

// Primitive x Primitive
const d = tf.distance(tf.point(0, 0, 0), tf.segment(tf.point(1, 0, 0), tf.point(1, 1, 0)));

// Form x Primitive
const d2 = tf.distance2(mesh, tf.point(0, 0, 0));

// Form x Form
const d3 = tf.distance(meshA, meshB);

// Batch — returns NDArrayFloat32
const pts = tf.point(new Float32Array([0,0,0, 1,1,1, 2,2,2]), 3);
const distances = tf.distance(pts, mesh);  // shape [3]
ABReturns
primitiveprimitivenumber
batch(N)primitive or formNDArrayFloat32 [N]
batch(N)batch(N)NDArrayFloat32 [N]
formprimitivenumber
formformnumber

Closest Point

tf.closestPoint(a, b) returns the closest point on b to a. tf.closestPointPair(a, b) returns the closest points on both sides. These work on primitives only — for forms, use neighborSearch which returns element IDs alongside closest points.

// Single
const r = tf.closestPoint(tf.point(0, 2, 0), segment);
console.log(r.point);      // NDArrayFloat32 [3] — closest point on segment
console.log(r.distance2);  // number — squared distance

const rp = tf.closestPointPair(segmentA, segmentB);
console.log(rp.point0);     // closest point on A
console.log(rp.point1);     // closest point on B
console.log(rp.distance2);  // squared distance

// Batch
const pts = tf.point(new Float32Array([0,1,0, 0,2,0, 0,3,0]), 3);
const rb = tf.closestPointPair(pts, segment);
console.log(rb.points0);    // NDArrayFloat32 [3, 3]
console.log(rb.points1);    // NDArrayFloat32 [3, 3]
console.log(rb.distances);  // NDArrayFloat32 [3]
ABclosestPointclosestPointPair
primitiveprimitive{ point, distance2 }{ point0, point1, distance2 }
batch(N)primitive{ points[N,3], distances[N] }{ points0[N,3], points1[N,3], distances[N] }

Intersection

tf.intersects(a, b) tests whether two geometric objects intersect.

// Primitive x Primitive
const hit = tf.intersects(aabb, segment);  // boolean

// Form x Primitive
const hit2 = tf.intersects(mesh, polygon);  // boolean

// Form x Form
const collide = tf.intersects(meshA, meshB);  // boolean

// Batch — returns NDArrayBool
const segs = tf.segment(batchData, 3);  // 3 segments
const hits = tf.intersects(mesh, segs);  // NDArrayBool [3]
ABReturns
primitiveprimitiveboolean
batch(N)primitive or formNDArrayBool [N]
batch(N)batch(N)NDArrayBool [N]
formprimitiveboolean
formformboolean

tf.neighborSearch(form, query) finds the nearest element in a form to a query primitive. This is the form equivalent of closestPoint — it returns an element ID alongside the closest point.

// Single query
const r = tf.neighborSearch(mesh, tf.point(0.5, 0.5, 1));
console.log(r.elementId);  // face index
console.log(r.point);      // closest point on that face
console.log(r.distance2);  // squared distance

// With search radius — returns null fields if nothing within radius
const r2 = tf.neighborSearch(mesh, point, { radius: 1.0 });

// Batch query
const pts = tf.point(new Float32Array([...]), 1000);
const rb = tf.neighborSearch(mesh, pts);
console.log(rb.elementIds);  // NDArrayInt32 [1000]
console.log(rb.points);      // NDArrayFloat32 [1000, 3]
console.log(rb.distances);   // NDArrayFloat32 [1000]

// Form x Form — closest pair between two forms
const rp = tf.neighborSearch(meshA, meshB);
console.log(rp.elementId0, rp.elementId1);  // element IDs on each form
console.log(rp.point0, rp.point1);          // closest points
console.log(rp.distance2);                  // squared distance
QueryReturns
single{ elementId, point, distance2 }
batch(N){ elementIds[N], points[N,3], distances[N] }
form x form{ elementId0, elementId1, point0, point1, distance2 }

kNN

Pass { k } for k-nearest neighbor queries:

// Single query — k nearest faces
const r = tf.neighborSearch(mesh, point, { k: 5 });
console.log(r.elementIds);  // NDArrayInt32 [5] (or fewer)
console.log(r.points);      // NDArrayFloat32 [5, 3]
console.log(r.distances);   // NDArrayFloat32 [5]

// With radius limit — may find fewer than k
const r2 = tf.neighborSearch(mesh, point, { k: 10, radius: 2.0 });

// Batch kNN
const pts = tf.point(new Float32Array([0,0,0, 0,0,5, 0,0,10]), 3);
const rb = tf.neighborSearch(mesh, pts, { k: 2 });
console.log(rb.elementIds.shape);  // [3, 2]
console.log(rb.distances.shape);   // [3, 2]
console.log(rb.counts);            // NDArrayInt32 [3] — actual count per query
// Unused slots in elementIds are filled with -1
kNN is not supported for form vs form queries.

Ray Casting

tf.rayCast(ray, target, opts?) casts a ray against a primitive or form. The result includes t — the ray parameter where hitPoint = origin + t * direction. Use { minT, maxT } to constrain the valid range — pass a scalar for uniform bounds or an NDArrayFloat32 [N] for per-ray bounds.

// Ray x Primitive
const ray = tf.ray(tf.point(0.5, 0.5, -1), tf.vector(0, 0, 1));
const r = tf.rayCast(ray, triangle);
if (r.hit) console.log(`Hit at t=${r.t}`);

// Ray x Form — also returns the element ID
const r2 = tf.rayCast(ray, mesh);
if (r2.hit) console.log(`Face ${r2.elementId} at t=${r2.t}`);

// With bounds
const r3 = tf.rayCast(ray, mesh, { minT: 0, maxT: 100 });

// Batch rays
const rays = tf.ray(batchData, 5);  // 5 rays
const rb = tf.rayCast(rays, mesh);
console.log(rb.hits);       // NDArrayBool [5]
console.log(rb.ts);         // NDArrayFloat32 [5] — NaN for misses
console.log(rb.elementIds); // NDArrayInt32 [5] — -1 for misses

// Per-ray bounds — each ray gets its own min/max
const minTs = tf.ndarray(new Float32Array([0, 0, 0.5, 1, 0]));
const maxTs = tf.ndarray(new Float32Array([10, 5, 10, 50, 100]));
const rb2 = tf.rayCast(rays, mesh, { minT: minTs, maxT: maxTs });
RayTargetReturns
singleprimitive{ hit, t, elementId }
singleform{ hit, t, elementId }
batch(N)primitive{ hits[N], ts[N] }
batch(N)form{ hits[N], ts[N], elementIds[N] }

Async Queries

All functions are available as tf.async.distance2(), tf.async.neighborSearch(), tf.async.rayCast(), etc. Signatures are identical — they return Promise<T> instead of T.

const d = await tf.async.distance(mesh, point);
const r = await tf.async.neighborSearch(mesh, points, { k: 5 });
const cast = await tf.async.rayCast(ray, mesh);
Use async queries for large batches or heavy computations to keep the UI thread responsive.