unroll / AGENTS.md
SharpMeow's picture
One page scroll bar and American English
757768f verified
|
Raw History Blame Contribute Delete
5.93 kB

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; 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 millimeters.
  • If the scroll fails to load, window.unroll still offers getState, version, openScroll and reload.

Start

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

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

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. 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.

Live sheet

/live.html streams a segment and a volume from the open data bucket and draws the volume on the sheet. It has its own small API, window.liveSheet: load({ seg, vol, crop, level, depth, up, reduce }), state(), values(), turn(deg), screenshot(), and the URL parameters in docs/LIVE.md.

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]
node tests/livesheet.mjs                     # lib/livesheet.js, no network