Compare commits
2 Commits
mimo/code/
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3626ee0e20 | ||
|
|
8ca25ccb76 |
113
app.js
113
app.js
@@ -4,6 +4,7 @@ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|||||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||||
|
import { SessionRooms } from './nexus/components/session-rooms.js';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// NEXUS v1.1 — Portal System Update
|
// NEXUS v1.1 — Portal System Update
|
||||||
@@ -705,6 +706,7 @@ async function init() {
|
|||||||
createWorkshopTerminal();
|
createWorkshopTerminal();
|
||||||
createAshStorm();
|
createAshStorm();
|
||||||
SpatialMemory.init(scene);
|
SpatialMemory.init(scene);
|
||||||
|
SessionRooms.init(scene, camera, null);
|
||||||
updateLoad(90);
|
updateLoad(90);
|
||||||
|
|
||||||
loadSession();
|
loadSession();
|
||||||
@@ -1915,8 +1917,19 @@ function setupControls() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 3: Session rooms (Mnemosyne #1171)
|
||||||
|
const roomMeshes = SessionRooms.getClickableMeshes();
|
||||||
|
if (roomMeshes.length > 0) {
|
||||||
|
const roomHits = raycaster.intersectObjects(roomMeshes, false);
|
||||||
|
if (roomHits.length > 0) {
|
||||||
|
const session = SessionRooms.handleRoomClick(roomHits[0].object);
|
||||||
|
if (session) { _showSessionRoomPanel(session); return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clicked empty space — dismiss panel
|
// Clicked empty space — dismiss panel
|
||||||
dismissMemoryPanel();
|
dismissMemoryPanel();
|
||||||
|
_dismissSessionRoomPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2734,6 +2747,62 @@ function _dismissMemoryPanelForce() {
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the session room HUD panel when a chamber is entered.
|
||||||
|
* @param {object} session — { id, timestamp, facts[] }
|
||||||
|
*/
|
||||||
|
function _showSessionRoomPanel(session) {
|
||||||
|
const panel = document.getElementById('session-room-panel');
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
const dt = session.timestamp ? new Date(session.timestamp) : new Date();
|
||||||
|
const tsEl = document.getElementById('session-room-timestamp');
|
||||||
|
if (tsEl) tsEl.textContent = isNaN(dt.getTime()) ? session.id : dt.toLocaleString();
|
||||||
|
|
||||||
|
const countEl = document.getElementById('session-room-fact-count');
|
||||||
|
const facts = session.facts || [];
|
||||||
|
if (countEl) countEl.textContent = facts.length + (facts.length === 1 ? ' fact' : ' facts') + ' in this chamber';
|
||||||
|
|
||||||
|
const listEl = document.getElementById('session-room-facts');
|
||||||
|
if (listEl) {
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
facts.slice(0, 8).forEach(f => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'session-room-fact-item';
|
||||||
|
item.textContent = f.content || f.id || '(unknown)';
|
||||||
|
item.title = f.content || '';
|
||||||
|
listEl.appendChild(item);
|
||||||
|
});
|
||||||
|
if (facts.length > 8) {
|
||||||
|
const more = document.createElement('div');
|
||||||
|
more.className = 'session-room-fact-item';
|
||||||
|
more.style.color = 'rgba(200,180,255,0.4)';
|
||||||
|
more.textContent = '\u2026 ' + (facts.length - 8) + ' more';
|
||||||
|
listEl.appendChild(more);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
const closeBtn = document.getElementById('session-room-close');
|
||||||
|
if (closeBtn) closeBtn.onclick = () => _dismissSessionRoomPanel();
|
||||||
|
|
||||||
|
panel.classList.remove('session-panel-fade-out');
|
||||||
|
panel.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the session room panel.
|
||||||
|
*/
|
||||||
|
function _dismissSessionRoomPanel() {
|
||||||
|
const panel = document.getElementById('session-room-panel');
|
||||||
|
if (!panel || panel.style.display === 'none') return;
|
||||||
|
panel.classList.add('session-panel-fade-out');
|
||||||
|
setTimeout(() => {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
panel.classList.remove('session-panel-fade-out');
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function gameLoop() {
|
function gameLoop() {
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
@@ -2765,6 +2834,9 @@ function gameLoop() {
|
|||||||
animateMemoryOrbs(delta);
|
animateMemoryOrbs(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project Mnemosyne - Session Rooms (#1171)
|
||||||
|
SessionRooms.update(delta);
|
||||||
|
|
||||||
|
|
||||||
const mode = NAV_MODES[navModeIdx];
|
const mode = NAV_MODES[navModeIdx];
|
||||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||||
@@ -3283,17 +3355,48 @@ init().then(() => {
|
|||||||
|
|
||||||
// Project Mnemosyne — seed demo spatial memories
|
// Project Mnemosyne — seed demo spatial memories
|
||||||
const demoMemories = [
|
const demoMemories = [
|
||||||
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95, connections: ['mem_mnemosyne_start'] },
|
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95, connections: ['mem_mnemosyne_start'] },
|
||||||
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85, connections: ['mem_nexus_birth'] },
|
{ 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_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.70, connections: [] },
|
||||||
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth', 'mem_spatial_schema'] },
|
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.90, connections: ['mem_nexus_birth', 'mem_spatial_schema'] },
|
||||||
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
|
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.80, connections: ['mem_mnemosyne_start'] },
|
||||||
];
|
];
|
||||||
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
|
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
|
||||||
|
|
||||||
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
||||||
SpatialMemory.runGravityLayout();
|
SpatialMemory.runGravityLayout();
|
||||||
|
|
||||||
|
// Project Mnemosyne — seed demo session rooms (#1171)
|
||||||
|
// Sessions group facts by conversation/work session with a timestamp.
|
||||||
|
const demoSessions = [
|
||||||
|
{
|
||||||
|
id: 'session_2026_03_01',
|
||||||
|
timestamp: '2026-03-01T10:00:00.000Z',
|
||||||
|
facts: [
|
||||||
|
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95 },
|
||||||
|
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'session_2026_03_15',
|
||||||
|
timestamp: '2026-03-15T14:30:00.000Z',
|
||||||
|
facts: [
|
||||||
|
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85 },
|
||||||
|
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7 },
|
||||||
|
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain homes', category: 'engineering', strength: 0.8 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'session_2026_04_10',
|
||||||
|
timestamp: '2026-04-10T09:00:00.000Z',
|
||||||
|
facts: [
|
||||||
|
{ id: 'mem_session_rooms', content: 'Session rooms introduced — holographic chambers per session', category: 'projects', strength: 0.88 },
|
||||||
|
{ id: 'mem_gravity_wells', content: 'Gravity-well clustering bakes crystal positions on load', category: 'engineering', strength: 0.75 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
SessionRooms.updateSessions(demoSessions);
|
||||||
|
|
||||||
fetchGiteaData();
|
fetchGiteaData();
|
||||||
setInterval(fetchGiteaData, 30000);
|
setInterval(fetchGiteaData, 30000);
|
||||||
runWeeklyAudit();
|
runWeeklyAudit();
|
||||||
|
|||||||
15
index.html
15
index.html
@@ -236,6 +236,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Room HUD Panel (Mnemosyne #1171) -->
|
||||||
|
<div id="session-room-panel" class="session-room-panel" style="display:none;">
|
||||||
|
<div class="session-room-panel-content">
|
||||||
|
<div class="session-room-header">
|
||||||
|
<span class="session-room-icon">□</span>
|
||||||
|
<div class="session-room-title">SESSION CHAMBER</div>
|
||||||
|
<button class="session-room-close" id="session-room-close" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="session-room-timestamp" id="session-room-timestamp">—</div>
|
||||||
|
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
|
||||||
|
<div class="session-room-facts" id="session-room-facts"></div>
|
||||||
|
<div class="session-room-hint">Flying into chamber…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Portal Atlas Overlay -->
|
<!-- Portal Atlas Overlay -->
|
||||||
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
|
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
|
||||||
<div class="atlas-content">
|
<div class="atlas-content">
|
||||||
|
|||||||
413
nexus/components/session-rooms.js
Normal file
413
nexus/components/session-rooms.js
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// PROJECT MNEMOSYNE — SESSION ROOMS (Issue #1171)
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
//
|
||||||
|
// Groups memories by session into holographic chambers.
|
||||||
|
// Each session becomes a wireframe cube floating in space.
|
||||||
|
// Rooms are arranged chronologically along a spiral.
|
||||||
|
// Click a room to fly inside; distant rooms LOD to a point.
|
||||||
|
//
|
||||||
|
// Usage from app.js:
|
||||||
|
// SessionRooms.init(scene, camera, controls);
|
||||||
|
// SessionRooms.updateSessions(sessions); // [{id, timestamp, facts[]}]
|
||||||
|
// SessionRooms.update(delta); // call each frame
|
||||||
|
// SessionRooms.getClickableMeshes(); // for raycasting
|
||||||
|
// SessionRooms.handleRoomClick(mesh); // trigger fly-in
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const SessionRooms = (() => {
|
||||||
|
|
||||||
|
// ─── CONSTANTS ───────────────────────────────────────
|
||||||
|
const MAX_ROOMS = 20;
|
||||||
|
const ROOM_SIZE = 9; // wireframe cube edge length
|
||||||
|
const ROOM_HALF = ROOM_SIZE / 2;
|
||||||
|
const LOD_THRESHOLD = 55; // distance: full → point
|
||||||
|
const LOD_HYSTERESIS = 5; // buffer to avoid flicker
|
||||||
|
const SPIRAL_BASE_R = 20; // spiral inner radius
|
||||||
|
const SPIRAL_R_STEP = 5; // radius growth per room
|
||||||
|
const SPIRAL_ANGLE_INC = 2.399; // golden angle (radians)
|
||||||
|
const SPIRAL_Y_STEP = 1.5; // vertical rise per room
|
||||||
|
const FLY_DURATION = 1.5; // seconds for fly-in tween
|
||||||
|
const FLY_TARGET_DEPTH = ROOM_HALF - 1.5; // how deep inside to stop
|
||||||
|
|
||||||
|
const ROOM_COLOR = 0x7b5cff; // violet — mnemosyne accent
|
||||||
|
const POINT_COLOR = 0x9b7cff;
|
||||||
|
const LABEL_COLOR = '#c8b4ff';
|
||||||
|
const STORAGE_KEY = 'mnemosyne_sessions_v1';
|
||||||
|
|
||||||
|
// ─── STATE ────────────────────────────────────────────
|
||||||
|
let _scene = null;
|
||||||
|
let _camera = null;
|
||||||
|
let _controls = null;
|
||||||
|
|
||||||
|
let _rooms = []; // array of room objects
|
||||||
|
let _sessionIndex = {}; // id → room object
|
||||||
|
|
||||||
|
// Fly-in tween state
|
||||||
|
let _flyActive = false;
|
||||||
|
let _flyElapsed = 0;
|
||||||
|
let _flyFrom = null;
|
||||||
|
let _flyTo = null;
|
||||||
|
let _flyLookFrom = null;
|
||||||
|
let _flyLookTo = null;
|
||||||
|
let _flyActiveRoom = null;
|
||||||
|
|
||||||
|
// ─── SPIRAL POSITION ──────────────────────────────────
|
||||||
|
function _spiralPos(index) {
|
||||||
|
const angle = index * SPIRAL_ANGLE_INC;
|
||||||
|
const r = SPIRAL_BASE_R + index * SPIRAL_R_STEP;
|
||||||
|
const y = index * SPIRAL_Y_STEP;
|
||||||
|
return new THREE.Vector3(
|
||||||
|
Math.cos(angle) * r,
|
||||||
|
y,
|
||||||
|
Math.sin(angle) * r
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CREATE ROOM ──────────────────────────────────────
|
||||||
|
function _createRoom(session, index) {
|
||||||
|
const pos = _spiralPos(index);
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.copy(pos);
|
||||||
|
|
||||||
|
// Wireframe cube
|
||||||
|
const boxGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
|
||||||
|
const edgesGeo = new THREE.EdgesGeometry(boxGeo);
|
||||||
|
const edgesMat = new THREE.LineBasicMaterial({
|
||||||
|
color: ROOM_COLOR,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.55
|
||||||
|
});
|
||||||
|
const wireframe = new THREE.LineSegments(edgesGeo, edgesMat);
|
||||||
|
wireframe.userData = { type: 'session_room_wireframe', sessionId: session.id };
|
||||||
|
group.add(wireframe);
|
||||||
|
|
||||||
|
// Collision mesh (invisible, for raycasting)
|
||||||
|
const hitGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
|
||||||
|
const hitMat = new THREE.MeshBasicMaterial({
|
||||||
|
visible: false,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0,
|
||||||
|
side: THREE.FrontSide
|
||||||
|
});
|
||||||
|
const hitMesh = new THREE.Mesh(hitGeo, hitMat);
|
||||||
|
hitMesh.userData = { type: 'session_room', sessionId: session.id, roomIndex: index };
|
||||||
|
group.add(hitMesh);
|
||||||
|
|
||||||
|
// LOD point (small sphere shown at distance)
|
||||||
|
const pointGeo = new THREE.SphereGeometry(0.5, 6, 4);
|
||||||
|
const pointMat = new THREE.MeshBasicMaterial({
|
||||||
|
color: POINT_COLOR,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7
|
||||||
|
});
|
||||||
|
const pointMesh = new THREE.Mesh(pointGeo, pointMat);
|
||||||
|
pointMesh.userData = { type: 'session_room_point', sessionId: session.id };
|
||||||
|
pointMesh.visible = false; // starts hidden; shown only at LOD distance
|
||||||
|
group.add(pointMesh);
|
||||||
|
|
||||||
|
// Timestamp billboard sprite
|
||||||
|
const sprite = _makeTimestampSprite(session.timestamp, session.facts.length);
|
||||||
|
sprite.position.set(0, ROOM_HALF + 1.2, 0);
|
||||||
|
group.add(sprite);
|
||||||
|
|
||||||
|
// Inner ambient glow
|
||||||
|
const glow = new THREE.PointLight(ROOM_COLOR, 0.4, ROOM_SIZE * 1.2);
|
||||||
|
group.add(glow);
|
||||||
|
|
||||||
|
_scene.add(group);
|
||||||
|
|
||||||
|
const room = {
|
||||||
|
session,
|
||||||
|
group,
|
||||||
|
wireframe,
|
||||||
|
hitMesh,
|
||||||
|
pointMesh,
|
||||||
|
sprite,
|
||||||
|
glow,
|
||||||
|
pos: pos.clone(),
|
||||||
|
index,
|
||||||
|
lodActive: false,
|
||||||
|
pulsePhase: Math.random() * Math.PI * 2
|
||||||
|
};
|
||||||
|
|
||||||
|
_rooms.push(room);
|
||||||
|
_sessionIndex[session.id] = room;
|
||||||
|
|
||||||
|
console.info('[SessionRooms] Created room for session', session.id, 'at index', index);
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TIMESTAMP SPRITE ────────────────────────────────
|
||||||
|
function _makeTimestampSprite(isoTimestamp, factCount) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 320;
|
||||||
|
canvas.height = 72;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Background pill
|
||||||
|
ctx.clearRect(0, 0, 320, 72);
|
||||||
|
ctx.fillStyle = 'rgba(20, 10, 40, 0.82)';
|
||||||
|
_roundRect(ctx, 4, 4, 312, 64, 14);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Border
|
||||||
|
ctx.strokeStyle = 'rgba(123, 92, 255, 0.6)';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
_roundRect(ctx, 4, 4, 312, 64, 14);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Timestamp text
|
||||||
|
const dt = isoTimestamp ? new Date(isoTimestamp) : new Date();
|
||||||
|
const label = _formatDate(dt);
|
||||||
|
ctx.fillStyle = LABEL_COLOR;
|
||||||
|
ctx.font = 'bold 15px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(label, 160, 30);
|
||||||
|
|
||||||
|
// Fact count
|
||||||
|
ctx.fillStyle = 'rgba(200, 180, 255, 0.65)';
|
||||||
|
ctx.font = '12px monospace';
|
||||||
|
ctx.fillText(factCount + (factCount === 1 ? ' fact' : ' facts'), 160, 52);
|
||||||
|
|
||||||
|
const tex = new THREE.CanvasTexture(canvas);
|
||||||
|
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.88 });
|
||||||
|
const sprite = new THREE.Sprite(mat);
|
||||||
|
sprite.scale.set(5, 1.1, 1);
|
||||||
|
sprite.userData = { type: 'session_room_label' };
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HELPERS ──────────────────────────────────────────
|
||||||
|
function _roundRect(ctx, x, y, w, h, r) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + r, y);
|
||||||
|
ctx.lineTo(x + w - r, y);
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||||||
|
ctx.lineTo(x + w, y + h - r);
|
||||||
|
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||||||
|
ctx.lineTo(x + r, y + h);
|
||||||
|
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||||||
|
ctx.lineTo(x, y + r);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatDate(dt) {
|
||||||
|
if (isNaN(dt.getTime())) return 'Unknown session';
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DISPOSE ROOM ────────────────────────────────────
|
||||||
|
function _disposeRoom(room) {
|
||||||
|
room.wireframe.geometry.dispose();
|
||||||
|
room.wireframe.material.dispose();
|
||||||
|
room.hitMesh.geometry.dispose();
|
||||||
|
room.hitMesh.material.dispose();
|
||||||
|
room.pointMesh.geometry.dispose();
|
||||||
|
room.pointMesh.material.dispose();
|
||||||
|
if (room.sprite.material.map) room.sprite.material.map.dispose();
|
||||||
|
room.sprite.material.dispose();
|
||||||
|
if (room.group.parent) room.group.parent.remove(room.group);
|
||||||
|
delete _sessionIndex[room.session.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC: UPDATE SESSIONS ─────────────────────────
|
||||||
|
// sessions: [{id, timestamp, facts:[{id,content,category,strength,...}]}]
|
||||||
|
// Sorted chronologically oldest→newest; max MAX_ROOMS shown.
|
||||||
|
function updateSessions(sessions) {
|
||||||
|
if (!_scene) return;
|
||||||
|
|
||||||
|
const sorted = [...sessions]
|
||||||
|
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
||||||
|
.slice(-MAX_ROOMS); // keep most recent MAX_ROOMS
|
||||||
|
|
||||||
|
// Remove rooms no longer present
|
||||||
|
const incoming = new Set(sorted.map(s => s.id));
|
||||||
|
for (let i = _rooms.length - 1; i >= 0; i--) {
|
||||||
|
const room = _rooms[i];
|
||||||
|
if (!incoming.has(room.session.id)) {
|
||||||
|
_disposeRoom(room);
|
||||||
|
_rooms.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add / update
|
||||||
|
sorted.forEach((session, idx) => {
|
||||||
|
if (_sessionIndex[session.id]) {
|
||||||
|
// Update position if index changed
|
||||||
|
const room = _sessionIndex[session.id];
|
||||||
|
if (room.index !== idx) {
|
||||||
|
room.index = idx;
|
||||||
|
const newPos = _spiralPos(idx);
|
||||||
|
room.group.position.copy(newPos);
|
||||||
|
room.pos.copy(newPos);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_createRoom(session, idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveToStorage(sorted);
|
||||||
|
console.info('[SessionRooms] Updated:', _rooms.length, 'session rooms');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC: INIT ─────────────────────────────────────
|
||||||
|
function init(scene, camera, controls) {
|
||||||
|
_scene = scene;
|
||||||
|
_camera = camera;
|
||||||
|
_controls = controls;
|
||||||
|
console.info('[SessionRooms] Initialized');
|
||||||
|
|
||||||
|
// Restore persisted sessions
|
||||||
|
const saved = loadFromStorage();
|
||||||
|
if (saved && saved.length > 0) {
|
||||||
|
updateSessions(saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC: UPDATE (per-frame) ───────────────────────
|
||||||
|
function update(delta) {
|
||||||
|
if (!_scene || !_camera) return;
|
||||||
|
|
||||||
|
const camPos = _camera.position;
|
||||||
|
|
||||||
|
_rooms.forEach(room => {
|
||||||
|
const dist = camPos.distanceTo(room.pos);
|
||||||
|
|
||||||
|
// LOD toggle
|
||||||
|
const threshold = room.lodActive
|
||||||
|
? LOD_THRESHOLD + LOD_HYSTERESIS // must come closer to exit LOD
|
||||||
|
: LOD_THRESHOLD;
|
||||||
|
|
||||||
|
if (dist > threshold && !room.lodActive) {
|
||||||
|
room.lodActive = true;
|
||||||
|
room.wireframe.visible = false;
|
||||||
|
room.sprite.visible = false;
|
||||||
|
room.pointMesh.visible = true;
|
||||||
|
} else if (dist <= threshold && room.lodActive) {
|
||||||
|
room.lodActive = false;
|
||||||
|
room.wireframe.visible = true;
|
||||||
|
room.sprite.visible = true;
|
||||||
|
room.pointMesh.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulse wireframe opacity
|
||||||
|
room.pulsePhase += delta * 0.6;
|
||||||
|
if (!room.lodActive) {
|
||||||
|
room.wireframe.material.opacity = 0.3 + Math.sin(room.pulsePhase) * 0.2;
|
||||||
|
room.glow.intensity = 0.3 + Math.sin(room.pulsePhase * 1.4) * 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slowly rotate each room
|
||||||
|
room.group.rotation.y += delta * 0.04;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fly-in tween
|
||||||
|
if (_flyActive) {
|
||||||
|
_flyElapsed += delta;
|
||||||
|
const t = Math.min(_flyElapsed / FLY_DURATION, 1);
|
||||||
|
const ease = _easeInOut(t);
|
||||||
|
|
||||||
|
_camera.position.lerpVectors(_flyFrom, _flyTo, ease);
|
||||||
|
|
||||||
|
// Interpolate lookAt
|
||||||
|
const lookNow = new THREE.Vector3().lerpVectors(_flyLookFrom, _flyLookTo, ease);
|
||||||
|
_camera.lookAt(lookNow);
|
||||||
|
if (_controls && _controls.target) _controls.target.copy(lookNow);
|
||||||
|
|
||||||
|
if (t >= 1) {
|
||||||
|
_flyActive = false;
|
||||||
|
if (_controls && typeof _controls.update === 'function') _controls.update();
|
||||||
|
console.info('[SessionRooms] Fly-in complete for session', _flyActiveRoom && _flyActiveRoom.session.id);
|
||||||
|
_flyActiveRoom = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── EASING ───────────────────────────────────────────
|
||||||
|
function _easeInOut(t) {
|
||||||
|
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC: GET CLICKABLE MESHES ─────────────────────
|
||||||
|
function getClickableMeshes() {
|
||||||
|
return _rooms.map(r => r.hitMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC: HANDLE ROOM CLICK ────────────────────────
|
||||||
|
function handleRoomClick(mesh) {
|
||||||
|
const { sessionId } = mesh.userData;
|
||||||
|
const room = _sessionIndex[sessionId];
|
||||||
|
if (!room || !_camera) return null;
|
||||||
|
|
||||||
|
// Fly into the room from the front face
|
||||||
|
_flyActive = true;
|
||||||
|
_flyElapsed = 0;
|
||||||
|
_flyActiveRoom = room;
|
||||||
|
|
||||||
|
_flyFrom = _camera.position.clone();
|
||||||
|
|
||||||
|
// Target: step inside the room toward its center
|
||||||
|
const dir = room.pos.clone().sub(_camera.position).normalize();
|
||||||
|
_flyTo = room.pos.clone().add(dir.multiplyScalar(FLY_TARGET_DEPTH));
|
||||||
|
|
||||||
|
_flyLookFrom = _controls && _controls.target
|
||||||
|
? _controls.target.clone()
|
||||||
|
: _camera.position.clone().add(_camera.getWorldDirection(new THREE.Vector3()));
|
||||||
|
_flyLookTo = room.pos.clone();
|
||||||
|
|
||||||
|
console.info('[SessionRooms] Flying into session room:', sessionId);
|
||||||
|
return room.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PERSISTENCE ──────────────────────────────────────
|
||||||
|
function saveToStorage(sessions) {
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ v: 1, sessions }));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[SessionRooms] Failed to save to localStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromStorage() {
|
||||||
|
if (typeof localStorage === 'undefined') return null;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || parsed.v !== 1 || !Array.isArray(parsed.sessions)) return null;
|
||||||
|
console.info('[SessionRooms] Restored', parsed.sessions.length, 'sessions from localStorage');
|
||||||
|
return parsed.sessions;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[SessionRooms] Failed to load from localStorage:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
console.info('[SessionRooms] Cleared localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PUBLIC API ───────────────────────────────────────
|
||||||
|
return {
|
||||||
|
init,
|
||||||
|
updateSessions,
|
||||||
|
update,
|
||||||
|
getClickableMeshes,
|
||||||
|
handleRoomClick,
|
||||||
|
clearStorage,
|
||||||
|
// For external inspection
|
||||||
|
getRooms: () => _rooms,
|
||||||
|
getSession: (id) => _sessionIndex[id] || null,
|
||||||
|
isFlyActive: () => _flyActive
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
export { SessionRooms };
|
||||||
119
style.css
119
style.css
@@ -1461,3 +1461,122 @@ canvas#nexus-canvas {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.session-room-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 125;
|
||||||
|
animation: sessionPanelIn 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-panel.session-panel-fade-out {
|
||||||
|
animation: sessionPanelOut 0.2s ease-in forwards !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sessionPanelIn {
|
||||||
|
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sessionPanelOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-panel-content {
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 480px;
|
||||||
|
background: rgba(8, 4, 28, 0.93);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
border: 1px solid rgba(123, 92, 255, 0.35);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
box-shadow: 0 0 32px rgba(123, 92, 255, 0.1), 0 8px 32px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-title {
|
||||||
|
font-family: var(--font-display, monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: #9b7cff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 2px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-close:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-timestamp {
|
||||||
|
font-family: var(--font-display, monospace);
|
||||||
|
font-size: 13px;
|
||||||
|
color: #c8b4ff;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-fact-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(200, 180, 255, 0.55);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-facts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
max-height: 140px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-fact-item {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(220, 210, 255, 0.75);
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(123, 92, 255, 0.07);
|
||||||
|
border-left: 2px solid rgba(123, 92, 255, 0.4);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-room-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(200, 180, 255, 0.35);
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user