diff --git a/app.js b/app.js index 9b0bf04..7a9ad2e 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; +import { SpatialMemory } from './nexus/components/spatial-memory.js'; // ═══════════════════════════════════════════ // NEXUS v1.1 — Portal System Update @@ -670,6 +671,10 @@ async function init() { updateLoad(40); createFloor(); updateLoad(50); + + // Project Mnemosyne — Spatial Memory Schema + SpatialMemory.init(scene); + createBatcaveTerminal(); updateLoad(60); @@ -2573,10 +2578,11 @@ function gameLoop() { updateAshStorm(delta, elapsed); - // Project Mnemosyne - Memory Orb Animation + // Project Mnemosyne - Memory Animation if (typeof animateMemoryOrbs === 'function') { animateMemoryOrbs(delta); } + SpatialMemory.update(delta); const mode = NAV_MODES[navModeIdx]; @@ -3093,6 +3099,16 @@ function spawnRetrievalOrbs(results, center) { init().then(() => { createAshStorm(); createPortalTunnel(); + // Project Mnemosyne — seed demo spatial memories + const demoMemories = [ + { id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95, connections: ['mem_first_portal'] }, + { id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85, connections: ['mem_nexus_birth'] }, + { id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7, connections: [] }, + { id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth'] }, + { id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] }, + ]; + demoMemories.forEach(m => SpatialMemory.placeMemory(m)); + fetchGiteaData(); setInterval(fetchGiteaData, 30000); runWeeklyAudit(); diff --git a/nexus/components/spatial-memory.js b/nexus/components/spatial-memory.js new file mode 100644 index 0000000..f963de6 --- /dev/null +++ b/nexus/components/spatial-memory.js @@ -0,0 +1,376 @@ +// ═══════════════════════════════════════════ +// PROJECT MNEMOSYNE — SPATIAL MEMORY SCHEMA +// ═══════════════════════════════════════════ +// +// Maps memories to persistent locations in the 3D Nexus world. +// Each region corresponds to a semantic category. Memories placed +// in a region stay there across sessions, forming a navigable +// holographic archive. +// +// World layout (hex cylinder, radius 25): +// North (z-) → Documents & Knowledge +// South (z+) → Projects & Tasks +// East (x+) → Code & Engineering +// West (x-) → Conversations & Social +// Center → Active Working Memory +// Below (y-) → Archive (cold storage) +// +// Usage from app.js: +// SpatialMemory.init(scene); +// SpatialMemory.placeMemory({ id, content, category, ... }); +// SpatialMemory.importIndex(savedIndex); +// SpatialMemory.update(delta); +// ═══════════════════════════════════════════ + +const SpatialMemory = (() => { + + // ─── REGION DEFINITIONS ─────────────────────────────── + const REGIONS = { + engineering: { + label: 'Code & Engineering', + center: [15, 0, 0], + radius: 10, + color: 0x4af0c0, + glyph: '\u2699', + description: 'Source code, debugging sessions, architecture decisions' + }, + social: { + label: 'Conversations & Social', + center: [-15, 0, 0], + radius: 10, + color: 0x7b5cff, + glyph: '\uD83D\uDCAC', + description: 'Chats, discussions, human interactions' + }, + knowledge: { + label: 'Documents & Knowledge', + center: [0, 0, -15], + radius: 10, + color: 0xffd700, + glyph: '\uD83D\uDCD6', + description: 'Papers, docs, research, learned concepts' + }, + projects: { + label: 'Projects & Tasks', + center: [0, 0, 15], + radius: 10, + color: 0xff4466, + glyph: '\uD83C\uDFAF', + description: 'Active tasks, issues, milestones, goals' + }, + working: { + label: 'Active Working Memory', + center: [0, 0, 0], + radius: 5, + color: 0x00ff88, + glyph: '\uD83D\uDCA1', + description: 'Current focus — transient, high-priority memories' + }, + archive: { + label: 'Archive', + center: [0, -3, 0], + radius: 20, + color: 0x334455, + glyph: '\uD83D\uDDC4', + description: 'Cold storage — rarely accessed, aged-out memories' + } + }; + + // ─── STATE ──────────────────────────────────────────── + let _scene = null; + let _regionMarkers = {}; + let _memoryObjects = {}; + let _connectionLines = []; + let _initialized = false; + + // ─── CRYSTAL GEOMETRY (persistent memories) ─────────── + function createCrystalGeometry(size) { + return new THREE.OctahedronGeometry(size, 0); + } + + // ─── REGION MARKER ─────────────────────────────────── + function createRegionMarker(regionKey, region) { + const cx = region.center[0]; + const cy = region.center[1] + 0.06; + const cz = region.center[2]; + + const ringGeo = new THREE.RingGeometry(region.radius - 0.5, region.radius, 6); + const ringMat = new THREE.MeshBasicMaterial({ + color: region.color, + transparent: true, + opacity: 0.15, + side: THREE.DoubleSide + }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.rotation.x = -Math.PI / 2; + ring.position.set(cx, cy, cz); + ring.userData = { type: 'region_marker', region: regionKey }; + + const discGeo = new THREE.CircleGeometry(region.radius - 0.5, 6); + const discMat = new THREE.MeshBasicMaterial({ + color: region.color, + transparent: true, + opacity: 0.03, + side: THREE.DoubleSide + }); + const disc = new THREE.Mesh(discGeo, discMat); + disc.rotation.x = -Math.PI / 2; + disc.position.set(cx, cy - 0.01, cz); + + _scene.add(ring); + _scene.add(disc); + + // Floating label + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 64; + const ctx = canvas.getContext('2d'); + ctx.font = '24px monospace'; + ctx.fillStyle = '#' + region.color.toString(16).padStart(6, '0'); + ctx.textAlign = 'center'; + ctx.fillText(region.glyph + ' ' + region.label, 128, 40); + + const texture = new THREE.CanvasTexture(canvas); + const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.6 }); + const sprite = new THREE.Sprite(spriteMat); + sprite.position.set(cx, 3, cz); + sprite.scale.set(4, 1, 1); + _scene.add(sprite); + + return { ring, disc, sprite }; + } + + // ─── PLACE A MEMORY ────────────────────────────────── + function placeMemory(mem) { + if (!_scene) return null; + + const region = REGIONS[mem.category] || REGIONS.working; + const pos = mem.position || _assignPosition(mem.category, mem.id); + const strength = Math.max(0.05, Math.min(1, mem.strength != null ? mem.strength : 0.7)); + const size = 0.2 + strength * 0.3; + + const geo = createCrystalGeometry(size); + const mat = new THREE.MeshStandardMaterial({ + color: region.color, + emissive: region.color, + emissiveIntensity: 1.5 * strength, + metalness: 0.6, + roughness: 0.15, + transparent: true, + opacity: 0.5 + strength * 0.4 + }); + + const crystal = new THREE.Mesh(geo, mat); + crystal.position.set(pos[0], pos[1] + 1.5, pos[2]); + crystal.castShadow = true; + + crystal.userData = { + type: 'spatial_memory', + memId: mem.id, + region: mem.category, + pulse: Math.random() * Math.PI * 2, + strength: strength, + createdAt: mem.timestamp || new Date().toISOString() + }; + + const light = new THREE.PointLight(region.color, 0.8 * strength, 5); + crystal.add(light); + + _scene.add(crystal); + _memoryObjects[mem.id] = { mesh: crystal, data: mem, region: mem.category }; + + if (mem.connections && mem.connections.length > 0) { + _drawConnections(mem.id, mem.connections); + } + + console.info('[Mnemosyne] Spatial memory placed:', mem.id, 'in', region.label); + return crystal; + } + + // ─── DETERMINISTIC POSITION ────────────────────────── + function _assignPosition(category, memId) { + const region = REGIONS[category] || REGIONS.working; + const cx = region.center[0]; + const cy = region.center[1]; + const cz = region.center[2]; + const r = region.radius * 0.7; + + let hash = 0; + for (let i = 0; i < memId.length; i++) { + hash = ((hash << 5) - hash) + memId.charCodeAt(i); + hash |= 0; + } + + const angle = (Math.abs(hash % 360) / 360) * Math.PI * 2; + const dist = (Math.abs((hash >> 8) % 100) / 100) * r; + const height = (Math.abs((hash >> 16) % 100) / 100) * 3; + + return [cx + Math.cos(angle) * dist, cy + height, cz + Math.sin(angle) * dist]; + } + + // ─── CONNECTIONS ───────────────────────────────────── + function _drawConnections(memId, connections) { + const src = _memoryObjects[memId]; + if (!src) return; + + connections.forEach(targetId => { + const tgt = _memoryObjects[targetId]; + if (!tgt) return; + + const points = [src.mesh.position.clone(), tgt.mesh.position.clone()]; + const geo = new THREE.BufferGeometry().setFromPoints(points); + const mat = new THREE.LineBasicMaterial({ color: 0x334455, transparent: true, opacity: 0.2 }); + const line = new THREE.Line(geo, mat); + line.userData = { type: 'connection', from: memId, to: targetId }; + _scene.add(line); + _connectionLines.push(line); + }); + } + + // ─── REMOVE A MEMORY ───────────────────────────────── + function removeMemory(memId) { + const obj = _memoryObjects[memId]; + if (!obj) return; + + if (obj.mesh.parent) obj.mesh.parent.remove(obj.mesh); + if (obj.mesh.geometry) obj.mesh.geometry.dispose(); + if (obj.mesh.material) obj.mesh.material.dispose(); + + for (let i = _connectionLines.length - 1; i >= 0; i--) { + const line = _connectionLines[i]; + if (line.userData.from === memId || line.userData.to === memId) { + if (line.parent) line.parent.remove(line); + line.geometry.dispose(); + line.material.dispose(); + _connectionLines.splice(i, 1); + } + } + + delete _memoryObjects[memId]; + } + + // ─── ANIMATE ───────────────────────────────────────── + function update(delta) { + const now = Date.now(); + + Object.values(_memoryObjects).forEach(obj => { + const mesh = obj.mesh; + if (!mesh || !mesh.userData) return; + + mesh.rotation.y += delta * 0.3; + + mesh.userData.pulse += delta * 1.5; + const pulse = 1 + Math.sin(mesh.userData.pulse) * 0.08; + mesh.scale.setScalar(pulse); + + if (mesh.material) { + const base = mesh.userData.strength || 0.7; + mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base; + } + }); + + Object.values(_regionMarkers).forEach(marker => { + if (marker.ring && marker.ring.material) { + marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05; + } + }); + } + + // ─── INIT ──────────────────────────────────────────── + function init(scene) { + _scene = scene; + _initialized = true; + + Object.entries(REGIONS).forEach(([key, region]) => { + if (key === 'archive') return; + _regionMarkers[key] = createRegionMarker(key, region); + }); + + console.info('[Mnemosyne] Spatial Memory Schema initialized —', Object.keys(REGIONS).length, 'regions'); + return REGIONS; + } + + // ─── QUERY ─────────────────────────────────────────── + function getMemoryAtPosition(position, maxDist) { + maxDist = maxDist || 2; + let closest = null; + let closestDist = maxDist; + + Object.values(_memoryObjects).forEach(obj => { + const d = obj.mesh.position.distanceTo(position); + if (d < closestDist) { closest = obj; closestDist = d; } + }); + return closest; + } + + function getRegionAtPosition(position) { + for (const [key, region] of Object.entries(REGIONS)) { + const dx = position.x - region.center[0]; + const dz = position.z - region.center[2]; + if (Math.sqrt(dx * dx + dz * dz) <= region.radius) return key; + } + return null; + } + + function getMemoriesInRegion(regionKey) { + return Object.values(_memoryObjects).filter(o => o.region === regionKey); + } + + function getAllMemories() { + return Object.values(_memoryObjects).map(o => o.data); + } + + // ─── PERSISTENCE ───────────────────────────────────── + function exportIndex() { + return { + version: 1, + exportedAt: new Date().toISOString(), + regions: Object.fromEntries( + Object.entries(REGIONS).map(([k, v]) => [k, { label: v.label, center: v.center, radius: v.radius, color: v.color }]) + ), + memories: Object.values(_memoryObjects).map(o => ({ + id: o.data.id, + content: o.data.content, + category: o.region, + position: [o.mesh.position.x, o.mesh.position.y - 1.5, o.mesh.position.z], + source: o.data.source || 'unknown', + timestamp: o.data.timestamp || o.mesh.userData.createdAt, + strength: o.mesh.userData.strength || 0.7, + connections: o.data.connections || [] + })) + }; + } + + function importIndex(index) { + if (!index || !index.memories) return 0; + let count = 0; + index.memories.forEach(mem => { + if (!_memoryObjects[mem.id]) { placeMemory(mem); count++; } + }); + console.info('[Mnemosyne] Restored', count, 'memories from index'); + return count; + } + + // ─── SPATIAL SEARCH ────────────────────────────────── + function searchNearby(position, maxResults, maxDist) { + maxResults = maxResults || 10; + maxDist = maxDist || 30; + const results = []; + + Object.values(_memoryObjects).forEach(obj => { + const d = obj.mesh.position.distanceTo(position); + if (d <= maxDist) results.push({ memory: obj.data, distance: d, position: obj.mesh.position.clone() }); + }); + + results.sort((a, b) => a.distance - b.distance); + return results.slice(0, maxResults); + } + + return { + init, placeMemory, removeMemory, update, + getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories, + exportIndex, importIndex, searchNearby, REGIONS + }; +})(); + +export { SpatialMemory };