diff --git a/app.js b/app.js index 9b0bf04..42cd37d 100644 --- a/app.js +++ b/app.js @@ -2576,6 +2576,8 @@ function gameLoop() { // Project Mnemosyne - Memory Orb Animation if (typeof animateMemoryOrbs === 'function') { animateMemoryOrbs(delta); + animateHolographicThreads(delta); + animateRoomTransitions(delta); } @@ -3090,7 +3092,232 @@ function spawnRetrievalOrbs(results, center) { }); } +// ═══════════════════════════════════════════ +// MNEMOSYNE — SPATIAL MEMORY INTEGRATION +// ═══════════════════════════════════════════ + +// Spatial memory schema state (loaded async) +let spatialMemorySchema = null; +const holographicThreads = []; // Active thread meshes + +/** + * Load the spatial memory schema and store it for room mapping. + * Called during init. Falls back gracefully if schema unavailable. + */ +async function loadSpatialMemorySchema() { + try { + const resp = await fetch('/spatial-memory-schema.json'); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + spatialMemorySchema = await resp.json(); + console.info('[Mnemosyne] Spatial memory schema loaded:', + Object.keys(spatialMemorySchema.rooms).length, 'rooms'); + } catch (err) { + console.warn('[Mnemosyne] Could not load spatial schema, using defaults:', err.message); + spatialMemorySchema = null; + } +} + +/** + * Get the room definition for a memory category. + * @param {string} category - Memory category (user_pref, project, tool, general) + * @returns {object|null} Room definition with spatial_bounds and visual_theme + */ +function getRoomForCategory(category) { + if (!spatialMemorySchema) return null; + for (const [roomId, room] of Object.entries(spatialMemorySchema.rooms)) { + if (room.category === category) return { id: roomId, ...room }; + } + // Fallback to commons for unknown categories + if (spatialMemorySchema.rooms.commons) { + return { id: 'commons', ...spatialMemorySchema.rooms.commons }; + } + return null; +} + +/** + * Calculate a random position within a room's spatial bounds. + * @param {object} room - Room definition with spatial_bounds + * @returns {THREE.Vector3} Position within room bounds + */ +function getPositionInRoom(room) { + const bounds = room.spatial_bounds; + const dims = bounds.dimensions; + const center = bounds.center; + + return new THREE.Vector3( + center[0] + (Math.random() - 0.5) * dims[0] * 0.8, + center[1] + (Math.random() - 0.5) * dims[1] * 0.6 + 1.5, // Float above floor + center[2] + (Math.random() - 0.5) * dims[2] * 0.8 + ); +} + +/** + * Spawn a categorized memory orb placed in its corresponding room. + * Extends spawnMemoryOrb with spatial placement based on category. + * + * @param {string} category - Memory category (user_pref, project, tool, general) + * @param {object} metadata - Memory metadata (source, content, score, etc.) + * @param {number} importance - 0-1 importance score (affects size/glow) + * @returns {THREE.Mesh} The spawned orb + */ +function spawnCategorizedOrb(category, metadata = {}, importance = 0.5) { + const room = getRoomForCategory(category); + const position = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0); + + // Color from schema trust mapping or room theme + let color = 0x4af0c0; // Default cyan + if (room && room.visual_theme) { + const accent = room.visual_theme.colors?.accent; + if (accent) color = parseInt(accent.replace('#', ''), 16); + } + + // Size scales with importance + const size = 0.2 + importance * 0.5; + + const orb = spawnMemoryOrb(position, color, size, { + ...metadata, + category: category, + room: room ? room.id : 'unknown' + }); + + return orb; +} + +/** + * Draw a holographic thread connecting two memory orbs. + * @param {THREE.Mesh} orbA - First orb + * @param {THREE.Mesh} orbB - Second orb + * @param {number} color - Thread color (default: 0x4af0c0) + * @returns {THREE.Line} The thread mesh + */ +function drawHolographicThread(orbA, orbB, color = 0x4af0c0) { + if (typeof THREE === 'undefined' || !orbA || !orbB) return null; + + const points = [orbA.position.clone(), orbB.position.clone()]; + const geometry = new THREE.BufferGeometry().setFromPoints(points); + const material = new THREE.LineBasicMaterial({ + color: color, + transparent: true, + opacity: 0.4, + linewidth: 1 + }); + + const thread = new THREE.Line(geometry, material); + thread.userData = { + type: 'holographic_thread', + orbA: orbA, + orbB: orbB, + createdAt: Date.now() + }; + + scene.add(thread); + holographicThreads.push(thread); + return thread; +} + +/** + * Update holographic threads to follow their connected orbs. + * Called from the animation loop. + * @param {number} delta - Time since last frame + */ +function animateHolographicThreads(delta) { + for (let i = holographicThreads.length - 1; i >= 0; i--) { + const thread = holographicThreads[i]; + if (!thread || !thread.userData) continue; + + const { orbA, orbB } = thread.userData; + + // Remove thread if either orb is gone + if (!orbA || !orbA.parent || !orbB || !orbB.parent) { + scene.remove(thread); + thread.geometry.dispose(); + thread.material.dispose(); + holographicThreads.splice(i, 1); + continue; + } + + // Update line positions to follow orbs + const positions = thread.geometry.attributes.position; + positions.setXYZ(0, orbA.position.x, orbA.position.y, orbA.position.z); + positions.setXYZ(1, orbB.position.x, orbB.position.y, orbB.position.z); + positions.needsUpdate = true; + + // Pulse opacity + const age = (Date.now() - thread.userData.createdAt) / 1000; + thread.material.opacity = 0.3 + Math.sin(age * 2) * 0.1; + } +} + +/** + * Spawn a memory orb and animate it transitioning to its room. + * @param {string} category - Memory category + * @param {object} metadata - Memory metadata + * @param {number} importance - 0-1 importance score + * @param {THREE.Vector3} startPos - Starting position (default: above avatar) + * @returns {THREE.Mesh} The orb (already in transit) + */ +function spawnWithRoomTransition(category, metadata = {}, importance = 0.5, startPos = null) { + if (!startPos) startPos = new THREE.Vector3(0, 2, 0); + + const room = getRoomForCategory(category); + const endPos = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0); + + let color = 0x4af0c0; + if (room && room.visual_theme) { + const accent = room.visual_theme.colors?.accent; + if (accent) color = parseInt(accent.replace('#', ''), 16); + } + + const size = 0.2 + importance * 0.5; + + // Spawn at start position + const orb = spawnMemoryOrb(startPos, color, size, { + ...metadata, + category: category, + room: room ? room.id : 'unknown', + transitioning: true, + targetPos: endPos, + transitionStart: Date.now(), + transitionDuration: 2000 + Math.random() * 1000 + }); + + return orb; +} + +/** + * Animate room transitions for orbs that are in transit. + * @param {number} delta - Time since last frame + */ +function animateRoomTransitions(delta) { + for (const orb of memoryOrbs) { + if (!orb.userData?.transitioning || !orb.userData?.targetPos) continue; + + const elapsed = Date.now() - orb.userData.transitionStart; + const duration = orb.userData.transitionDuration; + const progress = Math.min(1, elapsed / duration); + + // Ease-out cubic + const eased = 1 - Math.pow(1 - progress, 3); + + orb.position.lerpVectors( + orb.position, // Current (already partially moved) + orb.userData.targetPos, + eased * 0.05 // Smooth interpolation factor per frame + ); + + if (progress >= 1) { + orb.position.copy(orb.userData.targetPos); + orb.userData.transitioning = false; + delete orb.userData.targetPos; + delete orb.userData.transitionStart; + delete orb.userData.transitionDuration; + } + } +} + + init().then(() => { + loadSpatialMemorySchema(); createAshStorm(); createPortalTunnel(); fetchGiteaData(); diff --git a/spatial-memory-schema.json b/spatial-memory-schema.json new file mode 100644 index 0000000..d92ff55 --- /dev/null +++ b/spatial-memory-schema.json @@ -0,0 +1,312 @@ +{ + "version": "1.0.0", + "project": "Mnemosyne", + "description": "Spatial memory schema for holographic memory visualization", + "rooms": { + "library": { + "name": "The Library", + "category": "user_pref", + "description": "User preferences and personal settings", + "visual_theme": { + "lighting": "soft_ambient", + "colors": { + "primary": "#8B4513", + "secondary": "#DAA520", + "accent": "#FFD700", + "particle": "#FFE4B5" + }, + "materials": { + "floor": "dark_wood", + "walls": "bookshelf", + "ceiling": "vaulted_stone" + }, + "particle_effects": [ + "dust_motes", + "book_sparkles" + ] + }, + "spatial_bounds": { + "center": [ + 0, + 0, + 0 + ], + "dimensions": [ + 20, + 10, + 20 + ], + "orb_density": 0.7 + }, + "object_types": { + "preference": { + "shape": "sphere", + "base_size": 0.3, + "glow_intensity": 0.8 + }, + "setting": { + "shape": "cube", + "base_size": 0.4, + "glow_intensity": 0.6 + } + } + }, + "workshop": { + "name": "The Workshop", + "category": "project", + "description": "Active projects and development work", + "visual_theme": { + "lighting": "bright_work", + "colors": { + "primary": "#4682B4", + "secondary": "#B0C4DE", + "accent": "#00BFFF", + "particle": "#87CEEB" + }, + "materials": { + "floor": "polished_concrete", + "walls": "blueprint_paper", + "ceiling": "industrial_metal" + }, + "particle_effects": [ + "blueprint_lines", + "tool_sparks" + ] + }, + "spatial_bounds": { + "center": [ + 30, + 0, + 0 + ], + "dimensions": [ + 25, + 12, + 25 + ], + "orb_density": 0.8 + }, + "object_types": { + "project": { + "shape": "pyramid", + "base_size": 0.5, + "glow_intensity": 0.9 + }, + "task": { + "shape": "cube", + "base_size": 0.3, + "glow_intensity": 0.7 + } + } + }, + "armory": { + "name": "The Armory", + "category": "tool", + "description": "Tools, skills, and capabilities", + "visual_theme": { + "lighting": "neon_glow", + "colors": { + "primary": "#2E8B57", + "secondary": "#3CB371", + "accent": "#00FF7F", + "particle": "#98FB98" + }, + "materials": { + "floor": "chrome_grid", + "walls": "server_rack", + "ceiling": "neon_tube" + }, + "particle_effects": [ + "data_streams", + "circuit_traces" + ] + }, + "spatial_bounds": { + "center": [ + 0, + 0, + 30 + ], + "dimensions": [ + 15, + 8, + 15 + ], + "orb_density": 0.6 + }, + "object_types": { + "tool": { + "shape": "octahedron", + "base_size": 0.4, + "glow_intensity": 1.0 + }, + "skill": { + "shape": "sphere", + "base_size": 0.35, + "glow_intensity": 0.85 + } + } + }, + "commons": { + "name": "The Commons", + "category": "general", + "description": "General knowledge and miscellaneous facts", + "visual_theme": { + "lighting": "natural_daylight", + "colors": { + "primary": "#9370DB", + "secondary": "#BA55D3", + "accent": "#DA70D6", + "particle": "#E6E6FA" + }, + "materials": { + "floor": "grass", + "walls": "floating_islands", + "ceiling": "open_sky" + }, + "particle_effects": [ + "floating_pollen", + "lightning_bugs" + ] + }, + "spatial_bounds": { + "center": [ + 30, + 0, + 30 + ], + "dimensions": [ + 30, + 15, + 30 + ], + "orb_density": 0.5 + }, + "object_types": { + "fact": { + "shape": "sphere", + "base_size": 0.25, + "glow_intensity": 0.7 + }, + "memory": { + "shape": "dodecahedron", + "base_size": 0.3, + "glow_intensity": 0.65 + } + } + } + }, + "object_properties": { + "trust_mapping": { + "description": "Maps trust score (0.0-1.0) to visual properties", + "glow_intensity": { + "min": 0.2, + "max": 1.0, + "curve": "linear" + }, + "opacity": { + "min": 0.3, + "max": 1.0, + "curve": "ease_in_out" + } + }, + "importance_mapping": { + "description": "Maps importance (relation count) to visual properties", + "scale": { + "min": 0.2, + "max": 2.0, + "curve": "logarithmic" + }, + "particle_density": { + "min": 10, + "max": 100, + "curve": "linear" + } + }, + "lifecycle_events": { + "FACT_CREATED": { + "animation": "fade_in", + "duration": 1.5, + "particle_effect": "spawn_burst" + }, + "FACT_UPDATED": { + "animation": "pulse_glow", + "duration": 0.8, + "particle_effect": "update_ripple" + }, + "FACT_REMOVED": { + "animation": "dissolve", + "duration": 2.0, + "particle_effect": "scatter" + }, + "FACT_RECALLED": { + "animation": "beam_light", + "duration": 1.0, + "particle_effect": "recall_beam" + } + } + }, + "connections": { + "holographic_threads": { + "description": "Visual connections between related memory orbs", + "material": "transparent_glow", + "colors": { + "strong_relation": "#00FFFF", + "medium_relation": "#00CED1", + "weak_relation": "#5F9EA0" + }, + "thickness": { + "min": 0.02, + "max": 0.1, + "curve": "linear" + } + }, + "cross_room_portals": { + "description": "Portals connecting different memory rooms", + "effect": "swirling_vortex", + "colors": { + "library_workshop": "#FFD700", + "workshop_armory": "#00BFFF", + "armory_commons": "#00FF7F", + "commons_library": "#DA70D6" + } + } + }, + "rag_integration": { + "retrieval_visualization": { + "description": "How RAG retrieval results are visualized", + "highlight_effect": "golden_glow", + "spiral_arrangement": { + "radius": 3.0, + "height_step": 0.5, + "rotation_step": 0.618033988749895 + }, + "relevance_scoring": { + "high": { + "color": "#FFD700", + "size_multiplier": 1.5 + }, + "medium": { + "color": "#FFA500", + "size_multiplier": 1.2 + }, + "low": { + "color": "#FF8C00", + "size_multiplier": 1.0 + } + } + }, + "query_beam": { + "description": "Beam from user to relevant memory orbs", + "color": "#FFFFFF", + "opacity": 0.8, + "pulse_frequency": 2.0 + } + }, + "animation_timing": { + "orb_spawn_delay": 0.1, + "room_transition_duration": 2.0, + "connection_draw_speed": 0.5, + "particle_fade_time": 1.5 + } +} \ No newline at end of file