← back to Quadrille Showroom
scripts/shot-chunkK-avatar.mjs
92 lines
// Chunk K (first-person Steve avatar) + Chunk A′ (20'×20' room) PROOF BAR.
// Real-GPU headed Chrome on the live :7690 canvas. Captures the 4 proof shots and
// runs the asserts: camera.position constant across yaw (posDelta 0), body present +
// yaw-only parented (rig pitch 0 even when camera pitches down), 50 boards, click still
// → carousel + 4-wall clad, errorCount 0, room = 20'×20'.
import pw from '/Users/macstudio3/.npm-global/lib/node_modules/playwright/index.js';
const { chromium } = pw;
import { mkdirSync } from 'fs';
const OUT = '/Users/macstudio3/Projects/quadrille-showroom/recordings/chunkK';
mkdirSync(OUT, { recursive: true });
const errors = [];
const browser = await chromium.launch({ channel: 'chrome', headless: false, args: ['--use-angle=metal', '--ignore-gpu-blocklist', '--window-size=1440,900'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
page.on('console', m => { if (m.type() === 'error') errors.push('console.error: ' + m.text()); });
await page.goto('http://localhost:7690/', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window._qh && window._qh.wingBoards && window._qh.wingBoards.length >= 50, { timeout: 25000 });
await page.waitForTimeout(3500); // let the boot settle (resting-open middle)
const result = {};
// ---- baseline: room size + board count + avatar presence + camera pos ----
result.room = await page.evaluate(() => ({ w: window._qh.CONFIG.room.width, d: window._qh.CONFIG.room.depth }));
result.boardCount = await page.evaluate(() => window._qh.wingBoards.length);
result.avatarPresent = await page.evaluate(() => window._qh.avatarPresent);
result.avatarState0 = await page.evaluate(() => window._qh.avatarState());
const camPos0 = await page.evaluate(() => { const p = window._qh.camera.position; return { x: p.x, y: p.y, z: p.z }; });
// =================================================================
// PROOF 1 — look-ahead: straight at the wing bank, body NOT blocking
// =================================================================
await page.evaluate(() => window._qh.setSpin(0, -0.08)); // dead-ahead, canonical rest pitch
await page.waitForTimeout(900);
await page.screenshot({ path: OUT + '/01-look-ahead.png' });
result.lookAhead = await page.evaluate(() => ({ rigYaw: window._qh.avatarState().rigYaw, rigPitch: window._qh.avatarState().rigPitch, spinPitch: window._qh.spinPitch }));
// =================================================================
// PROOF 2 — look-down: pitch down → torso/hands/legs/feet visible
// =================================================================
await page.evaluate(() => window._qh.setSpin(0, -1.05)); // full down (~60°) — onto your own body
await page.waitForTimeout(900);
await page.screenshot({ path: OUT + '/02-look-down.png' });
result.lookDown = await page.evaluate(() => window._qh.avatarState());
// also a mid-down (~45°) shot — the natural "glance at hands" angle
await page.evaluate(() => window._qh.setSpin(0, -0.78));
await page.waitForTimeout(700);
await page.screenshot({ path: OUT + '/02b-look-down-mid.png' });
// =================================================================
// PROOF 3 — yaw-with-body: yaw right; body turns WITH the view (rig yaw tracks),
// rig pitch stays 0, boards still in frame, clamp holds, camera pos constant.
// =================================================================
await page.evaluate(() => window._qh.setSpin(0.9, -0.08)); // yaw right ~+51°, level-ish
await page.waitForTimeout(900);
await page.screenshot({ path: OUT + '/03-yaw-with-body.png' });
result.yawRight = await page.evaluate(() => window._qh.avatarState());
const camPosYaw = await page.evaluate(() => { const p = window._qh.camera.position; return { x: p.x, y: p.y, z: p.z }; });
// yaw past the clamp to confirm it still holds with the body
result.yawClamp = await page.evaluate(() => window._qh.testYawClamp());
// yaw left for a second body-turns shot
await page.evaluate(() => window._qh.setSpin(-0.9, -0.30));
await page.waitForTimeout(700);
await page.screenshot({ path: OUT + '/03b-yaw-left-down.png' });
result.yawLeft = await page.evaluate(() => window._qh.avatarState());
// posDelta across yaw — camera position MUST be constant (Slice-1)
result.posDelta = {
yaw: Math.hypot(camPosYaw.x - camPos0.x, camPosYaw.y - camPos0.y, camPosYaw.z - camPos0.z)
};
// =================================================================
// PROOF 4 — room-20ft: confirm larger room + bank fans correctly. Reset to a wide
// establishing pose and snapshot the full fanned bank at a comfortable dist.
// =================================================================
await page.evaluate(() => window._qh.setSpin(0, -0.10));
await page.waitForTimeout(700);
await page.screenshot({ path: OUT + '/04-room-20ft.png' });
// ---- regression: click still selects → carousel + 4-wall clad ----
await page.evaluate(() => { const wb = window._qh.wingBoards; window._qh.setSpin(0, -0.08); window._qh.focusOnWing(wb[Math.floor(wb.length / 2) + 4]); });
await page.waitForTimeout(4200); // carousel + flip + clad load
result.cladSides = await page.evaluate(() => window._qh.cladSides);
result.focused = await page.evaluate(() => !!window._qh.focusedWing);
await page.screenshot({ path: OUT + '/05-click-clad-regression.png' });
result.errorCount = errors.length;
result.errors = errors.slice(0, 10);
console.log(JSON.stringify(result, null, 2));
await browser.close();