# Driving Unroll as an agent

Unroll is a static page. Everything it does is reachable without touching the mouse:
through the JavaScript API at `window.unroll`, through URL parameters, or by keyboard.
The full machine-readable reference is [`/api.json`](api.json); `await unroll.schema()` returns
the same list at run time, including add-ons such as `unroll.bench`.

## Rules of the API

- Every method returns a Promise of `{ ok: true, ... }` or `{ ok: false, error, code }`. Nothing throws.
- Arguments are validated; errors say what was wrong and what is allowed.
- Codes: `invalid`, `not_found`, `unavailable` (not in this build or this scroll), `timeout`, `conflict`, `failed`, `load_failed`.
- Screen coordinates (Destroy tools) are CSS pixels in the viewport. Scene coordinates (camera) are millimetres.
- If the scroll fails to load, `window.unroll` still offers `getState`, `version`, `openScroll` and `reload`.

## Start

```js
await unroll.waitFor('ready');          // the scroll is loaded
const s = await unroll.getState();      // scroll, t, layer, winding, focus, panels, destroy, camera, description, url
await unroll.describe();                // the current view in words
await unroll.version();                 // { version, date, sha, api }
```

## Common tasks

| Task | Call |
| --- | --- |
| Unroll to a position | `setUnroll(0.5)` then `waitFor('settled')` |
| Help and schema | `help()`, `help('destroy')`, `help('setLayer')`, `schema()` |
| Animate | `play(true)`, `play(false)` |
| Switch layer | `setLayer('xray' \| 'ink' \| 'overlay')` |
| Close-up of a winding | `focus('w047', { view: 'overlay' })`; close with `focus(null)` |
| Compare swipe | `compare(true, 0.3)` |
| Zoom and pan the close-up | `closeup({ scale: 2, x: 40, y: 0 })`, `closeup({ fit: true })` |
| Panels | `panel('about' \| 'help' \| 'open' \| 'why', true/false)` |
| Camera | `camera.get()`, `camera.orbit(deg, deg)`, `camera.zoom(f)`, `camera.pan(dx, dy)`, `camera.set([x,y,z],[x,y,z])`, `camera.reset()` |
| Destroy | `destroy.mode(true)`, `destroy.tool('burn')`, `destroy.aim()` (a point on the papyrus), `destroy.tear(x, y, { dx, dy })`, `destroy.crush(x, y, { hold })`, `destroy.burn(x, y)`, `destroy.water(x, y, { hold })`, `destroy.crumble()`, `destroy.reset()`, `destroy.stats()`, `destroy.tools()` |
| Destroy Sandbox | `destroy.sandbox(true)`, `destroy.grab(x, y, { dx, dy })`, `destroy.shake()` |
| Roll it back up | `rollUp(true)`, `rollUp(false)` |
| Sound, fullscreen | `sound(true)`, `fullscreen(true)` |
| Image of the view | `screenshot()` gives `{ dataURL }` (PNG) |
| A keyboard shortcut | `key('2')`, `key('Space')` |
| Other scrolls | `listScrolls()`, `openScroll(id)` (the page reloads), `prizeOnly(true)` (filter the picker to prize eligible scrolls) |
| Settings and the control panel | `ui.settings()`, `ui.setSetting('quality', 'low')`, `ui.resetSettings()`, `ui.settingsPanel(true)`, `ui.moreControls(true)`, `ui.skipIntro()`, `ui.glossary('voxel')`, `ui.glossarySearch('patch')`, `ui.actions()`, `ui.action('tool.burn')` |
| Ink Bench | `bench.open('compare')`, `bench.compare({ segment, layer, thresholds })`, `bench.metrics()`, `bench.close()`; see docs/BENCH.md |
| Open your own | `open.fromURL(url)`, `open.fromFiles([{ name, data }])` with an ArrayBuffer, Uint8Array, Blob or base64 string; `open.example()`, `open.validate(manifest)`, `open.listLocal()`, `open.removeLocal(key)` |
| Share the view | `deepLink(true)` |

Tool calls take screen points; a point that is not on the papyrus answers `{ ok: false, code: 'not_found' }`.
`destroy.aim()` returns one that is.

## Waiting and events

```js
await unroll.waitFor('settled');                               // unroll and camera at rest
await unroll.waitFor({ layer: 'xray', t: { gte: 0.5 } }, { timeout: 5000 });
const { id } = await unroll.on('change', (e) => console.log(e.changed));
await unroll.off(id);
```

Events: change, unroll, layer, focus, panel, destroy, camera, settled, scroll, extension, error.

## Batches

```js
await unroll.run([
  ['setLayer', 'overlay'],
  ['focus', 'w047', { view: 'compare' }],
  ['compare', true, 0.4],
  ['screenshot'],
], { continueOnError: false });
```

## URL parameters

`scroll` (alias `s`), `t`, `layer` (alias `l`), `focus`, `view`, `compare`, `panel`, `destroy`, `tool`, `crush`, `sandbox`, `cam`.
They work in the query string or the hash (the hash wins), and the page keeps the hash up to date.

## Selectors for browser automation

Every interactive element has a stable `data-testid` and a `data-api` attribute naming its API equivalent,
for example `<button data-testid="layer-xray" data-api="setLayer('xray')">`.
`tests/coverage.mjs` fails if any control lacks either one.

## Embeds

In an iframe (`?embed=1`), host pages reach the same API with postMessage; see [docs/EMBED.md](docs/EMBED.md).
`embed.snippet()` returns an iframe snippet for the current view.

## Add-ons

Add-ons register a method group: `unroll.register('bench', { run: fn }, { run: { summary, params } })`.
A script that loads before the page can queue the same arguments on `window.unrollExtensions`.

## Versions

`version.json` is the single source of truth (semver). Bump the minor version for a feature push and the
patch version for a fix with `node tools/stamp_version.mjs minor|patch`, add a line to `CHANGELOG.md`,
and run `node tools/build_api_json.mjs`.

## Tests

```
node tests/coverage.mjs http://localhost:8080/
node tests/api.mjs http://localhost:8080/ [URL of a zip served with CORS]
```
