Modules | TS

Core

NDArray, Primitives, Mesh, PointCloud, Curves — the foundational types.

The Core module has two layers. ArraysNDArray and OffsetBlockedBuffer — are the numerical foundation: shaped, typed, broadcast-aware arrays that live on the WASM heap and run at native speed. Geometric typesPrimitive, Mesh, PointCloud, Curves — are built on top, combining arrays with spatial structure, lazy topology, and batch-transparent operations.

import * as tf from "@polydera/trueform";
Memory. All objects are WASM-resident and reference-counted. FinalizationRegistry frees memory automatically when the JavaScript object is garbage collected — calling delete() is never required. It exists for immediate release when you want deterministic cleanup, and is idempotent (safe to call multiple times). You can also use the using declaration for scope-based cleanup.

NDArray

An NDArray wraps a C++ array on the WASM heap. JavaScript accesses its data through typed array views (Float32Array, Int32Array, Int8Array). Arrays carry shape metadata and support n-dimensional operations.

Key Concepts

Broadcasting. Binary operations (add, sub, mul, div, eq, ...) follow NumPy broadcasting rules. A [100, 3] array can be added to a [3] array or a scalar — the smaller operand is broadcast across the larger:

const pts = tf.random("float32", [100, 3]);
const offset = tf.ndarray([1, 0, -1]);   // shape [3]

const shifted = pts.add(offset);          // [100, 3] + [3] → [100, 3]
const scaled = pts.mul(2.0);              // [100, 3] * scalar → [100, 3]

Methods and free functions. Most operations exist in both forms. Methods operate on this, free functions take the array as the first argument. Both return the same result:

pts.sum(0);          // method
tf.sum(pts, 0);      // free function — equivalent

Async. Free functions that perform heavy computation have async variants under tf.async. These yield to the event loop and return Promises:

await tf.async.sum(pts, 0);
await tf.async.sort(pts);

Types

Four dtypes are supported:

TypeStorageUse
NDArrayFloat32Float32ArrayCoordinates, scalars, distances
NDArrayInt32Int32ArrayIndices, face connectivity
NDArrayInt8Int8ArrayLabels, small integers
NDArrayBoolInt8Array (0/1)Masks, predicates

Creating Arrays

// From JavaScript data (number[] defaults to float32)
const a = tf.ndarray([1, 2, 3, 4, 5, 6], [2, 3]);  // shape [2, 3]
const b = tf.ndarray(new Int32Array([0, 1, 2]));     // int32, shape [3]
const c = tf.ndarray(new Float32Array([1, 2, 3]), [1, 3]);

// Filled arrays
const z = tf.zeros("float32", [100, 3]);
const o = tf.ones("int32", [10]);
const f = tf.full("float32", [4, 4], 0.5);

// Identity matrix
const I = tf.eye("float32", 4);  // [4, 4]

// Sequences
const r = tf.arange("int32", 10);            // [0, 1, ..., 9]
const s = tf.arange("float32", 0, 1, 0.1);  // [0.0, 0.1, ..., 0.9]
const l = tf.linspace(0, 1, 11);             // [0.0, 0.1, ..., 1.0]

// Random
const rnd = tf.random("float32", [100, 3], -1, 1);

Properties

PropertyTypeDescription
dataFloat32Array | Int32Array | Int8ArrayTyped array view into WASM heap
lengthnumberTotal element count
shapenumber[]Shape tuple, settable for reshape-in-place
ndimnumberNumber of dimensions
dtypestring"float32", "int32", "int8", or "bool"
const pts = tf.ndarray([1,2,3, 4,5,6], [2, 3]);

pts.length;  // 6
pts.shape;   // [2, 3]
pts.ndim;    // 2
pts.dtype;   // "float32"
pts.data;    // Float32Array([1, 2, 3, 4, 5, 6])

// Reshape in-place (must preserve total count)
pts.shape = [3, 2];

Iteration

Iteration depends on dimensionality. 1D arrays yield numbers, nD arrays yield row views:

const vec = tf.ndarray([10, 20, 30]);
for (const v of vec) console.log(v);  // 10, 20, 30

