[object Object]

← back to Quadrille Showroom

showroom P3: At-the-Table view — seated avatar + click-pattern-to-center + add/drag/remove samples on the table (basket persisted); fix addSample null-deref + table recorder

8dfe072c3eb909ad1ffc238612ee755eff5adb63 · 2026-07-01 07:02:47 -0700 · Steve Abrams

Files touched

Diff

commit 8dfe072c3eb909ad1ffc238612ee755eff5adb63
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 1 07:02:47 2026 -0700

    showroom P3: At-the-Table view — seated avatar + click-pattern-to-center + add/drag/remove samples on the table (basket persisted); fix addSample null-deref + table recorder
---
 public/js/viewmodes.js   |  8 +++++-
 scripts/record-table.mjs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 1 deletion(-)

diff --git a/public/js/viewmodes.js b/public/js/viewmodes.js
index 32c15cb..0795811 100644
--- a/public/js/viewmodes.js
+++ b/public/js/viewmodes.js
@@ -457,7 +457,12 @@ function boot() {
   function currentCenteredProduct() {
     const f = QH.focusedWing;
     if (f && f.userData && f.userData.product) return f.userData.product;
-    return null;
+    // No board focused yet — fall back to the current/middle board so "Add sample"
+    // always has a swatch to drop (prevents a null-deref when nothing's been clicked).
+    const boards = QH.wingBoards || [];
+    const i = curIdx();
+    const b = boards[i] || boards[Math.floor(boards.length / 2)];
+    return (b && b.userData && b.userData.product) ? b.userData.product : null;
   }
 
   function imgUrl(u) {
@@ -507,6 +512,7 @@ function boot() {
   }
 
   function addSampleToTable(p) {
+    if (!p) { flashAddHint && flashAddHint('Click a design first'); return; }
     const surface = document.getElementById('vm-sample-surface');
     if (!surface) return;
     // De-dupe: same SKU already on the table → don't stack, just nudge the count.
diff --git a/scripts/record-table.mjs b/scripts/record-table.mjs
new file mode 100644
index 0000000..cfdfb43
--- /dev/null
+++ b/scripts/record-table.mjs
@@ -0,0 +1,69 @@
+// record-table.mjs — screen-record the "At the Table" view (Phase 3):
+// enter table mode → add samples from a few different boards → drag one → click a
+// board to center the design. Playwright chromium + recordVideo, webm→mp4.
+import pw from '/Users/stevestudio2/.npm-global/lib/node_modules/playwright/index.js';
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+const { chromium } = pw;
+
+const URL = 'http://127.0.0.1:7690/';
+const OUT = 'recordings';
+fs.mkdirSync(OUT, { recursive: true });
+
+const errs = [];
+const b = await chromium.launch({ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
+const ctx = await b.newContext({ viewport: { width: 1600, height: 900 }, recordVideo: { dir: OUT, size: { width: 1600, height: 900 } } });
+const pg = await ctx.newPage();
+pg.on('pageerror', e => errs.push('PAGEERR ' + e.message));
+pg.on('console', m => { if (m.type() === 'error') { const t = m.text(); if (!/SwiftShader|GL_|GPU stall|ReadPixels/.test(t)) errs.push('CONSOLE ' + t); } });
+
+console.log('→ loading', URL);
+await pg.goto(URL, { waitUntil: 'networkidle', timeout: 40000 }).catch(() => {});
+await pg.waitForTimeout(3000);
+
+console.log('→ enter At-the-Table view');
+await pg.evaluate(() => window._viewmode.set('table'));
+await pg.waitForTimeout(2000);
+
+// Add samples from three different boards so they don't de-dup.
+for (const idx of [24, 28, 32]) {
+  await pg.evaluate((i) => {
+    const boards = window._qh.wingBoards || [];
+    const b = boards[i];
+    if (b) window._qh.focusOnWing(b);          // center that design
+  }, idx);
+  await pg.waitForTimeout(1400);
+  await pg.evaluate(() => window._viewmode.addSample());  // drop its swatch on the table
+  await pg.waitForTimeout(1100);
+}
+
+// Drag the first sample card to a new spot on the table.
+const card = await pg.$('.vm-sample');
+if (card) {
+  const box = await card.boundingBox();
+  if (box) {
+    await pg.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
+    await pg.mouse.down();
+    await pg.mouse.move(box.x + 180, box.y + 60, { steps: 20 });
+    await pg.mouse.move(box.x + 260, box.y - 40, { steps: 20 });
+    await pg.mouse.up();
+  }
+}
+await pg.waitForTimeout(1500);
+
+console.log('→ done recording; errors:', errs.length);
+errs.slice(0, 6).forEach(e => console.log('   ' + e.slice(0, 120)));
+const samples = await pg.evaluate(() => document.querySelectorAll('.vm-sample').length);
+console.log('   samples on table:', samples);
+
+await ctx.close();  // finalizes the webm
+await b.close();
+
+// rename newest webm + transcode to mp4
+const webms = fs.readdirSync(OUT).filter(f => f.endsWith('.webm')).map(f => ({ f, t: fs.statSync(`${OUT}/${f}`).mtimeMs })).sort((a, z) => z.t - a.t);
+if (webms.length) {
+  const src = `${OUT}/${webms[0].f}`, dstW = `${OUT}/table-view.webm`, dstM = `${OUT}/table-view.mp4`;
+  fs.renameSync(src, dstW);
+  try { execSync(`ffmpeg -y -i "${dstW}" -c:v libx264 -pix_fmt yuv420p "${dstM}"`, { stdio: 'ignore' }); console.log('✓ mp4:', dstM); }
+  catch { console.log('✓ webm (no ffmpeg mp4):', dstW); }
+}

← 0fb3c6b auto-save: 2026-06-30T21:43:31 (2 files) — public/js/showroo  ·  back to Quadrille Showroom  ·  showroom: boot = first-person user standing in the doorway a 90fe8f0 →