cadthe system

open the viewer →

A parametric solid modeller whose model is a JSON tree, whose files are records in your own repo, and whose first-class user is a machine.

Everything on this page is built and live unless it carries a not yet tag. Numbers are the ones the tests assert. The design record with the reasoning is docs/CAD.md; this page is what it became.

No files: MCP

Check, build, measure, what the mechanism does, export, read anyone's files. From any agent that speaks the protocol.

claude mcp add --transport http cad https://cad.mino.mobi/mcp

Four megabytes: the mirror

The whole package, node only. The skill loads on clone; the full loop including interference and render.

git clone https://tangled.org/morphyxmino.bsky.social/cad

A browser: the viewer

The judgement surface. Parts, assemblies, the report, named faces, measure, interference, files, sign-in.

cad.mino.mobi · SKILL.md · llms.txt · CHANGELOG.md

1. The system specthe one decision, and what follows

The model is the feature tree. Geometry is a cache. A part is a small, typed JSON document: parameters, sketches on planes, operations over them. The engine turns it into solids; the solids are never the source of truth. Because the tree is canonical it fits in a record, it diffs, an agent can write it, and every build is a pure function of it, so the kernel behind that function is replaceable.

{
  "$schema": "com.minomobi.cad.tree#v1",
  "units": "mm",
  "params": { "R": 20, "t": 1.5, "r_pivot": 0.16, "r_pivots": 12, "n_pivots": 5, "r_centre": 0.5 },
  "features": [
    { "op": "sketch",  "id": "outline", "loops": [
      { "name": "rim",    "circle": { "c": [0, 0], "r": "R" } },
      { "name": "centre", "circle": { "c": [0, 0], "r": "r_centre" } } ] },
    { "op": "sketch",  "id": "pivot", "loops": [ { "circle": { "c": ["r_pivots", 0], "r": "r_pivot" } } ] },
    { "op": "pattern", "id": "pivots", "of": "pivot", "kind": "circular", "count": "n_pivots", "name": "pivot" },
    { "op": "extrude", "id": "plate", "profile": ["outline", "pivots"], "depth": "t" }
  ]
}

What is load-bearing

Operations

opdoesnames
sketchclosed loops on a plane (XY/XZ/YZ, offset, or <extrude>.end): circle, rect, polygon, a path of lines, arcs (via), cubic Béziers (ctrl) and spline segments through points, or a closed spline loop (Catmull–Rom, one exact cubic per span)loop name → id.name[k]
patterncircular or linear copies of a sketch, giving a sketchinherits
extrudea profile (one sketch or a list) by depth; mode new / add / cut / intersectstart, end, side[k]
revolvea profile about an axis in sketch coordinates; profiles may touch the axisside[k], caps
gearthe exact involute from m, z, alpha, b, bore: three Bézier pieces per flank, chord-length parametrisedtooth[i].flank.l/r, tip, root[i], bore[k]
booleanunion / cut / intersect of bodies (prefer regions)kept from operands
fillet chamfer shelledge and face ops; unsupported in Truck, done by OCCT (§3)reference faces cross the seam

Assemblies

A document with components is an assembly. Each names a part: an inline tree, bench:<name>, or the AT URI of a published part (its head, or a revision to pin a version), with optional params overrides that make a distinct build, a placement (at, rotate) and a phase; or it holds a nested assembly, flattened with prefixed ids (stage2/arbor). mates are gear (za, zb) and fixed; drive is {component, rpm} or an escapement. Angles are a kinematic chain from the driven component, gear phases are set automatically, and it is not a constraint solver: placements are yours to get right and the interference check tells you when you have not.