const mat = tf.ndarray([1,2,3, 4,5,6], [2, 3]);
for (const row of mat) console.log(row.data);  // Float32Array([1,2,3]), ...

Indexing and Slicing

View operations are zero-copy — they share the underlying WASM buffer.

const pts = tf.ndarray([1,2,3, 4,5,6, 7,8,9], [3, 3]);

// Row view (zero-copy)
const row = pts.row(0);              // [1, 2, 3]

// Get: returns number for 1D, row view for nD
const val = pts.get(0);              // NDArray [1, 2, 3]

// Slice along axis 0 (zero-copy)
const first_two = pts.slice(0, 2);   // shape [2, 3]

take

take supports single-axis and multi-axis Cartesian indexing. Also available as tf.take(arr, indices, axis).

const pts = tf.random("float32", [100, 3]);

// Single-axis: gather rows by index array
const ids = tf.ndarray(new Int32Array([0, 5, 10]));
const selected = pts.take(ids);          // shape [3, 3]
const same = tf.take(pts, ids);          // free function — equivalent

// Multi-axis: each argument selects along one axis
pts.take(null, 0);       // column 0 → shape [100]
pts.take(null, [0, 2]);  // columns 0 and 2 → shape [100, 2]
pts.take(5);             // row 5 squeezed → shape [3]
pts.take([0, 2]);        // rows 0 and 2 → shape [2, 3]

takeAlongAxis

Per-element gather along an axis. Also available as tf.takeAlongAxis(arr, indices, axis).

const indices = tf.ndarray(new Int32Array([2, 0, 1]), [3, 1]);
const gathered = pts.takeAlongAxis(indices, 1);

Boolean Indexing

const mask = pts.take(null, 2).gt(0);    // z > 0
const above = pts.booleanIndex(mask);     // only rows where z > 0

Element-wise Operations

All binary operations broadcast and accept NDArray or scalar arguments.

const a = tf.random("float32", [100, 3]);
const b = tf.random("float32", [100, 3]);

const sum  = a.add(b);
const diff = a.sub(1.0);
const prod = a.mul(b);
const quot = a.div(2.0);
const rem  = a.mod(b);
const clmp = a.clip(0, 1);

// Matrix multiply (batch broadcast on leading dims)
const A = tf.random("float32", [4, 4]);
const B = tf.random("float32", [4, 4]);
const C = A.matMul(B);  // [4, 4]

In-place variants use _ suffix and return this for chaining:

a.add_(1.0).mul_(2.0).clip_(0, 10);
CopyIn-placeDescription
addadd_Addition
subsub_Subtraction
mulmul_Multiplication
divdiv_Division
modmod_Remainder (truncated)
clipclip_Clamp to lo, hi

Free-function equivalents: tf.mod(a, b), tf.mod_(a, b), tf.clip(a, lo, hi).

Relational and Logical

Relational operations return NDArrayBool. They broadcast.

const mask = a.gt(0);       // element-wise a > 0
const eq   = a.eq(b);       // element-wise a == b
MethodDescription
eq, neqEqual, not-equal
lt, gtLess-than, greater-than
lte, gteLess/greater-or-equal

Logical operations (bool arrays only):

const combined = mask_a.and(mask_b);
const negated  = mask_a.not();
const either   = mask_a.or(mask_b);

Conditional selection via free function:

const result = tf.where(mask, a, b);  // mask ? a : b, element-wise

Reductions

Available as both methods and free functions. Without axis: returns scalar. With axis: returns reduced array. Async: tf.async.sum, tf.async.min, etc.

const pts = tf.random("float32", [100, 3]);

// Scalar reductions (no axis → full reduction)
pts.sum();              // number
pts.min();              // number
pts.mean();             // number

// Per-axis reductions
pts.sum(0);             // shape [3] — sum each column
pts.norm(1);            // shape [100] — L2 norm of each row

// Free function form
tf.sum(pts, 0);         // equivalent to pts.sum(0)
tf.norm(pts, 1);        // equivalent to pts.norm(1)

// Async
await tf.async.sum(pts, 0);
await tf.async.norm(pts, 1);

Index-finding reductions:

pts.argmin();           // flat index of global minimum
pts.argmax(0);          // shape [3] — per-column argmax index

