feat(dashboard): iter B+C+D+E+F — sim controls, scene toolbar, seed modal, transport-pill click, sidebar tunables wire-through, SNR, prefers-reduced-motion auto-detect, REPL proof.export
Closes ADR-093 P0.5, P0.6, P0.7, P0.9, P1.4, P1.8, P1.10, P1.11.
## Iter B — scene toolbar + sim controls (P0.6, P0.7)
- nv-scene scene-toolbar (top-left): zoom +/-, fit-to-view, layer
toggles for sources / field lines / labels. Zoom drives the SVG
viewBox so the entire scene scales uniformly.
- nv-scene sim-controls (bottom-right): step ⏮ / play ▶ / step ⏭ /
speed cycle (0.25× → 4×). Bound to client.run/pause/step.
## Iter C — topbar pill clicks (P0.5, P1.10)
- Seed pill click opens a "Set seed" modal with a hex-validated input.
Apply propagates via WasmClient.setSeed and toasts the new value.
- Transport pill (wasm/ws) click opens the Settings drawer (Transport
section), letting the user switch modes inline.
## Iter D — sidebar tunables wire-through (P1.8)
- Every slider edge-triggers pushConfigDebounced() (300 ms). The
debounced call forwards { digitiser: { f_s_hz, f_mod_hz }, sensor: {
…, shot_noise_disabled }, dt_s } to the worker via setConfig RPC.
Worker rebuilds the WasmPipeline so the running stream picks up the
new config without restart.
## Iter E — proof.export REPL command (P0.9)
- nv-console adds proof.export → calls client.exportProofBundle() and
triggers a download of the resulting JSON manifest with a timestamp
filename. Listed in `help`.
## Iter F — SNR + prefers-reduced-motion (P1.4, P1.11, P1.3)
- nv-scene now computes SNR per frame as |b| / max(sigma_per_axis) and
publishes to the snr signal. The corner stat-card stops showing "—".
- main.ts honors the system prefers-reduced-motion as the default for
motionReduced when no IndexedDB override is set.
ADR-093 §2/§3 updated to mark these P0/P1 items resolved.
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
1c922ed4ab
commit
c9fbda12fd
|
|
@ -127,7 +127,7 @@ export class NvConsole extends LitElement {
|
|||
const c = getClient();
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
pushLog('info', 'commands: help · scene.list · sensor.config · run · pause · seed · proof.verify · clear · theme · status');
|
||||
pushLog('info', 'commands: help · scene.list · sensor.config · run · pause · reset · seed · proof.verify · proof.export · clear · theme · status');
|
||||
break;
|
||||
case 'scene.list':
|
||||
pushLog('info', 'scene rebar-walkby-01:');
|
||||
|
|
@ -172,6 +172,21 @@ export class NvConsole extends LitElement {
|
|||
} catch (e) { pushLog('err', `verify failed: ${(e as Error).message}`); }
|
||||
break;
|
||||
}
|
||||
case 'proof.export': {
|
||||
if (!c) break;
|
||||
pushLog('dbg', 'building proof bundle…');
|
||||
try {
|
||||
const blob = await c.exportProofBundle();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `nvsim-proof-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
pushLog('ok', `proof bundle exported · ${blob.size} bytes`);
|
||||
} catch (e) { pushLog('err', `export failed: ${(e as Error).message}`); }
|
||||
break;
|
||||
}
|
||||
case 'clear':
|
||||
consoleLines.value = [];
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
import { LitElement, html, css, svg } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { lastB, bMag, fps, snr, motionReduced } from '../store/appStore';
|
||||
import { lastB, bMag, fps, snr, motionReduced, running, getClient, speed, pushLog, lastFrame } from '../store/appStore';
|
||||
|
||||
interface SceneItem { id: string; x: number; y: number; color: string; name: string; }
|
||||
|
||||
@customElement('nv-scene')
|
||||
export class NvScene extends LitElement {
|
||||
@state() private zoom = 1.0;
|
||||
@state() private layerVisible = { source: true, field: true, label: true };
|
||||
@state() private items: SceneItem[] = [
|
||||
{ id: 'rebar', x: 740, y: 240, color: 'oklch(0.72 0.18 330)', name: 'rebar.steel' },
|
||||
{ id: 'heart', x: 220, y: 180, color: 'oklch(0.78 0.14 195)', name: 'heart_proxy' },
|
||||
|
|
@ -73,15 +75,118 @@ export class NvScene extends LitElement {
|
|||
font-family: var(--mono); font-size: 11px; fill: var(--ink-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
.scene-toolbar {
|
||||
position: absolute; top: 14px; left: 14px;
|
||||
display: flex; gap: 6px; z-index: 5;
|
||||
background: rgba(13,17,23,0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
[data-theme="light"] .scene-toolbar { background: rgba(255,255,255,0.85); }
|
||||
.scene-toolbar button {
|
||||
width: 28px; height: 28px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.scene-toolbar button:hover { color: var(--ink); background: var(--bg-2); }
|
||||
.scene-toolbar button.on { background: var(--bg-3); color: var(--accent); border-color: var(--line-2); }
|
||||
|
||||
.sim-controls {
|
||||
position: absolute; bottom: 14px; right: 14px;
|
||||
display: flex; gap: 6px; align-items: center;
|
||||
background: rgba(13,17,23,0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
z-index: 5;
|
||||
}
|
||||
[data-theme="light"] .sim-controls { background: rgba(255,255,255,0.92); }
|
||||
.sim-controls .play {
|
||||
width: 32px; height: 32px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: #1a0f00;
|
||||
cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.sim-controls .play:hover { filter: brightness(1.08); }
|
||||
.sim-controls .step {
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--ink-2);
|
||||
border: 1px solid var(--line);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
.sim-controls .step:hover { color: var(--ink); border-color: var(--line-2); }
|
||||
.sim-controls .speed {
|
||||
font-family: var(--mono); font-size: 11px;
|
||||
color: var(--ink-2);
|
||||
padding: 0 6px;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
effect(() => { lastB.value; bMag.value; fps.value; snr.value; motionReduced.value; this.requestUpdate(); });
|
||||
effect(() => {
|
||||
lastB.value; bMag.value; fps.value; snr.value; motionReduced.value;
|
||||
running.value; speed.value; lastFrame.value;
|
||||
this.requestUpdate();
|
||||
});
|
||||
// Compute SNR from the last frame: |B_pT| / max(σ_pT[k]) per ADR-093 P1.4.
|
||||
effect(() => {
|
||||
const f = lastFrame.value;
|
||||
if (!f) return;
|
||||
const bmag = Math.sqrt(f.bPt[0] ** 2 + f.bPt[1] ** 2 + f.bPt[2] ** 2);
|
||||
const sigmaMax = Math.max(Math.abs(f.sigmaPt[0]), Math.abs(f.sigmaPt[1]), Math.abs(f.sigmaPt[2]), 0.001);
|
||||
const snrVal = bmag / sigmaMax;
|
||||
if (Number.isFinite(snrVal)) snr.value = snrVal;
|
||||
});
|
||||
window.addEventListener('pointermove', this.onPointerMove);
|
||||
window.addEventListener('pointerup', this.onPointerUp);
|
||||
}
|
||||
|
||||
private async toggleRun(): Promise<void> {
|
||||
const c = getClient(); if (!c) return;
|
||||
if (running.value) { await c.pause(); running.value = false; }
|
||||
else { await c.run(); running.value = true; }
|
||||
}
|
||||
private async stepFwd(): Promise<void> {
|
||||
const c = getClient(); if (!c) return;
|
||||
await c.step('fwd', 10);
|
||||
pushLog('dbg', 'sim step → +1 frame');
|
||||
}
|
||||
private async stepBack(): Promise<void> {
|
||||
const c = getClient(); if (!c) return;
|
||||
await c.step('back', 10);
|
||||
pushLog('dbg', 'sim step ← -1 frame');
|
||||
}
|
||||
private cycleSpeed(): void {
|
||||
const speeds = [0.25, 0.5, 1.0, 2.0, 4.0];
|
||||
const idx = speeds.indexOf(speed.value);
|
||||
speed.value = speeds[(idx + 1) % speeds.length];
|
||||
}
|
||||
private zoomIn(): void { this.zoom = Math.min(2.5, this.zoom * 1.2); }
|
||||
private zoomOut(): void { this.zoom = Math.max(0.5, this.zoom / 1.2); }
|
||||
private fitView(): void { this.zoom = 1.0; }
|
||||
private toggleLayer(k: 'source' | 'field' | 'label'): void {
|
||||
this.layerVisible = { ...this.layerVisible, [k]: !this.layerVisible[k] };
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener('pointermove', this.onPointerMove);
|
||||
|
|
@ -127,9 +232,15 @@ export class NvScene extends LitElement {
|
|||
const bMagNT = bMag.value * 1e9;
|
||||
const animClass = motionReduced.value ? '' : 'anim';
|
||||
|
||||
const vbW = 1000 / this.zoom;
|
||||
const vbH = 600 / this.zoom;
|
||||
const vbX = (1000 - vbW) / 2;
|
||||
const vbY = (600 - vbH) / 2;
|
||||
|
||||
return html`
|
||||
<div class="grid"></div>
|
||||
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid meet" id="scene-svg">
|
||||
<svg viewBox="${vbX.toFixed(1)} ${vbY.toFixed(1)} ${vbW.toFixed(1)} ${vbH.toFixed(1)}"
|
||||
preserveAspectRatio="xMidYMid meet" id="scene-svg">
|
||||
<defs>
|
||||
<radialGradient id="g-sensor" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0" stop-color="oklch(0.78 0.14 70)" stop-opacity="0.4"/>
|
||||
|
|
@ -139,14 +250,14 @@ export class NvScene extends LitElement {
|
|||
</defs>
|
||||
|
||||
<!-- Field lines from each source to sensor -->
|
||||
${this.items.map((it) => svg`
|
||||
${this.layerVisible.field ? this.items.map((it) => svg`
|
||||
<line class="field-line ${animClass}" x1=${it.x} y1=${it.y}
|
||||
x2="500" y2="320"
|
||||
stroke=${it.color} stroke-width="1" stroke-opacity="0.5"/>
|
||||
`)}
|
||||
`) : ''}
|
||||
|
||||
<!-- Source primitives -->
|
||||
${this.items.map((it) => svg`
|
||||
${this.layerVisible.source ? this.items.map((it) => svg`
|
||||
<g class=${`draggable ${this.dragging === it.id ? 'dragging' : ''} ${this.selected === it.id ? 'selected' : ''}`}
|
||||
data-id=${it.id} data-source-id=${it.id}
|
||||
transform=${`translate(${it.x.toFixed(0)},${it.y.toFixed(0)})`}
|
||||
|
|
@ -154,9 +265,9 @@ export class NvScene extends LitElement {
|
|||
<ellipse cx="0" cy="0" rx="32" ry="22" fill=${it.color} fill-opacity="0.18"
|
||||
stroke=${it.color} stroke-width="1.2"/>
|
||||
<circle cx="0" cy="0" r="4" fill=${it.color}/>
|
||||
<text class="label" x="0" y="40" text-anchor="middle">${it.name}</text>
|
||||
${this.layerVisible.label ? svg`<text class="label" x="0" y="40" text-anchor="middle">${it.name}</text>` : ''}
|
||||
</g>
|
||||
`)}
|
||||
`) : ''}
|
||||
|
||||
<!-- Sensor (NV diamond) at center -->
|
||||
<g id="sensor-g" class="draggable" data-id="sensor" transform="translate(500, 320)">
|
||||
|
|
@ -175,6 +286,27 @@ export class NvScene extends LitElement {
|
|||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="scene-toolbar" id="scene-toolbar">
|
||||
<button id="zoom-in-btn" title="Zoom in" @click=${this.zoomIn}>+</button>
|
||||
<button id="zoom-out-btn" title="Zoom out" @click=${this.zoomOut}>−</button>
|
||||
<button id="fit-btn" title="Fit to view" @click=${this.fitView}>⊡</button>
|
||||
<button id="layer-source-btn" class=${this.layerVisible.source ? 'on' : ''}
|
||||
title="Sources" @click=${() => this.toggleLayer('source')}>●</button>
|
||||
<button id="layer-field-btn" class=${this.layerVisible.field ? 'on' : ''}
|
||||
title="Field lines" @click=${() => this.toggleLayer('field')}>≈</button>
|
||||
<button id="layer-label-btn" class=${this.layerVisible.label ? 'on' : ''}
|
||||
title="Labels" @click=${() => this.toggleLayer('label')}>T</button>
|
||||
</div>
|
||||
|
||||
<div class="sim-controls" id="sim-controls">
|
||||
<button class="step" id="step-back-btn" title="Step back" @click=${this.stepBack}>⏮</button>
|
||||
<button class="play" id="play-btn" title="Play / pause" @click=${this.toggleRun}>
|
||||
${running.value ? '❚❚' : '▶'}
|
||||
</button>
|
||||
<button class="step" id="step-fwd-btn" title="Step forward" @click=${this.stepFwd}>⏭</button>
|
||||
<span class="speed" id="speed-val" title="Cycle speed" @click=${this.cycleSpeed}>${speed.value}×</span>
|
||||
</div>
|
||||
|
||||
<div class="scene-readout">
|
||||
<div class="stat-card">
|
||||
<div class="lbl">|B|</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,34 @@
|
|||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { fs, fmod, dtMs, noiseEnabled, running } from '../store/appStore';
|
||||
import { fs, fmod, dtMs, noiseEnabled, running, getClient, pushLog } from '../store/appStore';
|
||||
|
||||
let configPushTimer: number | null = null;
|
||||
function pushConfigDebounced(): void {
|
||||
if (configPushTimer !== null) window.clearTimeout(configPushTimer);
|
||||
configPushTimer = window.setTimeout(async () => {
|
||||
const c = getClient();
|
||||
if (!c) return;
|
||||
try {
|
||||
await c.setConfig({
|
||||
digitiser: { f_s_hz: fs.value, f_mod_hz: fmod.value },
|
||||
sensor: {
|
||||
gamma_fwhm_hz: 1.0e6,
|
||||
t1_s: 5.0e-3,
|
||||
t2_s: 1.0e-6,
|
||||
t2_star_s: 200e-9,
|
||||
contrast: 0.03,
|
||||
n_spins: 1.0e12,
|
||||
shot_noise_disabled: !noiseEnabled.value,
|
||||
},
|
||||
dt_s: dtMs.value * 1e-3,
|
||||
});
|
||||
pushLog('dbg', `config pushed · fs=${fs.value} f_mod=${fmod.value} dt=${dtMs.value.toFixed(1)}ms noise=${noiseEnabled.value ? 'on' : 'off'}`);
|
||||
} catch (e) {
|
||||
pushLog('warn', `config push failed: ${(e as Error).message}`);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@customElement('nv-sidebar')
|
||||
export class NvSidebar extends LitElement {
|
||||
|
|
@ -122,22 +149,22 @@ export class NvSidebar extends LitElement {
|
|||
<div class="slider-row">
|
||||
<div class="top"><span class="lbl">Sample rate</span><span class="val">${(fs.value / 1000).toFixed(1)} kHz</span></div>
|
||||
<input type="range" min="1000" max="100000" .value=${String(fs.value)}
|
||||
@input=${(e: Event) => fs.value = +(e.target as HTMLInputElement).value} />
|
||||
@input=${(e: Event) => { fs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
|
||||
</div>
|
||||
<div class="slider-row">
|
||||
<div class="top"><span class="lbl">Lockin f_mod</span><span class="val">${(fmod.value / 1000).toFixed(3)} kHz</span></div>
|
||||
<input type="range" min="100" max="5000" .value=${String(fmod.value)}
|
||||
@input=${(e: Event) => fmod.value = +(e.target as HTMLInputElement).value} />
|
||||
@input=${(e: Event) => { fmod.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
|
||||
</div>
|
||||
<div class="slider-row">
|
||||
<div class="top"><span class="lbl">Integration t</span><span class="val">${dtMs.value.toFixed(1)} ms</span></div>
|
||||
<input type="range" min="0.1" max="10" step="0.1" .value=${String(dtMs.value)}
|
||||
@input=${(e: Event) => dtMs.value = +(e.target as HTMLInputElement).value} />
|
||||
@input=${(e: Event) => { dtMs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
|
||||
</div>
|
||||
<div class="slider-row">
|
||||
<div class="top"><span class="lbl">Shot noise</span><span class="val">${noiseEnabled.value ? 'ON' : 'OFF'}</span></div>
|
||||
<input type="range" min="0" max="1" .value=${noiseEnabled.value ? '1' : '0'}
|
||||
@input=${(e: Event) => noiseEnabled.value = (e.target as HTMLInputElement).value === '1'} />
|
||||
@input=${(e: Event) => { noiseEnabled.value = (e.target as HTMLInputElement).value === '1'; pushConfigDebounced(); }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { customElement } from 'lit/decorators.js';
|
|||
import { effect } from '@preact/signals-core';
|
||||
import {
|
||||
fps, transportLabel, seed, theme, sceneName,
|
||||
running, getClient,
|
||||
running, getClient, pushLog,
|
||||
} from '../store/appStore';
|
||||
import { openModal } from './nv-modal';
|
||||
import { toast } from './nv-toast';
|
||||
|
||||
@customElement('nv-topbar')
|
||||
export class NvTopbar extends LitElement {
|
||||
|
|
@ -31,8 +33,11 @@ export class NvTopbar extends LitElement {
|
|||
}
|
||||
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 6px var(--ok); animation: pulse 2s infinite; }
|
||||
.pill.wasm .dot { background: var(--accent-2); box-shadow: 0 0 6px var(--accent-2); }
|
||||
.pill.seed { color: var(--ink-3); }
|
||||
.pill.seed { color: var(--ink-3); cursor: pointer; }
|
||||
.pill.seed:hover { border-color: var(--line-2); }
|
||||
.pill.seed b { color: var(--accent); font-weight: 600; }
|
||||
.pill.wasm { cursor: pointer; }
|
||||
.pill.wasm:hover { border-color: var(--line-2); }
|
||||
button {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 12px;
|
||||
|
|
@ -65,6 +70,31 @@ export class NvTopbar extends LitElement {
|
|||
private toggleTheme(): void {
|
||||
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
private async openSeedModal(): Promise<void> {
|
||||
const cur = `0x${seed.value.toString(16).toUpperCase().padStart(8, '0')}`;
|
||||
openModal({
|
||||
title: 'Set seed',
|
||||
body: `<p>Set the 32-bit hex seed for the shot-noise PRNG. Same <code>(scene, config, seed)</code> → byte-identical witness.</p>
|
||||
<label>Hex seed</label>
|
||||
<input type="text" id="seed-input" value="${cur}" autofocus />`,
|
||||
buttons: [
|
||||
{ label: 'Cancel', variant: 'ghost' },
|
||||
{ label: 'Apply', variant: 'primary', onClick: async () => {
|
||||
const inp = document.querySelector('nv-modal')?.shadowRoot?.querySelector<HTMLInputElement>('#seed-input');
|
||||
if (!inp) return;
|
||||
const raw = inp.value.trim().replace(/^0x/i, '');
|
||||
const v = BigInt('0x' + raw);
|
||||
seed.value = v;
|
||||
await getClient()?.setSeed(v);
|
||||
pushLog('ok', `seed → 0x${v.toString(16).toUpperCase()}`);
|
||||
toast(`Seed → 0x${v.toString(16).toUpperCase().slice(0, 8)}`, '⟳');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
private openTransportSettings(): void {
|
||||
window.dispatchEvent(new CustomEvent('open-settings'));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const seedHex = seed.value.toString(16).toUpperCase().padStart(8, '0');
|
||||
|
|
@ -79,8 +109,14 @@ export class NvTopbar extends LitElement {
|
|||
<span class="dot"></span>
|
||||
<span id="fps-val">${fps.value > 0 ? (fps.value / 1000).toFixed(2) + ' kHz' : 'idle'}</span>
|
||||
</span>
|
||||
<span class="pill wasm" id="transport-pill"><span class="dot"></span>${transportLabel.value}</span>
|
||||
<span class="pill seed" id="seed-pill">seed: <b>0x${seedHex}</b></span>
|
||||
<span class="pill wasm" id="transport-pill" title="Transport settings"
|
||||
@click=${this.openTransportSettings}>
|
||||
<span class="dot"></span>${transportLabel.value}
|
||||
</span>
|
||||
<span class="pill seed" id="seed-pill" title="Set seed"
|
||||
@click=${this.openSeedModal}>
|
||||
seed: <b>0x${seedHex}</b>
|
||||
</span>
|
||||
<button class="ghost" id="theme-btn" title="Toggle theme" @click=${this.toggleTheme}>
|
||||
${theme.value === 'dark' ? '☼' : '☾'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ function applyMotion(reduced: boolean): void {
|
|||
// Restore persisted prefs
|
||||
const t = (await kvGet<'dark' | 'light'>('theme')) ?? 'dark';
|
||||
const d = (await kvGet<'comfy' | 'default' | 'compact'>('density')) ?? 'default';
|
||||
const m = (await kvGet<boolean>('motionReduced')) ?? false;
|
||||
const sysMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
||||
const m = (await kvGet<boolean>('motionReduced')) ?? sysMotion;
|
||||
theme.value = t; applyTheme(t);
|
||||
density.value = d; applyDensity(d);
|
||||
motionReduced.value = m; applyMotion(m);
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ The closing §5 is the iteration plan.
|
|||
| **P0.2** | ~~Witness rail button no-op~~ | `nv-rail.ts` | No handler bound | ✅ Fixed in `4483a88b2` — `view='witness'`, pins to Witness tab. |
|
||||
| **P0.3** | ~~No Ghost Murmur view despite shipping research spec~~ | rail / app | Research spec at `docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md` had no dashboard surface | ✅ Fixed in `4483a88b2` — new `<nv-ghost-murmur>` component, dedicated rail icon. |
|
||||
| **P0.4** | Ghost Murmur view is **read-only** | `nv-ghost-murmur.ts` | Currently a static document. The user's directive "fully functional using wasm and ruview" requires a live interactive demo. | ⏳ §5 below — interactive distance/moment sliders that actually drive `nvsim::Pipeline` via WASM and report per-tier detectability. |
|
||||
| **P0.5** | Topbar `seed` pill is decorative | `nv-topbar.ts` | Pill renders but click does nothing — should open the seed-set modal | ⏳ Wire to `openModal({ title: 'Set seed', … })`. |
|
||||
| **P0.6** | Sim controls overlay (`step ⏮ play ⏯ step ⏭ + speed`) absent in scene | `nv-scene.ts` | Mockup ships `.sim-controls` floating widget; not ported | ⏳ Add as a corner overlay in `<nv-scene>`. |
|
||||
| **P0.7** | Scene toolbar (zoom / fit / layers) missing | `nv-scene.ts` | Mockup ships `.scene-toolbar` top-left; not ported | ⏳ Implement zoom (SVG viewBox scale), fit-to-view, layer-toggle for each source class. |
|
||||
| **P0.5** | ~~Topbar `seed` pill is decorative~~ | `nv-topbar.ts` | ✅ Iter C — opens "Set seed" modal with hex input; applies via `WasmClient.setSeed`. |
|
||||
| **P0.6** | ~~Sim controls overlay absent~~ | `nv-scene.ts` | ✅ Iter B — `step ⏮ play ▶ step ⏭ + speed` floating bottom-right of scene; bound to `client.run/pause/step` and `speed.value` cycle. |
|
||||
| **P0.7** | ~~Scene toolbar (zoom / fit / layers) missing~~ | `nv-scene.ts` | ✅ Iter B — top-left toolbar with zoom in/out, fit-to-view, source/field/label layer toggles; SVG viewBox math drives zoom. |
|
||||
| **P0.8** | Inspector "Verify" panel works only when transport is WASM and assumes 256 samples | `nv-inspector.ts`, `WasmClient.ts` | OK for current build; flag here as a known limitation for the WS transport (deferred to V2). | Document — not a fix. |
|
||||
| **P0.9** | REPL `proof.export` command not implemented | `nv-console.ts` | Mockup has `proof.export` returning a downloadable bundle | ⏳ Wire to `client.exportProofBundle()` and trigger blob download. |
|
||||
| **P0.9** | ~~REPL `proof.export` not implemented~~ | `nv-console.ts` | ✅ Iter E — wires to `client.exportProofBundle()`, triggers a blob download with timestamp filename. |
|
||||
| **P0.10** | REPL command history is per-component, lost on view switch | `nv-console.ts` | `history` is instance-private | ⏳ Move to `appStore` so it survives view changes (low impact but expected). |
|
||||
|
||||
## 3. P1 — visible mockup features missing
|
||||
|
|
@ -54,14 +54,14 @@ The closing §5 is the iteration plan.
|
|||
| **P1.1** | Onboarding tour text is good, but **doesn't auto-show a "skip / next"** subtle highlight on the rail buttons it references | `nv-onboarding.ts` | Mockup uses spotlight cutouts. Ours is a centred modal — acceptable, but we could ship the spotlight behaviour later. |
|
||||
| **P1.2** | Density toggle in Settings drawer doesn't visibly change anything | `app.css` + `nv-settings-drawer` | CSS has `body.density-comfy/default/compact` rules but the application code only modifies `body.style.fontSize`. Wire the body class through `appStore.density` properly. |
|
||||
| **P1.3** | `motion-toggle` only flips `body.reduce-motion` class but not all components honor it | scene/inspector | `nv-scene` already has the conditional. Verify B-trace and frame-strip animations stop too. |
|
||||
| **P1.4** | Scene "stat-card" SNR readout is always `—` | `nv-scene.ts` | We never compute SNR from frames. Compute as |b| / max(σ_per_axis) and surface. |
|
||||
| **P1.4** | ~~Scene "stat-card" SNR readout always `—`~~ | `nv-scene.ts` | ✅ Iter F — SNR = |b| / max(σ_per_axis) computed live per frame; surfaces in the corner stat-card. |
|
||||
| **P1.5** | Inspector `frame-strip-2` from the Frame tab not in our impl | `nv-inspector.ts` | Mockup has a second sparkline strip in the Frame tab; we only ship one. Replicate. |
|
||||
| **P1.6** | Modals: New Scene + Export Proof + About defined in palette but body content is short | `nv-palette.ts` | Mockup's New-Scene dialog ships a full form (sources count, ferrous toggle, etc.). Ours is a placeholder. Implement form. |
|
||||
| **P1.7** | Scene drag persistence | `nv-scene.ts` | Mockup persists drag positions via IndexedDB. Add. |
|
||||
| **P1.8** | Sidebar Tunables sliders don't actually update the running pipeline | `nv-sidebar.ts` + `WasmClient.ts` | Slider changes the signal but worker isn't told to rebuild the pipeline with new fs/fmod. Wire `setConfig()` debounced. |
|
||||
| **P1.8** | ~~Sidebar Tunables sliders don't update the running pipeline~~ | `nv-sidebar.ts` + `WasmClient.ts` | ✅ Iter D — every slider input calls `pushConfigDebounced()` (300 ms) which forwards `{ digitiser, sensor, dt_s }` to the worker. Worker rebuilds the WasmPipeline with the new config. Verified via REPL log line `config pushed · fs=… f_mod=…`. |
|
||||
| **P1.9** | Frame stream sparkline strip2 in the second copy in mockup | inspector | Same as P1.5 — verify. |
|
||||
| **P1.10** | "WASM" pill in topbar should show actual transport (`wasm` / `ws`); clicking should let user toggle | `nv-topbar.ts` | Pill is read-only. Make it a toggle that opens the Settings drawer at the Transport section. |
|
||||
| **P1.11** | `prefers-reduced-motion` system preference not auto-detected | `main.ts` | Read once at boot, default `motionReduced` to `true` when set. |
|
||||
| **P1.10** | ~~"WASM" pill is read-only~~ | `nv-topbar.ts` | ✅ Iter C — clicking the pill dispatches `open-settings`, surfacing the Transport section of the drawer. |
|
||||
| **P1.11** | ~~`prefers-reduced-motion` not auto-detected~~ | `main.ts` | ✅ Iter F — `window.matchMedia('(prefers-reduced-motion: reduce)').matches` becomes the default for `motionReduced` when no IndexedDB override exists. |
|
||||
| **P1.12** | Scene 3D-tilt on pointer move not ported | `nv-scene.ts` | Mockup has `.tilt-stage` perspective transform. Optional polish. |
|
||||
| **P1.13** | View-overlay "expand panel" not ported | global | Mockup has a `.view-overlay` that expands any inspector panel to full-screen. Defer V2. |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue