This is the full developer documentation for CAD.
# Start of CAD documentation
# Roadmap
## v0.1 — Direct Modeling Foundation
- [x] WebGPU-based 3D viewer with truck B-Rep kernel (WASM)
- [x] 4 primitives: cube, sphere, cylinder, torus
- [x] Boolean operations: union, subtract, intersect (cubes/cylinders only)
- [x] Translate transform
- [x] Save/load scenes as JSON (full B-Rep, no tessellation loss)
- [x] Responsive UI: desktop sidebar, mobile bottom sheet + dock
- [x] Auto-offset primitives — new objects partially overlap, ready for booleans
- [x] Docs: auto-generated screenshots + lesson videos (R2-hosted)
- [x] Deployed to Cloudflare Workers + custom domain
## v0.2 — Undo/Redo + Gizmo
- [x] **UUID identity** — every object has a stable UUID v4, persisted through transforms and export/import
- [x] **Undo/redo** — snapshot-based with Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts
- [x] **Operation grouping** — related operations (add + offset) grouped into single undo steps
- [x] **Timeline UI** — visual strip showing recent operations as clickable chips
- [x] **Click-to-select** — ray-cast picking via bounding sphere intersection
- [x] **Translate gizmo** — 3-axis colored arrows (X=red, Y=green, Z=blue), drag to move
- [x] **Gizmo cancel** — Escape reverses drag, restores original position
- [x] **Automerge integration** — CRDT-based op log for collaborative editing
- [x] **Cross-tab sync** — BroadcastChannel adapter for same-browser collaboration
- [x] **Document management** — new doc, share URL, example scenes
- [x] **Keyboard shortcuts** — Ctrl+Z undo, Ctrl+Shift+Z redo, Escape cancel, Delete remove
- [x] **24 E2E tests** — Playwright tests covering all operations including gizmo
## v0.3 — Parametric Modeling (Current)
- [x] **ezpz constraint solver** — integrated KittyCAD/ezpz for 2D sketch constraints (11 constraint types)
- [x] **Sketch mode** — 2D sketch on XY/XZ/YZ planes with points and edges
- [x] **Sketch constraints** — fixed, horizontal, vertical, distance, H/V-distance, coincident, parallel, perpendicular, equal length, midpoint
- [x] **Constraint solver** — Newton-Raphson solver via ezpz, live preview of solved positions
- [x] **Extrude** — 2D profile → 3D solid via truck `tsweep` (closed loop detection)
- [x] **Quick rectangle** — one-click constrained rectangle with auto-constraints
- [x] **Sketch export/import** — JSON serialization for Automerge replay
- [x] **Automerge sketch ops** — `sketch_extrude` in op log, collaborative replay
- [x] **31 tests** — 9 Rust unit (sketch), 11 golden resource, 11 Playwright E2E sketch tests
- [ ] **Rotate gizmo** — circular handles for rotate-around-axis
- [ ] **Scale gizmo** — square handles for uniform/non-uniform scale
## Medium Term
- [ ] **Boolean ops for all shapes** — fix sphere/torus booleans in truck-shapeops
- [ ] **STEP import/export** — leverage truck-stepio for industry-standard CAD interchange
- [ ] **kkrpc integration** — bidirectional RPC for server-side modeling operations
- [ ] **Mesh raycasting** — upgrade from bounding sphere to triangle-level picking
- [ ] **Snap-to-grid** — constrain drag/sketch to grid increments
- [ ] **Multi-select** — Shift+click to select multiple objects
- [ ] **RDK (robotics) integration** — CAD modeling with robot kinematics/planning
## Long Term
- [ ] **Assembly mode** — multi-part assemblies with mates/joints
- [ ] **Server-side rendering** — headless WebGPU for thumbnails and CI screenshots (Tier 3)
- [ ] **Revolve** — 2D profile → 3D solid via truck `rsweep`
- [ ] **Plugin system** — extend with custom operations via WASM modules
- [x] **Hugo docs site** — full documentation site on Cloudflare Pages
- [ ] **Mobile-first touch gestures** — multi-touch transform gizmos
## Known Issues
- **Sphere/Torus booleans fail** — truck-shapeops NURBS surface intersection crashes. Cubes/cylinders work.
- **Object size changes after translate** — bounding-box normalization rescales rendered objects. B-Rep geometry is correct.
- **Large translations go off screen** — camera doesn't follow objects. Keep values small.
- **Bounding sphere picking** — imprecise for elongated/flat objects. Mesh raycasting planned.
# Gizmo — WYSIWYG Direct Manipulation
Fusion 360-style direct manipulation: click objects to select, drag gizmo handles to transform.
## Interaction State Machine (Rust)
```
IDLE ──click object──> SELECTED ──drag arrow──> DRAGGING
^ │ ^ │
└──click empty───────────┘ └──mouseup / Escape─────┘
```
Three modes in `InteractionMode` enum:
- **Idle** — no selection, normal camera orbit
- **Selected** — object highlighted, gizmo arrows visible, camera still works
- **Dragging** — constrained axis movement, camera orbit disabled, live preview
## Picking
- **Bounding sphere** per object, computed from solid AABB
- `Camera::ray(ndc)` casts ray from screen coordinates
- Ray-sphere intersection finds closest hit
- Gizmo arrow picking via ray-to-segment distance
## Gizmo Geometry
- 3 colored `WireFrameInstance` arrows (X=red, Y=green, Z=blue) at selected object center
- Arrows scaled proportionally to camera distance (constant screen size)
- During drag: active axis highlighted, others dimmed
## Drag Mechanics
- Screen-to-world: project mouse NDC delta onto constrained axis via ray-axis closest-point math
- Live preview: `translate_object()` called incrementally during drag
- Cumulative delta tracked for final commit
- Escape reverses cumulative translation (cancel)
## JS ↔ WASM API
Public methods called from JS:
- `select_object_at(ndc_x, ndc_y) → JsValue` — pick and select
- `begin_gizmo_drag(ndc_x, ndc_y) → JsValue` — start drag if clicking gizmo arrow
- `update_gizmo_drag(ndc_x, ndc_y, prev_ndc_x, prev_ndc_y)` — live preview
- `end_gizmo_drag() → JsValue` — finish drag, return { objectId, dx, dy, dz }
- `cancel_gizmo_drag() → bool` — reverse translation
- `set_on_select(f)` / `set_on_drag_complete(f)` — JS callbacks
## Automerge Integration
On drag complete, JS commits a single `translate` operation to the Automerge op log.
No ops created during drag (live preview only). Undo reverses the committed op.
## Future
- **Rotation gizmo** — circular handles for rotate-around-axis
- **Scale gizmo** — square handles for uniform/non-uniform scale
- **Mesh raycasting** — upgrade from bounding sphere to triangle-level picking
- **Snap-to-grid** — constrain drag to grid increments
- **Multi-select** — Shift+click to select multiple objects
# Direct vs. Parametric Modeling
## Vision
We provide both modeling paradigms:
- **Direct modeling** (like SketchUp) — for architects, quick concept work
- **Parametric modeling** (like Fusion 360) — for mechanical engineers, precise constraint-driven design
## Direct Modeling (v0.1–v0.2)
Implemented features:
- Primitive creation (cube, sphere, cylinder, torus)
- Boolean operations (union, subtract, intersect)
- Gizmo-based translate transform (click-drag)
- Undo/redo with Automerge CRDT op log
- Scene save/load as JSON B-Rep
- Collaborative editing via Automerge + BroadcastChannel
## Parametric Modeling (v0.3 — Current)
### Constraint Solver: ezpz
We use [KittyCAD/ezpz](https://github.com/KittyCAD/ezpz) as the 2D geometric constraint solver.
- **Language**: Pure Rust, WASM-compatible
- **What it does**: Takes a set of geometric constraints and solves positions/dimensions
- **Constraints implemented**: fixed, horizontal, vertical, distance, H/V-distance, coincident, parallel, perpendicular, equal length, midpoint
- **Solver**: Newton-Raphson (Gauss-Newton with Tikhonov regularization)
- **Local path**: `.src/ezpz/kcl-ezpz` (Cargo path dependency)
- **Integration**: Wrapper types in `sketch.rs` bridge serde-serializable sketch types to ezpz's non-serde internal types
### Parametric Workflow
```
Sketch Plane → Points + Edges → Constraints → Solver → Extrude → 3D B-Rep Solid
```
1. **Sketch plane**: User selects XY, XZ, or YZ plane
2. **2D profile**: Add points with (x, y) coordinates, connect with edges
3. **Constraints**: Add geometric constraints (fixed, distance, horizontal, etc.)
4. **Solver**: ezpz solves the constraint system, returns solved positions
5. **Closed loop**: Edge graph is walked to find a polygon boundary
6. **Extrude**: truck's `tsweep` sweeps the 2D face along the plane normal
7. **Automerge**: `sketch_extrude` op stores full sketch JSON for collaborative replay
### Quick Rectangle
One-click helper creates a fully constrained rectangle:
- 4 points at (0,0), (w,0), (w,h), (0,h)
- 4 edges forming a closed loop
- 7 constraints: fixed origin, 2 horizontal edges, 2 vertical edges, 2 distance constraints
### Integration Points
| Component | Role | Status |
|---|---|---|
| ezpz (`kcl-ezpz`) | 2D constraint solving | Integrated |
| truck-modeling | vertex → line → wire → face builder | Integrated |
| truck-modeling `tsweep` | 2D face → 3D solid extrusion | Integrated |
| Automerge | Op log with `sketch_extrude` operation | Integrated |
| WASM | All runs in browser via WebAssembly | Integrated |
### What's Next
- **Arc/circle entities** — curved sketch geometry
- **Revolve** — `rsweep` for rotational sweeps
- **Face-based sketch planes** — sketch on existing solid faces
- **Feature tree UI** — visual history with re-evaluation
# Architecture Overview
CAD/spatial platform. truck B-Rep kernel + Automerge CRDT + isomorphic API. Runs everywhere.
## Three Layers
| Layer | What | Tech |
|---|---|---|
| HTTP API | Isomorphic across all targets | Hono + Zod |
| GUI push | Server → client updates | Datastar + SSE (no websockets) |
| WASM boundary | JS/TS ↔ Go/Rust WASM | kkrpc |
## Four Target Classes
| Target | Rendering | WASM Transport | Build Tooling |
|---|---|---|---|
| Browser (web) | WebGPU (Tier 1) | SharedWorker | standard web |
| Native webviews | WebGPU (Tier 1) | Web Worker | goup-util |
| CF Workers | none | direct call | wrangler |
| Bare metal | none (or headless) | stdio | bun/node/deno |
Native webviews: WKWebView (macOS/iOS), WebView2 (Windows), WebKitGTK (Linux), Chromium WebView (Android).
## Rendering Tiers
- **Tier 1 (browser-native)**: truck B-Rep kernel + wgpu compiled to WASM, renders locally via WebGPU. Zero server cost. This is the product.
- **Tier 3 (server-rendered video)**: same Rust binary running natively on a GPU server, streaming H.264 video via WebRTC (LiveKit). Works on any device. Demo/fallback only.
- No Tier 2. Binary decision: can the browser handle WebGPU? Yes → Tier 1. No → Tier 3.
## State and Sync
- **Automerge**: CRDT op log for collaborative editing. Runs as WASM on browser + CF Workers + bare metal.
- **R2 + D1**: Persistent storage on Cloudflare.
- **NATS JetStream**: Real-time sync of Automerge ops between participants.
## What Runs Where
| Module | Browser | CF Workers | Bare Metal | GPU Server (Tier 3) |
|---|---|---|---|---|
| Automerge (Rust WASM) | Yes — CRDT state | Yes — sync/merge/R2 | Yes — sync | native Rust |
| truck (Rust WASM) | Yes — B-Rep + WebGPU | NO | maybe — headless | native Rust |
| Go business logic | Yes — validation | Yes — API logic | Yes | native Go |
truck does NOT ship to CF Workers. No rendering on CF.
## WASM Compilation
Two builds, not three. Browser + CF share `wasm32-unknown-unknown`. Bare metal gets `wasm32-wasip1`.
- Rust: cargo + wasm-bindgen + wasm-opt
- Go: TinyGo only (standard Go too large for CF/browser)
- Automerge: start with npm package, link Rust crates later if needed
## Key Decisions
- No websockets. Datastar + SSE for data push. WebRTC (LiveKit) only for Tier 3 video.
- kkrpc for all JS ↔ WASM communication. Same typed API, transport swapped per target.
- SharedWorker in browser (one WASM shared across tabs). Web Worker in native webviews.
- Design to CF Workers limits (3 MB compressed, 128 MB memory, 1s startup).
- Thin JS ↔ WASM boundary. Coarse operations. Typed arrays for bulk data. Lazy init.
## Related Docs
- [kkrpc](kkrpc.md) — WASM boundary layer, transports, compilation strategy
- [automerge](automerge.md) — CRDT sync, storage, state management
- [undo-redo](undo-redo.md) — Undo/redo and operation log
- [webgpu](webgpu.md) — GPU rendering architecture
- [gizmo](gizmo.md) — Direct manipulation (click-select, drag-transform)
- [direct-vs-parametric](direct-vs-parametric.md) — Parametric modeling with ezpz constraint solver
# MCP and API Stack
## Stack
| Layer | Technology | Purpose |
|---|---|---|
| Server | Hono + Zod | Isomorphic HTTP API with validation |
| Auth | Better-Auth | TypeScript authentication |
| Docs | @hono/zod-openapi | Auto-generated OpenAPI documentation |
| AI | @hono/mcp | Model Context Protocol integration |
## Cloudflare Worker
The CAD API runs as a Cloudflare Worker (`systems/truck/worker/`):
- Hono router for API endpoints
- Static asset serving for the web GUI
- R2 bucket bindings for document storage
- Deployed to `truck-cad.gedw99.workers.dev` and `cad.ubuntusoftware.net`
# Sketch and Extrude Pipeline
Technical documentation for the parametric modeling system: 2D constrained sketch → solve → extrude to 3D solid.
## Architecture
```
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────┐
│ sketch-ui.js│────▶│ SceneController│────▶│ sketch.rs │────▶│ truck │
│ (JS UI) │ │ (WASM API) │ │ (solve+loop) │ │ (B-Rep) │
└─────────────┘ └──────────────┘ └───────────────┘ └──────────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ kcl-ezpz │
│ │ (constraint │
│ │ solver) │
└──── Automerge op log ──────────▶└──────────────┘
```
## Rust Layer
### Types (`crates/truck-webgpu-gui/src/sketch.rs`)
| Type | Purpose |
|---|---|
| `SketchPlane` | XY, XZ, YZ — determines 2D→3D mapping and extrude direction |
| `SketchPoint` | UUID + (x, y) initial position |
| `SketchEdge` | UUID + two point UUIDs |
| `SketchConstraintKind` | Enum with 11 variants (Fixed, Horizontal, Vertical, Distance, etc.) |
| `SketchConstraint` | UUID + kind |
| `Sketch` | Full sketch: plane, points, edges, constraints |
| `SolvedSketch` | Result of constraint solving: `Vec<(Uuid, f64, f64)>` positions |
All types derive `Serialize` and `Deserialize` for JSON round-trip (Automerge storage).
### Constraint Solver Integration
ezpz types (`DatumPoint`, `DatumLineSegment`, `Constraint`) do **not** implement serde. The `SolveContext` struct bridges this gap:
1. Creates fresh ezpz datums from sketch points/edges at solve time
2. Maps sketch `SketchConstraintKind` variants to ezpz `Constraint` values
3. Builds guess vectors from initial positions
4. Calls `kcl_ezpz::solve()` with Newton-Raphson solver
5. Extracts solved positions via `outcome.final_value_point()`
Key detail: `Fixed { point_id, x, y }` expands to **two** ezpz constraints — `Constraint::Fixed(dp.x_id, x)` and `Constraint::Fixed(dp.y_id, y)`.
### Extrude Pipeline
`sketch_to_solid(sketch, height)` follows truck's builder pattern:
1. **Solve** — run constraint solver to get final 2D positions
2. **Closed loop** — `find_closed_loop()` walks the edge graph to order points into a polygon boundary
3. **3D vertices** — map 2D solved positions to 3D via `SketchPlane::to_3d(x, y)`
4. **Wire** — `builder::vertex()` → `builder::line()` → `Wire::from(edges)`
5. **Face** — `builder::try_attach_plane(&[wire])` creates a planar face
6. **Extrude** — `builder::tsweep(&face, normal * height)` sweeps along plane normal
This is the same pattern as `make_cube()` in lib.rs — vertex → line → wire → face → tsweep.
## WASM API (`crates/truck-webgpu-gui/src/wasm_app.rs`)
12 methods on `SceneController`:
| Method | Returns | Description |
|---|---|---|
| `begin_sketch(plane)` | sketch UUID | Start sketch on XY/XZ/YZ |
| `sketch_add_point(x, y)` | point UUID | Add a point |
| `sketch_add_edge(p0_id, p1_id)` | edge UUID | Connect two points |
| `sketch_add_constraint(type, params_json)` | constraint UUID | Add constraint (11 types) |
| `sketch_solve()` | JSON positions | Solve and return `[{id, x, y}]` |
| `sketch_extrude(height)` | object UUID | Extrude → solid, add to scene |
| `sketch_cancel()` | — | Discard active sketch |
| `sketch_export()` | JSON string | Serialize sketch for Automerge |
| `sketch_import(json)` | bool | Restore sketch from JSON |
| `has_active_sketch()` | bool | Check if sketch is active |
The active sketch is stored in `SharedState.active_sketch: Option`. Extrude consumes it (takes ownership via `.take()`). On extrude failure, the sketch is put back so the user can fix it.
## JavaScript Layer
### `web/gui/sketch-ui.js`
IIFE that manages sketch state client-side:
- Tracks `sketchPoints`, `sketchEdges`, `sketchConstraints` arrays
- Populates point/edge dropdowns for constraint UI
- Shows/hides constraint fields based on selected type
- Quick rectangle helper: 4 points + 4 edges + 7 constraints in one click
- Exposes `window.sketchUI = { isActive, cancel() }` for keyboard shortcuts
### `web/gui/cad-document.js`
Automerge integration:
- `sketch_extrude` operation type stores `{ sketchJson, height }` in op log
- On replay: `ctrl.sketch_import(sketchJson)` then `ctrl.sketch_extrude(height)`
- Enables collaborative sketch → extrude across peers
## Constraint Types
| Kind | ezpz mapping | Parameters |
|---|---|---|
| `Fixed` | 2x `Constraint::Fixed` (x, y separately) | point_id, x, y |
| `Horizontal` | `Constraint::Fixed` on y0==y1 | edge_id |
| `Vertical` | `Constraint::Fixed` on x0==x1 | edge_id |
| `Distance` | `Constraint::EuclideanDistance` | p0_id, p1_id, value |
| `HorizontalDistance` | `Constraint::HorizontalDistance` | p0_id, p1_id, value |
| `VerticalDistance` | `Constraint::VerticalDistance` | p0_id, p1_id, value |
| `Coincident` | `Constraint::Coincident` | p0_id, p1_id |
| `Parallel` | `Constraint::Parallel` | edge0_id, edge1_id |
| `Perpendicular` | `Constraint::Perpendicular` | edge0_id, edge1_id |
| `EqualLength` | `Constraint::EqualLength` | edge0_id, edge1_id |
| `Midpoint` | `Constraint::InternalMidpoint` | edge_id, point_id |
## Tests
### Rust Unit Tests (9)
In `crates/truck-webgpu-gui/src/sketch.rs`:
- `test_empty_sketch` — empty sketch solves to empty result
- `test_unconstrained_sketch` — points keep initial positions
- `test_fixed_point` — fixed constraint pins a point
- `test_solve_rectangle` — 4 points + constraints → correct solved positions
- `test_solve_triangle_with_distances` — triangle with distance constraints
- `test_sketch_serialization_roundtrip` — JSON round-trip preserves sketch
- `test_extrude_rectangle` — rectangle → box with 6 faces
- `test_extrude_triangle` — triangle → prism with 5 faces
- `test_sketch_plane_to_3d` — XY/XZ/YZ coordinate mapping
### Playwright E2E Tests (11)
In `tests/e2e/sketch.spec.ts`:
- WASM API tests: begin_sketch, add_point, add_edge, add_constraint, solve, extrude (rectangle + triangle)
- Round-trip: export/import preserves sketch
- Edge cases: < 3 edges fails gracefully, has_active_sketch tracks state
- Cancel: sketch_cancel clears active sketch
- Multi-plane: XZ plane extrude produces solid
### Running Tests
```sh
task truck:test:crate # Rust unit tests (sketch + golden)
task truck:test:sketch # Playwright E2E sketch tests (needs gui:serve)
task truck:ci # Full CI: check + test + WASM build
```
# WebGPU Rendering
GPU rendering architecture for the CAD platform.
## Tier 1: Browser-Native WebGPU
The primary rendering path. truck B-Rep kernel + wgpu compiled to WASM, renders locally via WebGPU.
- Zero server cost
- Full B-Rep precision
- Real-time interaction (gizmo, camera orbit)
- Requires Chrome 113+ or equivalent WebGPU support
### Stack
| Component | Role |
|---|---|
| truck-platform | Scene, Camera, Lights, event loop (winit) |
| truck-rendimpl | PBR shaders, InstanceCreator, WireFrameInstance |
| wgpu | WebGPU abstraction layer |
| wasm-bindgen | Rust ↔ JS bridge |
### Build
```sh
task truck:wasm:build-browser-renderer
# Outputs to web/gui/pkg-browser-renderer/
```
## Tier 3: Server-Rendered Video (Future)
For devices without WebGPU support. Same Rust binary running natively on a GPU server, streaming H.264 video via WebRTC (LiveKit).
See `docs/adr/webgpu_server.md` for the full Tier 3 specification.
## Canvas Setup
The browser renderer uses a single `