mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-12 22:42:10 +02:00
feat(graph): raw-WebGPU living field is now the main dashboard renderer
The main graph page defaults to the Observatory engine — GPU force sim,
HDR bloom, and the recall wavefront sweeping the REAL memory graph on a
12s loop — instead of the flat Three.js view. Visuals to the max, per
the locked ALIVE-field direction.
- ObservatoryStage gains embedded (fills parent, not viewport) and
chrome ('full' | 'none') props; chrome='none' is a pure living canvas
plus loading/error text, so the page's own DOM chrome stays in charge.
- New Field | Classic renderer toggle in the control bar. Field is the
default wherever WebGPU exists; no WebGPU forces classic. Choice
persists in localStorage.
- Classic (Three.js) remains one click away and untouched: node picking,
colour modes, node count, brightness, temporal scrubbing and legends
are classic-renderer features and now only render there. Dream,
Observatory takeover, Memory Cinema (protected) and Reload work in
both modes.
Verified live: fresh load defaults to Field (148 real nodes, 2865 real
edges, recall ribbon igniting on camera, zero console errors); Classic
toggle restores search/colour/brightness with a single canvas; the
choice round-trips through localStorage. svelte-check 940 files 0
errors 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7da5f4aa97
commit
c920f93b10
2 changed files with 113 additions and 24 deletions
|
|
@ -44,6 +44,19 @@
|
|||
ondemochange?: (demo: DemoMode) => void;
|
||||
/** When provided, renders the exit control (× back to the dashboard). */
|
||||
onexit?: () => void;
|
||||
/**
|
||||
* Embedded mode — the stage fills its PARENT (absolute) instead of the
|
||||
* viewport (fixed). Used by the main graph page, where the field is the
|
||||
* default renderer living under the page's own chrome.
|
||||
*/
|
||||
embedded?: boolean;
|
||||
/**
|
||||
* 'full' — telemetry strip, spine, verdicts, switcher, exit (the route
|
||||
* and the takeover overlay).
|
||||
* 'none' — pure living canvas + loading/error text only; the host page
|
||||
* provides all chrome (the main graph's field renderer).
|
||||
*/
|
||||
chrome?: 'full' | 'none';
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -53,7 +66,9 @@
|
|||
capture = false,
|
||||
showSwitcher = true,
|
||||
ondemochange,
|
||||
onexit
|
||||
onexit,
|
||||
embedded = false,
|
||||
chrome = 'full'
|
||||
}: Props = $props();
|
||||
|
||||
// Human labels for the switcher chips — short, mono, uppercase (visual DNA §7.3).
|
||||
|
|
@ -246,8 +261,11 @@
|
|||
|
||||
<svelte:window onkeydown={onKeydown} />
|
||||
|
||||
<!-- Full-bleed void stage: #05060a -->
|
||||
<div class="fixed inset-0 overflow-hidden bg-[#05060a]" class:cursor-none={capture}>
|
||||
<!-- Void stage: #05060a — full-bleed (route/takeover) or parent-filling (embedded) -->
|
||||
<div
|
||||
class="{embedded ? 'absolute' : 'fixed'} inset-0 overflow-hidden bg-[#05060a]"
|
||||
class:cursor-none={capture}
|
||||
>
|
||||
<!-- Canvas layer (z-index 0) — the living memory field -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<ObservatoryCanvas
|
||||
|
|
@ -260,9 +278,11 @@
|
|||
</div>
|
||||
|
||||
<!-- DOM overlay layer (pointer-events:none) — instruments only.
|
||||
capture hides it all (pure canvas for recording); H toggles. -->
|
||||
capture hides it all (pure canvas for recording); H toggles;
|
||||
chrome='none' leaves only loading/error (host page owns the chrome). -->
|
||||
{#if showHud}
|
||||
<div class="absolute inset-0 z-10 pointer-events-none">
|
||||
{#if chrome === 'full'}
|
||||
<!-- Top telemetry strip -->
|
||||
<TelemetryStrip
|
||||
demoMode={demo}
|
||||
|
|
@ -276,9 +296,10 @@
|
|||
{loading}
|
||||
{error}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Exit control — back to the dashboard graph. Esc works too. -->
|
||||
{#if onexit}
|
||||
{#if chrome === 'full' && onexit}
|
||||
<button
|
||||
onclick={onexit}
|
||||
class="absolute top-10 right-4 pointer-events-auto font-mono text-xs tracking-widest
|
||||
|
|
@ -291,7 +312,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- Demo moment switcher — the 5 cognitive moments, one click each. -->
|
||||
{#if showSwitcher}
|
||||
{#if chrome === 'full' && showSwitcher}
|
||||
<div class="absolute top-10 left-4 pointer-events-auto flex flex-col gap-1.5">
|
||||
{#each DEMO_MODES as mode (mode)}
|
||||
<button
|
||||
|
|
@ -327,15 +348,17 @@
|
|||
{/if}
|
||||
|
||||
<!-- Timeline spine: beat ticks + playhead riding the loop -->
|
||||
<TimelineSpine steps={pathSteps} frame={frameCount} />
|
||||
{#if chrome === 'full'}
|
||||
<TimelineSpine steps={pathSteps} frame={frameCount} />
|
||||
{/if}
|
||||
|
||||
<!-- Salience-rescue verdict card (frame-driven opacity) -->
|
||||
{#if demo === 'salience-rescue' && rescuePlan?.viable}
|
||||
{#if chrome === 'full' && demo === 'salience-rescue' && rescuePlan?.viable}
|
||||
<RescueVerdict frame={frameCount} verdict={rescuePlan.verdict} />
|
||||
{/if}
|
||||
|
||||
<!-- Firewall quarantine verdict card (frames 480-620, crimson tone) -->
|
||||
{#if demo === 'firewall' && firewallPlan?.viable}
|
||||
{#if chrome === 'full' && demo === 'firewall' && firewallPlan?.viable}
|
||||
<RescueVerdict
|
||||
frame={frameCount}
|
||||
tone="quarantine"
|
||||
|
|
|
|||
|
|
@ -64,6 +64,22 @@
|
|||
let showObservatory = $state(false);
|
||||
let obsDemo = $state<DemoMode>('recall-path');
|
||||
|
||||
// Main-canvas renderer: 'field' = the raw-WebGPU living memory field (the
|
||||
// Observatory engine — HDR bloom, GPU force sim, recall wavefront riding
|
||||
// the REAL graph; visuals to the max, the default wherever WebGPU exists).
|
||||
// 'classic' = the Three.js inspector (node picking, colour modes, temporal
|
||||
// scrubbing) — kept one click away until GPU picking lands in the field.
|
||||
const RENDERER_KEY = 'vestige-graph-renderer';
|
||||
let renderMode = $state<'field' | 'classic'>('classic');
|
||||
function setRenderMode(m: 'field' | 'classic') {
|
||||
renderMode = m;
|
||||
try {
|
||||
localStorage.setItem(RENDERER_KEY, m);
|
||||
} catch {
|
||||
/* storage unavailable (private mode) — session-only choice */
|
||||
}
|
||||
}
|
||||
|
||||
// Filtered graph data based on temporal mode
|
||||
let displayNodes = $derived.by((): GraphNode[] => {
|
||||
if (!graphData) return [];
|
||||
|
|
@ -112,6 +128,17 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
// Renderer choice: honor the saved preference, defaulting to the WebGPU
|
||||
// field wherever the GPU exists. No WebGPU → classic, always.
|
||||
const hasWebGpu = typeof navigator !== 'undefined' && 'gpu' in navigator;
|
||||
let saved: string | null = null;
|
||||
try {
|
||||
saved = localStorage.getItem(RENDERER_KEY);
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
renderMode = !hasWebGpu ? 'classic' : saved === 'classic' ? 'classic' : 'field';
|
||||
|
||||
const sp = new URLSearchParams(window.location.search);
|
||||
const requestedMode = sp.get('colorMode');
|
||||
if (isColorMode(requestedMode)) {
|
||||
|
|
@ -286,16 +313,25 @@ disown</code>
|
|||
</div>
|
||||
</div>
|
||||
{:else if graphData}
|
||||
<Graph3D
|
||||
nodes={displayNodes}
|
||||
edges={displayEdges}
|
||||
centerId={graphData.center_id}
|
||||
events={$eventFeed}
|
||||
{isDreaming}
|
||||
{colorMode}
|
||||
onSelect={onNodeSelect}
|
||||
onGraphMutation={handleGraphMutation}
|
||||
/>
|
||||
{#if renderMode === 'field'}
|
||||
<!-- The raw-WebGPU living memory field — the Observatory engine as the
|
||||
MAIN renderer: GPU force sim, HDR bloom, the recall wavefront
|
||||
sweeping the real graph on a 12s loop. Page chrome stays DOM. -->
|
||||
<div class="absolute inset-0">
|
||||
<ObservatoryStage embedded chrome="none" demo="recall-path" showSwitcher={false} />
|
||||
</div>
|
||||
{:else}
|
||||
<Graph3D
|
||||
nodes={displayNodes}
|
||||
edges={displayEdges}
|
||||
centerId={graphData.center_id}
|
||||
events={$eventFeed}
|
||||
{isDreaming}
|
||||
{colorMode}
|
||||
onSelect={onNodeSelect}
|
||||
onGraphMutation={handleGraphMutation}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Top controls bar -->
|
||||
|
|
@ -304,7 +340,8 @@ disown</code>
|
|||
[padding-top:max(0.75rem,env(safe-area-inset-top))] [padding-left:max(0.75rem,env(safe-area-inset-left))] [padding-right:max(0.75rem,env(safe-area-inset-right))]
|
||||
sm:[padding-top:0] sm:[padding-left:0] sm:[padding-right:0]"
|
||||
>
|
||||
<!-- Search -->
|
||||
<!-- Search — classic-renderer feature (centers + reloads the inspector) -->
|
||||
{#if renderMode === 'classic'}
|
||||
<div class="flex gap-2 w-full sm:flex-1 sm:w-auto sm:max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -319,8 +356,36 @@ disown</code>
|
|||
Focus
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<!-- Renderer toggle: the WebGPU living field (default) vs the classic
|
||||
Three.js inspector. The field is the banger; classic keeps node
|
||||
picking / colour modes / temporal until GPU picking lands. -->
|
||||
<div class="flex shrink-0 glass rounded-xl p-0.5 text-xs" role="radiogroup" aria-label="Renderer">
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={renderMode === 'field'}
|
||||
onclick={() => setRenderMode('field')}
|
||||
class="min-h-9 px-3 py-1.5 rounded-lg transition {renderMode === 'field' ? 'bg-[#5dcaa5]/25 text-[#7fe6c0]' : 'text-dim hover:text-text'}"
|
||||
title="Raw-WebGPU living memory field — visuals to the max"
|
||||
>
|
||||
✦ Field
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={renderMode === 'classic'}
|
||||
onclick={() => setRenderMode('classic')}
|
||||
class="min-h-9 px-3 py-1.5 rounded-lg transition {renderMode === 'classic' ? 'bg-synapse/25 text-synapse-glow' : 'text-dim hover:text-text'}"
|
||||
title="Classic 3D inspector — node picking, colour modes, temporal scrubbing"
|
||||
>
|
||||
Classic
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if renderMode === 'classic'}
|
||||
<!-- v2.0.8: colour mode toggle. Switches sphere tint between node type
|
||||
(fact / concept / event / …) and FSRS memory state (active / dormant /
|
||||
silent / unavailable). Legend auto-renders in state mode. -->
|
||||
|
|
@ -386,6 +451,7 @@ disown</code>
|
|||
{graphState.brightness.toFixed(1)}x
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<!-- Dream button -->
|
||||
<button
|
||||
|
|
@ -449,13 +515,13 @@ disown</code>
|
|||
|
||||
<!-- v2.0.8: FSRS memory-state legend. Only rendered in state mode so the
|
||||
legend doesn't compete with the node-type palette in type mode. -->
|
||||
{#if colorMode === 'state'}
|
||||
{#if renderMode === 'classic' && colorMode === 'state'}
|
||||
<div class="absolute bottom-4 right-4 z-10">
|
||||
<MemoryStateLegend />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if colorMode === 'ahagraph'}
|
||||
{#if renderMode === 'classic' && colorMode === 'ahagraph'}
|
||||
<div class="absolute bottom-4 right-4 z-10 glass rounded-xl px-4 py-3 text-xs">
|
||||
<div class="text-bright font-semibold mb-2">AhaGraph</div>
|
||||
<div class="space-y-1.5">
|
||||
|
|
@ -469,8 +535,8 @@ disown</code>
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Temporal playback slider -->
|
||||
{#if graphData}
|
||||
<!-- Temporal playback slider — scrubs the classic inspector only -->
|
||||
{#if graphData && renderMode === 'classic'}
|
||||
<TimeSlider
|
||||
nodes={graphData.nodes}
|
||||
onDateChange={(date) => { temporalDate = date; }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue