Core
The Core module has two layers. Arrays — NDArray and OffsetBlockedBuffer — are the numerical foundation: shaped, typed, broadcast-aware arrays that live on the WASM heap and run at native speed. Geometric types — Primitive, Mesh, PointCloud, Curves — are built on top, combining arrays with spatial structure, lazy topology, and batch-transparent operations.
import * as tf from "@polydera/trueform";
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
Five dtypes are supported:
| Type | Storage | Use |
|---|---|---|
NDArrayFloat32 | Float32Array | Coordinates, scalars, distances |
NDArrayFloat64 | Float64Array | Double-precision coordinates and scalars |
NDArrayInt32 | Int32Array | Indices, face connectivity |
NDArrayInt8 | Int8Array | Labels, small integers |
NDArrayBool | Int8Array (0/1) | Masks, predicates |
Creating Arrays
// From JavaScript data (number[] defaults to float64; JS numbers are doubles)
const a = tf.ndarray([1, 2, 3, 4, 5, 6], [2, 3]); // float64, 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]);
const d = tf.ndarray(new Float64Array([1, 2, 3]), [1, 3]);
// Filled arrays
const z = tf.zeros("float32", [100, 3]);
const z64 = tf.zeros("float64", [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
| Property | Type | Description |
|---|---|---|
data | Float32Array | Float64Array | Int32Array | Int8Array | Typed array view into WASM heap |
length | number | Total element count |
shape | number[] | Shape tuple, settable for reshape-in-place |
ndim | number | Number of dimensions |
dtype | string | "float32", "float64", "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; // "float64" — number[] inputs default to float64
pts.data; // Float64Array([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);
| Copy | In-place | Description |
|---|---|---|
add | add_ | Addition |
sub | sub_ | Subtraction |
mul | mul_ | Multiplication |
div | div_ | Division |
mod | mod_ | Remainder (truncated) |
clip | clip_ | 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
| Method | Description |
|---|---|
eq, neq | Equal, not-equal |
lt, gt | Less-than, greater-than |
lte, gte | Less/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
| Reduction | Output dtype | Async |
|---|---|---|
sum | int32 (from int), float32 (from float) | tf.async.sum |
min, max | Same as input | tf.async.min, tf.async.max |
mean | float32 | tf.async.mean |
norm | float32 | tf.async.norm |
argmin, argmax | int32 | tf.async.argmin, tf.async.argmax |
any, all | bool | tf.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)
| Function | In-place | Description |
|---|---|---|
tf.exp | tf.exp_ | e^x |
tf.log | tf.log_ | Natural log |
tf.log2 | tf.log2_ | Base-2 log |
tf.log10 | tf.log10_ | Base-10 log |
tf.pow | tf.pow_ | x^n (scalar exponent) |
tf.sqrt | tf.sqrt_ | Square root |
Rounding (float32 only)
| Function | In-place |
|---|---|
tf.floor | tf.floor_ |
tf.ceil | tf.ceil_ |
tf.round | tf.round_ |
Multi-dtype (all dtypes)
| Function | Description |
|---|---|
tf.abs | Absolute value |
tf.neg | Negation |
tf.clip | Clamp 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
Histograms and Counts
Free functions for integer index counting (bincount) and value binning (histogram). Semantics match NumPy (np.bincount, np.histogram): NaN and out-of-range values are dropped, the upper edge of the last bin is inclusive, and weighted or density-normalized outputs are returned as float32. Async: tf.async.bincount, tf.async.histogram.
const labels = tf.ndarray(new Int32Array([0, 1, 0, 2, 1, 0, 2, 2, 0]));
tf.bincount(labels); // int32 [4, 2, 3]
tf.bincount(labels, { minLength: 5 }); // int32 [4, 2, 3, 0, 0]
// Weighted → float32 sums per bin
const w = tf.ndarray(new Float32Array([0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]));
tf.bincount(labels, { weights: w }); // float32 weight sums
await tf.async.bincount(labels);
Bincount rejects negative values (throws). For signed integers or counts of distinct values in arbitrary ranges, use tf.unique(..., returnCounts) or tf.histogram with integer edges.
const x = tf.random("float32", [10000], -1, 1);
// Equal-width bins
const { counts, edges } = tf.histogram(x, { bins: 20, range: [-1, 1] });
// counts: int32 [20], edges: float32 [21]
// Auto range (min/max of x)
tf.histogram(x, { bins: 20 });
// Explicit edges
const userEdges = tf.ndarray(new Float32Array([-1, -0.5, 0, 0.5, 1]));
tf.histogram(x, { bins: userEdges });
// Probability density (PDF) — integrates to 1
const pdf = tf.histogram(x, { bins: 50, density: true });
// Weighted histogram
const weights = tf.random("float32", [10000]);
tf.histogram(x, { bins: 20, weights }); // float32 counts
await tf.async.histogram(x, { bins: 100 });
| Option | Type | Effect |
|---|---|---|
bins | number | Number of equal-width bins over range. Default 10. |
bins | NDArray | Explicit edges, length num_bins + 1, monotone. Coerced to float32. |
range | [number, number] | Bounds for equal-width bins; auto [min(x), max(x)] if omitted. |
weights | NDArray | Sum of weights per bin instead of counting. Coerced to float32. |
density | boolean | Normalize to PDF: counts[i] / (total * bin_width[i]). |
Output dtype rule: int32 counts by default, float32 if weights or density is supplied. edges is always float32.
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
int8 ↔ bool) are zero-copy. Cross-storage casts (float32 → int32) 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.
// Create from offsets + data arrays
const obb = tf.offsetBlockedBuffer(
tf.ndarray(new Int32Array([0, 3, 5]), [3]), // offsets [N+1]
tf.ndarray(new Int32Array([0, 1, 2, 3, 4]), [5]) // data
);
// obb.length === 2, obb.get(0).data → [0, 1, 2], obb.get(1).data → [3, 4]
// Use as paths for curves
const curves = tf.curves(obb, points);
// 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)
| Property | Type | Description |
|---|---|---|
length | number | Number of blocks |
offsets | NDArrayInt32 | Block boundary indices |
data | NDArrayInt32 | Flat 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, 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
| Type | Shape (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. All factories accept a trailing { dtype: "float32" | "float64" } option; numeric arguments default to float32, NDArray inputs inherit the source dtype.
// ── Points and Vectors ──────────────────────────────────────────────
const p = tf.point(1, 2, 3); // float32 [3]
const p64 = tf.point(1, 2, 3, { dtype: "float64" }); // float64 [3]
const v = tf.vector(0, 0, 1); // float32 [3]
const pts = tf.point(new Float32Array(300), 3); // batch [100, 3]
const pts64 = tf.point(new Float64Array(300), 3); // batch [100, 3] float64
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 meshBox = tf.aabbFrom(mesh); // from geometry
const ptsBox = tf.aabbFrom(points); // from NDArray
const poly = tf.polygon([tf.point(0,0,0), tf.point(1,0,0), tf.point(0,1,0)]);
Oriented Bounding Box
obbFrom computes a tight-fitting oriented bounding box via PCA. Takes a mesh or point array.
const obb = tf.obbFrom(mesh); // or tf.obbFrom(points)
obb.origin; // NDArrayFloat32 [3] — corner point
obb.axes; // NDArrayFloat32 [3, 3] — orthonormal axes (one per row)
obb.extent; // NDArrayFloat32 [3] — full extents along each axis
Available as tf.async.obbFrom(mesh) for off-main-thread computation.
Transformations
4x4 transformation matrices as NDArrayFloat32 or NDArrayFloat64 4, 4. The dtype follows the input arguments; pass { dtype: "float64" } (or float64 NDArrays / typed arrays) to build double-precision transforms:
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
const Tinv = tf.inverted(T); // inverse of 4x4 affine
const M2dInv = tf.inverted(mat3x3); // inverse of 3x3 affine
All vector parameters (axis, pivot, translation) accept [x, y, z] arrays or NDArray.
The axis parameter also accepts "x", "y", "z" for principal axes.
inverted works on 4x4 (3D affine) and 3x3 (2D affine) matrices, computing the inverse in double precision. Throws on any other shape.
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. Vertex coordinates are float32 by default; pass Float64Array points (or any factory's { dtype: "float64" } option) to build a double-precision mesh. 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(new Float32Array([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]
m.dtype; // "float32"
// Double-precision mesh
const points64 = tf.ndarray(new Float64Array([0,0,0, 1,0,0, 0,1,0, 0,0,1]), [4, 3]);
const m64 = tf.mesh(faces, points64);
m64.dtype; // "float64"
m64.points; // NDArrayFloat64 [4, 3]
Properties
| Property | Type | Description |
|---|---|---|
faces | NDArrayInt32 F, 3 | Face indices (get/set — setting invalidates topology) |
points | NDArrayFloat32 | NDArrayFloat64 V, 3 | Vertex coordinates (get/set) |
dtype | "float32" | "float64" | Coordinate precision |
numberOfFaces | number | Face count |
numberOfPoints | number | Vertex count |
transformation | NDArrayFloat32 | NDArrayFloat64 | null 4, 4 | Optional 4x4 transform, matches mesh dtype (get/set) |
Topology (lazy, cached)
Topology structures are built on first access and cached. Setting faces invalidates the cache.
| Property | Type | Description |
|---|---|---|
faceMembership | OffsetBlockedBuffer | Per-vertex → incident faces (descending order) |
manifoldEdgeLink | NDArrayInt32 F, 3 | Per-edge neighbor info (see sentinels below) |
faceLink | OffsetBlockedBuffer | Per-face → adjacent faces (by shared vertex) |
vertexLink | OffsetBlockedBuffer | Per-vertex → adjacent vertices (by shared edge) |
normals | NDArrayFloat32 | NDArrayFloat64 F, 3 | Face normals (matches mesh dtype) |
pointNormals | NDArrayFloat32 | NDArrayFloat64 V, 3 | Vertex normals, area-weighted (matches mesh dtype) |
manifoldEdgeLink sentinel values:
| Value | Meaning |
|---|---|
>= 0 | Neighbor face index (manifold edge) |
-1 | Boundary edge |
-2 | Non-manifold edge (3+ faces) |
-3 | Non-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
// Precompute off the main thread (result cached on mesh)
await tf.async.computeNormals(m);
await tf.async.computeFaceMembership(m);
await tf.async.buildTree(m);
See Geometry, Topology, and Spatial for full async precompute APIs.
Methods
// Shallow copy — new handle sharing buffers, with own caches and cleared transformation
const view = m.shallowCopy();
// 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]
| Property | Type | Description |
|---|---|---|
points | NDArrayFloat32 | NDArrayFloat64 V, 3 | Vertex coordinates (get/set) |
dtype | "float32" | "float64" | Coordinate precision |
numberOfPoints | number | Point count |
vertexLink | OffsetBlockedBuffer | null | Per-vertex adjacency (optional, get/set) |
normals | NDArrayFloat32 | NDArrayFloat64 | null V, 3 | Point normals, matches dtype (optional, get/set) |
transformation | NDArrayFloat32 | NDArrayFloat64 | null 4, 4 | Optional 4x4 transform, matches dtype (get/set) |
pc.buildTree(); // pre-build spatial tree
const view = pc.shallowCopy(); // new handle sharing buffers, own caches and pose
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
}
| Property | Type | Description |
|---|---|---|
paths | OffsetBlockedBuffer | Path index sequences (variable-length) |
points | NDArrayFloat32 | NDArrayFloat64 V, 3 | Shared curve points |
dtype | "float32" | "float64" | Coordinate precision |
length | number | Number 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
| Property | Type | Description |
|---|---|---|
f | NDArrayInt32 | Forward map (size = original count). Removed elements have value keptIds.length. |
keptIds | NDArrayInt32 | Retained 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
