← back to Quadrille Showroom

scripts/walkthrough-record.js

116 lines

#!/usr/bin/env node
/**
 * walkthrough-record.js — automated "move around" screen recording of the showroom.
 * Drives a real Chromium through: load → walk to the wall (proximity book-match) →
 * inspect a wing (new spec-sheet card) → fan cascade → Peruse auto-tour → scene stats.
 * Records WebM video + key screenshots; convert to MP4 via ffmpeg afterward.
 *
 * Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/walkthrough-record.js
 * Local 127.0.0.1 is auth-exempt, so no basic-auth needed.
 */
const path = require('path');
const fs = require('fs');
const { chromium } = require('playwright');

const URL = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
const OUT = path.join(__dirname, '..', 'recordings');
fs.mkdirSync(OUT, { recursive: true });
const SHOTS = path.join(OUT, 'shots');
fs.mkdirSync(SHOTS, { recursive: true });

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function holdKey(page, key, ms) {
  await page.keyboard.down(key);
  await sleep(ms);
  await page.keyboard.up(key);
}

(async () => {
  const browser = await chromium.launch({ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
  const ctx = await browser.newContext({
    viewport: { width: 1600, height: 900 },
    recordVideo: { dir: OUT, size: { width: 1600, height: 900 } },
  });
  const page = await ctx.newPage();
  const logs = [];
  page.on('console', m => logs.push(`[${m.type()}] ${m.text()}`));

  console.log('→ loading', URL);
  await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });

  // Wait for the engine to build the wing wall.
  await page.waitForFunction(() => window._wingBoards && window._wingBoards.length > 0, { timeout: 25000 }).catch(() => {});
  await sleep(2500); // let the loader fade + first textures stream in
  await page.screenshot({ path: path.join(SHOTS, '01-overview.png') });
  console.log('  wings:', await page.evaluate(() => (window._wingBoards || []).length));

  // 1) Walk forward toward the wall — triggers proximity book-match on the nearest wing.
  console.log('→ walking to the wall (W)');
  await holdKey(page, 'KeyW', 1600);
  await sleep(1200);
  await page.screenshot({ path: path.join(SHOTS, '02-bookmatch.png') });

  // Strafe a little so the footage "moves around".
  await holdKey(page, 'KeyD', 700);
  await holdKey(page, 'KeyA', 1100);
  await sleep(900);
  await page.screenshot({ path: path.join(SHOTS, '03-strafe.png') });

  // 2) Inspect a wing by clicking near screen-centre (a wing face is dead ahead at the wall).
  console.log('→ inspecting a wing (click)');
  await page.mouse.click(800, 430);
  await sleep(1800);
  // The spec-sheet detail card should now be visible — capture it.
  const detailVisible = await page.evaluate(() => {
    const el = document.getElementById('wing-detail');
    return el && !el.classList.contains('hidden');
  });
  await page.screenshot({ path: path.join(SHOTS, '04-spec-card.png') });
  console.log('  spec card visible:', detailVisible);
  // Pull the rendered spec rows so we can prove the specs surfaced.
  const specText = await page.evaluate(() => {
    const grid = document.getElementById('detail-spec-grid');
    if (!grid) return null;
    return [...grid.querySelectorAll('.wd-spec')].map(r => r.querySelector('dt').textContent + ': ' + r.querySelector('dd').textContent);
  });
  console.log('  SPEC SHEET:', JSON.stringify(specText));

  // 3) Fan cascade to browse adjacent patterns.
  console.log('→ fan cascade');
  await page.click('#btn-fan-right').catch(() => {});
  await sleep(1500);
  await page.screenshot({ path: path.join(SHOTS, '05-fan.png') });
  await page.keyboard.press('Escape');
  await sleep(800);

  // 4) Peruse — endless auto-tour. Flies wing→wing, opens each like a book, shows the card.
  console.log('→ Peruse auto-tour (P) for ~14s');
  await page.keyboard.press('KeyP');
  for (let i = 0; i < 4; i++) {
    await sleep(3500);
    await page.screenshot({ path: path.join(SHOTS, `06-peruse-${i + 1}.png`) });
  }
  await page.keyboard.press('Escape');
  await sleep(1000);

  // Scene diagnostics — proves the render is healthy.
  const stats = await page.evaluate(() => (window._getSceneStats ? window._getSceneStats() : 'n/a'));
  const fps = await page.evaluate(() => { const el = document.getElementById('fps-counter'); return el ? el.textContent : 'n/a'; });
  console.log('→ SCENE STATS:', JSON.stringify(stats));
  console.log('→ FPS:', fps);

  await page.close();        // finalizes the video file
  const vid = await page.video().path().catch(() => null);
  await ctx.close();
  await browser.close();

  // Rename the video deterministically.
  let finalWebm = path.join(OUT, 'walkthrough.webm');
  if (vid && fs.existsSync(vid)) { fs.renameSync(vid, finalWebm); }
  fs.writeFileSync(path.join(OUT, 'walkthrough-stats.json'), JSON.stringify({ url: URL, stats, fps, specText, detailVisible, when: new Date().toISOString() }, null, 2));
  fs.writeFileSync(path.join(OUT, 'walkthrough-console.log'), logs.join('\n'));
  console.log('✓ video:', finalWebm);
  console.log('✓ shots:', SHOTS);
})().catch(e => { console.error('RECORD FAILED:', e.message); process.exit(1); });