← 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();