mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
feat(dashboard): zero-DOM RouteStage + in-canvas WebGPU nav
RouteStage.svelte rebuilt: no slot chrome, no pause button, no telemetry/ loading/error/empty divs. One transparent pointer host + the canvas. Pause, telemetry, and all lifecycle states now render as MSDF glyphs via TextLayerPass; the sidebar nav is now an in-canvas WebGPU rail (nav-layer.ts) with all 19 routes, active route glowing cyan, hover ignition, click-to-navigate. check 0 errors 0 warnings (the slot-deprecation warning is gone), build green. The 5 organ routes still compile. Their own page DOM headers convert in Day-2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b85740d0e7
commit
a4a36ee220
2 changed files with 347 additions and 68 deletions
|
|
@ -1,8 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import ObservatoryCanvas from '$lib/components/ObservatoryCanvas.svelte';
|
||||
import type { ObservatoryEngine, FramePass } from '$lib/observatory/engine';
|
||||
import { CAUSAL, IMMUNE, RETENTION, rgb01 } from '$lib/observatory/cognitive-palette';
|
||||
import { type DemoMode } from '$lib/observatory/types';
|
||||
import { TextLayerPass, type TextLayerItem } from '$lib/observatory/text/text-layer';
|
||||
import {
|
||||
createNavLayerPass,
|
||||
type NavLayerPass,
|
||||
type NavPick
|
||||
} from '$lib/observatory/nav/nav-layer';
|
||||
import {
|
||||
assertProvenance,
|
||||
emptyScene,
|
||||
|
|
@ -58,6 +68,8 @@
|
|||
let currentScene = $derived(scene ?? emptyScene(organ));
|
||||
let canvasLayerEl: HTMLDivElement | null = $state(null);
|
||||
let engine = $state<ObservatoryEngine | null>(null);
|
||||
let chromeText: TextLayerPass | null = null;
|
||||
let navPass: NavLayerPass | null = null;
|
||||
let routePasses: RouteFramePass[] = [];
|
||||
let frameCount = $state(0);
|
||||
let fpsEstimate = $state(0);
|
||||
|
|
@ -65,6 +77,12 @@
|
|||
let userSetPause = $state(false);
|
||||
let ready = $state(false);
|
||||
|
||||
const CYAN = [...rgb01(CAUSAL.forward), 1] satisfies [number, number, number, number];
|
||||
const DIM_GREEN = [...rgb01(RETENTION.recall), 0.58] satisfies [number, number, number, number];
|
||||
const OXYGEN = [...rgb01(RETENTION.luciferin), 0.86] satisfies [number, number, number, number];
|
||||
const SCARLET = [...rgb01(IMMUNE.veto), 0.9] satisfies [number, number, number, number];
|
||||
const AMBER = [...rgb01(IMMUNE.caution), 0.78] satisfies [number, number, number, number];
|
||||
|
||||
function initReducedMotion() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
|
|
@ -79,10 +97,12 @@
|
|||
function togglePause() {
|
||||
userSetPause = true;
|
||||
paused = !paused;
|
||||
updateChromeText();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
engine?.setPaused(paused);
|
||||
updateChromeText();
|
||||
});
|
||||
|
||||
function upload(sceneToUpload: RouteSceneModel) {
|
||||
|
|
@ -90,19 +110,32 @@
|
|||
if (import.meta.env.DEV) assertProvenance(sceneToUpload);
|
||||
for (const pass of routePasses) pass.uploadScene?.(sceneToUpload);
|
||||
engine.demoClock.reset();
|
||||
updateChromeText();
|
||||
}
|
||||
|
||||
function handleFrame(frame: number, fps: number) {
|
||||
frameCount = frame;
|
||||
fpsEstimate = fps;
|
||||
navPass?.setActivePath(currentDashboardPath());
|
||||
updateChromeText(frame, fps);
|
||||
}
|
||||
|
||||
function handleReady(e: ObservatoryEngine) {
|
||||
ready = true;
|
||||
async function handleReady(e: ObservatoryEngine) {
|
||||
ready = false;
|
||||
engine = e;
|
||||
for (const pass of routePasses) pass.dispose?.();
|
||||
chromeText?.dispose();
|
||||
navPass?.dispose();
|
||||
routePasses = typeof passes === 'function' ? passes(e, currentScene) : (passes ?? []);
|
||||
for (const pass of routePasses) e.addPass(pass);
|
||||
|
||||
navPass = createNavLayerPass(e, { activePath: currentDashboardPath() });
|
||||
chromeText = new TextLayerPass(e);
|
||||
e.addPass(navPass);
|
||||
e.addPass(chromeText);
|
||||
await Promise.all([navPass.init(), chromeText.init()]);
|
||||
if (engine !== e) return;
|
||||
ready = true;
|
||||
upload(currentScene);
|
||||
}
|
||||
|
||||
|
|
@ -111,16 +144,131 @@
|
|||
upload(currentScene);
|
||||
});
|
||||
|
||||
async function handleFieldClick(e: MouseEvent) {
|
||||
if (!onpick || !canvasLayerEl) return;
|
||||
function currentDashboardPath(): string {
|
||||
const path = $page.url.pathname;
|
||||
return path.startsWith(base) ? path.slice(base.length) || '/' : path;
|
||||
}
|
||||
|
||||
function makeChromeItems(frame = frameCount, fps = fpsEstimate): TextLayerItem[] {
|
||||
const items: TextLayerItem[] = [
|
||||
{
|
||||
id: 'route-chrome:pause',
|
||||
kind: 'route-chrome',
|
||||
text: paused ? '> RESUME' : '|| PAUSE',
|
||||
x: 0.66,
|
||||
y: -0.86,
|
||||
size: 0.034,
|
||||
color: paused ? AMBER : CYAN,
|
||||
revealSpan: 1
|
||||
},
|
||||
{
|
||||
id: 'route-chrome:telemetry',
|
||||
kind: 'route-telemetry',
|
||||
text: `${organ.toUpperCase()} - ${frame}F - ${fps}FPS`,
|
||||
x: 0.44,
|
||||
y: 0.88,
|
||||
size: 0.022,
|
||||
color: DIM_GREEN,
|
||||
revealSpan: 1
|
||||
}
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
items.push({
|
||||
id: 'route-chrome:loading',
|
||||
kind: 'route-status',
|
||||
text: 'REPLAYING COGNITIVE RECEIPT...',
|
||||
x: -0.23,
|
||||
y: 0.02,
|
||||
size: 0.046,
|
||||
color: OXYGEN,
|
||||
startFrame: Math.max(0, frame - 90),
|
||||
revealSpan: 72
|
||||
});
|
||||
} else if (error) {
|
||||
items.push(
|
||||
{
|
||||
id: 'route-chrome:error-pulse',
|
||||
kind: 'route-status-pulse',
|
||||
text: '!!!!!!!!!!!!!!!!!!!!!!!!',
|
||||
x: -0.36,
|
||||
y: -0.035,
|
||||
size: 0.025,
|
||||
color: SCARLET,
|
||||
revealSpan: 1
|
||||
},
|
||||
{
|
||||
id: 'route-chrome:error',
|
||||
kind: 'route-status',
|
||||
text: `ERROR - ${error}`.slice(0, 72),
|
||||
x: -0.54,
|
||||
y: 0.025,
|
||||
size: 0.032,
|
||||
color: SCARLET,
|
||||
revealSpan: 14,
|
||||
maxWidthEm: 48
|
||||
}
|
||||
);
|
||||
} else if (!currentScene.alive) {
|
||||
items.push({
|
||||
id: 'route-chrome:empty',
|
||||
kind: 'route-status',
|
||||
text: emptyLabel,
|
||||
x: -0.36,
|
||||
y: 0.02,
|
||||
size: 0.034,
|
||||
color: DIM_GREEN,
|
||||
revealSpan: 24,
|
||||
maxWidthEm: 48
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function updateChromeText(frame = frameCount, fps = fpsEstimate) {
|
||||
chromeText?.setText(makeChromeItems(frame, fps));
|
||||
}
|
||||
|
||||
function pointerToNdc(e: PointerEvent | MouseEvent): { x: number; y: number } | null {
|
||||
if (!canvasLayerEl) return null;
|
||||
const rect = canvasLayerEl.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return;
|
||||
const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const ndcY = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
|
||||
if (rect.width === 0 || rect.height === 0) return null;
|
||||
return {
|
||||
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
y: -(((e.clientY - rect.top) / rect.height) * 2 - 1)
|
||||
};
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
const ndc = pointerToNdc(e);
|
||||
if (!ndc) return;
|
||||
const navHit = navPass?.setHoverFromNdc(ndc.x, ndc.y);
|
||||
const chromeHit = chromeText?.pickAt(ndc.x, ndc.y);
|
||||
if (canvasLayerEl) canvasLayerEl.style.cursor = navHit || chromeHit || onpick ? 'crosshair' : 'default';
|
||||
}
|
||||
|
||||
function handlePointerLeave() {
|
||||
navPass?.clearHover();
|
||||
if (canvasLayerEl) canvasLayerEl.style.cursor = 'default';
|
||||
}
|
||||
|
||||
async function handleFieldClick(e: MouseEvent) {
|
||||
const ndc = pointerToNdc(e);
|
||||
if (!ndc) return;
|
||||
const navHit: NavPick | null = navPass?.pickAt(ndc.x, ndc.y) ?? null;
|
||||
if (navHit) {
|
||||
await goto(`${base}${navHit.payload.href}`);
|
||||
return;
|
||||
}
|
||||
const chromeHit = chromeText?.pickAt(ndc.x, ndc.y);
|
||||
if (chromeHit?.id === 'route-chrome:pause') {
|
||||
togglePause();
|
||||
return;
|
||||
}
|
||||
for (const pass of routePasses) {
|
||||
const hit = await pass.pickAt?.(ndcX, ndcY);
|
||||
const hit = await pass.pickAt?.(ndc.x, ndc.y);
|
||||
if (hit) {
|
||||
onpick(hit);
|
||||
onpick?.(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +277,11 @@
|
|||
onMount(() => {
|
||||
return () => {
|
||||
for (const pass of routePasses) pass.dispose?.();
|
||||
chromeText?.dispose();
|
||||
navPass?.dispose();
|
||||
routePasses = [];
|
||||
chromeText = null;
|
||||
navPass = null;
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -137,63 +289,12 @@
|
|||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="{embedded ? 'absolute' : 'fixed'} inset-0 overflow-hidden route-stage">
|
||||
<div
|
||||
bind:this={canvasLayerEl}
|
||||
class="absolute inset-0 z-0"
|
||||
class:cursor-crosshair={!!onpick}
|
||||
onclick={handleFieldClick}
|
||||
>
|
||||
<ObservatoryCanvas {demo} {seed} onframe={handleFrame} onready={handleReady} />
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 z-10 pointer-events-none">
|
||||
<slot name="chrome" {frameCount} {fpsEstimate} {paused} {ready} />
|
||||
|
||||
<button
|
||||
onclick={togglePause}
|
||||
class="absolute bottom-4 right-4 pointer-events-auto flex items-center gap-2 px-3 py-1.5
|
||||
rounded-xl border border-[#22C7DE]/25 bg-[#020307]/80 backdrop-blur-sm
|
||||
font-mono text-[11px] tracking-wide text-[#22C7DE]/80 hover:text-[#22C7DE]
|
||||
hover:border-[#22C7DE]/50 transition-colors"
|
||||
title={paused ? 'Resume route organ motion' : 'Pause route organ motion'}
|
||||
aria-pressed={paused}
|
||||
>
|
||||
{paused ? '▶ RESUME' : '❚❚ PAUSE'}
|
||||
</button>
|
||||
|
||||
<div class="absolute top-4 right-4 font-mono text-[10px] tracking-[0.18em] text-[#7fe6c0]/60 uppercase">
|
||||
{organ} · {frameCount}f · {fpsEstimate}fps
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
|
||||
<div class="text-[#A8FF5E] font-mono text-sm tracking-widest animate-pulse">
|
||||
REPLAYING COGNITIVE RECEIPT...
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error && !loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
|
||||
<div class="text-[#ff8a8a] font-mono text-sm border border-[#FF3B30]/50 bg-[#1a0508]/70 px-4 py-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !loading && !error && !currentScene.alive}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="text-[#5dcaa5]/70 font-mono text-xs tracking-widest border border-[#5dcaa5]/15 bg-[#020307]/45 px-4 py-2 rounded-xl backdrop-blur-sm">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
bind:this={canvasLayerEl}
|
||||
class="{embedded ? 'absolute' : 'fixed'} inset-0 overflow-hidden"
|
||||
onclick={handleFieldClick}
|
||||
onpointermove={handlePointerMove}
|
||||
onpointerleave={handlePointerLeave}
|
||||
>
|
||||
<ObservatoryCanvas {demo} {seed} onframe={handleFrame} onready={handleReady} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.route-stage {
|
||||
background: #020307;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
178
apps/dashboard/src/lib/observatory/nav/nav-layer.ts
Normal file
178
apps/dashboard/src/lib/observatory/nav/nav-layer.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import type { ObservatoryEngine, FramePass } from '$lib/observatory/engine';
|
||||
import { CAUSAL, MEDIUM, RETENTION, rgb01 } from '$lib/observatory/cognitive-palette';
|
||||
import { TextLayerPass, type TextLayerItem } from '$lib/observatory/text/text-layer';
|
||||
|
||||
export type NavRoute = {
|
||||
label: string;
|
||||
href: string;
|
||||
shortcut?: string;
|
||||
};
|
||||
|
||||
export type NavPick = {
|
||||
id: string;
|
||||
kind: 'route-nav';
|
||||
payload: NavRoute;
|
||||
};
|
||||
|
||||
export interface NavLayerOptions {
|
||||
activePath?: string;
|
||||
routes?: NavRoute[];
|
||||
}
|
||||
|
||||
export interface NavLayerPass extends FramePass {
|
||||
init(): Promise<void>;
|
||||
setActivePath(path: string): void;
|
||||
setHoverFromNdc(ndcX: number, ndcY: number): NavPick | null;
|
||||
clearHover(): void;
|
||||
pickAt(ndcX: number, ndcY: number): NavPick | null;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export const COGNITIVE_OS_ROUTES: NavRoute[] = [
|
||||
{ href: '/observatory', label: 'Observatory', shortcut: 'O' },
|
||||
{ href: '/reasoning', label: 'Reasoning', shortcut: 'R' },
|
||||
{ href: '/contradictions', label: 'Contradictions', shortcut: 'X' },
|
||||
{ href: '/blackbox', label: 'Black Box', shortcut: 'B' },
|
||||
{ href: '/timeline', label: 'Timeline', shortcut: 'T' },
|
||||
{ href: '/duplicates', label: 'Duplicates', shortcut: 'U' },
|
||||
{ href: '/graph', label: 'Graph', shortcut: 'G' },
|
||||
{ href: '/memory-prs', label: 'Memory PRs', shortcut: 'Q' },
|
||||
{ href: '/memories', label: 'Memories', shortcut: 'M' },
|
||||
{ href: '/feed', label: 'Feed', shortcut: 'F' },
|
||||
{ href: '/explore', label: 'Explore', shortcut: 'E' },
|
||||
{ href: '/activation', label: 'Activation', shortcut: 'A' },
|
||||
{ href: '/dreams', label: 'Dreams', shortcut: 'D' },
|
||||
{ href: '/schedule', label: 'Schedule', shortcut: 'C' },
|
||||
{ href: '/importance', label: 'Importance', shortcut: 'P' },
|
||||
{ href: '/patterns', label: 'Patterns', shortcut: 'N' },
|
||||
{ href: '/intentions', label: 'Intentions', shortcut: 'I' },
|
||||
{ href: '/stats', label: 'Stats', shortcut: 'S' },
|
||||
{ href: '/settings', label: 'Settings', shortcut: ',' }
|
||||
];
|
||||
|
||||
const NAV_X = -0.93;
|
||||
const NAV_Y = 0.83;
|
||||
const NAV_SIZE = 0.024;
|
||||
const NAV_STEP = 0.058;
|
||||
const CYAN = [...rgb01(CAUSAL.forward), 1] satisfies [number, number, number, number];
|
||||
const HOVER = [...rgb01(RETENTION.luciferin), 1] satisfies [number, number, number, number];
|
||||
const DIM = [...rgb01(RETENTION.bridge), 0.36] satisfies [number, number, number, number];
|
||||
const BLACKWATER = [...rgb01(MEDIUM.blackwater), 0.15] satisfies [number, number, number, number];
|
||||
|
||||
export function createNavLayerPass(engine: ObservatoryEngine, opts: NavLayerOptions = {}): NavLayerPass {
|
||||
return new TextNavLayer(engine, opts);
|
||||
}
|
||||
|
||||
class TextNavLayer implements NavLayerPass {
|
||||
private readonly text: TextLayerPass;
|
||||
private readonly routes: NavRoute[];
|
||||
private activePath: string;
|
||||
private hoverHref: string | null = null;
|
||||
private ready = false;
|
||||
|
||||
constructor(engine: ObservatoryEngine, opts: NavLayerOptions) {
|
||||
this.text = new TextLayerPass(engine);
|
||||
this.routes = opts.routes ?? COGNITIVE_OS_ROUTES;
|
||||
this.activePath = opts.activePath ?? '';
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.text.init();
|
||||
this.ready = true;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
setActivePath(path: string): void {
|
||||
if (this.activePath === path) return;
|
||||
this.activePath = path;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
setHoverFromNdc(ndcX: number, ndcY: number): NavPick | null {
|
||||
const hit = this.pickAt(ndcX, ndcY);
|
||||
const next = hit?.payload.href ?? null;
|
||||
if (next !== this.hoverHref) {
|
||||
this.hoverHref = next;
|
||||
this.rebuild();
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
clearHover(): void {
|
||||
if (this.hoverHref === null) return;
|
||||
this.hoverHref = null;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
pickAt(ndcX: number, ndcY: number): NavPick | null {
|
||||
const hit = this.text.pickAt(ndcX, ndcY);
|
||||
if (!hit || hit.kind !== 'route-nav') return null;
|
||||
const route = hit.payload as TextLayerItem & { route?: NavRoute };
|
||||
if (!route.route) return null;
|
||||
return { id: hit.id, kind: 'route-nav', payload: route.route };
|
||||
}
|
||||
|
||||
render(pass: GPURenderPassEncoder): void {
|
||||
this.text.render(pass);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.text.dispose();
|
||||
}
|
||||
|
||||
private rebuild(): void {
|
||||
if (!this.ready) return;
|
||||
const items: TextLayerItem[] = [
|
||||
{
|
||||
id: 'route-nav:rail',
|
||||
kind: 'route-nav-rail',
|
||||
text: 'COGNITIVE OS',
|
||||
x: NAV_X,
|
||||
y: NAV_Y + 0.075,
|
||||
size: 0.021,
|
||||
color: BLACKWATER,
|
||||
revealSpan: 1
|
||||
},
|
||||
...this.routes.map((route, i) => {
|
||||
const active = this.isActive(route.href);
|
||||
const hovered = this.hoverHref === route.href;
|
||||
const color = active ? CYAN : hovered ? HOVER : DIM;
|
||||
const marker = active ? '>' : hovered ? '+' : '-';
|
||||
const tail = route.shortcut ? ` ${route.shortcut}` : '';
|
||||
return {
|
||||
id: `route-nav:${route.href}`,
|
||||
kind: 'route-nav',
|
||||
text: `${marker} ${route.label.toUpperCase()}${tail}`,
|
||||
x: active || hovered ? NAV_X + 0.012 : NAV_X,
|
||||
y: NAV_Y - i * NAV_STEP,
|
||||
size: active || hovered ? NAV_SIZE * 1.06 : NAV_SIZE,
|
||||
color,
|
||||
startFrame: 0,
|
||||
revealSpan: 1,
|
||||
maxWidthEm: 16,
|
||||
route
|
||||
} satisfies TextLayerItem & { route: NavRoute };
|
||||
}),
|
||||
...(this.hoverHref
|
||||
? [
|
||||
{
|
||||
id: 'route-nav:hover-ring',
|
||||
kind: 'route-nav-focus',
|
||||
text: '>>>>>>>>>>>>>>>>',
|
||||
x: NAV_X - 0.01,
|
||||
y: NAV_Y - this.routes.findIndex((r) => r.href === this.hoverHref) * NAV_STEP - 0.029,
|
||||
size: 0.012,
|
||||
color: CYAN,
|
||||
revealSpan: 1
|
||||
} satisfies TextLayerItem
|
||||
]
|
||||
: [])
|
||||
];
|
||||
this.text.setText(items);
|
||||
}
|
||||
|
||||
private isActive(href: string): boolean {
|
||||
const path = this.activePath || '';
|
||||
return path === href || path.endsWith(href) || (href === '/observatory' && (path === '/' || path === ''));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue