[claude] Mnemosyne gravity well clustering — related memories attract in 3D (#1175) #1176

Merged
claude merged 1 commits from claude/issue-1175 into main 2026-04-10 22:18:47 +00:00
2 changed files with 80 additions and 1 deletions

3
app.js
View File

@@ -3175,6 +3175,9 @@ init().then(() => {
];
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
SpatialMemory.runGravityLayout();
fetchGiteaData();
setInterval(fetchGiteaData, 30000);
runWeeklyAudit();

View File

@@ -456,6 +456,81 @@ const SpatialMemory = (() => {
return count;
}
// ─── GRAVITY WELL CLUSTERING ──────────────────────────
// Force-directed layout: same-category crystals attract, unrelated repel.
// Run on load (bake positions, not per-frame). Spec from issue #1175.
const GRAVITY_ITERATIONS = 20;
const ATTRACT_FACTOR = 0.10; // 10% closer to same-category centroid per iteration
const REPEL_FACTOR = 0.05; // 5% away from nearest unrelated crystal
function runGravityLayout() {
const objs = Object.values(_memoryObjects);
if (objs.length < 2) {
console.info('[Mnemosyne] Gravity layout: fewer than 2 crystals, skipping');
return;
}
console.info('[Mnemosyne] Gravity layout starting —', objs.length, 'crystals,', GRAVITY_ITERATIONS, 'iterations');
for (let iter = 0; iter < GRAVITY_ITERATIONS; iter++) {
// Accumulate displacements before applying (avoids order-of-iteration bias)
const dx = new Float32Array(objs.length);
const dy = new Float32Array(objs.length);
const dz = new Float32Array(objs.length);
objs.forEach((obj, i) => {
const pos = obj.mesh.position;
const cat = obj.region;
// ── Attraction toward same-category centroid ──────────────
let sx = 0, sy = 0, sz = 0, sameCount = 0;
objs.forEach(o => {
if (o === obj || o.region !== cat) return;
sx += o.mesh.position.x;
sy += o.mesh.position.y;
sz += o.mesh.position.z;
sameCount++;
});
if (sameCount > 0) {
dx[i] += ((sx / sameCount) - pos.x) * ATTRACT_FACTOR;
dy[i] += ((sy / sameCount) - pos.y) * ATTRACT_FACTOR;
dz[i] += ((sz / sameCount) - pos.z) * ATTRACT_FACTOR;
}
// ── Repulsion from nearest unrelated crystal ───────────────
let nearestDist = Infinity;
let rnx = 0, rny = 0, rnz = 0;
objs.forEach(o => {
if (o === obj || o.region === cat) return;
const ex = pos.x - o.mesh.position.x;
const ey = pos.y - o.mesh.position.y;
const ez = pos.z - o.mesh.position.z;
const d = Math.sqrt(ex * ex + ey * ey + ez * ez);
if (d < nearestDist) {
nearestDist = d;
rnx = ex; rny = ey; rnz = ez;
}
});
if (nearestDist > 0.001 && nearestDist < Infinity) {
const len = Math.sqrt(rnx * rnx + rny * rny + rnz * rnz);
dx[i] += (rnx / len) * nearestDist * REPEL_FACTOR;
dy[i] += (rny / len) * nearestDist * REPEL_FACTOR;
dz[i] += (rnz / len) * nearestDist * REPEL_FACTOR;
}
});
// Apply displacements
objs.forEach((obj, i) => {
obj.mesh.position.x += dx[i];
obj.mesh.position.y += dy[i];
obj.mesh.position.z += dz[i];
});
}
// Bake final positions to localStorage
saveToStorage();
console.info('[Mnemosyne] Gravity layout complete — positions baked to localStorage');
}
// ─── SPATIAL SEARCH ──────────────────────────────────
function searchNearby(position, maxResults, maxDist) {
maxResults = maxResults || 10;
@@ -516,7 +591,8 @@ const SpatialMemory = (() => {
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, searchNearby, REGIONS,
saveToStorage, loadFromStorage, clearStorage
saveToStorage, loadFromStorage, clearStorage,
runGravityLayout
};
})();