Boolean reductions (on NDArrayBool):

const mask = pts.gt(0);
mask.any();             // 1 if any true
mask.all();             // 1 if all true
mask.any(1);            // shape [100] — per-row any
ReductionOutput dtypeAsync
sumint32 (from int), float32 (from float)tf.async.sum
min, maxSame as inputtf.async.min, tf.async.max
meanfloat32tf.async.mean
normfloat32tf.async.norm
argmin, argmaxint32tf.async.argmin, tf.async.argmax
any, allbooltf.async.any, tf.async.all

Math Functions

Math functions are free functions only (tf.sin, not arr.sin). In-place variants use _ suffix.

Trigonometric (float32 only)

const angles = tf.linspace(0, Math.PI, 100);

tf.sin(angles);    tf.sin_(angles);    // in-place
tf.cos(angles);    tf.cos_(angles);
tf.tan(angles);    tf.tan_(angles);

// Inverse
tf.asin(arr);      tf.asin_(arr);
tf.acos(arr);      tf.acos_(arr);
tf.atan(arr);      tf.atan_(arr);

// Two-argument (broadcasts). Async: tf.async.atan2
tf.atan2(y, x);

Exponential, Logarithmic, Power (float32 only)

FunctionIn-placeDescription
tf.exptf.exp_e^x
tf.logtf.log_Natural log
tf.log2tf.log2_Base-2 log
tf.log10tf.log10_Base-10 log
tf.powtf.pow_x^n (scalar exponent)
tf.sqrttf.sqrt_Square root

Rounding (float32 only)

FunctionIn-place
tf.floortf.floor_
tf.ceiltf.ceil_
tf.roundtf.round_

Multi-dtype (all dtypes)

FunctionDescription
tf.absAbsolute value
tf.negNegation
tf.clipClamp to lo, hi

Vector Operations

const a = tf.random("float32", [100, 3]);
const b = tf.random("float32", [100, 3]);

// Dot product along last axis. [N,3]·[N,3] → [N]. Scalar for 1D inputs.
tf.dot(a, b);

// Cross product (last axis must be 3). [N,3]×[N,3] → [N,3].
tf.cross(a, b);

// L2-normalize (copy and in-place)
tf.normalize(a, 1);      // per-row normalize → new array
tf.normalize_(a, 1);     // in-place

Sorting and Set Operations

Sort and argsort are available as both methods and free functions. Async: tf.async.sort, tf.async.argsort, tf.async.unique, tf.async.setUnion, tf.async.setIntersection, tf.async.setDifference.

const arr = tf.ndarray(new Int32Array([3, 1, 4, 1, 5]));

// Method form
arr.sort();             // sorted copy: [1, 1, 3, 4, 5]
arr.sort_();            // sort in-place
arr.argsort();          // permutation indices (int32)

// Free function form
tf.sort(arr);           // equivalent to arr.sort()
tf.sort_(arr);          // equivalent to arr.sort_()
tf.argsort(arr);        // equivalent to arr.argsort()

// Async
await tf.async.sort(arr);
await tf.async.argsort(arr);

Set operations (inputs must be sorted, free functions only):

const a = tf.ndarray(new Int32Array([1, 2, 3, 4]));
const b = tf.ndarray(new Int32Array([2, 4, 5]));

tf.unique(a);              // [1, 2, 3, 4]
tf.setUnion(a, b);         // [1, 2, 3, 4, 5]
tf.setIntersection(a, b);  // [2, 4]
tf.setDifference(a, b);    // [1, 3] — elements in a not in b

Assignment

In-place assignment with broadcasting:

const pts = tf.zeros("float32", [100, 3]);

// Fill with scalar
pts.assign(1.0);

// Broadcast array
const row = tf.ndarray([1, 2, 3]);
pts.assign(row);                          // broadcast [3] → all rows

// Masked assignment (set where mask is true)
const mask = pts.take(null, 2).gt(0);
pts.assign(mask, 0.0);                    // zero out where z > 0

// Indexed assignment (set at given row indices)
const ids = tf.ndarray(new Int32Array([0, 5, 10]));
pts.assign(ids, row);                     // set rows 0, 5, 10

Combining Arrays

