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() or pc.buildTree() explicitly. 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);

Shared Views

sharedView() creates a new instance that shares the same underlying data — points, faces, and spatial tree — but has its own transformation (initially null). This lets you query the same geometry at multiple poses without duplicating anything.

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

// Trigger tree build on the original
tf.distance(mesh, tf.point(0, 0, 0));

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

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

// Both share the same tree — only transformation differs
const d = tf.distance(viewA, viewB);
Build the spatial tree on the original form (by running any query), then create shared views. All views benefit from the cached tree.

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.

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.