# AfterPencil JavaScript API (`window.afterpencil`)

Available on every editor page as `window.afterpencil` (`afterpencil.version` is `1`). It works on the project open in the page, as the person using it: read-only projects reject edits, and every edit is one step that the person can undo.

- Units are meters; angles are degrees.
- Points are `{ x, y }` or `[x, y]` in plan coordinates: x grows to the right and y grows down on screen.
- Commands use the active level unless you pass `level` (a level id from `summary()`).
- Edits return `{ ok: true, created: [...ids], removed: [...ids] }` or `{ ok: false, error: { code, message, elementId?, causeId? } }`. Nothing changes when `ok` is false. A `causeId` names the locked element that blocks the edit.

```js
const s = afterpencil.summary()
const r = afterpencil.drawRoom({ x: 0, y: 0, width: 4, depth: 3, name: 'Kitchen' })
if (!r.ok) console.log(r.error)
afterpencil.placeOpening({ kind: 'door', at: [2, 3] })
afterpencil.placeFurniture({ catalogId: 'fridge', at: [0.5, 0.5], rotation: 0 })
```

## Read

| Method | Returns |
|---|---|
| `summary()` | Compact JSON: `levels[]` (id, name, elevation, height, `rooms` with id, name, area and center, `walls` with id, from, to, length, thickness and height, `openings` with id, kind, wallId, from, to, width, height and sill, `furniture`, `lights`, `switches`, and `counts` of slabs, stairs, roofs, columns, beams and installations), `materials[]` and `heightGuides[]`. Start here. |
| `plan()` | The full plan as saved (see [the model](/agents/model.md)). Use it for what `summary()` leaves out. |
| `takeoff()` | Quantities: masonry, rooms, floors, finishes by material, openings, stairs, roofs, structure, installations, outdoor, furniture and totals. |
| `catalog()` | Furniture and fixtures: `id`, `name` (`en`, `es`), `category`, default `size` and allowed `ranges`. |
| `materialPresets()` | Material preset ids (`paint-white`, `wall-tile`, `porcelain`…), usable wherever a material is asked for. |
| `openingProducts()` | Every opening product: `id` (`door-hinged`, `door-balcony-sliding`, `window-sliding`, `gate-sliding`…), `kind` (`door`, `window`, `passage`, `gate`), `mechanism`, `leaves`, the allowed `width` and `height` as `[min, max]` in meters, and the `default` size. |
| `wallTypes()` | The wall types of the project (`types`, each with `layers` and their total `thickness`), which ones exterior and interior walls take by default (`defaults`), and the `presets` to create others with `addWallType`. |
| `state()` | `projectName`, `levelId`, `selection`, `view`, `wallFace`, `readOnly`, `canUndo`, `canRedo`, `lastError`. |

## Edit with commands

