Fast and Robust Principal Curvatures of a Mesh in JavaScript
Compute principal curvatures, principal directions, Gaussian and mean curvature, and the shape index of a triangle mesh in JavaScript and TypeScript. WebAssembly, runs in Node.js and the browser. 60.6 ms on a 1M-face mesh.
Principal curvatures tell you how a surface bends at each point. k0 is the largest bend, k1 the smallest. From them you get Gaussian curvature, mean curvature, and the shape index. trueform computes all of it per vertex. One npm install. It runs as WebAssembly in Node.js and the browser, with TypeScript types. This tutorial covers principal curvatures, principal directions, the shape index, and how to read the signs.

Install
npm install @polydera/trueform
The WebAssembly binary loads on first use. In the browser, multithreading needs SharedArrayBuffer headers. See the installation guide.
Loading the mesh
import * as tf from "@polydera/trueform";
import { readFileSync } from "node:fs";
const mesh = tf.readStl(readFileSync("part.stl"));
In the browser, pass the bytes from fetch instead. tf.readObj works the same way. If you already have typed arrays, pass them to tf.mesh(faces, points).
To follow along without a file, make a sphere of radius 2. Its curvature is 1/2 everywhere, so you can check the numbers:
const mesh = tf.sphereMesh(2.0, 100, 200);
Principal curvatures
const { k0, k1 } = tf.principalCurvatures(mesh);
// k0, k1: NDArray, shape [numberOfPoints], the mesh's dtype
// k0 >= k1 at every point

On the sphere both are close to 0.5. Each vertex fits a quadric to its neighborhood. The neighborhood is the k-ring, two rings by default. A larger ring smooths out noise:
const smooth = tf.principalCurvatures(mesh, 3);
Gaussian and mean curvature follow directly:
const gaussian = k0.mul(k1);
const mean = k0.add(k1).mul(0.5);
On the sphere, Gaussian curvature is close to 0.25 and mean curvature close to 0.5.
Principal directions
Ask for the directions too:
const { k0, k1, d0, d1 } = tf.principalDirections(mesh);
// d0, d1: NDArray, shape [numberOfPoints, 3]

d0 is the direction of k0, d1 the direction of k1. Both are unit vectors, perpendicular to each other and to the vertex normal. You can check:
const n = mesh.pointNormals;
tf.abs(d0.mul(d1).sum(1)).max(); // close to 0
tf.abs(d0.mul(n).sum(1)).max(); // close to 0
Directions are useful for drawing hatching along a surface, for aligning tool paths, and for finding ridges and valleys.
Shape index
The shape index folds the two curvatures into one number in -1, 1. It describes the local shape, not its size. A small ball and a large ball have the same shape index.
const si = tf.shapeIndex(mesh); // shape [numberOfPoints]
const si3 = tf.shapeIndex(mesh, 3); // larger neighborhood

| Range | Shape |
|---|---|
| [-1, -5/8) | Cup |
| [-5/8, -3/8) | Trough |
| [-3/8, 3/8) | Saddle |
| [3/8, 5/8) | Ridge |
| 5/8, 1 | Cap |
Count the vertices of each shape:
const edges = [-1, -5/8, -3/8, 3/8, 5/8, 1.0001];
const names = ["cup", "trough", "saddle", "ridge", "cap"];
const counts = Object.fromEntries(names.map((name, i) =>
[name, si.gte(edges[i]).and(si.lt(edges[i + 1])).sum()]));
On the sphere every vertex is a cap.
Reading the signs
Signs follow the face orientation. With outward-facing normals, a convex surface has positive curvature. On the sphere k0 and k1 are positive and the shape index is near 1. Flip the faces and the signs flip with them:
const flipped = tf.reverseWinding(mesh);
tf.shapeIndex(flipped); // near -1 on the same sphere

If your results look inverted, check the winding first.
A height field shows all the shapes. Take a flat grid with +z normals and bend it:
const plane = tf.planeMesh(2.0, 2.0, 100, 100);
const bend = (f) => {
const pts = plane.points.clone();
const q = pts.data;
for (let i = 0; i < q.length; i += 3) q[i + 2] = f(q[i], q[i + 1]);
return tf.mesh(plane.faces, pts);
};
const saddle = bend((x, y) => x * x - y * y); // shape index 0 at the center
const ridge = bend((x, y) => -(x * x)); // bends away from the normal: +0.5
const trough = bend((x, y) => x * x); // bends toward the normal: -0.5
Off the main thread
Every curvature function has an async variant. It runs off the main thread and keeps the UI responsive:
const { k0, k1 } = await tf.async.principalCurvatures(mesh);
const si = await tf.async.shapeIndex(mesh);
Working with results
All results are NDArrays with one value per point, in the mesh's dtype. .data gives the typed array. Upload it as a vertex attribute to color the mesh, or use it as a weight or a field for other tools.
Draw the lines where the surface changes from saddle to ridge with isocontours:
const lines = tf.isocontours(mesh, si, 3 / 8);
Cut the mesh into regions by shape with isobands:
const { mesh: regions, labels, faceLabels } =
tf.isobands(mesh, si, [-3 / 8, 3 / 8]);
// labels: 0 concave, 1 saddle, 2 convex
Flag sharp spots for inspection:
const isSharp = tf.abs(k0).gt(10.0).or(tf.abs(k1).gt(10.0));
const sharp = tf.arange("int32", mesh.numberOfPoints).booleanIndex(isSharp);
Combine curvature with geodesic distances to look only at the neighborhood of a point.
Related tutorials
- Fast and Accurate Geodesic Distances in JavaScript — distance along the surface from one seed or many.
- Fast and Robust Isocontours on a Triangle Mesh in JavaScript — curves and bands of equal value on a scalar field.
- Fast and Robust Mesh Slicing and Cross-Sections in JavaScript — plane sections, layers and slabs of a mesh.
- Fast STL and OBJ Reading in JavaScript — load and write meshes, the first step of every pipeline.
Performance
Apple M4 Max 16 threads Node.js WebAssembly
Principal curvatures on a 1M-face mesh take 60.6 ms in Node.js. The JavaScript benchmark has no competitor, so there is no comparison here. The same core is measured against libigl on the Python and C++ pages.

Full benchmarks · Documentation
@article{polydera:principal-curvatures-of-a-mesh-in-javascript,
title={Fast and Robust Principal Curvatures of a Mesh in JavaScript},
author={Sajovic, {\v{Z}}iga, Polydera},
year={2026},
url={https://polydera.com/tutorials/principal-curvatures-of-a-mesh-in-javascript},
organization={Polydera}
}