Spatial
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.
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;
| Property | Type | Description |
|---|---|---|
transformation (get) | NDArrayFloat32 | null | Current 4x4 transformation matrix |
transformation (set) | NDArrayFloat32 | Float32Array | null | Set, replace, or clear (null) the transformation |
[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);
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]
| A | B | Returns |
|---|---|---|
| primitive | primitive | number |
| batch(N) | primitive or form | NDArrayFloat32 [N] |
| batch(N) | batch(N) | NDArrayFloat32 [N] |
| form | primitive | number |
| form | form | number |
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]
| A | B | closestPoint | closestPointPair |
|---|---|---|---|
| primitive | primitive | { 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]
| A | B | Returns |
|---|---|---|
| primitive | primitive | boolean |
| batch(N) | primitive or form | NDArrayBool [N] |
| batch(N) | batch(N) | NDArrayBool [N] |
| form | primitive | boolean |
| form | form | boolean |
Neighbor Search
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
| Query | Returns |
|---|---|
| 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
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 });
| Ray | Target | Returns |
|---|---|---|
| single | primitive | { hit, t, elementId } |
| single | form | { 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);
