Compare commits
3 Commits
feat/mnemo
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ec3da6e30 | |||
| b153613352 | |||
| 36a152b9c2 |
245
app.js
245
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,12 +2578,11 @@ function gameLoop() {
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
// Project Mnemosyne - Memory Animation
|
||||
if (typeof animateMemoryOrbs === 'function') {
|
||||
animateMemoryOrbs(delta);
|
||||
animateHolographicThreads(delta);
|
||||
animateRoomTransitions(delta);
|
||||
}
|
||||
SpatialMemory.update(delta);
|
||||
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
@@ -3092,234 +3096,19 @@ 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();
|
||||
// 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();
|
||||
|
||||
376
nexus/components/spatial-memory.js
Normal file
376
nexus/components/spatial-memory.js
Normal file
@@ -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 };
|
||||
@@ -1,312 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user