← back to Quadrille Showroom
scripts/verify-openface.mjs
92 lines
// Open-face proof — captures the OPEN hero as ONE clean full 30"x6' pattern face with
// NO neighbour slat-bars / frames crossing it, plus a mid-swivel "flip-through" frame.
// Also computes a screen-space occlusion check: raycast a grid of points over the open
// hero's face rect and assert the FIRST hit mesh is the hero's own face (nothing in front).
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/openface';
mkdirSync(OUT, { recursive: true });
const result = {}, 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: 1600, height: 1000 } });
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);
// Click a left-of-centre board -> it conveyors to centre + opens. (Same interaction as
// the proven verify-carousel: pick floor(n/2)-8 then sample frames so the page stays hot.)
const picked = await page.evaluate(() => {
const n = window._qh.wingBoards.length;
const idx = Math.max(0, Math.floor(n / 2) - 8);
window._qh.focusOnWing(window._qh.wingBoards[idx]);
return idx;
});
// Sample across animation frames (keeps rAF active) + grab the mid-swivel flip-through.
for (let i = 0; i < 4; i++) {
await page.waitForTimeout(70);
await page.evaluate(() => window._qh.indexOffset);
}
await page.screenshot({ path: OUT + '/flip-through.png' });
// Let the shift settle + the centred board fan fully open + walls clad.
await page.waitForTimeout(3800);
// OCCLUSION CHECK — grid of screen points over the open hero's face; raycast and confirm
// the first hit is the hero's own face mesh (i.e. nothing crosses in front of the pattern).
const occ = await page.evaluate(() => {
const qh = window._qh, THREE = qh.THREE;
const hero = qh.focusedWing; if (!hero) return { ok: false, reason: 'no focus' };
const face = hero.userData.face;
// Project the face's 4 corners to screen to get its on-screen rect.
const cam = window._cam, rend = window._renderer || qh.renderer;
const w = window.innerWidth, h = window.innerHeight;
const geo = face.geometry; geo.computeBoundingBox();
const bb = geo.boundingBox;
const corners = [
new THREE.Vector3(bb.min.x, bb.min.y, bb.max.z),
new THREE.Vector3(bb.max.x, bb.min.y, bb.max.z),
new THREE.Vector3(bb.min.x, bb.max.y, bb.max.z),
new THREE.Vector3(bb.max.x, bb.max.y, bb.max.z),
].map(v => { face.localToWorld(v); v.project(cam); return { x: (v.x * 0.5 + 0.5) * w, y: (-v.y * 0.5 + 0.5) * h }; });
const xs = corners.map(c => c.x), ys = corners.map(c => c.y);
const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys);
// Sample an inset grid (avoid the very edge where the frame lives).
const rc = new THREE.Raycaster();
let total = 0, heroFirst = 0; const offenders = {};
// collect raycastable face meshes of all boards
const faces = qh.wingBoards.map(b => b.userData.face).filter(Boolean);
for (let gx = 0.12; gx <= 0.88; gx += 0.19) {
for (let gy = 0.10; gy <= 0.90; gy += 0.16) {
const sx = x0 + (x1 - x0) * gx, sy = y0 + (y1 - y0) * gy;
const ndc = new THREE.Vector2((sx / w) * 2 - 1, -(sy / h) * 2 + 1);
rc.setFromCamera(ndc, cam);
const hits = rc.intersectObjects(faces, false);
if (!hits.length) continue;
total++;
if (hits[0].object === face) heroFirst++;
else { const idx = hits[0].object.userData.parentPivot ? hits[0].object.userData.parentPivot.userData.index : '?'; offenders[idx] = (offenders[idx] || 0) + 1; }
}
}
return {
ok: true, total, heroFirst, allHeroFirst: total > 0 && heroFirst === total,
offenders, faceScreenRect: { x0: Math.round(x0), y0: Math.round(y0), x1: Math.round(x1), y1: Math.round(y1), wpx: Math.round(x1 - x0), hpx: Math.round(y1 - y0) },
flip: +hero.userData.flip.toFixed(3),
heroIdx: hero.userData.index,
realTexture: !!(face.material && face.material.map),
};
});
result.pickedIdx = picked;
result.occlusion = occ;
await page.screenshot({ path: OUT + '/open-clean.png' });
result.errorCount = errors.length;
result.errors = errors;
console.log(JSON.stringify(result, null, 2));
await browser.close();