Geometry
The Geometry module provides tools for computing geometric properties of meshes — normals, curvature, area, volume — along with mesh generation, triangulation, smoothing, and point cloud alignment.
Every function also has an async variant via tf.async for off-main-thread execution.
Mesh Generation
Generate common primitive meshes for testing or as building blocks.
import * as tf from "@polydera/trueform";
// UV sphere
const sphere = tf.sphereMesh(1.0, 20, 20); // radius, stacks, segments
// Cylinder along z-axis
const cyl = tf.cylinderMesh(1.0, 2.0, 20); // radius, height, segments
// Axis-aligned box
const box = tf.boxMesh(2, 1, 3);
// Subdivided box for deformation/simulation
const boxSub = tf.boxMesh(2, 1, 3, 4, 2, 6); // + widthTicks, heightTicks, depthTicks
// Flat plane in XY
const plane = tf.planeMesh(10, 5);
// Subdivided plane
const planeSub = tf.planeMesh(10, 5, 20, 10); // + widthTicks, heightTicks
| Function | Parameters | Description |
|---|---|---|
sphereMesh | radius, stacks, segments | UV sphere |
cylinderMesh | radius, height, segments | Cylinder along z-axis |
boxMesh | width, height, depth, [ticks] | Axis-aligned box |
planeMesh | width, height, [ticks] | Flat plane in XY |
Measurements
Area
Total surface area of a mesh.
const box = tf.boxMesh(2, 3, 4);
const a = tf.area(box); // 52.0
Volume
Volume of a closed 3D mesh. signedVolume is positive when face normals point outward.
const box = tf.boxMesh(2, 3, 4);
tf.volume(box); // 24.0
tf.signedVolume(box); // 24.0
Edge Length
tf.meanEdgeLength(mesh);
tf.minEdgeLength(mesh);
tf.maxEdgeLength(mesh);
| Function | Returns | Description |
|---|---|---|
area | number | Total surface area |
volume | number | Absolute volume (3D closed mesh) |
signedVolume | number | Signed volume (positive = outward normals) |
meanEdgeLength | number | Mean edge length across all faces |
minEdgeLength | number | Minimum edge length |
maxEdgeLength | number | Maximum edge length |
mesh.transformation. When a mesh has a transformation, measurements are computed in the transformed coordinate space.Normals
Face and vertex normals are lazily computed on first access and cached on the mesh.
const mesh = tf.mesh(faces, points);
// Face normals — one unit normal per face
const n = mesh.normals; // NDArrayFloat32 [F, 3]
// Vertex normals — area-weighted average of adjacent face normals
const pn = mesh.pointNormals; // NDArrayFloat32 [V, 3]
| Property | Returns | Description |
|---|---|---|
mesh.normals | NDArrayFloat32 [F, 3] | Unit face normals |
mesh.pointNormals | NDArrayFloat32 [V, 3] | Unit vertex normals |
Curvature Analysis
Surface curvature is computed by fitting a quadric to the local k-ring neighborhood at each vertex.
Principal Curvatures
// Curvature values only
const { k0, k1 } = tf.principalCurvatures(mesh);
// k0: NDArrayFloat32 [V] — maximum principal curvature
// k1: NDArrayFloat32 [V] — minimum principal curvature
// With principal directions
const { k0, k1, d0, d1 } = tf.principalDirections(mesh);
// d0: NDArrayFloat32 [V, 3] — direction of max curvature
// d1: NDArrayFloat32 [V, 3] — direction of min curvature
// Custom k-ring size (default k=2)
const curv = tf.principalCurvatures(mesh, 3);
// Derived measures
const gaussian = k0.mul(k1); // Gaussian curvature
const mean = k0.add(k1).mul(0.5); // Mean curvature
Shape Index
Maps principal curvatures to -1, 1, characterizing local surface type.
const si = tf.shapeIndex(mesh); // NDArrayFloat32 [V]
const si3 = tf.shapeIndex(mesh, 3); // custom k-ring
| Index Range | Surface Type |
|---|---|
| [-1, -5/8) | Concave ellipsoid (cup) |
| [-5/8, -3/8) | Concave cylinder (trough) |
| [-3/8, 3/8) | Hyperboloid (saddle) |
| [3/8, 5/8) | Convex cylinder (ridge) |
| 5/8, 1 | Convex ellipsoid (cap) |
Orientation
For closed 3D meshes, ensure faces are oriented so that normals point outward (positive signed volume).
// Orient mesh to have outward-facing normals
const oriented = tf.positivelyOriented(mesh);
// Skip consistency step if mesh is already consistently oriented
const oriented2 = tf.positivelyOriented(mesh, true);
This function:
- Orients faces consistently (unless
isConsistentistrue) - Computes the signed volume
- Reverses all face windings if the signed volume is negative
Triangulation
Convert polygon meshes to triangle meshes using ear-cutting triangulation.
// Triangulate a polygon
const poly = tf.polygon(new Float32Array([
0,0,0, 1,0,0, 1,1,0, 0.5,1.5,0, 0,1,0
]));
const triMesh = tf.triangulate(poly);
// Triangulate a quad mesh
const quadMesh = { faces: quadFaces, points: pts };
const triMesh2 = tf.triangulate(quadMesh);
// Triangulate a dynamic (variable n-gon) mesh
const dynMesh = { faces: offsetBlockedFaces, points: pts };
const triMesh3 = tf.triangulate(dynMesh);
// Pass a Mesh directly — returned as-is if already triangulated
const same = tf.triangulate(existingTriMesh);
Smoothing
Laplacian Smoothing
Iteratively moves vertices toward their neighbors' centroid.
// 100 iterations, lambda=0.5 (default)
const smoothed = tf.laplacianSmoothed(mesh, 100);
// Custom lambda
const smoothed2 = tf.laplacianSmoothed(mesh, 100, 0.3);
| Parameter | Type | Default | Description |
|---|---|---|---|
m | Mesh | Input mesh | |
iterations | number | Number of smoothing passes | |
lambda | number | 0.5 | Movement factor in 0, 1 |
Taubin Smoothing
Volume-preserving smoothing by alternating shrink and inflate passes.
// 50 iterations
const smoothed = tf.taubinSmoothed(mesh, 50);
// Custom parameters
const smoothed2 = tf.taubinSmoothed(mesh, 50, 0.5, 0.1);
| Parameter | Type | Default | Description |
|---|---|---|---|
m | Mesh | Input mesh | |
iterations | number | Number of passes (each = shrink + inflate) | |
lambda | number | 0.5 | Shrink factor in (0, 1] |
kpb | number | 0.1 | Pass-band frequency in (0, 1) |
Point Cloud Alignment
Compute rigid transformations to align point sets. All functions return a 4x4 delta transformation matrix (source world → target world).
| Scenario | Function | Notes |
|---|---|---|
| Known correspondences | fitRigidAlignment | Optimal closed-form solution |
| No correspondences, need initialization | fitObbAlignment | Coarse alignment via OBBs |
| No correspondences, need refinement | fitIcpAlignment | Full ICP loop |
// Typical pipeline: OBB → ICP
const T_coarse = tf.fitObbAlignment(source, target);
source.transformation = T_coarse;
const T_fine = tf.fitIcpAlignment(source, target, { maxIterations: 50 });
// Compose for total transformation
// T_fine is a delta from source's current world position
Rigid Alignment
Kabsch/Procrustes algorithm for optimal rotation + translation. Requires 1:1 correspondence (source and target must have the same number of points).
const T = tf.fitRigidAlignment(source, target); // NDArrayFloat32 [4, 4]
Coarse Alignment
Align using oriented bounding boxes (OBBs) for initialization.
const T = tf.fitObbAlignment(source, target);
// Custom sample size for orientation disambiguation
const T2 = tf.fitObbAlignment(source, target, { sampleSize: 200 });
sampleSize points.Iterative Refinement
Refine an initial alignment using Iterative Closest Point (ICP).
source.transformation = T_init;
const T = tf.fitIcpAlignment(source, target, {
maxIterations: 50,
nSamples: 1000,
});
| Option | Type | Default | Description |
|---|---|---|---|
maxIterations | number | 100 | Maximum iterations |
nSamples | number | 1000 | Subsample size per iteration (0 = all) |
k | number | 1 | Nearest neighbors (k=1 is classic ICP) |
sigma | number | -1 | Gaussian width (-1 = adaptive) |
outlierProportion | number | 0 | Outlier rejection ratio (0-1) |
minRelativeImprovement | number | 1e-6 | Convergence threshold |
emaAlpha | number | 0.3 | EMA smoothing factor |
Chamfer Error
One-way Chamfer distance — mean nearest-neighbor distance from source to target.
const error = tf.chamferError(source, target);
// With outlier rejection
const error2 = tf.chamferError(source, target, { outlierProportion: 0.05 });
// Symmetric chamfer distance
const sym = (tf.chamferError(a, b) + tf.chamferError(b, a)) / 2;
Async
All functions are available as tf.async.area(), tf.async.principalCurvatures(), tf.async.fitIcpAlignment(), etc. Signatures are identical — they return Promise<T> instead of T.
const a = await tf.async.area(mesh);
const { k0, k1 } = await tf.async.principalCurvatures(mesh);
const T = await tf.async.fitIcpAlignment(source, target, { maxIterations: 50 });
