← back to Quadrille Showroom
Slice-2 contrarian fixes: HOLE-3 detail starts COLLAPSED on every fresh selection (drop localStorage override footgun) + spec-legal proof harness (real ArrowL/R yaw to +/-72 clamp, NO mode switch). HOLE-1/HOLE-2 verified already-correct: hero opens flip=0.999 / 6.02x neighbour width; clad walls visible front (252 cells) + side at clamp (680/643 cells); posDelta 0; 16/16 assertions pass, errorCount 0.
c4a14bb4fb085218aa9ee528b782dfe28ec64729 · 2026-06-28 18:18:58 -0700 · steve
Files touched
A scripts/verify-slice2-spec-legal.mjs
Diff
commit c4a14bb4fb085218aa9ee528b782dfe28ec64729
Author: steve <steve@designerwallcoverings.com>
Date: Sun Jun 28 18:18:58 2026 -0700
Slice-2 contrarian fixes: HOLE-3 detail starts COLLAPSED on every fresh selection (drop localStorage override footgun) + spec-legal proof harness (real ArrowL/R yaw to +/-72 clamp, NO mode switch). HOLE-1/HOLE-2 verified already-correct: hero opens flip=0.999 / 6.02x neighbour width; clad walls visible front (252 cells) + side at clamp (680/643 cells); posDelta 0; 16/16 assertions pass, errorCount 0.
---
scripts/verify-slice2-spec-legal.mjs | 192 +++++++++++++++++++++++++++++++++++
1 file changed, 192 insertions(+)
diff --git a/scripts/verify-slice2-spec-legal.mjs b/scripts/verify-slice2-spec-legal.mjs
new file mode 100644
index 0000000..64e981f
--- /dev/null
+++ b/scripts/verify-slice2-spec-legal.mjs
@@ -0,0 +1,192 @@
+// ============================================================
+// SPEC-LEGAL Slice-2 proof — regenerates the 4 contrarian screenshots using ONLY
+// the real fixed-centre yaw-spin interaction. NO setMode('room'), NO park(), NO
+// view-mode change, NO camera translation. Every "spin" is a real ArrowLeft/
+// ArrowRight/ArrowUp keydown dispatched on window, eased by the app's own
+// updateWASD() at the ±72° clamp. Click-to-select goes through window._qh.focusOnWing
+// (identical payload to onCanvasClick).
+//
+// Asserts:
+// • boot: no selection, no detail panel (Slice-1 intact)
+// • selected: hero board dead-front (focusedIdx == clicked), flip >= 0.9 (dramatic
+// open), hero on-screen width >= 3x its closed sliver neighbour, detail card
+// COLLAPSED, all 4 wall SIDES clad, clad front-wall pattern visible around the bank
+// • detail-expanded: chevron toggle reveals 27" China Seas specs
+// • spin-to-clad-side-wall: real ArrowRight yaw to the clamp shows a clad SIDE wall
+// (raycast confirms a large block of clad-wall pixels on screen) — REAL yaw, no mode
+// • HOLE-3 determinism: with localStorage qh_detail_collapsed='0' (stored "expanded"),
+// a FRESH selection STILL starts collapsed
+// • camera posDelta 0 across the whole spin; yaw clamp ±72°; errorCount 0
+// ============================================================
+import pw from '/Users/stevestudio2/.npm-global/lib/node_modules/playwright/index.js';
+const { chromium } = pw;
+import { mkdirSync } from 'fs';
+
+const OUT = '/Users/stevestudio2/Projects/quadrille-showroom/recordings/slice2-spec-legal';
+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));
+
+// HOLE-3 hostile setup: pre-seed localStorage with a stored EXPANDED flag so we prove a
+// fresh selection ignores it and still starts collapsed.
+await page.addInitScript(() => { try { localStorage.setItem('qh_detail_collapsed','0'); } catch(e){} });
+
+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);
+
+// raycast sampler for clad-wall on-screen pixels (40x24 grid). Counts screen cells whose
+// camera ray first hits a large room-wall plane carrying a clad texture map.
+await page.addScriptTag({ content: `
+window.__measureClad = function() {
+ const qh=window._qh, T=qh.THREE, cam=qh.camera, scene=qh.scene;
+ const rc=new T.Raycaster(); const cladMeshes=[]; const targets=[];
+ scene.traverse(o=>{ if(o.isMesh){ targets.push(o);
+ if(o.material&&o.material.map&&o.geometry&&o.geometry.type==='PlaneGeometry'){
+ const g=o.geometry.parameters||{}; if(g.width>=1.0&&g.height>=2.0) cladMeshes.push(o); } } });
+ let cladHits=0,wingHits=0,other=0,total=0;
+ for(let yi=0;yi<24;yi++)for(let xi=0;xi<40;xi++){
+ const nx=(xi+0.5)/40*2-1, ny=-((yi+0.5)/24*2-1);
+ rc.setFromCamera({x:nx,y:ny},cam); const h=rc.intersectObjects(targets,false)[0];
+ total++; if(!h)continue;
+ if(cladMeshes.includes(h.object))cladHits++;
+ else if(h.object.userData&&h.object.userData.isWingFace)wingHits++; else other++;
+ }
+ return { cladHits,wingHits,other,total,yawDeg:+(qh.spinYaw*180/Math.PI).toFixed(1) };
+};
+window.__heroWidth = function() {
+ const qh=window._qh,T=qh.THREE,cam=qh.camera,f=qh.focusedWing; if(!f)return null;
+ const wb=qh.wingBoards;
+ function wpx(p){ const ud=p.userData,face=ud.face; if(!face)return 0;
+ const g=face.geometry.parameters,hw=g.width/2,hh=g.height/2;
+ let mn=1e9,mx=-1e9;
+ for(const[cx,cy]of[[-hw,-hh],[hw,-hh],[hw,hh],[-hw,hh]]){
+ const v=new T.Vector3(cx,cy,0); face.localToWorld(v); v.project(cam);
+ const px=(v.x*0.5+0.5)*window.innerWidth; mn=Math.min(mn,px);mx=Math.max(mx,px);} return mx-mn; }
+ const hi=f.userData.index; const nb=wb[hi+3]||wb[hi-3]||wb[hi+1]||wb[hi-1];
+ const heroW=wpx(f), nbW=nb?wpx(nb):0;
+ return { heroW:+heroW.toFixed(1), nbW:+nbW.toFixed(1), ratio:+(heroW/(nbW||1)).toFixed(2), flip:+f.userData.flip.toFixed(3), idx:hi };
+};
+`});
+
+// helper: real held-key yaw (dispatch keydown, wait, keyup). updateWASD eases each frame.
+async function holdKey(key, ms) {
+ await page.evaluate(k=>window.dispatchEvent(new KeyboardEvent('keydown',{key:k,code:k,bubbles:true})), key);
+ await page.waitForTimeout(ms);
+ await page.evaluate(k=>window.dispatchEvent(new KeyboardEvent('keyup',{key:k,code:k,bubbles:true})), key);
+ await page.waitForTimeout(450);
+}
+
+// ---------- 1. BOOT ----------
+const boot = await page.evaluate(() => ({
+ focused: !!window._qh.focusedWing,
+ resting: !!window._qh.restingOpenWing,
+ detailHidden: document.getElementById('wing-detail').classList.contains('hidden'),
+ camPos: [window._cam.position.x, window._cam.position.y, window._cam.position.z],
+}));
+result.boot = boot;
+const bootCam = boot.camPos;
+await page.screenshot({ path: OUT + '/01-boot.png' });
+
+// ---------- 2. SELECTED (real focus path) ----------
+const pickIdx = await page.evaluate(() => {
+ const wb = window._qh.wingBoards;
+ const idx = Math.floor(wb.length/2) + 4; // a clearly off-middle board
+ window.__pick = idx; window._qh.focusOnWing(wb[idx]); return idx;
+});
+await page.waitForTimeout(4800); // carousel settle + flip + clad-image onload
+const selected = await page.evaluate(() => {
+ const panel = document.getElementById('wing-detail');
+ const cladSides = window._qh.cladSides, wallSides = window._qh.wallSides;
+ return {
+ focusedIdx: window._qh.focusedWing ? window._qh.focusedWing.userData.index : null,
+ pick: window.__pick,
+ deadFront: window._qh.focusedWing && window._qh.focusedWing.userData.index === window.__pick,
+ detailHidden: panel.classList.contains('hidden'),
+ detailCollapsed: panel.classList.contains('collapsed'),
+ cladSides, wallSides,
+ allFourClad: wallSides.length===4 && wallSides.every(s=>cladSides.includes(s)),
+ carouselActive: window._qh.carouselActive,
+ };
+});
+selected.hero = await page.evaluate(() => window.__heroWidth());
+selected.cladFrontView = await page.evaluate(() => window.__measureClad());
+result.selected = selected;
+await page.screenshot({ path: OUT + '/02-selected-front.png' });
+
+// ---------- 3. DETAIL EXPANDED (real chevron handler) ----------
+await page.evaluate(() => document.getElementById('detail-collapse').click());
+await page.waitForTimeout(600);
+const expanded = await page.evaluate(() => {
+ const panel = document.getElementById('wing-detail');
+ const grid = document.getElementById('detail-spec-grid');
+ const specText = grid ? grid.textContent.replace(/\s+/g,' ').trim() : '';
+ return {
+ detailCollapsed: panel.classList.contains('collapsed'),
+ pattern: (document.getElementById('detail-pattern')||{}).textContent||'',
+ vendor: (document.getElementById('detail-vendor')||{}).textContent||'',
+ specText, has27: specText.includes('27'),
+ };
+});
+result.detailExpanded = expanded;
+await page.screenshot({ path: OUT + '/03-detail-expanded.png' });
+// re-collapse so the spin screenshot is clean
+await page.evaluate(() => { const p=document.getElementById('wing-detail'); if(!p.classList.contains('collapsed')) document.getElementById('detail-collapse').click(); });
+await page.waitForTimeout(300);
+
+// ---------- 4. SPIN-TO-CLAD-SIDE-WALL (REAL ArrowRight yaw to clamp) ----------
+// Hold ArrowRight long enough to ride the ease to the +72 clamp. Measure clad wall pixels
+// + assert the camera position did NOT move (posDelta 0 vs boot). NO mode switch.
+await holdKey('ArrowRight', 1700);
+await holdKey('ArrowRight', 1200); // top up to clamp
+const spinRight = await page.evaluate(() => window.__measureClad());
+const camAfterSpin = await page.evaluate(() => [window._cam.position.x, window._cam.position.y, window._cam.position.z]);
+result.spinRight = spinRight;
+await page.screenshot({ path: OUT + '/04-spin-to-clad-side-wall.png' });
+
+// also sample the left clamp for the record (no extra screenshot needed)
+await holdKey('ArrowLeft', 1700);
+await holdKey('ArrowLeft', 1700);
+await holdKey('ArrowLeft', 1200);
+const spinLeft = await page.evaluate(() => window.__measureClad());
+result.spinLeft = spinLeft;
+
+// ---------- CAMERA + YAW-CLAMP ASSERTIONS ----------
+const dx = camAfterSpin[0]-bootCam[0], dy = camAfterSpin[1]-bootCam[1], dz = camAfterSpin[2]-bootCam[2];
+result.posDelta = +Math.sqrt(dx*dx+dy*dy+dz*dz).toFixed(5);
+const clamp = await page.evaluate(() => {
+ const t = window._qh.testYawClamp();
+ return { rightDeg:+(t.right*180/Math.PI).toFixed(2), leftDeg:+(t.left*180/Math.PI).toFixed(2), clampDeg:+(t.clamp*180/Math.PI).toFixed(2) };
+});
+result.yawClamp = clamp;
+result.errorCount = errors.length;
+result.errors = errors;
+
+// ---------- VERDICT ----------
+const A = [];
+A.push(['boot: no selection', boot.focused===false]);
+A.push(['boot: no detail panel', boot.detailHidden===true]);
+A.push(['selected: dead-front == clicked', selected.deadFront===true]);
+A.push(['selected: hero flip >= 0.9', selected.hero && selected.hero.flip>=0.9]);
+A.push(['selected: hero >= 3x neighbour width', selected.hero && selected.hero.ratio>=3]);
+A.push(['selected: detail COLLAPSED (HOLE-3, stored=expanded ignored)', selected.detailCollapsed===true]);
+A.push(['selected: all 4 walls clad', selected.allFourClad===true]);
+A.push(['selected: clad front-wall visible around bank (>=80 cells)', selected.cladFrontView.cladHits>=80]);
+A.push(['expanded: shows 27" specs', expanded.has27===true]);
+A.push(['expanded: card now open', expanded.detailCollapsed===false]);
+A.push(['spin-right: real yaw reached side (>=40 deg)', Math.abs(spinRight.yawDeg)>=40]);
+A.push(['spin-right: clad SIDE wall visible (>=150 cells)', spinRight.cladHits>=150]);
+A.push(['spin-left: clad SIDE wall visible (>=150 cells)', spinLeft.cladHits>=150]);
+A.push(['camera posDelta ~0', result.posDelta<=0.01]);
+A.push(['yaw clamp == +/-72', clamp.clampDeg===72 && clamp.rightDeg===72 && clamp.leftDeg===-72]);
+A.push(['errorCount 0', errors.length===0]);
+result.assertions = A.map(([n,p])=>({ name:n, pass:!!p }));
+result.allPass = A.every(([,p])=>p);
+
+console.log(JSON.stringify(result, null, 2));
+await browser.close();
+process.exit(result.allPass ? 0 : 1);
← 34871bc auto-save: 2026-06-28T18:16:07 (2 files) — public/js/showroo
·
back to Quadrille Showroom
·
Slice-3 H: globally-unique element build codes (1000QH<versi 1b5eb93 →