Free functions for combining arrays:

const a = tf.random("float32", [10, 3]);
const b = tf.random("float32", [10, 3]);

// Stack along new axis (all inputs must have identical shape)
tf.stack([a, b], 0);         // shape [2, 10, 3]

// Concatenate along existing axis
tf.concatenate([a, b], 0);   // shape [20, 3]

// Tile (repeat)
tf.tile(a, [2, 1]);          // shape [20, 3]

Shape Operations

All shape operations return zero-copy shared views:

const pts = tf.random("float32", [100, 3]);

pts.flatten();              // shape [300]
pts.reshape([50, 6]);       // shape [50, 6]
pts.T;                      // shape [3, 100] — transpose shorthand
pts.transpose([1, 0]);      // explicit axis permutation

const row = pts.row(0);     // shape [3]
row.unsqueeze(0);           // shape [1, 3]
row.unsqueeze(0).squeeze(); // shape [3]

Type Casting

const f = tf.random("float32", [10, 3]);

f.as("int32");              // new array, truncated values
f.gt(0).as("int8");         // same-storage cast (bool → int8): zero-copy
Same-storage casts (int8bool) are zero-copy. Cross-storage casts (float32int32) allocate a new array.

Cloning

const copy = arr.clone();  // deep copy — independent WASM buffer

OffsetBlockedBuffer

OffsetBlockedBuffer stores variable-length blocks of int32 data using two flat arrays. Block i spans data[offsets[i] .. offsets[i+1]]. Used for topology structures like faceMembership, faceLink, and vertexLink.

// Returned by topology operations
const fm = mesh.faceMembership;

// Number of blocks
fm.length;

// Access individual blocks
const block = fm.get(0);    // NDArrayInt32 — faces adjacent to vertex 0

// Iterate over all blocks
for (const block of fm) {
  console.log(block.data);  // Int32Array view of this block
}

// Access underlying arrays
fm.offsets;  // NDArrayInt32 [N+1]
fm.data;     // NDArrayInt32 (flat packed data)
PropertyTypeDescription
lengthnumberNumber of blocks
offsetsNDArrayInt32Block boundary indices
dataNDArrayInt32Flat packed data

Primitive

A Primitive extends NDArray<Float32Array> with a type discriminator. All geometric objects — points, vectors, segments, planes, etc. — are primitives. Because they extend NDArray, all array operations work on them (arithmetic, slicing, broadcasting).

const p = tf.point(1, 2, 3);
p.type;    // "point"
p.shape;   // [3]
p.data;    // Float32Array([1, 2, 3])
p.sum();   // 6 — all NDArray operations work
NDArray operations on primitives return plain NDArray, not Primitive. To recover the primitive type, wrap the result: tf.point(p.add(1)).

Batching

Every primitive supports batching. A single Point has shape [3]; a batch of N points has shape [N, 3]. The same factory function, the same operations, and the same spatial queries work on both. Pass one point or 100k points — the API is identical, and the WASM backend processes the batch in a single call.

// Single point
const p = tf.point(1, 2, 3);                      // shape [3]
p.isBatch;  // false
p.count;    // 1

// Batch of 100 points from flat data
const pts = tf.point(new Float32Array(300), 3);    // shape [100, 3]
pts.isBatch;  // true
pts.count;    // 100

// Batch from an NDArray (zero-copy)
const pts2 = tf.point(tf.random("float32", [50, 3]));

// Index into a batch
const first = pts.at(0);   // single Point, shape [3]

Primitive Types

TypeShape (single)Shape (batch)Description
Point[D][N, D]Position in space
Vector[D][N, D]Direction vector
Segment[2, D][N, 2, D]Two endpoints
Triangle[3, D][N, 3, D]Three vertices
Ray[2, D][N, 2, D]Origin + direction
Line[2, D][N, 2, D]Origin + direction (infinite)
Plane[4][N, 4]Normal + offset (ax+by+cz+d=0)
AABB[2, D][N, 2, D]Min/max bounding box
Polygon[V, D]Ordered vertices

Creating Primitives

Every primitive type has a factory function. The same function creates singles or batches — pass NDArrays with a leading batch dimension to get a batch:

