feat(dashboard): wire Memory Cinema UI into graph page

- MemoryCinema.svelte: launch button + fullscreen overlay, typewriter caption
  stream, SpeechSynthesis voice toggle, opt-in lazy-loaded Local AI toggle,
  progress/beat indicators, replay. Director-driven master loop hardened so a
  WebGPU render failure drops to camera-only without stalling the tour.
- sandbox: construct Scene/Camera from the three/webgpu module instance so all
  objects fed to the WebGPU renderer are instance-compatible (fixes
  'multiple instances of Three.js' incompatibility).
- graph page: Cinema button beside Dream, gated on having nodes.

Verified live: button renders, overlay opens, WebGPU boots + reports active,
7-beat path plans, narration resolves to live captions, bundle code-splits
(WebGPU 479K chunk loads on demand only). 926 tests + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-06-21 20:23:26 -05:00
parent 1ca5941491
commit a6798c2fca
3 changed files with 452 additions and 11 deletions

View file

@ -37,8 +37,13 @@ export class CinemaSandbox {
private container: HTMLElement;
private deps!: SandboxDeps;
private renderer!: SandboxDeps['WebGPURenderer']['prototype'];
private scene = new THREE.Scene();
private camera: THREE.PerspectiveCamera;
// Scene/camera are created in boot() from the three/webgpu module so every
// object handed to the WebGPU renderer comes from the SAME Three.js instance
// (avoids the "multiple instances of Three.js" incompatibility — the base
// three import is used only for the shared Vector3 math type the director
// mutates, which is identical across instances).
private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
private storm!: SemanticComputeStorm;
private post: { renderAsync: () => Promise<void> } | null = null;
private booted = false;
@ -48,14 +53,6 @@ export class CinemaSandbox {
constructor(container: HTMLElement) {
this.container = container;
this.camera = new THREE.PerspectiveCamera(
60,
container.clientWidth / Math.max(1, container.clientHeight),
0.1,
2000
);
this.camera.position.set(0, 18, 60);
this.scene.background = new THREE.Color(0x02020a);
}
get cameraRef(): THREE.PerspectiveCamera {
@ -74,6 +71,9 @@ export class CinemaSandbox {
const webgpu = (await import('three/webgpu')) as unknown as {
WebGPURenderer: SandboxDeps['WebGPURenderer'];
PostProcessing: SandboxDeps['PostProcessing'];
Scene: new () => THREE.Scene;
PerspectiveCamera: new (fov: number, aspect: number, near: number, far: number) => THREE.PerspectiveCamera;
Color: new (hex: number) => THREE.Color;
};
const tsl = (await import('three/tsl')) as typeof import('three/tsl');
// bloom() lives in the TSL display helpers; import the node module.
@ -90,9 +90,18 @@ export class CinemaSandbox {
bloomMod,
};
// Build scene + camera from the SAME (webgpu) module instance the
// renderer + storm use, so all objects are instance-compatible.
const w = Math.max(1, this.container.clientWidth);
const h = Math.max(1, this.container.clientHeight);
this.scene = new webgpu.Scene();
this.scene.background = new webgpu.Color(0x02020a);
this.camera = new webgpu.PerspectiveCamera(60, w / h, 0.1, 2000);
this.camera.position.set(0, 18, 60);
const renderer = new this.deps.WebGPURenderer({ antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(this.container.clientWidth, this.container.clientHeight);
renderer.setSize(w, h);
// CRITICAL FOOTGUN: WebGPU init is async. Must await before first render
// or the canvas silently draws nothing.
await renderer.init();