Compare commits

...

3 Commits

Author SHA1 Message Date
9a7e554568 fix: add missing MemoryPulse import, init, and update calls
Some checks failed
CI / test (pull_request) Failing after 10s
CI / validate (pull_request) Failing after 14s
Review Approval Gate / verify-review (pull_request) Failing after 2s
2026-04-11 20:47:45 +00:00
d12bd7a806 feat(mnemosyne): wire MemoryPulse into app.js
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 14s
Review Approval Gate / verify-review (pull_request) Failing after 2s
- Import MemoryPulse component
- Initialize with scene + SpatialMemory
- Call update() in animation loop
- Trigger pulse on crystal click via raycasting
2026-04-11 20:46:34 +00:00
9355c02417 feat(mnemosyne): Memory Pulse — holographic ripple propagation
When a memory crystal is accessed, a visual pulse wave radiates
outward through the connection graph, illuminating linked memories
by BFS hop distance.

Features:
- Expanding ring effect at each crystal (color-matched to region)
- Connection line flash between pulsed memories
- Travel time based on spatial distance
- Intensity decay per hop (0.65^hop)
- Depth-limited to 5 hops to prevent runaway
- Fully self-contained component, integrates via SpatialMemory API
2026-04-11 20:45:16 +00:00
2 changed files with 321 additions and 0 deletions

16
app.js
View File

