# EX3D Studio — guide for AI models

You can model buildings in EX3D Studio (https://www.ex3d.com/studio/) through one
small JSON API, `window.EX3D`. This guide is everything you need. It is written for any model,
small or large: follow it literally and you will build correctly.

**Worked example:** the public Farnsworth House sample was built only with this API —
`llm/examples/farnsworth.ops.json` (open it in the app: `studio/?public=farnsworth`).

---

## 1. How to connect

- **Through a browser tool (works today, any model).** Open the app, then evaluate JavaScript in
  the page: `EX3D.info()`, `EX3D.run([...])`. Any MCP server that drives a browser works
  (Claude in Chrome, Playwright MCP, Chrome DevTools MCP).
- **Start clean:** open `studio/?new` (a fresh project, no login) and pick "Just open the app",
  or load a public project with `studio/?public=farnsworth`.
- **Headless (scripts):** `node dev/build-samples.js` replays an ops file with the same API.

## 2. The three rules

1. **Millimetres everywhere.** A 4 m wall is `4000`.
2. **Plan axes: x → east, y → north.** Points are `[x, y]`. Heights (z) are measured **up from the
   floor of the element's story** — except camera eyes and targets, which are **absolute** (from
   the ground, z = 0).
3. **`EX3D.run(ops)` is all-or-nothing.** If any op is wrong, *nothing* changes and you get
   `errors` in plain words (`"op 3 (wall w2): the two ends are the same point"`). Fix and resend
   the whole batch. A successful batch is one undo step for the user.

## 3. Read before you write

```js
EX3D.info()                    // stories, families (doors, windows, objects), materials, site, counts, ops
EX3D.elements({ type: 'wall', story: 'Floor' })   // compact list: ids, points, sizes
EX3D.describe('w12')           // one element in words
EX3D.bounds()                  // { x0, y0, x1, y1, z0, z1 } of everything, mm
EX3D.plan({ story: 'Floor' })  // a small SVG of the plan (1 unit = 10 mm, north up) — cheap to read
EX3D.views()                   // the boards: plans, 3D views, sheets
await EX3D.screenshot({ view: '3D View', w: 960, h: 600 })   // a JPEG data URL of a 3D view — LOOK at it
EX3D.info().sections           // the steel section catalogue (W8x48, IPE 300, C15x33.9, SHS 100x6 …)
```

## 4. The ops

Give an op an `id` of your own (letters, digits, `_`, `-`) and later ops in the same batch — and
later batches — can refer to it. Stories and materials can be named by id **or** name.

