Documentation
Mesh and CSG kernel
Closed triangle meshes from parametric solids, BSP Booleans with a convex fast path, cleanup and topology checks.
What the mesh kernel is
The mesh kernel works on closed triangle meshes: it builds them from parametric primitives (meshOf), measures them (meshVolume, meshBounds, meshTopology), and combines them with Booleans (booleanMesh, booleanAll). It is what the viewport shows, what the CAM generators machine, and what STL export writes.
It is not an exact boundary representation. A cylinder that was cut stays a faceted cylinder, and there is no face topology to fillet or measure on. For exact faces, fillets, mass properties and STEP, the B-rep kernel is a separate piece of code.
| File | Lines | Role |
|---|---|---|
cad/solid.ts | 659 | Solid schemas (Box, Cylinder, Sphere, Cone, Torus, Extrude, Revolve), meshOf, meshVolume, meshBounds |
cad/csg.ts | 459 | BSP Booleans, convex classifier, translate/rotate/merge |
cad/mesh-clean.ts | 626 | weld, T-junction repair, coplanar merge, re-triangulation |
cad/mesh-topology.ts | 110 | manifold, shells, genus, volume, area |
cad/triangulate.ts | 383 | polygon-with-holes triangulation |
The Mesh type and primitives
export interface Mesh {
readonly positions: Float32Array // x, y, z per vertex
readonly indices: Uint32Array // three per triangle, counter-clockwise seen from outside
}
export const meshOf: (solid: Solid, tolerance?: number /* 0.1 mm */) => Mesh
export const meshVolume: (mesh: Mesh) => number // signed; positive when outward-facing
export const meshBounds: (mesh: Mesh) => SolidBounds | Solid | Fields | Limits |
|---|---|---|
| Box | origin, size {x, y, z} | sizes > 0 |
| Cylinder | origin, radius, height, segments, axis? | segments 3–512; axis z | x | y |
| Sphere | center, radius, rings | rings 2–256 |
| Cone | origin, baseRadius, topRadius, height, segments, axis? | topRadius ≥ 0; segments 3–512 |
| Torus | center, majorRadius, minorRadius, rotation?, majorSegments, minorSegments, axis? | 3–512 and 3–256 |
| Extrude | profile (≥ 3 points, not closed), height, origin? | profile in XY |
| Revolve | profile (≥ 2 points in XZ), angle, segments? | about Z; segments 3–512, default 16 |
segments and rings are minimums: meshOf raises them until the chord error is within tolerance, using step = 4·asin(√(min(1, tol/r)/2)). Two budgets stop runaway tolerances.
| Guard | Value | Error message |
|---|---|---|
| tolerance must be positive and finite | — | Mesh tolerance must be positive and finite |
| segments per curve | 65 536 | Mesh tolerance exceeds subdivision budget |
| grid rows × columns (sphere, torus, revolve) | 1 000 000 | Mesh tolerance exceeds vertex budget |
A Ø6 cylinder with segments: 24 at tolerance 0.05 came out with 96 triangles (24 sides, two 24-gon caps); a Ø20 one with 128, because the larger radius needed 32 segments to stay within 0.05 mm.
Booleans by binary space partitioning
Each operand becomes a classifier. A general mesh gets a BSP tree; a convex mesh (closed, connected, every edge convex) is just its distinct face planes, so a box is six planes and a sphere of ten thousand triangles is classified without building the quadratic chain a BSP of a convex solid degenerates to. Polygons are classified as lists, and a polygon clear of the other operand's bounding box is kept or dropped without walking anything.
- If either mesh is empty, or the bounding boxes do not meet (within tolerance), answer directly: union = merge, subtract = a, intersect = empty.
- Build the classifiers of A and B (convex planes or BSP).
- Union: keep A outside B, B outside A, and drop B's faces coincident with A's pointing the same way, so a shared face is kept once.
- Subtract and intersect follow the classic a.clipTo(b), b.clipTo(a), b.invert() sequence on polygon lists, without copying trees.
- Clean the polygon soup: weld points within max(10·eps, 1e-9·size), close T-junctions, close gaps of up to 1e-4·size between slivers, merge coplanar fragments and re-triangulate.
export type BooleanOp = "union" | "subtract" | "intersect"
export interface CsgOptions {
/** Distance below which a point counts as on a plane, mm. Default 1e-7 × the larger extent. */
readonly tolerance?: number
/** Merge coplanar fragments and re-triangulate (default true). */
readonly mergeCoplanar?: boolean
}
export const booleanMesh: (a: Mesh, b: Mesh, op: BooleanOp, options?: CsgOptions) => Mesh
/** Union reduced as a balanced tree; subtract and intersect fold left to right. */
export const booleanAll: (meshes: ReadonlyArray<Mesh>, op: BooleanOp, options?: CsgOptions) => Mesh
export const translateMesh: (mesh: Mesh, dx: number, dy: number, dz: number) => Mesh
export const rotateMesh: (mesh: Mesh, axis: V3, radians: number, origin?: V3) => Mesh
export const mergeMeshes: (meshes: ReadonlyArray<Mesh>) => Mesh The triangle count grows with the features, not with the number of Booleans: the merge step puts coplanar slivers back into whole faces. In the plate above, the box had 12 triangles, after four holes 412, after the boss 540, after the bore 640; the whole chain ran in 96 ms.
Union, subtract, intersect on curved input
A 20 mm cube at the origin against a sphere of radius 12 centred on its far corner (20, 20, 20), meshed at tolerance 0.2 (624 triangles). Real volumes:
| Result | Volume, mm³ | Triangles |
|---|---|---|
| sphere alone | 7 063.84 (exact 7 238.23) | 624 |
| cube ∩ sphere | 882.98 | 112 |
| cube ∪ sphere | 14 180.86 | — |
| cube − sphere | 7 117.02 | — |
Check: 8 000 + 7 063.84 − 882.98 = 14 180.86 and 8 000 − 882.98 = 7 117.02, so the three results are consistent with each other to the hundredth. The sphere mesh itself is 2.4 % under the exact sphere, which is the faceting of a coarse tolerance, not Boolean error.
Topology check
meshTopology counts vertices, edges and triangles, finds boundary, non-manifold and mis-oriented edges, degenerate triangles and shells, and derives Euler characteristic and genus. describeTopology prints the one-line summary the assistant and the viewport use.
{"vertices":312,"triangles":640,"edges":960,"boundaryEdges":0,"nonManifoldEdges":0,
"misorientedEdges":0,"degenerateTriangles":0,"shells":1,"eulerCharacteristic":-8,"genus":5,
"closed":true,"solid":true,"volume":23981.43509870267,"area":8238.640705862534}
// describeTopology → "closed solid, 1 shell, genus 5, 640 triangles, V 23981.4 mm³, A 8238.6 mm²" V − E + F = 312 − 960 + 640 = −8 = 2 − 2g, so g = 5: the four drilled holes plus the bore, which runs through the boss and the plate.
Polygon triangulation
triangulatePolygon(outer, holes) returns index triples into the concatenation of the outer ring and the holes. A 4 × 4 square with a 2 × 2 hole gives eight triangles:
triangulatePolygon(
[{x:0,y:0},{x:4,y:0},{x:4,y:4},{x:0,y:4}],
[[{x:1,y:1},{x:1,y:3},{x:3,y:3},{x:3,y:1}]])
// [[0,4,5],[7,4,0],[3,0,5],[7,0,1],[2,3,5],[6,7,1],[2,5,6],[6,1,2]] triangulatePolygonChecked returns undefined instead of a partial result when the input cannot be triangulated (self-intersecting rings, holes outside the outer ring).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Boolean result has holes or flipped faces | an input is open or wound clockwise | check both inputs with meshTopology: boundaryEdges and misorientedEdges must be 0 |
| a face shared by both operands is missing or doubled | the faces are nearly but not exactly coincident (farther apart than the tolerance) | snap the operands to the same plane, or raise CsgOptions.tolerance |
| RangeError: Mesh tolerance exceeds subdivision budget | tolerance far too fine for the radius | use the tolerance presets (0.1 to 0.0005 mm) |
| volume differs from the formula | faceting at coarse tolerance | mesh finer, or use the B-rep kernel for exact mass properties |
| Boolean slow on many tools | union of tools built first | subtract tools one by one (booleanAll does this): each convex tool is a handful of planes |