@@ -6,6 +6,7 @@ import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -710,6 +711,7 @@ async function init() {
createAshStorm();
SpatialMemory.init(scene);
MemoryBirth.init(scene);
MemoryPulse.init(scene, SpatialMemory);
MemoryBirth.wrapSpatialMemory(SpatialMemory);
SpatialMemory.setCamera(camera);
updateLoad(90);
@@ -1918,6 +1920,19 @@ function setupControls() {
const portal = portals.find(p => p.ring === clickedRing);
if (portal) activatePortal(portal);
}
// Raycasting for memory crystals — trigger pulse
const crystalMeshes = SpatialMemory.getCrystalMeshes();
if (crystalMeshes.length > 0) {
const crystalHits = raycaster.intersectObjects(crystalMeshes);
if (crystalHits.length > 0) {
const hitMesh = crystalHits[0].object;
const memData = SpatialMemory.getMemoryFromMesh(hitMesh);
if (memData) {
MemoryPulse.triggerPulse(memData.data.id);
}
}
}
}
}
});
@@ -2872,6 +2887,7 @@ function gameLoop() {
if (typeof animateMemoryOrbs === 'function') {
SpatialMemory.update(delta);
MemoryBirth.update(delta);
MemoryPulse.update(delta);
animateMemoryOrbs(delta);
}

View File

@@ -0,0 +1,305 @@
// ═══════════════════════════════════════════
// PROJECT MNEMOSYNE — MEMORY PULSE ENGINE
// ═══════════════════════════════════════════
//
// Holographic ripple propagation: when a memory crystal is accessed,
// a visual pulse wave radiates outward through the connection graph,
// illuminating linked memories in decreasing intensity by hop distance.
//
// This makes the archive feel alive — one thought echoing through
// the holographic field of related knowledge.
//
// Issue: Mnemosyne Pulse Effect
// ═══════════════════════════════════════════
const MemoryPulse = (() => {
let _scene = null;
let _spatialMemory = null;
let _activePulses = []; // Currently propagating pulse waves
let _pulseRings = []; // Active ring meshes being rendered
let _connectionFlashes = []; // Active connection line flashes
const PULSE_SPEED = 8; // Units per second propagation
const PULSE_MAX_HOPS = 5; // Max graph depth to traverse
const RING_DURATION = 1.5; // Seconds each ring is visible
const RING_MAX_RADIUS = 2.0; // Max expansion of pulse ring
const FLASH_DURATION = 0.8; // Seconds connection lines flash
const BASE_INTENSITY = 3.0; // Emissive boost at pulse origin
const HOP_DECAY = 0.65; // Intensity multiplier per hop
// ─── INIT ────────────────────────────────────────────
function init(scene, spatialMemory) {
_scene = scene;
_spatialMemory = spatialMemory;
console.info('[Mnemosyne] Pulse engine initialized');
}
// ─── TRIGGER PULSE ──────────────────────────────────
/**
* Fire a pulse from a memory crystal. Propagates through
* connected memories by BFS, creating visual rings and
* connection line flashes at each hop.
* @param {string} sourceId - Memory ID to pulse from
*/
function triggerPulse(sourceId) {
if (!_scene || !_spatialMemory) return;
const memories = _spatialMemory.getAllMemories();
const source = memories.find(m => m.id === sourceId);
if (!source) return;
// BFS through connection graph
const visited = new Set();
const queue = [{ id: sourceId, hop: 0, delay: 0 }];
visited.add(sourceId);
const memMap = {};
memories.forEach(m => { memMap[m.id] = m; });
while (queue.length > 0) {
const { id, hop, delay } = queue.shift();
if (hop > PULSE_MAX_HOPS) continue;
const mem = memMap[id];
if (!mem) continue;
// Schedule ring spawn
_scheduleRing(id, hop, delay);
// Schedule connection flashes to neighbors
const connections = mem.connections || [];
connections.forEach(targetId => {
if (visited.has(targetId)) return;
visited.add(targetId);
const target = memMap[targetId];
if (!target) return;
const travelDelay = delay + _travelTime(mem, target);
_scheduleConnectionFlash(id, targetId, delay, travelDelay);
queue.push({ id: targetId, hop: hop + 1, delay: travelDelay });
});
}
}
// ─── TRAVEL TIME ────────────────────────────────────
function _travelTime(src, dst) {
const sp = src.position || [0, 0, 0];
const dp = dst.position || [0, 0, 0];
const dx = sp[0] - dp[0], dy = sp[1] - dp[1], dz = sp[2] - dp[2];
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
return dist / PULSE_SPEED;
}
// ─── SCHEDULE RING ──────────────────────────────────
function _scheduleRing(memId, hop, delay) {
const startTime = performance.now() + delay * 1000;
_activePulses.push({
type: 'ring',
memId,
hop,
startTime,
duration: RING_DURATION,
intensity: BASE_INTENSITY * Math.pow(HOP_DECAY, hop),
});
}
// ─── SCHEDULE CONNECTION FLASH ─────────────────────
function _scheduleConnectionFlash(fromId, toId, startDelay, endDelay) {
const startTime = performance.now() + startDelay * 1000;
_activePulses.push({
type: 'flash',
fromId,
toId,
startTime,
duration: endDelay - startDelay + FLASH_DURATION,
intensity: BASE_INTENSITY * Math.pow(HOP_DECAY, 0),
});
}
// ─── UPDATE (called per frame) ──────────────────────
function update(delta) {
const now = performance.now();
// Process scheduled pulses
for (let i = _activePulses.length - 1; i >= 0; i--) {
const pulse = _activePulses[i];
if (now < pulse.startTime) continue; // Not yet active
const elapsed = (now - pulse.startTime) / 1000;
const progress = Math.min(1, elapsed / pulse.duration);
if (progress >= 1) {
_activePulses.splice(i, 1);
continue;
}
if (pulse.type === 'ring') {
_renderRing(pulse, elapsed, progress);
} else if (pulse.type === 'flash') {
_renderConnectionFlash(pulse, elapsed, progress);
}
}
// Update existing ring meshes
for (let i = _pulseRings.length - 1; i >= 0; i--) {
const ring = _pulseRings[i];
ring.age += delta;
if (ring.age >= ring.maxAge) {
if (ring.mesh.parent) ring.mesh.parent.remove(ring.mesh);
ring.mesh.geometry.dispose();
ring.mesh.material.dispose();
_pulseRings.splice(i, 1);
continue;
}
const t = ring.age / ring.maxAge;
const scale = 1 + t * RING_MAX_RADIUS;
ring.mesh.scale.set(scale, scale, scale);
ring.mesh.material.opacity = ring.baseOpacity * (1 - t * t);
}
// Update connection flashes
for (let i = _connectionFlashes.length - 1; i >= 0; i--) {
const flash = _connectionFlashes[i];
flash.age += delta;
if (flash.age >= flash.maxAge) {
// Restore original material
if (flash.line && flash.line.material) {
flash.line.material.opacity = flash.originalOpacity;
flash.line.material.color.setHex(flash.originalColor);
}
_connectionFlashes.splice(i, 1);
continue;
}
const t = flash.age / flash.maxAge;
if (flash.line && flash.line.material) {
// Pulse opacity with travel effect
const wave = Math.sin(t * Math.PI);
flash.line.material.opacity = flash.originalOpacity + wave * 0.6;
flash.line.material.color.setHex(
_lerpColor(flash.originalColor, flash.flashColor, wave * 0.8)
);
}
}
}
// ─── RENDER RING ────────────────────────────────────
function _renderRing(pulse, elapsed, progress) {
// Find crystal position
const allMeshes = _spatialMemory.getCrystalMeshes();
let sourceMesh = null;
const memories = _spatialMemory.getAllMemories();
for (const mem of memories) {
if (mem.id === pulse.memId) {
// Find matching mesh
sourceMesh = allMeshes.find(m => m.userData.memId === pulse.memId);
break;
}
}
if (!sourceMesh) return;
// Only create ring once (check if we already have one for this pulse)
if (pulse._ringCreated) return;
pulse._ringCreated = true;
const ringGeo = new THREE.RingGeometry(0.1, 0.15, 32);
const region = memories.find(m => m.id === pulse.memId);
const color = _getRegionColor(region ? region.category : 'working');
const ringMat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.8 * pulse.intensity / BASE_INTENSITY,
side: THREE.DoubleSide,
depthWrite: false,
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.position.copy(sourceMesh.position);
ring.position.y += 0.1; // Slight offset above crystal
ring.rotation.x = -Math.PI / 2; // Flat on XZ plane
ring.lookAt(ring.position.x, ring.position.y + 1, ring.position.z);
_scene.add(ring);
_pulseRings.push({
mesh: ring,
age: 0,
maxAge: RING_DURATION,
baseOpacity: ringMat.opacity,
});
}
// ─── RENDER CONNECTION FLASH ────────────────────────
function _renderConnectionFlash(pulse, elapsed, progress) {
if (pulse._flashCreated) return;
// Find the connection line between from and to
const fromMesh = _findMesh(pulse.fromId);
const toMesh = _findMesh(pulse.toId);
if (!fromMesh || !toMesh) return;
// Create a temporary line for the flash
const points = [fromMesh.position.clone(), toMesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({
color: 0x4af0c0,
transparent: true,
opacity: 0.0,
linewidth: 2,
});
const line = new THREE.Line(geo, mat);
_scene.add(line);
pulse._flashCreated = true;
_connectionFlashes.push({
line,
age: 0,
maxAge: pulse.duration,
originalOpacity: 0.0,
originalColor: 0x334455,
flashColor: 0x4af0c0,
});
}
// ─── HELPERS ────────────────────────────────────────
function _findMesh(memId) {
const meshes = _spatialMemory.getCrystalMeshes();
return meshes.find(m => m.userData.memId === memId) || null;
}
function _getRegionColor(category) {
const colors = {
documents: 0x4af0c0,
projects: 0xff6b35,
code: 0x7b5cff,
social: 0xff4488,
working: 0xffd700,
archive: 0x445566,
};
return colors[category] || colors.working;
}
function _lerpColor(a, b, t) {
const ar = (a >> 16) & 0xff, ag = (a >> 8) & 0xff, ab = a & 0xff;
const br = (b >> 16) & 0xff, bg = (b >> 8) & 0xff, bb = b & 0xff;
const rr = Math.round(ar + (br - ar) * t);
const rg = Math.round(ag + (bg - ag) * t);
const rb = Math.round(ab + (bb - ab) * t);
return (rr << 16) | (rg << 8) | rb;
}
// ─── PUBLIC API ─────────────────────────────────────
return {
init,
triggerPulse,
update,
};
})();
export { MemoryPulse };