// ── Points and Vectors ──────────────────────────────────────────────
const p = tf.point(1, 2, 3);                           // single [3]
const v = tf.vector(0, 0, 1);                          // single [3]

const pts = tf.point(new Float32Array(300), 3);         // batch [100, 3]
const pts2 = tf.point(tf.random("float32", [50, 3]));   // batch from NDArray (zero-copy)

// ── Segments, Rays, Lines ───────────────────────────────────────────
// Single — from two primitives
const seg = tf.segment(tf.point(0, 0, 0), tf.point(1, 1, 1));   // [2, 3]
const r   = tf.ray(tf.point(0, 0, 0), tf.vector(0, 0, 1));     // [2, 3]
const ln  = tf.line(tf.point(0, 0, 0), tf.vector(1, 0, 0));    // [2, 3]

// Batch — from two NDArrays with shape [N, 3]
const origins = tf.random("float32", [100, 3]);
const dirs    = tf.random("float32", [100, 3]);
const rays = tf.ray(origins, dirs);                              // [100, 2, 3]

// ── Triangles ───────────────────────────────────────────────────────
// Single — from three primitives
const tri = tf.triangle(tf.point(0,0,0), tf.point(1,0,0), tf.point(0,1,0));  // [3, 3]

// Batch — from three NDArrays with shape [N, 3]
const a = tf.random("float32", [50, 3]);
const b = tf.random("float32", [50, 3]);
const c = tf.random("float32", [50, 3]);
const tris = tf.triangle(a, b, c);                              // [50, 3, 3]

// ── Plane, AABB, Polygon ───────────────────────────────────────────
const pl  = tf.plane(tf.vector(0, 0, 1), 5.0);                  // [4]
const box = tf.aabb(tf.point(0, 0, 0), tf.point(1, 1, 1));     // [2, 3]
const poly = tf.polygon([tf.point(0,0,0), tf.point(1,0,0), tf.point(0,1,0)]);

Transformations

4x4 transformation matrices as NDArrayFloat32 [4, 4]:

const T = tf.makeTranslation(1, 0, 0);                    // three numbers
const T2 = tf.makeTranslation([1, 0, 0]);                 // or array
const T3 = tf.makeTranslation(someNDArray);                // or NDArray [3]

const R = tf.makeRotation(90, "z");                        // 90° around Z
const Rp = tf.makeRotation(45, [1, 0, 0], [0, 0, 5]);     // 45° around X, pivot at [0,0,5]

const Rr = tf.makeRandomRotation();                        // uniform random rotation
const Rr2 = tf.makeRandomRotation(centroid);               // random rotation around pivot

All vector parameters (axis, pivot, translation) accept [x, y, z] arrays or NDArray. The axis parameter also accepts "x", "y", "z" for principal axes.

Matrices are row-major — each row is contiguous in memory. Translation lives in elements [0,3], [1,3], [2,3]. This matches C++ and NumPy but differs from Three.js and WebGL (column-major). Convert with .T:

// Three.js → trueform
const mat = tf.ndarray(threeMatrix4.elements, [4, 4]).T;

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

Form

Forms are WASM-resident geometric structures that combine NDArrays with spatial acceleration and lazy topology. Two form types exist: Mesh and PointCloud. Both support spatial queries, transformations, and shared views (see Spatial).

Mesh

A triangle mesh with face indices and vertex coordinates. Topology structures (face membership, edge link, vertex link, normals) are computed lazily on first access and cached.

const faces  = tf.ndarray(new Int32Array([0,1,2, 0,2,3, 0,3,1, 1,3,2]), [4, 3]);
const points = tf.ndarray([0,0,0, 1,0,0, 0,1,0, 0,0,1], [4, 3]);

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

m.numberOfFaces;   // 4
m.numberOfPoints;  // 4
m.faces;           // NDArrayInt32 [4, 3]
m.points;          // NDArrayFloat32 [4, 3]

Properties

PropertyTypeDescription
facesNDArrayInt32 F, 3Face indices (get/set — setting invalidates topology)
pointsNDArrayFloat32 V, 3Vertex coordinates (get/set)
numberOfFacesnumberFace count
numberOfPointsnumberVertex count
transformationNDArrayFloat32 | null 4, 4Optional 4x4 transform (get/set)

