Skip to content
CADCAMFUN
Docs

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.

FileLinesRole
cad/solid.ts659Solid schemas (Box, Cylinder, Sphere, Cone, Torus, Extrude, Revolve), meshOf, meshVolume, meshBounds
cad/csg.ts459BSP Booleans, convex classifier, translate/rotate/merge
cad/mesh-clean.ts626weld, T-junction repair, coplanar merge, re-triangulation
cad/mesh-topology.ts110manifold, shells, genus, volume, area
cad/triangulate.ts383polygon-with-holes triangulation
Drilled plate with a bored boss Drag to orbit · scroll to zoom · right-drag to pan
Real booleanMesh output: a 60 × 40 × 10 box minus four Ø6 holes (booleanAll subtract), united with a Ø20 × 8 boss, then bored Ø10 through. 640 triangles; meshTopology reports closed solid, 1 shell, genus 5, V 23981.4 mm³.

The Mesh type and primitives

solid.ts
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
SolidFieldsLimits
Boxorigin, size {x, y, z}sizes > 0
Cylinderorigin, radius, height, segments, axis?segments 3–512; axis z | x | y
Spherecenter, radius, ringsrings 2–256
Coneorigin, baseRadius, topRadius, height, segments, axis?topRadius ≥ 0; segments 3–512
Toruscenter, majorRadius, minorRadius, rotation?, majorSegments, minorSegments, axis?3–512 and 3–256
Extrudeprofile (≥ 3 points, not closed), height, origin?profile in XY
Revolveprofile (≥ 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.
GuardValueError message
tolerance must be positive and finite—Mesh tolerance must be positive and finite
segments per curve65 536Mesh tolerance exceeds subdivision budget
grid rows × columns (sphere, torus, revolve)1 000 000Mesh 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.

  1. If either mesh is empty, or the bounding boxes do not meet (within tolerance), answer directly: union = merge, subtract = a, intersect = empty.
  2. Build the classifiers of A and B (convex planes or BSP).
  3. 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.
  4. Subtract and intersect follow the classic a.clipTo(b), b.clipTo(a), b.invert() sequence on polygon lists, without copying trees.
  5. 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.
csg.ts
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:

ResultVolume, mm³Triangles
sphere alone7 063.84 (exact 7 238.23)624
cube ∩ sphere882.98112
cube ∪ sphere14 180.86—
cube − sphere7 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.

cube − sphere Drag to orbit · scroll to zoom · right-drag to pan
Subtract result: the cube with an eighth-sphere scooped out of its corner.
cube ∩ sphere Drag to orbit · scroll to zoom · right-drag to pan
Intersect result: the eighth of the sphere that lies inside the cube.

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.
meshTopology(bored) — real output
{"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:
Real output
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

SymptomCauseFix
Boolean result has holes or flipped facesan input is open or wound clockwisecheck both inputs with meshTopology: boundaryEdges and misorientedEdges must be 0
a face shared by both operands is missing or doubledthe 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 budgettolerance far too fine for the radiususe the tolerance presets (0.1 to 0.0005 mm)
volume differs from the formulafaceting at coarse tolerancemesh finer, or use the B-rep kernel for exact mass properties
Boolean slow on many toolsunion of tools built firstsubtract tools one by one (booleanAll does this): each convex tool is a handful of planes