Documentation
Exact B-rep kernel
Exact boundary representation: analytic and NURBS geometry, Booleans, fillets, hole recognition, mass properties, validation, tessellation and STEP.
What the B-rep kernel is
The exact kernel in packages/core/src/cad/brep (24 source files, about 8 660 lines, plus tests) represents solids by their exact boundary: analytic and NURBS surfaces, trimmed by loops of exact edges. Everything is plain data and pure functions, so a B-rep serialises to JSON, crosses a worker boundary and diffs in the project history unchanged. Booleans, fillets and chamfers, hole recognition, mass properties, validation, watertight tessellation and STEP exchange all work on this one structure.
Topology tables
A Brep is a set of index tables. Entities refer to each other by index, never by object reference.
| Entity | Holds | STEP counterpart |
|---|---|---|
| Vertex | point, tolerance | VERTEX_POINT |
| Edge | exact curve, [t0, t1] with t0 < t1, v0 = C(t0), v1 = C(t1), tolerance | EDGE_CURVE |
| Coedge | one use of an edge by a loop: edge index, reversed, optional p-curve in the face (u, v) | ORIENTED_EDGE + PCURVE |
| Loop | closed cycle of coedges; material on the left seen from outside (outer CCW, holes CW) | EDGE_LOOP |
| Face | region of one surface; reversed means the outward normal is −(S_u × S_v) | ADVANCED_FACE |
| Shell | connected set of faces; a closed shell bounds a volume | CLOSED_SHELL |
Seams (an edge used twice by the same face, such as the generator line of a full cylinder) and poles (a loop through a degenerate point, such as a cone apex) need no special entities. That is why the exact sphere has 2 vertices, 1 edge and 1 face, and the torus 1 vertex, 2 edges and 1 face.
makeBox(60, 40, 15) {"V":8,"E":12,"F":6,"S":1,"surfaces":["plane"]}
makeSphere(10) {"V":2,"E":1,"F":1,"S":1,"surfaces":["sphere"]}
makeTorus(20, 5) {"V":1,"E":2,"F":1,"S":1,"surfaces":["torus"]}
makeCone(10, 4, 20) {"V":2,"E":3,"F":3,"S":1,"surfaces":["cone","plane"]} Curves and surfaces
Analytic types use the STEP (ISO 10303-42) parameterisations, so faces read from and written to STEP keep their parameters.
| Type | Parameterisation |
|---|---|
| line | C(t) = O + t·d, d unit, t arc length |
| circle | C(t) = O + r(cos t·x + sin t·y) |
| ellipse | C(t) = O + a cos t·x + b sin t·y |
| plane | S(u,v) = O + u·x + v·y |
| cylinder | S(u,v) = O + r(cos u·x + sin u·y) + v·z |
| cone | S(u,v) = O + (R + v tan α)(cos u·x + sin u·y) + v·z |
| sphere | S(u,v) = O + r cos v(cos u·x + sin u·y) + r sin v·z |
| torus | S(u,v) = O + (R + r cos v)(cos u·x + sin u·y) + r sin v·z |
| revolution | S(u,v) = rotate(C(v), axis, u) |
| extrusion | S(u,v) = C(u) + v·d |
| nurbs | rational B-spline curve or surface (Piegl & Tiller algorithms in nurbs.ts) |
Exact Booleans
- Intersect. Edges of each solid against the faces of the other (exact points by Newton), edges lying on the other solid's surfaces, and the intersection curves of every pair of non-coincident faces, refined onto both surfaces and recognised as lines and circles, otherwise fitted as cubic B-splines within the fit tolerance.
- Imprint. Every intersection point becomes a vertex in one registry for both solids, edges are split there, and each face is cut by the curves on it through a planar arrangement in its (u, v) domain.
- Classify. A point inside each fragment is located against the other solid: in, out, or on with the same or opposite orientation.
- Sew. Fragments are kept by the operation's rules, coincident pieces become one edge, coplanar neighbours merge, and the result is validated. A result that is not a closed manifold solid throws
BooleanError; it is never returned.
export const booleanBrep: (A: Brep, B: Brep, op: BooleanOp, options?: BooleanOptions) => BooleanResult
export interface BooleanOptions {
tol?: number // coincidence, default 1e-8 × model size, at least 1e-7 mm
fitTol?: number // fitted intersection curves, default 1e-6 × size, at least 1e-6 mm
seedAngleDeg?: number // normal turn per seed-mesh triangle, default 10
debug?: (stage: string, data: unknown) => void
}
export interface BooleanResult {
brep: Brep; warnings: string[]
stats: { intersectionCurves: number; splitPoints: number; fragments: number; keptFaces: number }
} import { brep } from "@cadcamfun/core"
const { makeBox, makeCylinder, frame, booleanBrep, validateBrep, massProperties } = brep
const box = makeBox(60, 40, 15)
const hole = makeCylinder(6, 25, frame({ x: 30, y: 20, z: -5 }))
const r = booleanBrep(box, hole, "subtract") // 141 ms
r.stats // {"intersectionCurves":2,"splitPoints":12,"fragments":13,"keptFaces":7}
r.warnings // []
validateBrep(r.brep)
// {"valid":true,"manifoldSolid":true,"genus":1,"volume":34303.53996706151,"issues":[]}
massProperties(r.brep).volume // 34303.53996706151 = 60·40·15 − π·6²·15 exactly Mass properties
Volume, area, centroid and the inertia tensor (unit density, about the centroid, row-major) come from the divergence theorem over the boundary faces. Planar faces are integrated exactly by Green's theorem along the exact edge curves; curved faces are integrated on a parameter-step tessellation of the exact surface.
| Body | massProperties | Closed form |
|---|---|---|
| box − Ø12 hole, volume | 34303.53996706151 | 34303.539967061515 |
| same, area | 8139.292006587684 | 8139.292006587698 |
| same, centroid | (30.000, 20.000, 7.500) | (30, 20, 7.5) |
| sphere r10, volume | 4188.790204786379 | 4188.790204786391 |
| torus 20/5, volume | 9869.60440109475 | 9869.604401089358 |
| filleted plate, volume | 34097.52220392302 | 34097.522203923065 |
The filleted-plate closed form is the subtracted plate minus four corner slivers of (r² − πr²/4)·h = (16 − 4π)·15 each.
Fillets, chamfers and hole recognition
export const blendCandidates: (b: Brep, minAngleDegrees?: number /* 0.5 */) => BlendCandidate[]
export const selectBlendEdges: (edges: ReadonlyArray<BlendCandidate>, s?: BlendSelector) => BlendCandidate[]
// BlendSelector: parallel?: "x"|"y"|"z", within?: box, kind?: "convex"|"concave"|"both" (default convex), minLength?, curved?
export const blendBrep: (b: Brep, edges: ReadonlyArray<BlendCandidate>, size: number,
style?: "fillet" | "chamfer", options?: BlendOptions) => BlendResult
// BlendResult: brep, blended, vertices (rolling-ball corners), rejected (edge ids), ...
export const recognizeHoles: (b: Brep, options?: HoleRecognitionOptions) => ReadonlyArray<RecognizedHole> On the box − cylinder body there were 14 blend candidates; selectBlendEdges(c, { parallel: "z" }) picked the 4 vertical outer edges, and blendBrep(b, sel, 4) filleted all 4 with none rejected in 94 ms. The hole read back from the faces alone:
[{"entry":{"x":30,"y":20,"z":15},"direction":{"x":0,"y":0,"z":-1},"depth":15,"diameter":12,
"form":"simple","bottom":"through",
"segments":[{"kind":"cylinder","start":0,"end":15,"diameterStart":12,"diameterEnd":12,"faces":[6]}]}] Forms are simple, counterbore and countersink; bottoms are through, flat and drill-point. Recognition reads faces, not history, so a STEP import, a Boolean result and a parametric part are recognised alike.
Watertight tessellation
Every edge is sampled once on its exact curve and every face that uses it shares those samples, so the welded mesh has no cracks. Faces are triangulated by constrained Delaunay in their (u, v) domain.
| Option | Default | Meaning |
|---|---|---|
| chordTol | 1/2000 of the model diagonal, clamped to [0.001, 0.05] mm | max distance between mesh and exact surface |
| angleTolDeg | 15 | max normal turn across a triangle (0 disables) |
| maxEdge | 0 (none) | edge-length target |
| maxPointsPerFace | 200 000 | safety cap |
| paramStep | — | integration mode used by massProperties |
For the filleted plate defaultChordTol returned 0.0368 mm and tessellateBrep produced 652 triangles plus per-face meshes, edge polylines and warnings.
STEP exchange
writeStepBreps writes an AP214 (AUTOMOTIVE_DESIGN) file in millimetres and radians, one ADVANCED_BREP_SHAPE_REPRESENTATION per part. readStepBreps reads AP203, AP214 and AP242 geometry and topology, including assemblies with their transformations and product names. Unknown entities are reported in warnings and their faces skipped; nothing is guessed.
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('CADCAMFUN exact B-rep'),'2;1');
FILE_NAME('plate','2026-10-15T04:49:21',(''),(''),'CADCAMFUN B-rep kernel','CADCAMFUN','');
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
ENDSEC;
DATA;
#1=APPLICATION_CONTEXT('core data for automotive mechanical design processes');
#2=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#1);
#3=PRODUCT_CONTEXT('',#1,'mechanical');
#4=PRODUCT_DEFINITION_CONTEXT('part definition',#1,'design');
#5=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.)); Reading that file back gave one part whose volume was 34097.52220392303 mm³, equal to the written body to 11 significant digits.
Time budget
The kernel is synchronous and runs on the caller's thread. withBudget(ms, f) sets a deadline; long loops (face pairs of a Boolean, face splitting, surface intersection, tessellation) call checkBudget, which throws BudgetExceeded. Nested budgets keep the earlier deadline; without a budget a check costs one comparison.
withBudget(1, () => booleanBrep(makeSphere(10), makeBox(10, 10, 10), "union"))
// BudgetExceeded: the exact model exceeded its time budget of 1 ms Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
BooleanError | the sewn result is not a closed manifold solid (tangent or near-coincident faces) | move an operand slightly off tangency; check both with validateBrep first |
BudgetExceeded | the operation took longer than its budget | simplify, or give the caller a larger budget |
| RangeError from a primitive | non-positive size, or torus minor ≥ major | e.g. "torus minor radius must be smaller than the major radius" |
blend edge in rejected | face too narrow for the radius, knife edge or slanted wall | smaller radius, or a chamfer |
| STEP part missing faces | unsupported entity in the file | read warnings from readStepBreps |
| validateBrep reports issues | index, orientation or edge-on-surface error in a built body | the issue names the entity; nothing is repaired automatically |