← back to Quadrille Showroom
scripts/verify-carousel.mjs
184 lines
// Chunk I verification — the dry-cleaner / tie-rack CAROUSEL.
// Drives the real focus path (window._qh.focusOnWing == onCanvasClick) and asserts:
// - before-click: boot resting state (middle open, no selection, no detail)
// - click a LEFT-of-centre board -> the RACK conveyor-shifts (indexOffset animates over
// frames, NOT a teleport), the clicked board reaches the centre slot, fans open,
// detail card visible+collapsed, all 4 walls clad
// - click a RIGHT-of-centre board -> carousels to centre (closes prior)
// - the CAMERA never moves across the whole interaction (posDelta ~0)
// - errorCount 0
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/carousel';
mkdirSync(OUT, { recursive: true });
const result = {};
const errors = [];
const browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
await page.goto('http://localhost:7690/', { waitUntil: 'networkidle' });
await page.waitForFunction(() => window._qh && window._qh.wingBoards && window._qh.wingBoards.length > 0, { timeout: 20000 });
await page.waitForTimeout(2500); // boot resting-open ease + first textures
// Camera-position sampler (fixed-centre invariant): grab the eye now and after every step.
const camNow = () => page.evaluate(() => {
const p = window._cam.position; return [p.x, p.y, p.z];
});
const dist = (a, b) => Math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2);
// ---------- 1. BEFORE-CLICK (boot resting state) ----------
const boot = await page.evaluate(() => ({
focused: !!window._qh.focusedWing,
resting: !!window._qh.restingOpenWing,
restingIdx: window._qh.restingOpenWing ? window._qh.restingOpenWing.userData.index : null,
detailHidden: document.getElementById('wing-detail').classList.contains('hidden'),
indexOffset: +window._qh.indexOffset.toFixed(4),
centerSlotIndex: window._qh.centerSlotIndex(),
boards: window._qh.wingBoards.length,
}));
result.boot = boot;
const camBoot = await camNow();
await page.screenshot({ path: OUT + '/01-before-click.png' });
// Helper: world-X of a board's pivot (centre slot pivot sits at world x≈0, z≈front).
const pivotWorldX = (idx) => page.evaluate((i) => {
const THREE = window._qh.THREE;
const v = new THREE.Vector3();
window._qh.wingBoards[i].getWorldPosition(v);
return { x: +v.x.toFixed(4), z: +v.z.toFixed(4) };
}, idx);
// ---------- 2. CLICK A LEFT-OF-CENTRE SLIVER ----------
// Pick a board well LEFT of centre. Lower index = left side of the fanned arc.
const leftIdx = await page.evaluate(() => {
const n = window._qh.wingBoards.length;
const idx = Math.max(0, Math.floor(n / 2) - 8); // 8 slots left of the middle
window.__leftIdx = idx;
// Stamp the offset BEFORE the click so we can prove it animates, not teleports.
window.__offBefore = window._qh.indexOffset;
window.__offSamples = [];
window._qh.focusOnWing(window._qh.wingBoards[idx]);
return idx;
});
// Sample indexOffset across several animation frames to PROVE it eases (not a teleport).
const leftSamples = [];
for (let i = 0; i < 6; i++) {
await page.waitForTimeout(60);
leftSamples.push(await page.evaluate(() => +window._qh.indexOffset.toFixed(4)));
}
// MID-SHIFT screenshot (rack caught mid-conveyor).
await page.screenshot({ path: OUT + '/02-click-left-sliver.png' });
// Let the shift settle + the centred board fan open + walls clad.
await page.waitForTimeout(3200);
const leftPivot = await pivotWorldX(leftIdx);
const centerPivotX = await page.evaluate(() => {
// The clicked board now occupies the centre slot. Its open hero is pushed forward + the
// pivot is offset left by halfWidth so the FACE (not the left-hinge pivot) lands dead-front;
// measure the face-centre world-X — THAT is what should read ~0 (the centred 30" pattern).
const THREE = window._qh.THREE;
const v = new THREE.Vector3();
const f = window._qh.focusedWing.userData.face || window._qh.focusedWing;
f.getWorldPosition(v);
return +v.x.toFixed(4);
});
const afterLeft = await page.evaluate(() => {
const panel = document.getElementById('wing-detail');
const cladSides = (window._cladSides || []).slice();
const wallSides = (window._wallSides || []).slice();
return {
focusedIdx: window._qh.focusedWing ? window._qh.focusedWing.userData.index : null,
pickedIdx: window.__leftIdx,
indexOffset: +window._qh.indexOffset.toFixed(4),
targetOffset: +window._qh.carouselTarget.toFixed(4),
carouselActive: window._qh.carouselActive,
detailHidden: panel.classList.contains('hidden'),
detailCollapsed: panel.classList.contains('collapsed'),
boardFlip: window._qh.focusedWing ? +window._qh.focusedWing.userData.flip.toFixed(3) : null,
cladSides, wallSides,
allFourClad: wallSides.length === 4 && wallSides.every(s => cladSides.includes(s)),
offsetChanged: window.__offBefore !== window._qh.indexOffset,
};
});
const camAfterLeft = await camNow();
result.afterLeftShift = {
...afterLeft,
leftSamples,
centeredPivotWorldX: centerPivotX,
reachedCenter: Math.abs(centerPivotX) < 0.05,
posDeltaFromBoot: +dist(camAfterLeft, camBoot).toFixed(4),
};
await page.screenshot({ path: OUT + '/03-after-shift.png' });
// ---------- 3. CLICK A RIGHT-OF-CENTRE SLIVER ----------
const rightIdx = await page.evaluate(() => {
const n = window._qh.wingBoards.length;
const idx = Math.min(n - 1, Math.floor(n / 2) + 8); // 8 slots right of the middle
window.__rightIdx = idx;
window.__offBefore2 = window._qh.indexOffset;
window._qh.focusOnWing(window._qh.wingBoards[idx]);
return idx;
});
const rightSamples = [];
for (let i = 0; i < 6; i++) {
await page.waitForTimeout(60);
rightSamples.push(await page.evaluate(() => +window._qh.indexOffset.toFixed(4)));
}
await page.screenshot({ path: OUT + '/04-click-right-sliver.png' });
await page.waitForTimeout(3200);
const centerPivotX2 = await page.evaluate(() => {
const THREE = window._qh.THREE;
const v = new THREE.Vector3();
const f = window._qh.focusedWing.userData.face || window._qh.focusedWing;
f.getWorldPosition(v);
return +v.x.toFixed(4);
});
const afterRight = await page.evaluate(() => {
const panel = document.getElementById('wing-detail');
const cladSides = (window._cladSides || []).slice();
const wallSides = (window._wallSides || []).slice();
return {
focusedIdx: window._qh.focusedWing ? window._qh.focusedWing.userData.index : null,
pickedIdx: window.__rightIdx,
indexOffset: +window._qh.indexOffset.toFixed(4),
targetOffset: +window._qh.carouselTarget.toFixed(4),
detailCollapsed: panel.classList.contains('collapsed'),
boardFlip: window._qh.focusedWing ? +window._qh.focusedWing.userData.flip.toFixed(3) : null,
allFourClad: wallSides.length === 4 && wallSides.every(s => cladSides.includes(s)),
offsetChanged: window.__offBefore2 !== window._qh.indexOffset,
};
});
const camAfterRight = await camNow();
result.afterRightShift = {
...afterRight,
rightSamples,
centeredPivotWorldX: centerPivotX2,
reachedCenter: Math.abs(centerPivotX2) < 0.05,
posDeltaFromBoot: +dist(camAfterRight, camBoot).toFixed(4),
};
await page.screenshot({ path: OUT + '/05-after-shift2.png' });
// ---------- FIXED-CENTRE INVARIANT (whole interaction) ----------
result.camera = {
camBoot,
camAfterLeft,
camAfterRight,
posDeltaBootToLeft: +dist(camBoot, camAfterLeft).toFixed(4),
posDeltaLeftToRight: +dist(camAfterLeft, camAfterRight).toFixed(4),
posDeltaWhole: +dist(camBoot, camAfterRight).toFixed(4),
};
result.errorCount = errors.length;
result.errors = errors;
console.log(JSON.stringify(result, null, 2));
await browser.close();