File size: 6,786 Bytes
cc6b6d8 e31bb23 cc6b6d8 e31bb23 cc6b6d8 757768f cc6b6d8 757768f cc6b6d8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | // Writes api.json from lib/apispec.js, lib/manifest.js and version.json. Run after changing any of them:
// node tools/build_api_json.mjs (tests/api.mjs checks that api.json is up to date)
import { readFileSync, writeFileSync } from 'node:fs';
import { API_VERSION, METHODS, EVENTS, URL_PARAMS, KEYS } from '../lib/apispec.js';
import { CT_SPECS } from '../lib/ctapi.js';
import { SCHEMA, LOCAL_LIMITS, REGISTRY_LIMITS, MESH_EXT, IMAGE_EXT, ROLES, FORMATS } from '../lib/manifest.js';
const ver = JSON.parse(readFileSync(new URL('../version.json', import.meta.url), 'utf8'));
const jsType = (t) => t.split('|').map((x) => ({ string: 'string', number: 'number', boolean: 'boolean', object: 'object', array: 'array', null: 'null', function: 'function' }[x] || x));
const methods = {};
for (const [name, m] of [...Object.entries(METHODS), ...Object.entries(CT_SPECS).map(([k, v]) => [`ct.${k}`, { group: 'ct', ...v, extension: true }])]) {
methods[name] = {
group: m.group, summary: m.summary, call: `window.unroll.${name}(${m.params.map((p) => p.name + (p.required === false ? '?' : '')).join(', ')})`,
params: m.params.map((p) => ({ name: p.name, type: jsType(p.type), required: p.required !== false, ...(p.desc ? { description: p.desc } : {}), ...(p.enum ? { enum: p.enum } : {}), ...(p.min !== undefined ? { minimum: p.min } : {}), ...(p.max !== undefined ? { maximum: p.max } : {}) })),
returns: 'Promise<{ ok: true, ... } | { ok: false, error: string, code: string }>',
};
}
const path = { type: 'string', description: 'Relative path inside the package: no URL, no leading slash, no ".."' };
const layer = {
type: 'object', required: ['id', 'role', 'label'],
properties: {
id: { type: 'string' }, role: { enum: ROLES }, label: { type: 'string' }, shortLabel: { type: 'string', description: 'Label where space is tight (layer chip, compare swipe)' },
resolutionUM: { type: 'number', exclusiveMinimum: 0, description: 'Resolution in micrometers' },
windings: { description: '"all", a list of ids, or { range: [first, last] }' },
texture: { ...path, description: 'Per-winding image path pattern containing {id}' },
files: { type: 'object', additionalProperties: path, description: 'Winding id to image path, overriding texture' },
atlas: path, atlasFallback: path, hi: { ...path, description: 'Close-up image pattern containing {id}' }, stats: path,
source: { type: 'string', description: 'Where the layer comes from' }, aboutHtml: { type: 'string', description: 'Registry scrolls only' },
},
};
const manifest = {
$schema: 'https://json-schema.org/draft/2020-12/schema', title: 'Unroll scroll manifest', type: 'object',
required: ['schema', 'id', 'name', 'voxelMM', 'geometry', 'windings', 'layers'],
properties: {
schema: { const: SCHEMA }, id: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' }, name: { type: 'string', maxLength: 120 },
voxelMM: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 10 }, base: { type: 'string', description: 'Relative folder that paths start from' },
geometry: { type: 'object', required: ['format'], properties: { format: { enum: FORMATS, description: 'grid-int16 is for registry scrolls only' }, units: { enum: ['voxels', 'mm'] }, axis: { enum: ['z', 'y'] }, index: path, bin: path } },
windings: { description: 'A list of { id, mesh } or { range: [first, last], mesh: pattern with {id}, outermost }', oneOf: [{ type: 'array', items: { type: 'object', required: ['id'], properties: { id: { type: 'string' }, mesh: { ...path, description: `.${MESH_EXT.join(', .')}` } } } }, { type: 'object', required: ['range'] }] },
outermost: { enum: ['first', 'last'] },
layers: { type: 'array', minItems: 1, items: layer, description: 'One layer per role; at least one ct or ink layer' },
description: { type: 'string' },
credits: { type: 'object', properties: { text: { type: 'string' }, url: { type: 'string', format: 'uri' }, citation: { type: 'string' }, holder: { type: 'string' } } },
dataTerms: { type: 'object', properties: { license: { type: 'string' }, url: { type: 'string', format: 'uri' }, notes: { type: 'string' } } },
display: { type: 'object', description: 'Registry scrolls only' },
},
'x-imageExtensions': IMAGE_EXT, 'x-meshExtensions': MESH_EXT, 'x-limits': { openYourOwn: LOCAL_LIMITS, registry: REGISTRY_LIMITS },
};
const out = {
name: 'Unroll', app: { version: ver.version, date: ver.date, sha: ver.sha }, apiVersion: API_VERSION,
docs: { llms: '/llms.txt', agents: '/AGENTS.md', addingAScroll: '/docs/ADDING_A_SCROLL.md', changelog: '/CHANGELOG.md', embed: '/docs/EMBED.md', oembed: '/oembed.json' },
global: 'window.unroll',
conventions: [
'Every method returns a Promise of { ok: true, ... } or { ok: false, error, code }. Nothing throws.',
'Error codes: invalid (bad arguments), not_found, unavailable (feature missing in this build or scroll), timeout, conflict, failed, load_failed.',
'Screen coordinates are CSS pixels of the viewport; scene coordinates are millimeters.',
'Add-ons register more groups with unroll.register(group, methods, specs), for example unroll.bench. They appear in unroll.schema() with extension: true.',
'Every interactive element has data-testid and data-api naming its API equivalent.',
],
embed: {
url: 'https://sharpmeow-unroll.static.hf.space/index.html?embed=1',
params: { embed: '1: compact layout, click to interact', autoplay: '1: play on load', controls: '0: hide the controls' },
iframeAllow: 'fullscreen; clipboard-write',
},
postMessage: {
description: 'Host pages drive an embedded viewer with postMessage. Every method above is callable by name; on, off and register are replaced by subscribe and unsubscribe.',
toViewer: {
call: { unroll: 1, id: 'number|string', method: 'string (an API method name)', args: 'array' },
subscribe: { unroll: 1, id: 'number|string', method: 'subscribe', args: ['eventName'] },
unsubscribe: { unroll: 1, id: 'number|string', method: 'unsubscribe', args: ['eventName'] },
},
fromViewer: {
ready: { unroll: 1, type: 'ready', version: '{ version, date, sha, api }', methods: 'array of method names' },
result: { unroll: 1, type: 'result', id: 'the id you sent', result: '{ ok: true, ... } | { ok: false, error, code }' },
event: { unroll: 1, type: 'event', event: 'string', payload: 'object' },
},
forbidden: ['open.listLocal', 'open.removeLocal'],
},
methods, events: EVENTS, urlParams: URL_PARAMS, keys: KEYS, manifestSchema: manifest,
};
writeFileSync(new URL('../api.json', import.meta.url), JSON.stringify(out, null, 2) + '\n');
console.log(`api.json: ${Object.keys(methods).length} methods, app ${ver.version}`);
|