|
|
|
|
@@ -173,9 +173,7 @@ const SpatialMemory = (() => {
|
|
|
|
|
let _entityLines = []; // entity resolution lines (issue #1167)
|
|
|
|
|
let _camera = null; // set by setCamera() for LOD culling
|
|
|
|
|
const ENTITY_LOD_DIST = 50; // hide entity lines when camera > this from midpoint
|
|
|
|
|
const CONNECTION_LOD_DIST = 60; // hide connection lines when camera > this from midpoint
|
|
|
|
|
let _initialized = false;
|
|
|
|
|
let _constellationVisible = true; // toggle for constellation view
|
|
|
|
|
|
|
|
|
|
// ─── CRYSTAL GEOMETRY (persistent memories) ───────────
|
|
|
|
|
function createCrystalGeometry(size) {
|
|
|
|
|
@@ -320,43 +318,10 @@ const SpatialMemory = (() => {
|
|
|
|
|
if (!obj || !obj.data.connections) return;
|
|
|
|
|
obj.data.connections.forEach(targetId => {
|
|
|
|
|
const target = _memoryObjects[targetId];
|
|
|
|
|
if (target) _drawSingleConnection(obj, target);
|
|
|
|
|
if (target) _createConnectionLine(obj, target);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _drawSingleConnection(src, tgt) {
|
|
|
|
|
const srcId = src.data.id;
|
|
|
|
|
const tgtId = tgt.data.id;
|
|
|
|
|
// Deduplicate — only draw from lower ID to higher
|
|
|
|
|
if (srcId > tgtId) return;
|
|
|
|
|
// Skip if already exists
|
|
|
|
|
const exists = _connectionLines.some(l =>
|
|
|
|
|
(l.userData.from === srcId && l.userData.to === tgtId) ||
|
|
|
|
|
(l.userData.from === tgtId && l.userData.to === srcId)
|
|
|
|
|
);
|
|
|
|
|
if (exists) return;
|
|
|
|
|
|
|
|
|
|
const points = [src.mesh.position.clone(), tgt.mesh.position.clone()];
|
|
|
|
|
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
|
|
|
|
const srcStrength = src.mesh.userData.strength || 0.7;
|
|
|
|
|
const tgtStrength = tgt.mesh.userData.strength || 0.7;
|
|
|
|
|
const blendedStrength = (srcStrength + tgtStrength) / 2;
|
|
|
|
|
const lineOpacity = 0.15 + blendedStrength * 0.55;
|
|
|
|
|
const srcColor = new THREE.Color(REGIONS[src.region]?.color || 0x334455);
|
|
|
|
|
const tgtColor = new THREE.Color(REGIONS[tgt.region]?.color || 0x334455);
|
|
|
|
|
const lineColor = new THREE.Color().lerpColors(srcColor, tgtColor, 0.5);
|
|
|
|
|
const mat = new THREE.LineBasicMaterial({
|
|
|
|
|
color: lineColor,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: lineOpacity
|
|
|
|
|
});
|
|
|
|
|
const line = new THREE.Line(geo, mat);
|
|
|
|
|
line.userData = { type: 'connection', from: srcId, to: tgtId, baseOpacity: lineOpacity };
|
|
|
|
|
line.visible = _constellationVisible;
|
|
|
|
|
_scene.add(line);
|
|
|
|
|
_connectionLines.push(line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ring, disc, glowDisc, sprite };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -434,7 +399,7 @@ const SpatialMemory = (() => {
|
|
|
|
|
return [cx + Math.cos(angle) * dist, cy + height, cz + Math.sin(angle) * dist];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── CONNECTIONS (constellation-aware) ───────────────
|
|
|
|
|
// ─── CONNECTIONS ─────────────────────────────────────
|
|
|
|
|
function _drawConnections(memId, connections) {
|
|
|
|
|
const src = _memoryObjects[memId];
|
|
|
|
|
if (!src) return;
|
|
|
|
|
@@ -445,23 +410,9 @@ const SpatialMemory = (() => {
|
|
|
|
|
|
|
|
|
|
const points = [src.mesh.position.clone(), tgt.mesh.position.clone()];
|
|
|
|
|
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
|
|
|
|
// Strength-encoded opacity: blend source/target strengths, min 0.15, max 0.7
|
|
|
|
|
const srcStrength = src.mesh.userData.strength || 0.7;
|
|
|
|
|
const tgtStrength = tgt.mesh.userData.strength || 0.7;
|
|
|
|
|
const blendedStrength = (srcStrength + tgtStrength) / 2;
|
|
|
|
|
const lineOpacity = 0.15 + blendedStrength * 0.55;
|
|
|
|
|
// Blend source/target region colors for the line
|
|
|
|
|
const srcColor = new THREE.Color(REGIONS[src.region]?.color || 0x334455);
|
|
|
|
|
const tgtColor = new THREE.Color(REGIONS[tgt.region]?.color || 0x334455);
|
|
|
|
|
const lineColor = new THREE.Color().lerpColors(srcColor, tgtColor, 0.5);
|
|
|
|
|
const mat = new THREE.LineBasicMaterial({
|
|
|
|
|
color: lineColor,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: lineOpacity
|
|
|
|
|
});
|
|
|
|
|
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, baseOpacity: lineOpacity };
|
|
|
|
|
line.visible = _constellationVisible;
|
|
|
|
|
line.userData = { type: 'connection', from: memId, to: targetId };
|
|
|
|
|
_scene.add(line);
|
|
|
|
|
_connectionLines.push(line);
|
|
|
|
|
});
|
|
|
|
|
@@ -538,43 +489,6 @@ const SpatialMemory = (() => {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _updateConnectionLines() {
|
|
|
|
|
if (!_constellationVisible) return;
|
|
|
|
|
if (!_camera) return;
|
|
|
|
|
const camPos = _camera.position;
|
|
|
|
|
|
|
|
|
|
_connectionLines.forEach(line => {
|
|
|
|
|
const posArr = line.geometry.attributes.position.array;
|
|
|
|
|
const mx = (posArr[0] + posArr[3]) / 2;
|
|
|
|
|
const my = (posArr[1] + posArr[4]) / 2;
|
|
|
|
|
const mz = (posArr[2] + posArr[5]) / 2;
|
|
|
|
|
const dist = camPos.distanceTo(new THREE.Vector3(mx, my, mz));
|
|
|
|
|
|
|
|
|
|
if (dist > CONNECTION_LOD_DIST) {
|
|
|
|
|
line.visible = false;
|
|
|
|
|
} else {
|
|
|
|
|
line.visible = true;
|
|
|
|
|
const fade = Math.max(0, 1 - (dist / CONNECTION_LOD_DIST));
|
|
|
|
|
// Restore base opacity from userData if stored, else use material default
|
|
|
|
|
const base = line.userData.baseOpacity || line.material.opacity || 0.4;
|
|
|
|
|
line.material.opacity = base * fade;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleConstellation() {
|
|
|
|
|
_constellationVisible = !_constellationVisible;
|
|
|
|
|
_connectionLines.forEach(line => {
|
|
|
|
|
line.visible = _constellationVisible;
|
|
|
|
|
});
|
|
|
|
|
console.info('[Mnemosyne] Constellation', _constellationVisible ? 'shown' : 'hidden');
|
|
|
|
|
return _constellationVisible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isConstellationVisible() {
|
|
|
|
|
return _constellationVisible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── REMOVE A MEMORY ─────────────────────────────────
|
|
|
|
|
function removeMemory(memId) {
|
|
|
|
|
const obj = _memoryObjects[memId];
|
|
|
|
|
@@ -630,7 +544,6 @@ const SpatialMemory = (() => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_updateEntityLines();
|
|
|
|
|
_updateConnectionLines();
|
|
|
|
|
|
|
|
|
|
Object.values(_regionMarkers).forEach(marker => {
|
|
|
|
|
if (marker.ring && marker.ring.material) {
|
|
|
|
|
@@ -902,6 +815,42 @@ const SpatialMemory = (() => {
|
|
|
|
|
return results.slice(0, maxResults);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── CONTENT SEARCH ─────────────────────────────────
|
|
|
|
|
/**
|
|
|
|
|
* Search memories by text content — case-insensitive substring match.
|
|
|
|
|
* @param {string} query - Search text
|
|
|
|
|
* @param {object} [options] - Optional filters
|
|
|
|
|
* @param {string} [options.category] - Restrict to a specific region
|
|
|
|
|
* @param {number} [options.maxResults=20] - Cap results
|
|
|
|
|
* @returns {Array<{memory: object, score: number, position: THREE.Vector3}>}
|
|
|
|
|
*/
|
|
|
|
|
function searchByContent(query, options = {}) {
|
|
|
|
|
if (!query || !query.trim()) return [];
|
|
|
|
|
const { category, maxResults = 20 } = options;
|
|
|
|
|
const needle = query.trim().toLowerCase();
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
Object.values(_memoryObjects).forEach(obj => {
|
|
|
|
|
if (category && obj.region !== category) return;
|
|
|
|
|
const content = (obj.data.content || '').toLowerCase();
|
|
|
|
|
if (!content.includes(needle)) return;
|
|
|
|
|
|
|
|
|
|
// Score: number of occurrences + strength bonus
|
|
|
|
|
let matches = 0, idx = 0;
|
|
|
|
|
while ((idx = content.indexOf(needle, idx)) !== -1) { matches++; idx += needle.length; }
|
|
|
|
|
const score = matches + (obj.mesh.userData.strength || 0.7);
|
|
|
|
|
|
|
|
|
|
results.push({
|
|
|
|
|
memory: obj.data,
|
|
|
|
|
score,
|
|
|
|
|
position: obj.mesh.position.clone()
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
results.sort((a, b) => b.score - a.score);
|
|
|
|
|
return results.slice(0, maxResults);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ─── CRYSTAL MESH COLLECTION (for raycasting) ────────
|
|
|
|
|
function getCrystalMeshes() {
|
|
|
|
|
@@ -951,9 +900,9 @@ const SpatialMemory = (() => {
|
|
|
|
|
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
|
|
|
|
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
|
|
|
|
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
|
|
|
|
exportIndex, importIndex, searchNearby, REGIONS,
|
|
|
|
|
exportIndex, importIndex, searchNearby, searchByContent, REGIONS,
|
|
|
|
|
saveToStorage, loadFromStorage, clearStorage,
|
|
|
|
|
runGravityLayout, setCamera, toggleConstellation, isConstellationVisible
|
|
|
|
|
runGravityLayout, setCamera
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|