Placements are expressions. A document may carry params (the tree's own language, any order) and derived, a second map resolved in dependency order at each instant (any order — a PDS hands keys back sorted) with two reserved variables, t in seconds and theta, the driven component's angle in degrees. Every at element, rotate.deg, rotate.axis element and the drive's numbers take a number or an expression over them; component parameter overrides are bound in the assembly's scope when they can be. That is how a lead screw moves its nut and a crank its slider without a mate for either: the pose math lives in the document, check.mjs --t and spin sweep the real motion, and the interference check stays the safety net. The evaluator is lib/expr.js, a mirror of the engine's, held to the bit by a test. bench/crank.json is the worked example.

Mates carry travel as well as turning. gear and belt turn the other member by a ratio (opposite and same sense), fixed carries turn and travel, screw moves a nut by lead per turn along its axis, rack moves a rack by r·θ, slider passes travel on by a ratio; each works in either direction from the drive. A component's pose is its placement, then its travel in its own frame, then its turn about its own z. Repeat (repeat: 4, with i in scope) makes four bolts one component, and place by feature (at: "@platform.pivot[i]", rotate.align) puts each on a named face of another component — a bore's centre and axis from the exact kernel's geometry — and follows that component through its motion. bench/lift.json is the lead-screw lift that proves the three together; check.mjs --sweep checks a whole cycle.

Drawings, from the same mesh. lib/drawing.js writes an engineering drawing as SVG with no kernel and no browser: third-angle views, hidden lines found by casting a ray from each piece of each edge toward the viewer through a BVH of every body, the overall dimensions, and every cylindrical hole called out by count, diameter and depth when blind — a bore's four exact arcs grouped back into one hole by their shared axis, a surface that faces away from its axis read as a boss and not called out. The page's drawing button, agent/drawing.mjs and the MCP drawing tool all produce the same bytes, so two drawings of one tree diff cleanly. A part sheet is dimensioned inside as well: the engine names each face after the sketch loop it came from, so the features come back grouped, and every hole centre and pocket edge gets an ordinate from a datum at the part's corner, with an evenly spaced run of holes collapsed into one pitch line and each named loop called out by name and size.

An assembly is a document, not a picture. lib/report.js writes one self-contained HTML page for an assembly: the three views, an exploded isometric with a numbered balloon on every item, a parts list with quantities and volumes, a drawing of each distinct part, a Motion section (a rate curve per input with the dead points marked, and a table of rate, mechanical advantage, travel and turn), and the assembly steps — with a link into the viewer on every row, so whoever receives it can open the live thing. The steps are derived: a placement, a @component.face reference, a mate and its numbers, a declared fit. Nothing is inferred, and a test asserts no step contains a sentence the document does not state.

More than one input, and a grid rather than a period. A drive is the input that runs with time, and for a clock that is the whole story. A gripper that grips and rolls has two independent inputs, and its interference question is two-dimensional: does anything touch anywhere in grip × roll? So a document declares inputs — named axes of its own motion, each with a range, in scope by name in every expression — and revolute and prismatic joints consume one (two jaws opening from one input are two prismatic joints, one with scale: -1). --grid enumerates every combination and reports the state where each pair came closest, because a path through that space is not a proof: it visits a curve and says nothing about the corners it misses, and the corners are exactly where a jaw at full stroke meets a post at one angle of the wrist. A component placed on another's face with rigid: true takes that component's whole pose, so it can ride it and move relative to it — which is what makes the joints enough, and placement expressions over the drive unnecessary.

A fixed mate means bolted, not "may be the same solid". A touch the mates imply is expected up to a budget — a cubic millimetre of shared volume (or a thousandth of the smaller part) and a tenth of a millimetre of depth — and past it the pair is a collision like any other; a real press fit declares its own with interfere: {max, depth}. A document's fits pair by index rather than by cross product ([*] on both sides means the same index, over: {k: n} walks one), a fit naming a component that does not exist is an error rather than silence, and a sub-assembly's fits reach the top like its mates. A component placed by an expression over the drive that also carries a mate is driven twice, and the check refuses it.

What it DOES: rates, mechanical advantage, effort, dead points. Interference says nothing touches; it cannot say what the thing is for. lib/mechanism.js differentiates the poser against each input — two poses per number, no geometry, no kernel — and every velocity ratio in the assembly falls out; the mechanical advantage is the reciprocal of a ratio, by virtual work, and the effort at an input that holds a load is Σ F·∂p/∂q, in newtons for an input in millimetres, newton-metres for one in degrees, watts for a drive. A rate passing through zero is a dead point: self-locking, infinite advantage, and a toggle seen from the other side. The distance between two components and its rate turn an author's hand-written oracle into one number — “the link is a link” and “rolling does not change the grip” are both d(distance)/d(input) = 0. It is lossless: friction, preload and backlash are not modelled, so an effort is a floor and not the answer; and it asks only about degrees of freedom the document already has, so it is not a constraint solver, not contact statics not built and not FEA not built. agent/mechanism.mjs, the MCP mechanism tool and the report's Motion section are the same instrument.

Clearance, not only collision. lib/proximity.js measures every pair's nearest approach from the exact meshes with no kernel at all — a BVH, exact triangle distances, crossings, containment, a penetration depth, contact told from collision — and lib/sweep.js chases each pair's minimum between the sweep's samples, so a graze between two instants is found. check.mjs --clearance 1 prints the table a reviewer reads first with a verdict per pair — collision, contact, expected, fit, close, loose, clear; a document's fits declare the clearances it intends so a running fit is judged on its own numbers; because it needs no kernel, the MCP interference tool runs it on this server. measure takes an assembly and two component.face names at a time t. A sweep on the server is windowed against a budget counted in work rather than time — a Worker freezes its clock during synchronous execution, so wall-clock budgeting cannot work — and it answers with the instants it managed, the one to resume from, and whether parts are still to build; exact meshes are cached between calls. An assembly whose single instant is past the budget is refused with its numbers and the suggestion to coarsen res or run locally, rather than killed mid-request.

Clearance, continued. Refinement only runs for pairs within four times the clearance being demanded, so a sweep pays for the pairs that could graze and not for the ones ten millimetres apart. A component marked reference is drawn translucent and left out of checks and export.

That is the front plate of the clock, minus its three pillar holes: a disc with a centre hole and five pivot holes on a circle, no booleans, and it builds in a hundred milliseconds with 42 named faces. The reference assembly is a mechanical clock: a three-stage going train, a lever escapement as the drive, motion works, hands, dial and case, eighteen components from nine parts with parameter overrides. It ticks: sixty beats turn the escape wheel two revolutions and the minute hand one minute, to the fourth decimal.

2. The pipelinewhat happens between an edit and a picture

feature treeJSON, params, names engine · resolveRust → wasm, 1.7 MBexpressions, regions, gears Manifold · previewmesh booleans, millisecondspolygons, no names Truck · exactB-rep, named faces + geometrysweeps fast, booleans weak OCCT · on demandfillets, the booleans Truck fails66 MB, lazily, cached mesh + face idsweld · invariants · edgesWebGL2 renderer, id-buffer pickreport · measure · STL · STEPinterference (Manifold)the numbers you judge by one build request answers twice: preview, then exact. a newer edit supersedes an older build in between. drag a parameter and the preview follows at frame rate.
The engine resolves; two kernels build; the renderer, the report and the tools all read one welded mesh with a face id per triangle. In the page this runs in a module Worker off the main thread. Under node the same modules run from files. On the worker at /mcp the engine runs alone (§5).

The engine is a Rust crate compiled to wasm32-unknown-unknown with a raw C ABI, no wasm-bindgen: cad_alloc, cad_resolve, cad_build, cad_out_ptr, cad_out_len, cad_free_all, and one host import for the clock. The same bytes run in the browser, under node, and inside a Cloudflare Worker. The compiled cad.wasm is committed; engine/build.sh rebuilds it and runs the selftests.

3. Kernel jugglingthe bake-off, and the rules it produced

No open kernel that runs in wasm does everything. Rather than marry one, the tree is built behind a seam and a bake-off measured four candidates on the clock parts, with STEP round-trip as a mandatory column. The rules below are what it decided; the table is what each kernel is for.

kernelrolegood atcannotwhere
Manifold 3.5preview, interference, export fallbackmesh booleans in milliseconds; always builds; watertight by constructionno exact geometry, no face names, polygonspage worker · node not the /mcp worker
Truck 0.6 (Rust)exact, firstsweeps, revolves, splines as exact Béziers, named faces with geometry, STEP out; the plate in 100 msmany booleans (boolean union failed), no fillets; the 60-tooth crossed gear takes ~17 severywhere: page, node, /mcp
OCCT 7.4 (opencascade.js)exact, on demandfillets, chamfers, shells, every boolean; the escape wheel in 1.6 s cold; STEP the world trusts66 MB, so not shipped: imported from unpkg after one press of exact with OCCT; not in the node scripts yetpage worker · bake-off harness
implicit SDF spikesimulation representationuniform fields for a future stress pathnot a modelling kernelengine, behind a flag

The rules

Two adapter defects surfaced in the bake-off and were fixed in the harness as well: OCCT's outer-versus-hole decision must be taken over an op's combined profile, so the engine emits per-op oriented rings; and the involute's Bézier fit is chord-length parametrised, because the uniform fit folded at the base circle.

4. The data modela file tree over records, on the designer's own repo

A person's parts live in their ATProto repo as records. Two collections make a filesystem with history, the way a git tree is a view over blobs:

recordholdsmutability
com.minomobi.cad.partpath, name, kind (part or assembly), head: a strongRef to a revision, createdAt, updatedAta head: it moves
com.minomobi.cad.revisiontree, parents[] as strongRefs, createdAt, message, the kernel that built it and the invariants it was judged by, forkedFromimmutable
alice's repo part · clock/train/gearhead → rev C rev Cz 60 → 72 rev Bbore 1 → 1.2 rev Afirst cut bob's repo part · parts/gear-72head → rev D rev Dfork of alice's parents are strongRefs — a URI and a content hash — so a parent may sit in another repo. that is a fork: history walks straight across the boundary, and alice's next edit cannot break bob.
Rename edits the path and the URI survives, so a machine's reference outlives a human's tidy-up. The PDS keeps no history of its own, which is why revisions are explicit records.

Rules learned from the first real write

One drive, four backends

lib/drive.js is one class over a five-call contract: getRecord, listRecords, createRecord, putRecord, deleteRecord, the ATProto repo methods and nothing else. list, tree, get, put, rename, history, fork, push and remove are the same over all of them.

backendwherewho
memorya Map with a persist hook; the node CLI keeps a whole repo in ~/.cad-drive.jsontests, agents
localIndexedDB, did:local; survives reload; no sign-inevery visitor
publicany repo over the two public XRPC reads; in the page through this site's gateway (§5)anyone, no sign-in
auththe signed-in user's repo through auth.mino.mobi, scope atproto repo:com.minomobi.cad.part repo:com.minomobi.cad.revisionthe designer
sessionan app-password session, for terminals and scripts; never a browsera local agent, the publisher

Addresses

5. The backend, such as it existsdeliberately small

There is no database and no server that knows about your parts. The site is a static package served by one Cloudflare Worker, and the Worker does three things:

What is around it

piecestatuswhat it is
auth.mino.mobilivethe shared OAuth worker for every mino.mobi site (PKCE, DPoP, PAR, private_key_jwt). It holds the tokens and proxies writes, so the page never holds a PDS token. The two cad collections are in its scope ceiling; sign-in asks for exactly those.
the service accountlivethe identity that owns minomobi.com. A workflow publishes the bench into it on every change, idempotently: an unchanged tree is skipped, a changed one becomes exactly one revision.
the mirrorlivethe package force-pushed to tangled on every push, minus the Rust build directory and the deployment files, with the skill in place. Four megabytes; runs its selftests before it pushes.
a geometry cachenot yetthe design record's content-addressed cache of built solids by tree hash. Everything rebuilds from the tree today, which is fast enough at these sizes and is the honest place to start.
/parts/livethe social layer, a second worker mounted here through a service binding (the zone is at Cloudflare's hundred-custom-domain ceiling, so it has no host of its own): communities, posts, comments and votes as records in their authors' repos, indexed by one Durable Object that rebuilds from the network (self-reports plus Constellation backlinks, no firehose). A post points at a revision, so what people voted on cannot change.
content-negotiated facesnot yet/at/<did>/<rkey>.stl, .png: the part as a URL you can paste into a post or a purchase order. The gateway is the first step.

A push to this package's branch deploys it; there is no staging. The deploy runs the node selftests first and verifies the live host answers afterwards, and the surface's own notes say green is not proof: the log must bind the custom domain.

6. The headless testswhat green means here

Every claim above is gated by a test that runs under node or in headless Chromium, and the deploy runs the node ones before it ships. The tests assert invariants with tolerances against closed forms, never pixels or mesh bits.

testrunsasserts
cad.selftest.mjsthe committed wasm, from bytes, under nodethe ABI; the gear's volume against its profile area; the plate against its closed form; Euler characteristics; watertightness; that names come out. 21 checks.
assembly.selftest.mjsthe engine and the committed wasmnineteen expressions and ten error messages evaluated by lib/expr.js and by the engine agree to the bit; the crank–slider's rod and block against the closed form at six instants; sub-assembly scopes; theta from an escapement; static documents unchanged; the six mates in both directions; the lift's nut, platform and bolts by repeat and reference, a bolt on a tilted plate aligned to its bore, a bolt on a turning plate orbiting; errors that name the component and the field; proximity on cube pairs and the lift's meshes, and a sweep that finds a 1 mm graze between eight samples.
mechanism.selftest.mjsthe poser alone, no kernelthe virtual-work instrument against numbers a person can derive on paper: a prismatic joint moving its follower one for one and nothing else; a jaw at radius 6 and 18 moving exactly r·π/180 per degree; a span rate of zero as an invariant through the roll; 100 N at a jaw 18 mm out needing 1.8 N·m at the wrist, and 50 N on each jaw 100 N at the input; a crank–slider's block against the derivative of r cosθ + √(L² − r²sin²θ) at three instants to 2e-5, and its two dead points at top and bottom centre; travel integrated over a whole input; and the clock's escape wheel stepping two of thirty teeth a period while the minute hand advances twelve times the hour hand — read from two end poses, because uniform sampling cannot integrate an escapement. 16 checks, 18 components over 24 states in 15 ms.
drawing.selftest.mjsthe committed wasm, under nodethe plate's nine holes recovered from their thirty-six arc faces and called out as 3× ⌀1.2, 5× ⌀0.32, ⌀1; the case's interior read as one blind bore ⌀42 ↧7 while its rim is not a hole; the overall dimensions written on the sheet; hidden lines where the bores are seen through the plate; views on demand and an unknown view refused; the lift posed at t = 0.5 s as seven bodies on one sheet; and the same input drawing the same SVG twice.
report.selftest.mjsthe committed wasm, under nodethe lift's parts list and quantities; four steps with the repeat collapsed into one; the nut's step carrying the screw mate's own lead and the declared 0.05–0.15 mm fit; no step containing a word the document does not state; a gear mate stating its ratio in either direction; the exploded view turning seven touching pairs into none; and one document giving one page, byte for byte.
agent/audit.mjsa published repo, on every publishevery part rebuilt and diffed against the invariants its revision recorded — volume, χ, watertightness, face count — and Truck against Manifold on volume: the whole corpus as the kernels' regression suite.
drive.selftest.mjstwo in-memory repos and a fake PDSput, history, rename, fork with lineage across repos, push with cid rewriting, no float survives in a stored record, the gateway resolves a handle and refuses other collections.
mcp.selftest.mjsthe tool surface with kernels injectedthe transport (initialize, notifications, batches, error codes), every tool against the bench, the sliced assembly build, and the reduced tool list a host without Manifold advertises.
browser.selftest.mjsthe page in headless Chromium, served under the production headersfirst with no script injection at all, that the worker comes up and the exact build lands under the real policy; then every bench part, the plate against its closed form, picking a named face at the centre of the view, measure, a parameter edit growing the volume, the clock's ratios and escapement to the fourth decimal, interference clean mid-beat, the crank–slider's block where the closed form puts it while spinning, touch gestures, the phone layout with an on-screen keyboard taking half the screen, the interference check reading a mated touch past its budget as a collision, a gripper's two inputs each getting their own control and the jaws riding the rotor as it rolls, the header picker and the document panel naming what is loaded, a revision landing in the repo reaching the open assembly by itself (and an edited one being offered the update instead), OCCT from a locally served copy, save and reload through IndexedDB, a stranger's file opened through a mocked gateway and forked, an assembly whose parts are AT URIs, STEP and STL export.
preflightthe monorepo's own gatethe registry, the generated files, the synced copies (the skill, the auth client), catalogue coverage, the loop's blast radius, and the selftests of changed directories.

The verification habit

After a deploy the live host is curled: the wasm's content type, the gateway with a real handle, a real PAR from this origin against the auth worker with the cad scope, a build through /mcp. The bench is the golden set; an agent working on its own part is told to write its own expected.json and rerun it after every edit.

Report what watertight and χ say, not what the picture looks like. That sentence is in the skill, in the tests, and in the report's layout.

7. Agentic accessthree doors, one loop

The loop is the same everywhere: write tree → build → measure → check → render → judge → edit. What differs is how much you have to install.

Door one: nothing. MCP at /mcp

Streamable HTTP, JSON responses; GET returns the descriptor. Tools on the live host: check, build, measure, mechanism (rates, mechanical advantage, effort, dead points — no geometry, so it is free here), interference (clearance mode: nearest approach per pair, no volumes), drawing (an SVG drawing as an embedded resource), report (a whole assembly report as one HTML page), step, list_files, get_file. Every tool takes a tree object, bench:<name> or an at:// URI. build returns the invariants, every named face with its geometry, and a viewer link to hand a human; on an assembly it builds three parts per call and returns partKeys and remaining, so you pass parts to continue.

curl -sX POST https://cad.mino.mobi/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"measure","arguments":{"tree":"bench:plate","a":"plate.rim[0]","b":"plate.pivot[2][0]"}}}'
→ {"kind":"cylinder-cylinder","parallel":true,"distance":12,"diameters":[40,0.32], …}

What is not on the server, and why: no render (a browser's job; the link is the picture), no preview kernel and so no shared volumes (Manifold's glue generates code from strings, which Workers forbid; interference runs there in clearance mode from the exact meshes instead), no write (a person's parts are saved with the person's own sign-in), no OCCT. The descriptor says all of this so an agent does not have to discover it.

Door two: four megabytes. The mirror and the skill

Clone the mirror; Claude Code loads .claude/skills/cad/SKILL.md on open. Node 22 is the only requirement; the engine is the committed wasm. The full loop: agent/build.mjs, measure.mjs, check.mjs, export.mjs, render.mjs (headless Chromium, one install), and drive.mjs for files, including --login with an app password to write to your own repo and hand back a ?at= link.

Door three: a browser

An open page does not go stale. A document on screen is a photograph of records — its own part head, and the head of every part it references by AT URI. The page re-reads them every twenty seconds and whenever the tab comes back; a document nobody has edited on screen is reloaded in place, camera and all, so a revision saved from anywhere — by the designer, by an agent, by a publish workflow — reaches an open page with no reload and no signing in again. A document that has been edited on screen is told instead, and offered the update; a component pinned to a revision URI is never followed, and the panel counts those. The document panel says what is watched and when it was last checked, and the header picker lists the document on screen, the bench, and every assembly in each repo the files tab has open.

The viewer is the judgement surface for the human at the other end: the report with preview and exact side by side, the face under the cursor with its names and geometry, pin one face and click another to measure, section — a plane taken from the pinned face, pushed through the part by the pan gesture while orbit and zoom keep working, with the id pass clipped alongside the picture so a pick lands on what the eye can see — check interference through the motion, three views for the record, STL and STEP, the files tab with save, history, fork and push. A link from an agent opens here.

Reading

8. What it is not, yetsaid plainly