Block-based CAD that runs in your browser — with a REST API, live WebSocket updates, and a built-in MCP server so Claude Code can drive it end-to-end: build models, inspect geometry, render previews, and export STL and STEP.
cadgang carries two geometry representations in one node graph: exact B-rep solids (OpenCascade — real fillets, real STEP out) and implicit distance fields (lattices, TPMS infill, smooth blends, drape). They meet at a deliberately one-way bridge.
A modeling session — building a gyroid-latticed spikeball from scratch in the node editor (mp4 in the repo):
demo.mp4
cadgang sits in the lineage of functional-representation CAD: kokopelli, Matt Keeter's Python-scripted f-rep CAD/CAM tool (and its successors Antimony and libfive), and the implicit-modeling approach nTopology built a company on. Where kokopelli describes models as code and nTop as a graph of implicit operations, cadgang does both: models are graphs of blocks evaluated as signed distance fields (SDFs), and every graph compiles down to a one-line functional formula (shown live in the footer). Because geometry is a function, not a boundary mesh, booleans never fail, shells and offsets are exact, and TPMS lattices are a single block.
| Raymarched preview | Gyroid lattice infill |
|---|---|
![]() |
![]() |
Requires Node.js ≥ 18 and a modern browser. No build step — the web app is plain ES modules.
git clone https://github.com/cheewee2000/cadgang.git
cd cadgang
npm install
npm start # → http://localhost:4477Then:
npm run demo # builds a gyroid-filled demo part via the REST API
npm test # 80 kernel/API unit testsOpen http://localhost:4477 — the viewport live-updates (WebSocket) whenever the model changes, whether from the UI, the REST API, or Claude via MCP. The model autosaves to data/document.json, so state survives restarts and the UI and MCP always share one model. CADGANG_PORT and CADGANG_DOC select an alternate port/document for scratch instances.
- Double-click the graph to add a block, drag between ports to wire, drag blocks to move, right-drag to pan, scroll to zoom, marquee-drag to multi-select, ⌘C/⌘V copy/paste, ⌘Z undo, Arrange for a tidy dependency layout
- VARS bar — define named variables (
w = 60); any numeric param accepts an expression (w/2 + 3) that re-evaluates when the variable changes - STACK / SIDE toggles the graph/viewport split between stacked and side-by-side; DARK toggles the theme; COLOR switches per-part color vs. stainless render
- Save / Open stores named models server-side (
saves/) - Click the footer formula to see the whole model as a nested functional expression
The repo ships a .mcp.json, so opening it in Claude Code auto-registers the cadgang MCP server. To register manually:
claude mcp add cadgang -- node /path/to/cadgang/src/mcp/index.jsThe cadgang web server must be running (npm start). Set CADGANG_URL if it's not on http://localhost:4477.
| Tool | What it does |
|---|---|
cadgang_list_node_types |
Discover every block type, its params and input slots |
cadgang_get_document |
Read the full model graph |
cadgang_create_node / cadgang_update_node / cadgang_delete_node |
Edit the graph |
cadgang_set_output |
Choose which block is meshed/exported |
cadgang_clear_document |
Wipe the model (destructive) |
cadgang_undo |
Undo (or redo) the last model edit |
cadgang_import_step |
Import a STEP/IGES/BREP file from disk as an imported_mesh block |
cadgang_export_step |
Write an exact STEP B-rep file (refuses field geometry) |
cadgang_eval_sdf |
Sample signed distances at points (thickness/clearance checks) |
cadgang_mesh_stats |
Triangle count, volume, surface area, bounds, and whether the result is exact |
cadgang_export_stl |
Write a binary STL to exports/ |
cadgang_render_preview |
Server-side raymarched PNG — Claude can see the model |
Ask Claude Code things like: "Build a 60×40×24 mm rounded enclosure with a 2 mm wall, fill it with a 9 mm gyroid lattice, show me a preview, and export it for printing."
A block belongs to one of two representations, and the block's colour tells you which — blueprint blue for exact, machined metal for fields.
| B-rep (exact) | Fields (implicit) | |
|---|---|---|
| Geometry is | trimmed analytic surfaces + topology | a function d(x,y,z) |
| Meshed by | OpenCascade tessellation | surface nets over a grid |
| Exports to | STEP (and STL) | STL |
| Good at | precise dimensions, real fillets, machining handoff | lattices, TPMS infill, smooth blends, drape |
| Can't do | lattices, field blends | exact fillets, exact circles, STEP |
A B-rep solid can feed any field block. cadgang derives the distance field from the exact solid automatically, so you can fillet a part exactly and then fill it with a gyroid.
A field can never go back. Recovering exact trimmed surfaces from a distance field is a fitting problem, not a conversion, and it fails outright on the blends and lattices fields are best at. So the moment a field block touches a shape, that branch loses its B-rep and becomes STL-only — and export_step refuses it with an explanation rather than writing a faceted mesh into a .step file that no CAD kernel will fillet.
Practically: keep the whole chain in B-rep blocks for anything that has to ship as STEP, and branch into fields at the end.
- Sketches —
sketch_rect(with corner rounding),sketch_circle,sketch_polygon,sketch_profile(authored point list; a third number on a point rounds that corner). Each sits on a plane (XY/XZ/YZ/…) at anoffset. A sketch is a 2D profile, not a solid — extrude or revolve it. - Solids —
brep_box,brep_cylinder,brep_sphere,brep_extrude(withsymmetricto centre on the sketch plane),brep_revolve - Operations —
brep_boolean(union / subtract / intersect, computing the real intersection curves),brep_fillet(true rolling-ball fillet),brep_chamfer,brep_shell,brep_transform - Output —
export_step(pass-through sink with a download button)
Edge selection on brep_fillet/brep_chamfer is all or the edges running along x/y/z. Viewport edge picking is not wired up yet — see Limitations.
- Primitives —
sphere,box(with rounding),cylinder,torus,capsule,plane,gyroid,schwarz_p(TPMS lattices),polyhedron,spiky_sphere,imported_mesh(STEP/IGES import),extrude_face(extrude a selected surface of an import) - Booleans —
union,intersect,subtract,smooth_union,smooth_intersect,smooth_subtract(blended fillets) - Modifiers —
shell(hollow to wall thickness),offset,transform(translate / rotate / scale),drape(vacuum-form a sheet over shapes, with smoothness control),linear_array,polar_array - Output —
export_stl(pass-through sink with a download button; params: filename, resolution). Fed by an exact solid, it tessellates the real surfaces instead of remeshing the field — smaller and more faithful.
Units are millimeters, world is Z-up. plane/gyroid/schwarz_p are unbounded fields — intersect them with a bounded body (that is how lattice infills are made).
Honest about what this is not, yet:
- No interactive sketcher. Profiles are authored as numbers (
sketch_profile's point list), not drawn on a plane with dimensional constraints. A real sketcher needs a constraint solver — FreeCAD'splanegcscompiled to WASM is the usual answer — and would write into this samepointsparam. - No viewport edge/face picking for fillets.
brep_filletselects by direction, not by clicking an edge. The tessellation already ships per-face B-rep ids, so the data is there; the UI is not. brep_revolveis full-turn only. Partial sweeps need a wedge cut that isn't built yet.- STEP import is still tessellated (see below), so an imported file enters the field lineage and cannot be filleted or re-exported as STEP. Exact B-rep import is the obvious next step — OpenCascade's
importSTEPis already linked in. - The OCCT heap creeps. Compiling B-rep blocks grows OpenCascade's WASM heap by roughly 100 MB per 800 mesh requests and it never shrinks. cadgang forces a GC after each burst of B-rep work, which reclaims what replicad's JS wrappers hold (16 → 40 MB becomes 16 → 19 MB over 300 boolean compiles), but the remainder is inside OCCT's own allocator — collecting after every operation does not change it.
GET /api/healthreportsbrep.heapBytesso you can watch it; restarting the server clears it. Running the kernel in a recycled worker thread is the real fix. - Fillets fail on tangent seams. Filleting the vertical edges of an already-rounded profile asks OCCT to fillet a tangent seam and it refuses. The error says so in OCCT's own words.
Upload a .step/.stp (or IGES/BREP) file — Import STEP in the web UI, POST /api/import/step, or the cadgang_import_step MCP tool. The file is tessellated (WASM OpenCascade via occt-import-js), welded, stored as a document asset, and exposed as an imported_mesh block with an exact BVH signed-distance field. Each B-rep face of the import stays addressable as a triangle range, so surfaces are selectable in the viewport — click one to spawn an extrude_face block.
drape drops a virtual sheet straight down (−Z) over its input shapes, like vacuum forming: it raycasts a top-surface heightfield at compile time, smooths it with a rolling-ball (parabolic) dilation of radius blend, and shells the result to thickness. blend controls how tightly the sheet wraps — 0 hugs every crease, larger values bridge gaps and round shoulders. floor sets where the skirt ends, margin how far the sheet overhangs.
| Endpoint | Description |
|---|---|
GET /api/node-types |
Block type catalog |
GET /api/document |
Model graph + revision |
POST /api/nodes · PATCH /api/nodes/:id · DELETE /api/nodes/:id |
Graph editing |
POST /api/document/output |
Set output block |
POST /api/vars · DELETE /api/vars/:name |
User variables (usable in param expressions) |
POST /api/eval |
Evaluate SDF at points |
POST /api/undo · POST /api/redo |
Step the edit history (last 100 steps) |
GET /api/files · POST /api/files/save · POST /api/files/load · DELETE /api/files/:name |
Named model save/open |
POST /api/import/step?name=file.step |
Import STEP/IGES/BREP (raw body) → asset + imported_mesh node (&node=id attaches to an existing block instead) |
GET /api/assets · GET /api/assets/:id · DELETE /api/assets/:id |
Imported mesh assets (:id returns full triangles + per-face ranges) |
GET /api/mesh?resolution=90 |
Surface-nets mesh (JSON) |
GET /api/mesh/stats |
Stats only |
GET /api/export/stl?resolution=128&file=name |
Binary STL (exact tessellation when the chain is B-rep) |
GET /api/export/step?file=name |
STEP B-rep file; 400s with an explanation if the chain crossed into a field |
GET /api/preview.png?yaw=-35&pitch=25&node=id |
Raymarched preview (any block, not just the output) |
ws://…/ws broadcasts {type: "document_changed", revision} on every edit.
src/core/ geometry kernel — pure JS, no server dependency
sdf.js merged block registry, graph compiler ({fn, bbox, brep}), bbox propagation
brep.js exact B-rep kernel: OCCT/replicad lifecycle, ops, tessellation,
STEP I/O, shape-memory scopes, and the one-way bridge to SDF
brepnodes.js exact block definitions (sketches, solids, booleans, fillets)
errors.js GraphError, split out so brep.js and sdf.js can share it
expr.js safe expression evaluator for variable-driven params
mesher.js naive surface-nets mesher (watertight, SDF-gradient normals)
mesh.js mesh utilities: welding, BVH signed distance, per-face ranges
step.js STEP/IGES/BREP tessellation (WASM OpenCascade)
stl.js binary STL writer
render.js CPU sphere-tracer + dependency-free PNG encoder
document.js persistent model document, undo history, autosave
src/server/ Express REST API + WebSocket + static hosting
src/mcp/ cadgang-mcp-server (stdio, @modelcontextprotocol/sdk)
web/ Three.js viewport + node-graph editor (no build step, no framework)
- kokopelli → Antimony → libfive — Matt Keeter's f-rep CAD tools, the reason this way of thinking about geometry exists in open source
- nTopology — implicit modeling at industrial scale; the gyroid-infill demo is their party trick
- three.js (MIT, vendored in
web/vendor/) · occt-import-js (OpenCascade WASM) · replicad + opencascade.js (the B-rep kernel, MIT/LGPL) · Space Mono (SIL OFL 1.1, license inweb/fonts/OFL.txt)
Built by CW&T with Claude Code.