| op | fields | notes |
|---|---|---|
| `name` | `name` | the project's name |
| `site` | `lat, lon, utc, date 'YYYY-MM-DD', time 'HH:MM', north, sky, clouds, fog, weather, night` | sun position, sky. `sky: 'atmosphere'` = physical sky. `clouds: { on, type: 'cumulus'\|'overcast'\|'cirrus', coverage 0–1 }`. `fog: { on, visibility m, height m, base m, albedo 0–1, g, tint }` or `{ on, preset: 'haze'\|'forest'\|'mist'\|'fog'\|'dense'\|'smog' }` = ground fog. `weather: 'clear'\|'fair'\|'hazy'\|'overcast'\|'damp'\|'mist'\|'fog'` sets air + clouds + fog at once |
| `story` | `id, name, elevation, height` | a level. With an existing `id` it edits that story. A new project has one story, id `story-0` |
| `material` | `from, id, name, color '#rrggbb', roughness 0–1, metalness 0–1, opacity 0–1, antiTile 0–1, macroVar 0–1, cut` | adds or edits. `antiTile` breaks visible texture repeats (hex-tile blending), `macroVar` adds large-scale colour variation — both 0 = off, good on lawns, concrete, stone, gravel. `cut` = plan cut pattern: `solid`, `none`, `concrete`, `brick`, `steel`, `wood`, `insulation`. Built in: `Concrete`, `Brick masonry`, `Drywall`, `Plaster`, `Insulation`, `Glass` (`mat-vidrio`) `from`: copy one of the EX3D materials with its textures (travertine-pavers, primavera-veneer, oak-chevron, marble-bianco-lassa, steel-brushed, leather-tan, silk-shantung, concrete-formwork, brick-capuccino … — `info().library` lists all 37; real sizes, colour / normal / roughness maps); name / cut / … still apply on top |
| `wall` | `story, a [x,y], b [x,y], thickness, height, base, material, refline` | the line a→b is the wall's **centre** (`refline: 'center'`). `height` null = the story height |
| `slab` | `story, poly [[x,y]…], thickness, top, material, finish, underside, fascia { section, material }` | `top` = the slab's top above the story floor; it hangs `thickness` below that. Floors: `top: 0`. A roof: `top: story height + thickness`. `fascia` = a steel channel round the whole edge, web outside, flush with top and bottom (`section` a channel, e.g. `C15x33.9`, `UPN 300`) `finish` = the top face (the floor you walk on), `underside` = the ceiling below; default: the slab's own material (the plan shows the finish) |
| `column` | `story, at [x,y], w, d, height, base, angle, material, section \| profile, tw, tf` | a rectangle `w` (along x) × `d` (along y), centred on `at`. `section: 'W8x48'` makes a real steel section (its sizes from the catalogue, `info().sections`); or `profile: 'I'\|'C'\|'round'\|'tube'\|'pipe'` with your own `w, d, tw` (web / wall) `, tf` (flange). An I's web runs along y; turn it with `angle` |
| `beam` | `story, a [x,y], b [x,y], top, section \| profile + w, d, tw, tf, roll, material` | a steel member from a to b; `top` = its top above the story floor (default: the story height). `roll` turns the section about its axis (degrees) |
| `stair` | `story, a [x,y], b [x,y], wide, rise, base, risers, kind, tread, nosing, material, stringer` | a straight flight walking **up from a to b** (the run, on the stair's centre line); `wide` across. `rise` = total height, from `base` above the story floor. `risers` defaults to rise / ~175. `kind: 'solid'` (a stepped mass), `'floating'` (slabs `tread` thick), `'stringers'` (floating treads + two steel plates, `stringer` = their material). The top tread is one riser below the landing: the landing itself is the last step |
| `glazing` | `story, a [x,y], b [x,y], height, base, panel \| panels, mullionW, mullionD, head, sill, transoms [h…], glassT, material, glass` | a curtain wall: glass on the line a→b, mullions every ~`panel` mm (the length is shared evenly) or `panels` bays, frame bars `head` / `sill` high (0 = none), horizontal `transoms` at those heights. `material` = the frame, `glass` = the glass material (default `mat-vidrio`). `height` null = the story height |
| `curtain` | `story, a [x,y], b [x,y], open, split, top, drop, fullness, pleat, track, material` | pleated fabric on a track a→b. `open` 0 (drawn shut) … 1 (pushed open); `split: 'center'` stacks at both ends, `'left'` at a, `'right'` at b. `top` = the track height (default: the ceiling), `drop` = fabric height (default: to the floor). Built-in fabric `mat-cortina` (a sheer) |
| `cabinet` | `story, a [x,y], b [x,y], kind, modules, depth, height, elev, plinth, handle, worktop, flip, material, front, top, handles` | a run of cabinets whose BACK is the line a→b; they stand on its **right** (draw along the wall as you face it, left to right; `flip: true` = the other side). `kind: 'base'` (worktop) `\| 'wall'` (hangs at 1450) `\| 'tall' \| 'island' \| 'wardrobe'`. `modules: '600 drawers, 900 sink, dishwasher, hob'` — a width is optional (none = they share the rest); fronts: `doors, door, drawers, drawers2, drawers4, open, sink, hob, oven, dishwasher, fridge, blank`. `handle: 'bar' \| 'knob' \| 'push'`. Materials: `material` = carcass, `front`, `top` (worktop), `handles` |
| `tree` | `story, at [x,y], species, height, seed, season, density, prune, fullness, z, bark, leaves` | a procedural tree: `species: 'maple' \| 'oak' \| 'ash' \| 'birch' \| 'beech' \| 'poplar' (Lombardy, columnar) \| 'willow' (weeping) \| 'pine' \| 'scotspine' (bare trunk, high crown) \| 'spruce' \| 'fir' \| 'larch' \| 'shrub'`; `height` mm (null = the species' own); `seed` = which tree (the same seed, the same tree — vary it for a group); `season: 'spring' \| 'summer' \| 'autumn' \| 'winter'` (bare; conifers keep their needles); `prune` 0.03…0.85 = where the crown starts up the trunk (low = branches near the ground; null = the species' own); `fullness` 0.3…2.5 (1 = the species) = more branches and leaves |
| `scatter` | `story, poly [[x,y]…], species [..], density, spacing, height, vary, seed, season, edge, prune, fullness` | a wood, a hedge, a planting: plants of the listed species inside the outline, `density` per 100 m² (at most 400), at least `spacing` mm apart, sizes ± `vary`; `edge` 0…1 crowds them toward the outline (a woodland edge); `prune` / `fullness` as for `tree`, for every plant |
| `lawn` | `story, poly [[x,y]…], look, z, stripes, stripeW, stripeAngle, grass, bladeH, tufts, dry, dryScale, leaves, weeds, dandelions, flowers, flowerColor, bumps, clump, clover, seed, material, stripe` | a grass surface. `look: 'manicured' \| 'natural' \| 'meadow' \| 'wild'` sets all the rest at once (then override any). `dry` 0…1 = dry patches (`dryScale` mm), `leaves` = fallen leaves (heaped under trees), `weeds` / `dandelions` per m², `flowers` = wildflowers in patches (`flowerColor` '#rrggbb'); `bumps` mm = a gently uneven ground under the blades, `clump` 0…1 = tufts growing in clumps of varied height, `clover` 0…1 = clover patches; mowing stripes `stripeW` mm toward `stripeAngle`°; `z` = its top above the story floor |
| `object` (Mies pieces) | `family: 'fam-barcelona-chair' \| 'fam-barcelona-stool' \| 'fam-barcelona-couch' \| 'fam-barcelona-table' \| 'fam-brno-chair' \| 'fam-mies-table' \| 'fam-bed' \| 'fam-rug'`, `params { W, D, H … }` | parametric furniture with plan symbols; a seat faces +y (turn it with `angle`) |
| `zone` | `story, poly [[x,y]…], name, number, category 'living'\|'sleeping'\|'wet'\|'kitchen'\|'circulation'\|'service'\|'outdoor', showArea` | a room: coloured on the plan with a stamp (name, number, net area). `poly` = the inside faces of its walls. Numbers itself (01, 02 …) per story when `number` is left out |
| `roof` | `story, poly [[x,y]…], kind 'hip'\|'gable'\|'shed'\|'flat', pitch (°, 30), overhang (500), thickness (200), base (null = story height), eave (edge index; null = longest), gableWalls (true), material` | `poly` = the pivot line (the wall centre lines); the overhang is added outside. Gable: the edges parallel to `eave` slope, the others are gables (filled up to the roof when `gableWalls`); shed: only `eave` slopes |
| `opening` | `wall, family, at [x,y] or t, params, flip` | a door / window **in a wall**. `t` = mm from the wall's start `a`; or give a point `at` near the wall. Doors: `fam-puerta` (swing), `fam-door-double`, `fam-door-glazed`, `fam-door-sliding`, `fam-door-pocket`, `fam-door-bypass`, `fam-door-bifold`, `fam-door-sidelight` (`DW` door width), `fam-door-garage`. Windows: `fam-ventana` (sliding), `fam-win-casement`, `fam-win-french`, `fam-win-fixed`, `fam-win-awning`, `fam-win-double-hung`, `fam-win-louvre`, `fam-win-ribbon`. All take `W, H, SILL` |
| `object` | `story, family, at [x,y], z, angle, params` | furniture and other object families (see `info().families`). Fixtures (back = −y against the wall, `Z` = mounting height): `fam-wc`, `fam-washbasin`, `fam-shower-tray` (`SH` screen, 0 = none), `fam-bathtub`, `fam-kitchen-sink`, `fam-urinal`, `fam-floor-drain`, `fam-water-heater`, `fam-socket`, `fam-switch`, `fam-data-outlet`, `fam-panel-board`, `fam-smoke-detector`, `fam-ac-split`, `fam-radiator` |
| `light` | `story, at [x,y], z, kind, power, color, target [x,y,z]` | `kind: 'point'\|'spot'\|'rect'\|…`, `power` in lumens |
| `view` | `kind 'plan'\|'3d'\|'sheet', name, story, eye [x,y,z], target [x,y,z], fov` | adds a board. Plans need `story`. 3D eyes/targets are **absolute** heights. A sheet takes `size [w,h]` (px), `paper`, `first: true` (the project opens on it) and `items: [{ type: 'text', at, text, width, size, weight, color, lineHeight } · { type: 'image', at, w, h, src } · { type: 'rect', at, w, h, fill } · { type: 'schedule', at, kind: 'doors'\|'windows'\|'rooms', size, title }]` in sheet px, y down. A schedule lists the model as it is at every draw |
| `edit` | `id, set { field: value }` | change any fields: `a, b, at, poly, thickness, height, material, story, top, base…` |
| `remove` | `id` | removing a wall removes its doors and windows |

Check a batch without applying it: `EX3D.run(ops, { dry: true })`.

## 5. The loop that works

1. `EX3D.info()` — learn the stories, families, materials.
2. Plan the batch on paper: list every point. Build **from the ground up**: site → stories →
   materials → slabs → columns / walls → openings → objects → lights → views.
3. `EX3D.run(batch)` — if `ok` is false, read `errors`, fix *those ops*, resend the whole batch.
4. **Verify**: `EX3D.plan({ story })` for positions, `EX3D.bounds()` for extents,
   `EX3D.screenshot({ view })` for the look. Compare with what you intended.
5. Correct with `edit` / `remove` ops, small batches.

## 6. Recipes

**A room 4 × 3 m with a door**
```js
EX3D.run([
  { op: 'slab', id: 'floor', poly: [[0,0],[4000,0],[4000,3000],[0,3000]], thickness: 200 },
  { op: 'wall', id: 'ws', a: [0,0], b: [4000,0], thickness: 200 },
  { op: 'wall', a: [4000,0], b: [4000,3000], thickness: 200 },
  { op: 'wall', a: [4000,3000], b: [0,3000], thickness: 200 },
  { op: 'wall', a: [0,3000], b: [0,0], thickness: 200 },
  { op: 'opening', wall: 'ws', family: 'fam-puerta', t: 1000 },
  { op: 'view', kind: '3d', name: 'Outside', eye: [-4000,-6000,1600], target: [2000,1500,1200], fov: 50 },
]);
```

**A raised glass pavilion** (the Farnsworth House idea): a story whose floor is in the air, a floor
slab and a roof slab with steel fascias, steel columns welded to the fascias, glazing between the
slabs, floating stairs — see the worked example `llm/examples/farnsworth.ops.json`.
```js
EX3D.run([
  { op: 'story', id: 'fl', name: 'Floor', elevation: 1600, height: 2896 },
  { op: 'slab', story: 'fl', poly: [[0,0],[12000,0],[12000,6000],[0,6000]], thickness: 381, top: 0,    fascia: { section: 'C15x33.9' } },
  { op: 'slab', story: 'fl', poly: [[0,0],[12000,0],[12000,6000],[0,6000]], thickness: 381, top: 3277, fascia: { section: 'C15x33.9' } },
  // a W8x48 is 216 deep: its centre 10 (the channel web) + 108 outside the slab edge touches the fascia
  { op: 'column', story: 'story-0', at: [1500, -118], section: 'W8x48', height: 1600 },
  { op: 'column', story: 'fl',      at: [1500, -118], section: 'W8x48', height: 3277 },
  { op: 'glazing', story: 'fl', a: [0,60], b: [12000,60], panel: 1676, mullionW: 40, mullionD: 90 },
  { op: 'stair', story: 'story-0', a: [3000,-1400], b: [3000,0], wide: 2400, rise: 1600, kind: 'floating', tread: 60 },
]);
```

**A steel frame:** columns with `section: 'HEB 200'` on a grid, `beam` ops between their centres
with `top` at the floor above (`top: story height`), a slab on top.

**A kitchen:** base cabinets along the back wall, wall cabinets above them on the same line,
a tall unit for the fridge:
```js
EX3D.run([
  { op: 'cabinet', a: [0, 3000], b: [3000, 3000], flip: true, kind: 'base', modules: '600 drawers, 900 sink, 600 dishwasher, 900 hob' },
  { op: 'cabinet', a: [0, 3000], b: [3000, 3000], flip: true, kind: 'wall', modules: '600, 900, 600, 900' },
  { op: 'cabinet', a: [3000, 3000], b: [3600, 3000], flip: true, kind: 'tall', modules: 'fridge' },
  { op: 'curtain', a: [100, 150], b: [2900, 150], open: 0.5 },
]);
```

**A garden:** a lawn round the house, a big tree, a group of birches (different seeds), shrubs:
```js
EX3D.run([
  { op: 'lawn', poly: [[-20000,-20000],[30000,-20000],[30000,20000],[-20000,20000]], stripeW: 1200 },
  { op: 'tree', at: [18000, -8000], species: 'maple', height: 20000, seed: 11 },
  { op: 'tree', at: [-9000, 9000], species: 'birch', seed: 1 },
  { op: 'tree', at: [-7500, 11000], species: 'birch', seed: 2 },
  { op: 'tree', at: [2000, -3000], species: 'shrub', height: 1500, seed: 5 },
]);
```

**A shopfront:** `glazing` with `panels: 3, transoms: [2400], sill: 0` between two walls.

## 7. Pitfalls (each one happened while building the sample)

- **Camera heights are absolute; element heights are story-relative.** An eye at the floor of a
  story at +1600 looking at 1.5 m is `z: 3100`, not 1500.
- **A wall's line is its centre.** A 200 mm wall on `y: 0` spans y −100…+100. To keep glass inside a
  slab edge, move its line in by half its thickness (or more).
- **Slab `top` is the TOP.** A 381 mm slab with `top: 0` occupies −381…0 under the story floor.
- **Columns start at their story's floor.** A column that must reach a roof on another story needs
  `height` = the distance from its own floor to the roof's top.
- **A plan shows only its own story.** A column running from the ground to the roof is invisible on
  the upper plan: model one column per story (same `at`, heights meeting at the floor).
- **Cabinets stand on the RIGHT of a→b.** On a north wall (y = 3000) drawn west → east that is
  south… of the line, i.e. into the room only if the room is south of it; otherwise `flip: true`.
  Check with `EX3D.plan()` — the worktop outline must be inside the room.
- **Trees are tall.** A mature maple is 14–25 m, a birch 10–15 m, a shrub 1–2 m: `height` is in mm.
  Leave room: a 20 m maple's crown is ~12 m across. Lawns and trees go on the ground story.
- **A stair's `a` is its bottom, `b` its top**, both on the centre line. `a → b` is the run, not the
  width — `wide` is across.
- **An I column's web runs along y.** Columns on a south or north edge welded to a fascia are right
  as they are; on an east or west edge give them `angle: 90`.
- **Glazing and walls on the same line fight.** Glass goes on its own line inside the slab edge
  (the fascia is outside it); leave walls for opaque parts.
- **`screenshot` is async** in a live browser: `await` it. A view that has just been created may need
  a moment before it can draw.
- **Ids are unique for good.** Reusing an id (even one removed long ago in the same batch) is refused.
- **Doors and windows need a wall** and a family of category `door` / `window` (`info().families`).
- **One batch, one story's worth of intent.** Very long batches are fine but harder to debug; group
  by building part.

## 8. What is coming (and will be added here)

Doors in glazing, furniture as GLB models, image planes / backdrops, terrain and water — each as a new op. A dedicated MCP server with typed tools will follow for
smaller models.

## Sheets that link to views

Any sheet item (`text`, `image`, `rect`) can carry `link`: a board's name (`'Living room'`, `'Plan - floor'`)
or an `https://` address. In Showcase a click or tap opens it (the board slides in, a Back chip returns);
while editing, Ctrl+click. Make buttons from a `rect` (`fill`, `stroke`, `radius`) and a `text` on it,
both with the same link. Names are turned into board ids when the run finishes.

3D views take `fx` for the camera's look: `{ autoEv: true, eye: true }` (measured exposure),
`autoWb: true` (auto white balance), `ev`, `temperature`, `tint`, `contrast`, `saturation`, `vignette`, `bloom`.

## Dressing furniture

`object` takes `materials: { slot: material }` — the family's part slots (`info().families`: e.g. a
Barcelona couch has `frame`, `legs`, `cushion`). `material` takes `tint: '#rrggbb'` to grade a textured
material (`from`) — one leather texture gives cognac, brown, navy and cream.
