P2b: offline WAV render of the arrangement
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"record-smoke": "node scripts/record-smoke.mjs",
|
||||
"faces-smoke": "node scripts/faces-smoke.mjs",
|
||||
"arrangement-smoke": "node scripts/arrangement-smoke.mjs",
|
||||
"render-ui-smoke": "node scripts/render-ui-smoke.mjs",
|
||||
"restored-smoke": "node scripts/restored-smoke.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { chromium } from 'playwright-core';
|
||||
const chrome = process.env.CHROME_PATH ?? '/home/emil/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
|
||||
const browser = await chromium.launch({ executablePath: chrome, args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 });
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
await page.goto('http://127.0.0.1:5173/', { waitUntil: 'load' });
|
||||
await page.waitForTimeout(2200);
|
||||
await page.locator('.render-button').click();
|
||||
await page.waitForFunction(
|
||||
() => [...document.querySelectorAll('.wire-label')].some((el) => el.textContent?.trim() === 'render'),
|
||||
null,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: '../preview-p2-render.png' });
|
||||
const report = await page.evaluate(() => ({
|
||||
wire: [...document.querySelectorAll('.wire-entry')].slice(-4).map((el) => el.textContent?.replace(/\s+/g, ' ').trim()),
|
||||
title: document.querySelector('.render-button')?.getAttribute('title'),
|
||||
button: document.querySelector('.render-button')?.textContent?.trim()
|
||||
}));
|
||||
console.log(JSON.stringify({ report, errors }, null, 2));
|
||||
const failures = [];
|
||||
if (!report.title?.startsWith('last render:')) failures.push('render path is not on the button');
|
||||
if (!report.wire.some((line) => line?.includes('16.5 s'))) failures.push('render event is not on the wire');
|
||||
if (errors.length) failures.push('console errors');
|
||||
await browser.close();
|
||||
if (failures.length) { console.error('RENDER UI SMOKE FAILED: ' + failures.join('; ')); process.exit(1); }
|
||||
console.log('render UI smoke passed');
|
||||
@@ -38,6 +38,8 @@
|
||||
let selectedFace: number = $state(-1);
|
||||
let selectedRow: number = $state(0);
|
||||
let importing: boolean = $state(false);
|
||||
let rendering: boolean = $state(false);
|
||||
let lastRender: string | null = $state(null);
|
||||
let bridge: Bridge | null = null;
|
||||
let entryId = 0;
|
||||
let snapshotLogged = false;
|
||||
@@ -109,6 +111,9 @@
|
||||
project.arrangement = event.sections;
|
||||
project.active_section = event.active;
|
||||
project.pattern = event.sections[event.active] ?? project.pattern;
|
||||
} else if (event.kind === 'rendered') {
|
||||
lastRender = event.path;
|
||||
addEntry('recv', 'render', `${event.duration_sec.toFixed(1)} s · ${event.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +221,16 @@
|
||||
void run({ type: 'copy_section', from, to });
|
||||
}
|
||||
|
||||
async function renderProject() {
|
||||
if (!bridge || rendering) return;
|
||||
rendering = true;
|
||||
try {
|
||||
await run({ type: 'render' });
|
||||
} finally {
|
||||
rendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (project?.can_undo) void run({ type: 'undo' });
|
||||
}
|
||||
@@ -322,6 +337,12 @@
|
||||
<div class="history" aria-label="history">
|
||||
<button class="history-button" onclick={undo} disabled={!project?.can_undo} aria-label="undo" title="undo · ctrl+z">undo</button>
|
||||
<button class="history-button" onclick={redo} disabled={!project?.can_redo} aria-label="redo" title="redo · shift+ctrl+z">redo</button>
|
||||
<button
|
||||
class="history-button render-button"
|
||||
onclick={renderProject}
|
||||
disabled={rendering || status !== 'live'}
|
||||
title={lastRender ? `last render: ${lastRender}` : 'render the arrangement to WAV'}
|
||||
>{rendering ? 'rendering…' : 'render'}</button>
|
||||
</div>
|
||||
<div class="net" class:is-live={status === 'live'}>
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
|
||||
@@ -37,6 +37,7 @@ export function commandLabel(command: Command): string {
|
||||
case 'select_section': return `section ${String.fromCharCode(65 + command.index)}`;
|
||||
case 'clear_section': return `clear section ${String.fromCharCode(65 + command.index)}`;
|
||||
case 'copy_section': return `copy ${String.fromCharCode(65 + command.from)} → ${String.fromCharCode(65 + command.to)}`;
|
||||
case 'render': return 'render wav';
|
||||
case 'undo': return 'undo';
|
||||
case 'redo': return 'redo';
|
||||
case 'get_snapshot': return 'snapshot';
|
||||
|
||||
+3
-1
@@ -84,7 +84,8 @@ export type Event =
|
||||
| { kind: 'face_struck'; core: string; face: number; velocity: number }
|
||||
| { kind: 'history'; can_undo: boolean; can_redo: boolean }
|
||||
| { kind: 'core_faces'; core: CoreSummary }
|
||||
| { kind: 'arrangement'; sections: Pattern[]; active: number };
|
||||
| { kind: 'arrangement'; sections: Pattern[]; active: number }
|
||||
| { kind: 'rendered'; path: string; duration_sec: number };
|
||||
|
||||
export type Command =
|
||||
| { type: 'get_snapshot' }
|
||||
@@ -100,6 +101,7 @@ export type Command =
|
||||
| { type: 'select_section'; index: number }
|
||||
| { type: 'clear_section'; index: number }
|
||||
| { type: 'copy_section'; from: number; to: number }
|
||||
| { type: 'render' }
|
||||
| { type: 'undo' }
|
||||
| { type: 'redo' };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user