P1b: SQLite project, content-addressed assets, autosave and undo
This commit is contained in:
+3
-1
@@ -9,7 +9,9 @@
|
||||
"preview": "vite preview --port 5173 --strictPort",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"smoke": "node scripts/smoke.mjs",
|
||||
"quarry-smoke": "node scripts/quarry-smoke.mjs"
|
||||
"quarry-smoke": "node scripts/quarry-smoke.mjs",
|
||||
"project-smoke": "node scripts/project-smoke.mjs",
|
||||
"restored-smoke": "node scripts/restored-smoke.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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', '--autoplay-policy=no-user-gesture-required'] });
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 });
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on('console', (message) => { if (message.type() === 'error') errors.push(`console: ${message.text()}`); });
|
||||
|
||||
await page.goto('http://127.0.0.1:5173/', { waitUntil: 'load' });
|
||||
await page.waitForTimeout(2200);
|
||||
const projectName = (await page.locator('.project').textContent())?.trim();
|
||||
const undoInitially = await page.locator('.history-button[aria-label="undo"]').isDisabled();
|
||||
|
||||
await page.locator('.room', { hasText: 'quarry' }).click();
|
||||
await page.waitForTimeout(400);
|
||||
await page.locator('input[type=file]').setInputFiles('../fixtures/clicks-01.wav');
|
||||
await page.waitForFunction(() => document.querySelector('.core-name')?.textContent?.includes('clicks-01'), null, { timeout: 20000 });
|
||||
await page.waitForTimeout(700);
|
||||
await page.locator('.face').nth(10).click();
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('.assign').click();
|
||||
await page.waitForTimeout(500);
|
||||
const assigned = (await page.locator('.pattern-row .pattern-face').first().textContent())?.trim();
|
||||
const undoEnabled = !(await page.locator('.history-button[aria-label="undo"]').isDisabled());
|
||||
await page.locator('.history-button[aria-label="undo"]').click();
|
||||
await page.waitForTimeout(400);
|
||||
const undone = (await page.locator('.pattern-row .pattern-face').first().textContent())?.trim();
|
||||
await page.locator('.history-button[aria-label="redo"]').click();
|
||||
await page.waitForTimeout(400);
|
||||
const redone = (await page.locator('.pattern-row .pattern-face').first().textContent())?.trim();
|
||||
if (!(await page.locator('.play.is-playing').count())) await page.locator('.play').click();
|
||||
await page.waitForTimeout(1600);
|
||||
await page.screenshot({ path: '../preview-p1b-quarry.png' });
|
||||
|
||||
const report = await page.evaluate(() => ({
|
||||
project: document.querySelector('.project')?.textContent?.trim(),
|
||||
cores: [...document.querySelectorAll('.core-name')].map((el) => el.textContent),
|
||||
faces: document.querySelectorAll('.face').length,
|
||||
patternFace: document.querySelector('.pattern-row .pattern-face')?.textContent?.trim(),
|
||||
undoDisabled: document.querySelector('.history-button[aria-label="undo"]')?.hasAttribute('disabled'),
|
||||
redoDisabled: document.querySelector('.history-button[aria-label="redo"]')?.hasAttribute('disabled'),
|
||||
wire: [...document.querySelectorAll('.wire-label')].map((el) => el.textContent).slice(-6),
|
||||
master: document.querySelector('.master-db')?.textContent
|
||||
}));
|
||||
console.log(JSON.stringify({ projectName, undoInitially, assigned, undoEnabled, undone, redone, report }, null, 2));
|
||||
|
||||
const failures = [];
|
||||
if (!projectName) failures.push('no project name');
|
||||
if (!undoInitially) failures.push('undo was enabled on a fresh project');
|
||||
if (!report.cores.length) failures.push('no core imported');
|
||||
if (report.faces !== 16) failures.push('expected 16 faces');
|
||||
if (!assigned || assigned.startsWith('procedural')) failures.push('face was not assigned');
|
||||
if (!undoEnabled) failures.push('undo did not enable');
|
||||
if (!undone || !undone.startsWith('procedural')) failures.push('undo did not revert the face');
|
||||
if (!redone || redone.startsWith('procedural')) failures.push('redo did not restore the face');
|
||||
if (errors.length) failures.push('console errors');
|
||||
await browser.close();
|
||||
if (failures.length) {
|
||||
console.error('PROJECT SMOKE FAILED: ' + failures.join('; '));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('project smoke passed');
|
||||
@@ -0,0 +1,23 @@
|
||||
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('.room', { hasText: 'quarry' }).click();
|
||||
await page.waitForTimeout(700);
|
||||
const report = await page.evaluate(() => ({
|
||||
project: document.querySelector('.project')?.textContent?.trim(),
|
||||
cores: [...document.querySelectorAll('.core-name')].map((el) => el.textContent),
|
||||
faces: document.querySelectorAll('.face').length,
|
||||
patternFace: document.querySelector('.pattern-row .pattern-face')?.textContent?.trim(),
|
||||
undoDisabled: document.querySelector('.history-button[aria-label="undo"]')?.hasAttribute('disabled'),
|
||||
wire: [...document.querySelectorAll('.wire-label')].map((el) => el.textContent).slice(0, 4)
|
||||
}));
|
||||
await page.screenshot({ path: '../preview-p1b-restored.png' });
|
||||
console.log(JSON.stringify({ report, errors }, null, 2));
|
||||
if (!report.cores.length || !report.patternFace || report.undoDisabled === false || errors.length) process.exit(1);
|
||||
await browser.close();
|
||||
+29
-5
@@ -72,7 +72,7 @@
|
||||
}
|
||||
if (!snapshotLogged) {
|
||||
snapshotLogged = true;
|
||||
addEntry('info', 'snapshot', `${next.veins.length} veins · ${next.pattern.steps} steps · ${cores.length} cores · rev ${next.revision}`);
|
||||
addEntry('info', 'snapshot', `${next.project_name} · ${next.veins.length} veins · ${next.pattern.steps} steps · ${cores.length} cores · rev ${next.revision}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
} else if (event.kind === 'pattern_face') {
|
||||
const row = project.pattern.rows[event.row];
|
||||
if (row) row.face = { core: event.core, face: event.face };
|
||||
} else if (event.kind === 'history') {
|
||||
project.can_undo = event.can_undo;
|
||||
project.can_redo = event.can_redo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +192,14 @@
|
||||
void run({ type: 'set_pattern_step', row, step, velocity: on ? 0 : 96 });
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (project?.can_undo) void run({ type: 'undo' });
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (project?.can_redo) void run({ type: 'redo' });
|
||||
}
|
||||
|
||||
function select(vein: VeinId) {
|
||||
selected = vein;
|
||||
}
|
||||
@@ -209,6 +220,12 @@
|
||||
else play();
|
||||
return;
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) redo();
|
||||
else undo();
|
||||
return;
|
||||
}
|
||||
if (event.key === 's' || event.key === 'S') {
|
||||
event.preventDefault();
|
||||
strikeSelected();
|
||||
@@ -257,6 +274,7 @@
|
||||
<div class="brand">
|
||||
<span class="mark">ZHILA</span>
|
||||
<span class="version">0.1</span>
|
||||
<span class="project">{project?.project_name ?? 'descent-04'}</span>
|
||||
</div>
|
||||
<div class="rail-center">
|
||||
<button class="room" class:is-on={view === 'nave'} aria-pressed={view === 'nave'} onclick={() => (view = 'nave')}>
|
||||
@@ -276,10 +294,16 @@
|
||||
: 'five veins'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="net" class:is-live={status === 'live'}>
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<span class="net-label">engine</span>
|
||||
<b>{status === 'live' ? 'live' : status === 'connecting' ? 'opening' : 'offline'}</b>
|
||||
<div class="rail-right">
|
||||
<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>
|
||||
</div>
|
||||
<div class="net" class:is-live={status === 'live'}>
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<span class="net-label">engine</span>
|
||||
<b>{status === 'live' ? 'live' : status === 'connecting' ? 'opening' : 'offline'}</b>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -131,6 +131,49 @@ a:focus-visible,
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.project {
|
||||
overflow: hidden;
|
||||
max-width: 190px;
|
||||
padding-left: 9px;
|
||||
border-left: 1px solid var(--line-soft);
|
||||
font-size: 8.5px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ash-2);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rail-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.history-button {
|
||||
padding: 3px 5px;
|
||||
border: 1px solid rgba(155, 133, 121, 0.16);
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ash);
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.history-button:hover:not(:disabled) {
|
||||
border-color: rgba(232, 64, 42, 0.4);
|
||||
color: var(--ember);
|
||||
}
|
||||
|
||||
.history-button:disabled {
|
||||
opacity: 0.28;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.rail-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1669,6 +1712,9 @@ button.room {
|
||||
.brand {
|
||||
min-width: 0;
|
||||
}
|
||||
.project {
|
||||
max-width: 110px;
|
||||
}
|
||||
.mark {
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.2em;
|
||||
@@ -1682,6 +1728,15 @@ button.room {
|
||||
.rail-note {
|
||||
display: none;
|
||||
}
|
||||
.project {
|
||||
display: none;
|
||||
}
|
||||
.rail-right {
|
||||
gap: 8px;
|
||||
}
|
||||
.history-button {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.net {
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
|
||||
@@ -33,6 +33,8 @@ export function commandLabel(command: Command): string {
|
||||
case 'strike_face': return `strike ${command.core} f${command.face + 1}`;
|
||||
case 'set_pattern_step': return `step ${command.row + 1}:${command.step + 1} at ${command.velocity}`;
|
||||
case 'set_pattern_face': return `${command.core} f${command.face + 1} on row ${command.row + 1}`;
|
||||
case 'undo': return 'undo';
|
||||
case 'redo': return 'redo';
|
||||
case 'get_snapshot': return 'snapshot';
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -61,6 +61,9 @@ export interface Pattern {
|
||||
|
||||
export interface State {
|
||||
revision: number;
|
||||
project_name: string;
|
||||
can_undo: boolean;
|
||||
can_redo: boolean;
|
||||
transport: TransportState;
|
||||
veins: VeinState[];
|
||||
pattern: Pattern;
|
||||
@@ -76,7 +79,8 @@ export type Event =
|
||||
| { kind: 'struck'; vein: VeinId; velocity: number }
|
||||
| { kind: 'pattern_step'; row: number; step: number; velocity: number }
|
||||
| { kind: 'pattern_face'; row: number; core: string; face: number }
|
||||
| { kind: 'face_struck'; core: string; face: number; velocity: number };
|
||||
| { kind: 'face_struck'; core: string; face: number; velocity: number }
|
||||
| { kind: 'history'; can_undo: boolean; can_redo: boolean };
|
||||
|
||||
export type Command =
|
||||
| { type: 'get_snapshot' }
|
||||
@@ -87,7 +91,9 @@ export type Command =
|
||||
| { type: 'set_vein_gain'; vein: VeinId; gain: number }
|
||||
| { type: 'strike_face'; core: string; face: number; velocity: number }
|
||||
| { type: 'set_pattern_step'; row: number; step: number; velocity: number }
|
||||
| { type: 'set_pattern_face'; row: number; core: string; face: number };
|
||||
| { type: 'set_pattern_face'; row: number; core: string; face: number }
|
||||
| { type: 'undo' }
|
||||
| { type: 'redo' };
|
||||
|
||||
export type CommandResult =
|
||||
| { kind: 'snapshot'; state: State }
|
||||
|
||||
Reference in New Issue
Block a user