From 22e22f7b0d136d9e0c81a1119d0b5ce004fbb3ab Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Mon, 23 Mar 2026 21:25:54 -0400 Subject: [PATCH] feat: add 3D memory graph & sovereignty loop visualization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements issue #18 — unified persistent memory & spatial agentic loop visualization: - **Memory Graph** (`createMemoryGraph`): 9 typed nodes (user/feedback/ project/reference) as glowing pulsing spheres with labeled edges, floating in the left wing of the Nexus at (-14, 2.5, -8). Nodes bob and breathe independently; graph slowly sways on Y. - **Sovereignty Loop** (`createSovereigntyLoop`): EXPORT → COMPRESS → TRAIN → EVAL ring with stage nodes, halos, and a traveling pulse sphere that changes color per stage. Central soul icosahedron pulses at the core. Group slowly rotates. - **HUD Legend**: top-right color key panel for memory node types and sovereignty loop stages. Fixes #18 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 14 +++ style.css | 44 +++++++++ 3 files changed, 313 insertions(+) diff --git a/app.js b/app.js index 60689a0..360b569 100644 --- a/app.js +++ b/app.js @@ -35,6 +35,28 @@ let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; +// Memory & Sovereignty loop state +let memoryNodeMeshes = []; +let sovereigntyGroup; +let loopPulseSphere; +let loopPulseT = 0; + +// Memory node type colors +const MEMORY_COLORS = { + user: 0x4af0c0, + feedback: 0xffd700, + project: 0x7b5cff, + reference: 0x4488ff, +}; + +// Sovereignty loop stages +const LOOP_STAGES = [ + { label: 'EXPORT', color: 0x4af0c0, angle: Math.PI / 2 }, + { label: 'COMPRESS', color: 0xffd700, angle: 0 }, + { label: 'TRAIN', color: 0x7b5cff, angle: -Math.PI / 2 }, + { label: 'EVAL', color: 0xff4466, angle: Math.PI }, +]; + // ═══ INIT ═══ function init() { clock = new THREE.Clock(); @@ -77,6 +99,9 @@ function init() { createDustParticles(); updateLoad(85); createAmbientStructures(); + updateLoad(87); + createMemoryGraph(); + createSovereigntyLoop(); updateLoad(90); // Post-processing @@ -787,6 +812,202 @@ function createAmbientStructures() { scene.add(pedestal); } +// ═══ MEMORY GRAPH ═══ +function createMemoryGraph() { + const group = new THREE.Group(); + group.position.set(-14, 2.5, -8); + group.name = 'memory-graph'; + + const NODES = [ + { type: 'user', label: 'Role: Sovereign Dev', pos: [ 0, 2.5, 0 ] }, + { type: 'user', label: 'Prefs: Terse replies', pos: [-1.8, 3.8, -0.8 ] }, + { type: 'feedback', label: 'No trailing summaries', pos: [ 1.8, 3.5, 0.5 ] }, + { type: 'feedback', label: 'Real DB in tests', pos: [ 2.2, 1.2, -1.2 ] }, + { type: 'project', label: 'Nexus v1 build', pos: [-2.2, 1.5, 1.2 ] }, + { type: 'project', label: 'Portal system', pos: [ 0.5, -0.2, 2.0 ] }, + { type: 'reference', label: 'Gitea API', pos: [-1.2, -0.2, -1.8 ] }, + { type: 'reference', label: 'Issue tracker', pos: [ 1.0, -1.2, -0.5 ] }, + { type: 'project', label: 'Sovereignty loop', pos: [-0.5, -1.5, 1.0 ] }, + ]; + + const EDGES = [ + [0, 1], [0, 2], [0, 4], [0, 6], + [1, 2], [2, 3], [3, 7], [4, 5], + [5, 8], [6, 7], [7, 8], [4, 8], + ]; + + // Build node meshes + NODES.forEach((n, i) => { + const color = MEMORY_COLORS[n.type]; + const geo = new THREE.SphereGeometry(0.22, 12, 12); + const mat = new THREE.MeshStandardMaterial({ + color, + emissive: color, + emissiveIntensity: 1.2, + roughness: 0.2, + metalness: 0.6, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(...n.pos); + mesh.name = `mem-node-${i}`; + group.add(mesh); + memoryNodeMeshes.push(mesh); + + // Outer glow ring + const ringGeo = new THREE.TorusGeometry(0.35, 0.025, 6, 24); + const ringMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.45 }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.position.set(...n.pos); + ring.rotation.x = Math.PI / 2; + group.add(ring); + + // Label sprite + const lc = document.createElement('canvas'); + lc.width = 320; lc.height = 48; + const lctx = lc.getContext('2d'); + lctx.font = '18px "JetBrains Mono", monospace'; + lctx.fillStyle = '#' + new THREE.Color(color).getHexString(); + lctx.fillText(n.label, 6, 32); + const ltex = new THREE.CanvasTexture(lc); + ltex.minFilter = THREE.LinearFilter; + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, depthWrite: false, side: THREE.DoubleSide }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 0.22), lmat); + lmesh.position.set(n.pos[0] + 0.85, n.pos[1], n.pos[2]); + group.add(lmesh); + }); + + // Build edge lines + EDGES.forEach(([a, b]) => { + const pa = new THREE.Vector3(...NODES[a].pos); + const pb = new THREE.Vector3(...NODES[b].pos); + const points = [pa, pb]; + const lineGeo = new THREE.BufferGeometry().setFromPoints(points); + const lineMat = new THREE.LineBasicMaterial({ + color: 0x2a3a5a, + transparent: true, + opacity: 0.5, + }); + const line = new THREE.Line(lineGeo, lineMat); + group.add(line); + }); + + // Section title label + const tc = document.createElement('canvas'); + tc.width = 512; tc.height = 56; + const tctx = tc.getContext('2d'); + tctx.font = 'bold 26px "Orbitron", sans-serif'; + tctx.fillStyle = '#4af0c0'; + tctx.textAlign = 'center'; + tctx.fillText('◈ MEMORY GRAPH', 256, 38); + const ttex = new THREE.CanvasTexture(tc); + ttex.minFilter = THREE.LinearFilter; + const tmat = new THREE.MeshBasicMaterial({ map: ttex, transparent: true, depthWrite: false, side: THREE.DoubleSide }); + const tmesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.44), tmat); + tmesh.position.set(0, 5.5, 0); + group.add(tmesh); + + scene.add(group); +} + +// ═══ SOVEREIGNTY LOOP ═══ +function createSovereigntyLoop() { + const group = new THREE.Group(); + group.position.set(-14, 2.5, 5); + group.name = 'sovereignty-loop'; + sovereigntyGroup = group; + + const RADIUS = 2.5; + + // Orbit ring + const orbitGeo = new THREE.TorusGeometry(RADIUS, 0.04, 8, 80); + const orbitMat = new THREE.MeshBasicMaterial({ color: 0x1a2a4a, transparent: true, opacity: 0.6 }); + group.add(new THREE.Mesh(orbitGeo, orbitMat)); + + // Stage nodes + LOOP_STAGES.forEach((stage) => { + const x = Math.cos(stage.angle) * RADIUS; + const z = Math.sin(stage.angle) * RADIUS; + + const geo = new THREE.SphereGeometry(0.35, 14, 14); + const mat = new THREE.MeshStandardMaterial({ + color: stage.color, + emissive: stage.color, + emissiveIntensity: 1.0, + roughness: 0.15, + metalness: 0.7, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(x, 0, z); + group.add(mesh); + + // Halo + const hgeo = new THREE.TorusGeometry(0.5, 0.03, 6, 20); + const hmat = new THREE.MeshBasicMaterial({ color: stage.color, transparent: true, opacity: 0.4 }); + const halo = new THREE.Mesh(hgeo, hmat); + halo.position.set(x, 0, z); + group.add(halo); + + // Stage label + const lc = document.createElement('canvas'); + lc.width = 320; lc.height = 56; + const lctx = lc.getContext('2d'); + lctx.font = 'bold 28px "Orbitron", sans-serif'; + lctx.fillStyle = '#' + new THREE.Color(stage.color).getHexString(); + lctx.textAlign = 'center'; + lctx.fillText(stage.label, 160, 38); + const ltex = new THREE.CanvasTexture(lc); + ltex.minFilter = THREE.LinearFilter; + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, depthWrite: false, side: THREE.DoubleSide }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 0.28), lmat); + lmesh.position.set(x * 1.5, 0.7, z * 1.5); + group.add(lmesh); + }); + + // Central soul sphere + const soulGeo = new THREE.IcosahedronGeometry(0.45, 2); + const soulMat = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + emissive: 0x8877ff, + emissiveIntensity: 2.5, + roughness: 0, + metalness: 1, + transmission: 0.3, + }); + const soul = new THREE.Mesh(soulGeo, soulMat); + soul.name = 'soul-sphere'; + group.add(soul); + + // Traveling pulse sphere + const pGeo = new THREE.SphereGeometry(0.15, 8, 8); + const pMat = new THREE.MeshStandardMaterial({ + color: 0xffffff, + emissive: 0xffffff, + emissiveIntensity: 3, + roughness: 0, + metalness: 1, + }); + loopPulseSphere = new THREE.Mesh(pGeo, pMat); + loopPulseSphere.name = 'loop-pulse'; + group.add(loopPulseSphere); + + // Section title + const tc = document.createElement('canvas'); + tc.width = 512; tc.height = 56; + const tctx = tc.getContext('2d'); + tctx.font = 'bold 24px "Orbitron", sans-serif'; + tctx.fillStyle = '#7b5cff'; + tctx.textAlign = 'center'; + tctx.fillText('◈ SOVEREIGNTY LOOP', 256, 38); + const ttex = new THREE.CanvasTexture(tc); + ttex.minFilter = THREE.LinearFilter; + const tmat = new THREE.MeshBasicMaterial({ map: ttex, transparent: true, depthWrite: false, side: THREE.DoubleSide }); + const tmesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.44), tmat); + tmesh.position.set(0, 4, 0); + group.add(tmesh); + + scene.add(group); +} + // ═══ CONTROLS ═══ function setupControls() { document.addEventListener('keydown', (e) => { @@ -950,6 +1171,40 @@ function gameLoop() { core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5; } + // Animate memory graph — pulse nodes and slow float + memoryNodeMeshes.forEach((mesh, i) => { + mesh.material.emissiveIntensity = 0.8 + 0.6 * Math.sin(elapsed * 1.5 + i * 0.8); + mesh.position.y += Math.sin(elapsed * 0.9 + i * 1.1) * 0.003; + mesh.rotation.y = elapsed * 0.4 + i; + }); + const memGraph = scene.getObjectByName('memory-graph'); + if (memGraph) { + memGraph.rotation.y = Math.sin(elapsed * 0.15) * 0.2; + } + + // Animate sovereignty loop + const soul = scene.getObjectByName('soul-sphere'); + if (soul) { + soul.rotation.y = elapsed * 0.8; + soul.rotation.x = elapsed * 0.4; + soul.material.emissiveIntensity = 2 + Math.sin(elapsed * 2.5) * 0.8; + } + if (sovereigntyGroup) { + sovereigntyGroup.rotation.y = elapsed * 0.12; + } + // Traveling pulse + if (loopPulseSphere) { + loopPulseT = (elapsed * 0.4) % 1; + const angle = loopPulseT * Math.PI * 2; + const RADIUS = 2.5; + loopPulseSphere.position.x = Math.cos(angle) * RADIUS; + loopPulseSphere.position.z = Math.sin(angle) * RADIUS; + // Color matches current stage + const stageIdx = Math.floor(loopPulseT * 4) % 4; + loopPulseSphere.material.emissive.setHex(LOOP_STAGES[stageIdx].color); + loopPulseSphere.material.color.setHex(LOOP_STAGES[stageIdx].color); + } + // Render composer.render(); diff --git a/index.html b/index.html index 3a2c6ea..5aad500 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,20 @@ + +
+
MEMORY NODES
+
User
+
Feedback
+
Project
+
Reference
+
SOVEREIGNTY LOOP
+
EXPORT
+
COMPRESS
+
TRAIN
+
EVAL
+
+
WASD move   Mouse look   Enter chat diff --git a/style.css b/style.css index 519b05e..146b4f2 100644 --- a/style.css +++ b/style.css @@ -359,3 +359,47 @@ canvas#nexus-canvas { display: none; } } + +/* === MEMORY LEGEND === */ +.hud-memory-legend { + position: fixed; + top: var(--space-4); + right: var(--space-4); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--panel-radius); + padding: var(--space-3) var(--space-4); + font-family: var(--font-body); + font-size: var(--text-xs); + color: var(--color-text-muted); + backdrop-filter: blur(var(--panel-blur)); + pointer-events: none; + z-index: 100; + min-width: 140px; +} + +.hud-memory-legend .legend-title { + font-family: var(--font-display); + font-size: 9px; + letter-spacing: 0.1em; + color: var(--color-primary); + margin-bottom: var(--space-1); + text-transform: uppercase; +} + +.hud-memory-legend .legend-item { + display: flex; + align-items: center; + gap: var(--space-2); + line-height: 1.8; + color: var(--color-text); +} + +.hud-memory-legend .legend-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + box-shadow: 0 0 4px currentColor; +} -- 2.43.0