Topology (lazy, cached)

Topology structures are built on first access and cached. Setting faces invalidates the cache.

PropertyTypeDescription
faceMembershipOffsetBlockedBufferPer-vertex → incident faces (descending order)
manifoldEdgeLinkNDArrayInt32 F, 3Per-edge neighbor info (see sentinels below)
faceLinkOffsetBlockedBufferPer-face → adjacent faces (by shared vertex)
vertexLinkOffsetBlockedBufferPer-vertex → adjacent vertices (by shared edge)
normalsNDArrayFloat32 F, 3Face normals
pointNormalsNDArrayFloat32 V, 3Vertex normals (area-weighted)

manifoldEdgeLink sentinel values:

ValueMeaning
>= 0Neighbor face index (manifold edge)
-1Boundary edge
-2Non-manifold edge (3+ faces)
-3Non-manifold representative
// Topology is lazy — first access computes and caches
const fm = m.faceMembership;
const neighbors = fm.get(0);  // faces incident to vertex 0

const mel = m.manifoldEdgeLink;
// mel.row(0).data → Int32Array with 3 neighbor face IDs for face 0

Methods

// Shared view — cheap copy sharing data and caches (no transformation)
const view = m.sharedView();

// Pre-build the spatial AABB tree
m.buildTree();

// Apply a transformation
m.transformation = tf.makeRotation(90, "z");

// Free WASM memory immediately (idempotent, never required)
m.delete();

PointCloud

A set of points with an optional spatial tree. No face connectivity — use for distance queries and nearest-neighbor search.

// From flat points
const pc = tf.pointCloud(tf.random("float32", [1000, 3]));

// From a Mesh (copies points, vertex link, and point normals)
const pc2 = tf.pointCloud(m);

pc.numberOfPoints;   // 1000
pc.points;           // NDArrayFloat32 [1000, 3]
PropertyTypeDescription
pointsNDArrayFloat32 V, 3Vertex coordinates (get/set)
numberOfPointsnumberPoint count
vertexLinkOffsetBlockedBuffer | nullPer-vertex adjacency (optional, get/set)
normalsNDArrayFloat32 | null V, 3Point normals (optional, get/set)
transformationNDArrayFloat32 | null 4, 4Optional 4x4 transform (get/set)
pc.buildTree();                // pre-build spatial tree
const view = pc.sharedView();  // cheap shared copy
pc.delete();                   // immediate cleanup (idempotent, never required)

Curves

A collection of polyline paths with shared points. Each path is a sequence of vertex indices into the shared points buffer. Returned by intersection and isocontour operations. Unlike forms, Curves does not carry a spatial tree or support spatial queries.

const c = tf.curves(paths, points);

c.length;    // number of paths
c.paths;     // OffsetBlockedBuffer — iterate or index into paths
c.points;    // NDArrayFloat32 [V, 3] — shared vertex pool

for (const path of c.paths) {
  console.log(path.data);  // Int32Array of vertex indices for this path
}
PropertyTypeDescription
pathsOffsetBlockedBufferPath index sequences (variable-length)
pointsNDArrayFloat32 V, 3Shared curve points
lengthnumberNumber of paths

IndexMap

Returned by cleaning and reindexing operations. Maps old indices to new indices after element removal or deduplication.

// Returned by tf.cleaned(), tf.reindexedByMask(), etc.
const { mesh, faceMap, pointMap } = tf.cleaned(m, { returnIndexMap: true });

faceMap.f;         // NDArrayInt32 — f[oldFaceId] = newFaceId
faceMap.keptIds;   // NDArrayInt32 — original face IDs that survived
PropertyTypeDescription
fNDArrayInt32Forward map (size = original count). Removed elements have value keptIds.length.
keptIdsNDArrayInt32Retained original IDs (size = kept count)

Removed elements in f are marked with the sentinel value keptIds.length — one past the last valid new index. This lets you distinguish kept from removed elements:

const removed = faceMap.f.eq(faceMap.keptIds.length);  // boolean mask of removed elements
For detailed implementation and generic programming concepts, see the C++ Core documentation.