Contract 2.0.0 · package sha256
a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e· generated fromCHECK_HEADLESSbyservices/onsite-scripter/scripts/build-contract-docs.mjs; edit the source, not this file.
How an author with no human confirms, before hand-off, that the product is on screen, lit and framed. The fake (/testing) checks your calls but draws no pixels, so this runs the real runtime in a headless browser: Playwright's bundled Chromium draws WebGL 2 in software (SwiftShader) on any machine, with or without a GPU.
Save it as check.html beside scene.json, the scene model you hand off (what your server's read returns, llms.txt's "Your server's read of the stored configurator"). It boots, sets the scene model and the start view your page sets (llms.txt's boot pages: the stored defaultView, else a fit), waits for idle(), captures the view at a pixel ratio of 1 over a transparent background, so a product pixel is an opaque one whatever colour the runtime draws, and runs the three checks in the page. window.holoCheck is the result, a Promise.
<!doctype html>
<meta charset="utf-8">
<div id="viewer" style="width: 800px; height: 600px"></div>
<script type="module">
const RUNTIME_URL = 'https://onsite.holostep.io/serve/runtime/v2-canary/configurator-runtime.js';
const ACCOUNT_ID = 164; // your Holostep account id
const SCENE_URL = './scene.json'; // the scene model you hand off
// Your page's start view, as llms.txt's boot pages set it after the first model.
// A stored view converts as the package's cameraViewFromStored does (degrees; a stored
// `position` wins over the angles; points copied as { x, y, z }); none frames the product.
function startView(session, view) {
if (!view) return session.camera.fit(undefined, { durationMs: 0 });
const xyz = (p) => ({ x: p.x, y: p.y, z: p.z });
const t = xyz(view.target);
const p = view.position && xyz(view.position);
const d = p && { x: p.x - t.x, y: p.y - t.y, z: p.z - t.z };
const deg = 180 / Math.PI;
const camera = d
? {
azimuthDeg: Math.atan2(d.x, d.z) * deg,
polarDeg: Math.atan2(Math.hypot(d.x, d.z), d.y) * deg,
distance: Math.hypot(d.x, d.y, d.z),
}
: { azimuthDeg: view.azimuth, polarDeg: view.polar, distance: view.radius };
const focal = view.focalOffset ? { focalOffset: xyz(view.focalOffset) } : {};
return session.camera.set({ target: t, ...camera, ...focal }, { durationMs: 0 });
}
/** The product's world bounds as a screen rectangle, CSS px; null when a corner is behind the camera. */
function projectedRect(session) {
const box = session.find({ role: 'product' }).bounds();
if (!box) return null;
const corners = [];
for (const x of [box.min.x, box.max.x])
for (const y of [box.min.y, box.max.y])
for (const z of [box.min.z, box.max.z]) corners.push({ x, y, z });
const points = session.camera.project(corners);
if (points.some((p) => p === null)) return null;
const xs = points.map((p) => p.x);
const ys = points.map((p) => p.y);
return { left: Math.min(...xs), top: Math.min(...ys), right: Math.max(...xs), bottom: Math.max(...ys) };
}
async function check() {
const runtime = await import(RUNTIME_URL);
const el = document.getElementById('viewer');
const session = await runtime.boot({ el, accountId: ACCOUNT_ID, contract: '2.0' });
const model = await (await fetch(SCENE_URL)).json();
const report = await session.setSceneModel(model);
// the view your page starts on: its stored view, else a fit (no scene write moves the camera)
await startView(session, model.defaultView);
await session.idle();
// Transparent: no colour stands for 'no product', so a pixel is the product's when it is
// opaque (a ground shadow or a see-through part never is).
const shot = await session.capture({ pixelRatio: 1, background: 'transparent' });
const { width, height } = shot.snapshot.size; // CSS px, the image's pixels at ratio 1
const image = await createImageBitmap(shot.image);
const canvas = Object.assign(document.createElement('canvas'), { width, height });
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
const { data } = context.getImageData(0, 0, width, height);
// 1. Pixels over the projected bounds: framed, drawn and lit.
const rect = projectedRect(session);
const area = (r) => Math.max(0, r.right - r.left) * Math.max(0, r.bottom - r.top);
const seen = rect && {
left: Math.max(0, Math.floor(rect.left)), top: Math.max(0, Math.floor(rect.top)),
right: Math.min(width, Math.ceil(rect.right)), bottom: Math.min(height, Math.ceil(rect.bottom)),
};
let inside = 0, drawn = 0, brightness = 0;
for (let y = seen?.top ?? 0; y < (seen?.bottom ?? 0); y++) {
for (let x = seen.left; x < seen.right; x++) {
const i = (y * width + x) * 4;
inside++;
// a product pixel: opaque in the transparent capture
if (data[i + 3] >= 250) {
drawn++;
brightness += Math.max(data[i], data[i + 1], data[i + 2]) / 255;
}
}
}
const problems = session.info().problems.map((p) => `${p.code}: ${p.message}`);
const checks = {
// the whole product is in the view (90 % of its rectangle) and fills 5 % of it at least
framed: rect !== null && area(seen) >= 0.9 * area(rect) && area(rect) >= 0.05 * width * height,
// a tenth of its rectangle at least is product pixels
drawn: inside > 0 && drawn / inside >= 0.1,
// its pixels are not black: a light is on it
lit: drawn > 0 && brightness / drawn >= 0.15,
// 2. no Problem recorded, and the scene model committed
problems: problems.length === 0 && report.outcome === 'committed',
// 3. nothing was still moving or loading in the image
settled: shot.snapshot.settled && shot.completed,
};
const ok = Object.values(checks).every(Boolean);
return { ok, checks, problems, rect, drawnShare: inside ? drawn / inside : 0, brightness: drawn ? brightness / drawn : 0 };
}
window.holoCheck = check().catch((error) => ({ ok: false, error: String(error?.stack ?? error) }));
</script>
Save it as check.mjs. It needs Playwright and its Chromium once: npm i -D playwright && npx playwright install chromium. Serve the folder over http (npx http-server . -p 8080, or python3 -m http.server 8080): a file:// page can't import the runtime module from another origin. Then node check.mjs http://localhost:8080/check.html: it prints the result and exits 0 when every check passes.
import { chromium } from 'playwright';
const url = process.argv[2];
// WebGL 2 in software, so it draws on any machine, GPU or not.
const browser = await chromium.launch({
args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist'],
});
try {
const page = await browser.newPage({ viewport: { width: 1000, height: 800 } });
page.on('console', (m) => ['warning', 'error'].includes(m.type()) && console.error(m.text()));
await page.goto(url);
const result = await page.evaluate(() => window.holoCheck);
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.ok ? 0 : 1;
} finally {
await browser.close();
}
find({ role: 'product' }).bounds() projected with camera.project gives the product's rectangle on screen. framed: the whole rectangle is in the view (90 % of it at least) and fills 5 % of the view or more, from your page's start view (startView, as the boot pages set it: no scene write moves the camera; a page with another start view writes its own there); a product off to one side, behind the camera or a speck fails it. drawn: a tenth of the rectangle at least is opaque in the transparent capture; a model that failed to load, is hidden or is drawn transparent fails it, and so does a glass product (lower the bar for one). lit: the opaque pixels average 15 % brightness or more; a black frame (no light, a stage whose lights the page document turned off) fails it, and a product authored black reads dark, so lower the bar for one. An opaque floor or backdrop in the page document counts as product inside the rectangle: check such a stage with a page document without them.info().problems is empty and the scene model committed (Report.outcome). Each Problem prints with its code and message: its page under problems/ says what to do. A page with a configuratorId but no stored viewer-document layer records 'tenant-document-absent' by design; drop that code from the check for such a page.snapshot.settled (and completed): no clip, tween, fade, accumulation or late texture was in flight when the image was taken, so the pixels are the product at rest. An autoRotate or a looping clip keeps it false: stop it for the check.Check the image itself too: shot.image is a PNG; send it to your own reviewer or save it (await page.evaluate(...) can return it as a data URL). The fake answers the same calls without pixels, so keep your unit tests on createFakeSession and run this check once per scene model you hand off.