| Method | What it does |
|---|---|
| `drawWalls(points, { closed?, thickness?, height?, wallType?, role?, level? })` | Walls through the points, joined at their ends and to existing walls; an end within 2 mm of a wall or a wall end joins it (it is moved onto it). `closed` joins the last point to the first. Height defaults to the editor's. Rooms appear by themselves when walls close a loop. See [thickness and wall types](#thickness-and-wall-types). |
| `drawRoom({ x, y, width, depth, name?, thickness?, height?, wallType?, role?, level? })` | A rectangular room from its corner (`x`, `y`), with its name. |
| `nameRoom({ at, name, level? })` | Names the room that contains the point. |
| `placeOpening({ kind, at, product?, width?, height?, sill?, swing?, level? })` | A `door`, `window` or `passage` centered at the point of the nearest wall, or a `gate` on the nearest fence (within 0.5 m). `product` picks the mechanism and leaves (see `openingProducts()`); sizes have to be within the product's ranges, or the edit fails with `invalid-value` and the range in the message. A passage in a wall lower than 1.9 m (a railing, a low wall) cuts it from the floor to its top. `swing: { hinge?, side? }` sets how a door or window opens: `hinge` is the jamb with the hinges, the one toward the `from` or the `to` end of the wall; `side` is the face it opens toward, `left` or `right` of the wall going from `from` to `to` (see [left and right](/agents/model.md#walls): on screen, `left` looks like the right-hand side). |
| `placeFurniture({ catalogId, at, rotation?, size?, level? })` | A catalog item centered at the point; `rotation` 0 faces +y. |
| `placeLight({ at, kind?, level? })` | A light: `ceiling` (default), `pendant`, `wall` (anchored to the nearest wall face) or `floor-lamp`. |
| `placeSwitch({ at, level? })` | A switch anchored to the nearest wall face, wired to the lights of its room. |
| `paint(target, material?)` | Assigns a material (a project material id or a preset id) to a surface; without one, takes it off. Targets: `{ kind: 'room', levelId, roomId, surface: 'floor'\|'walls'\|'ceiling'\|'baseboard' }`, `{ kind: 'wall', wallId, side: 'left'\|'right' }`, `{ kind: 'project', surface }` (the default for every room), `{ kind: 'opening', openingId, surface: 'frame'\|'leaf' }`, `{ kind: 'furniture', furnitureId, surface: 'body'\|'soft'\|'accent' }`, `{ kind: 'slab'\|'roof'\|'pathway'\|'garden'\|'column'\|'beam', elementId }`. |
| `addWallZone({ wallId, side, material, bottom, top, start?, end?, name? })` | Covers part of a wall face with another material, measured from the floor of that face: a band between two heights (tiling up to 1.20 m) or, with `start` and `end` along the wall from its `from` end, a rectangle (a backsplash). |
| `moveWall({ wallId, offset })` | Moves a wall across its axis (positive to its left, from `from` to `to`); what is hosted and anchored follows. |
| `deleteElements(ids)` | Deletes elements and what depends on them, as the editor does. |
| `addLevel({ name, below?, slab? })` | A level on top of the highest one, as tall as it (its elevation is the top of the one below), with a floor slab copied from the level below (or over its walls; `slab: false` to skip it), or with `below` a basement. `created[0]` is its id: pass it as `level` to draw on it. |
| `drawSlab({ points, thickness?, use?, level? })` | A slab by its outline (any direction), 0.12 m thick by default. `use: 'balcony'` or `'terrace'` is open air, a bit lower, with railings or parapets on its free edges. |
| `drawRoof({ points, kind?, pitch?, overhang?, material?, edge?, level? })` | A roof over the walls of `level` (the walls carry it), by its outline at the wall faces (any direction); the overhang (0.4 m) is added outside. `kind`: `gable` (default), `hip`, `shed` or `flat`; `pitch` in degrees (25); `material`: `tile`, `metal-sheet`, `concrete` or `shingle`. `edge` is the side (index `i`: from point `i` to `i + 1`) that is an eave: the ridge runs parallel to it; by default the longest. |
| `drawStair({ points, width?, fromGround?, level? })` | A stair along its walking line (from the foot to the arrival, with its turns), from `level` up to the next level, 0.9 m wide. Risers and treads come from the height. With `fromGround`, an outdoor stair from the ground up to `level`. |
| `drawLot({ points, level? })` | The lot by its outline (any direction); replaces the lot if there is one. Streets and setbacks of each side: `updateLot` (see [the model](/agents/model.md#lot)). |
| `drawFence({ points, style?, height?, level? })` | A fence along the points: `bars` (default), `wood`, `mesh`, `iron-wood` or `railing` (a 1 m balustrade). Gates: `placeOpening({ kind: 'gate', at, product? })`. |
| `addWallType({ preset?, name?, system?, layers? })` | A wall type, from a preset of `wallTypes().presets` or by `system` and `layers` (`{ material, thickness }` from the exterior face). `created[0]` is its id. |
| `renameProject(name)` | Changes the name of the project. |

### Thickness and wall types

Walls of a new project have a wall type: exterior walls take the exterior default (0.22 m, hollow brick with plaster) and interior walls the interior one (0.15 m), and a wall with a type is as thick as its layers. Whether a wall is exterior or interior is its `role`, set when it is drawn (interior if it falls inside a room, exterior if not) and kept until changed: opening or closing a room later does not change it. So:

- `role` in `drawWalls` and `drawRoom` sets it for the new walls (`exterior`, `interior` or `retaining`). A wall drawn next to a house (an extension) keeps the house's wall as it was: to make that wall interior, `apply([{ type: 'updateWall', wallId, changes: { role: 'interior' } }])`.

- `thickness` in `drawWalls` and `drawRoom` gives the walls a type with that thickness: one of the project, or a copy of the type they would take with its main layer resized.
- `wallType` gives them that type (and its thickness). `wallTypes()` lists them; `addWallType` creates one.
- To change existing walls, `apply([{ type: 'updateWall', wallId, changes: { typeId } }])`. An `updateWall` with a `thickness` other than its type's fails with `thickness-from-type`. Changing the layers of a type (`updateWallType`) changes every wall that takes it.

## Edit with operations

| Method | What it does |
|---|---|
| `apply(operations)` | Applies [model operations](/agents/model.md#operations) as one undo step. An unknown `type` fails with `unknown-operation`, and an `add…` whose element misses a field with `invalid-value` naming the operation and the field. |
| `check(operations)` | `{ ok: true }` if the model accepts them, or the error, without changing anything. |
| `undo()`, `redo()` | Undo or redo the last step (the person's or yours). |

## See

Look at the result after a few edits: it is the quickest way to catch a wall in the wrong place.

| Method | Returns |
|---|---|
| `planImage({ level?, format?, style?, show? })` | Promise of the plan of a level (the active one by default). `style: 'editor'` (default) is the plan as the editor shows it, as a PNG data URL (`format: 'png'`, default) or SVG text (`format: 'svg'`); `style: 'technical'` is the technical drawing with dimensions, as SVG text. |
| `capture3d({ camera?, preset?, eye?, target?, width?, height?, show? })` | Promise of a PNG data URL of the 3D, without changing what the person sees: from a saved camera (`camera`, an id of `plan().cameras`), framing the whole house (`preset: 'iso'`, `'top'` or `'front'`), from a point looking at another (`eye` and `target` as `[x, y, elevation]`, elevation in meters, eyes usually 1.6 m above the floor) or, with no options, the 3D view as it is. Default size 1280 × 800. If the 3D is not on screen, it opens for a moment. |
| `hideImage()` | Removes the image shown with `show`. |

With `show: true`, the image is also shown on the page, over the editor, until `hideImage()`, its close button or Escape: if you see the page through screenshots, take one to look at it.

```js
const png = await afterpencil.capture3d({ preset: 'iso' })
const plan = await afterpencil.planImage({ style: 'technical', format: 'svg', show: true })
```

## View

| Method | What it does |
|---|---|
| `select(ids)` | Selects elements (the side panel shows their properties). |
| `setLevel(levelId)` | Makes a level active. |
| `setView('2d'\|'3d'\|'split')` | Plan, 3D or both. |
| `openWallFace(wallId?, side?)` | Opens the front view of a wall face (`left` or `right` of the wall from `from` to `to`); without arguments, closes it. |
| `ready()` | Promise resolved when the editor finished drawing: wait for it before a screenshot. |
| `onChange(listener)` | Calls `listener(summary)` on every change of the plan, also undo and redo. Returns a function that stops listening. |
| `help()` | This list, short. |

## Error codes

The model's codes are stable: `element-not-found`, `element-locked` (with `causeId`), `invalid-value`, `unknown-operation`, `level-mismatch`, `opening-out-of-wall`, `opening-overlap`, `opening-too-tall`, `thickness-from-type`, `wall-has-openings`, `wall-has-services`, `wall-has-anchored`, `material-not-found`, `wall-type-not-found`, `no-level-above`, `lot-exists`, `duplicate-id`, among others. The commands add `read-only`, `level-not-found`, `wall-not-found` (no wall near the point), `fence-not-found` (no fence near the point, for a gate), `room-not-found`, `catalog-item-not-found` and `material-not-found`. `unknown` is an error the API did not expect, usually an argument with another shape.
