Compare commits

...

1 Commits

Author SHA1 Message Date
08117ef43a feat(mnemosyne): trust-based crystal rendering (#1166)
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
Wire crystal visual properties to fact trust scores:
- Trust > 0.8: bright glow, full opacity
- Trust 0.5-0.8: medium glow, 80% opacity
- Trust < 0.5: dim, 40% opacity
- Trust < 0.3: near-invisible, pulsing red

Adds:
- _getTrustVisuals() helper computes material props from trust
- updateMemoryVisual() for runtime trust updates
- trust persisted in exportIndex and localStorage
- Low-trust crystals pulse red in update() loop

Closes #1166
2026-04-10 23:45:23 +00:00

View File

@@ -140,6 +140,47 @@ const SpatialMemory = (() => {
return new THREE.OctahedronGeometry(size, 0);
}
// ─── TRUST-BASED VISUALS ─────────────────────────────
// Wire crystal visual properties to fact trust score (0.0-1.0).
// Issue #1166: Trust > 0.8 = bright glow/full opacity,
// 0.5-0.8 = medium/80%, < 0.5 = dim/40%, < 0.3 = near-invisible pulsing red.
function _getTrustVisuals(trust, regionColor) {
const t = Math.max(0, Math.min(1, trust));
if (t >= 0.8) {
return {
opacity: 1.0,
emissiveIntensity: 2.0 * t,
emissiveColor: regionColor,
lightIntensity: 1.2,
glowDesc: 'high'
};
} else if (t >= 0.5) {
return {
opacity: 0.8,
emissiveIntensity: 1.2 * t,
emissiveColor: regionColor,
lightIntensity: 0.6,
glowDesc: 'medium'
};
} else if (t >= 0.3) {
return {
opacity: 0.4,
emissiveIntensity: 0.5 * t,
emissiveColor: regionColor,
lightIntensity: 0.2,
glowDesc: 'dim'
};
} else {
return {
opacity: 0.15,
emissiveIntensity: 0.3,
emissiveColor: 0xff2200,
lightIntensity: 0.1,
glowDesc: 'untrusted'
};
}
}
// ─── REGION MARKER ───────────────────────────────────
function createRegionMarker(regionKey, region) {
const cx = region.center[0];
@@ -216,17 +257,20 @@ const SpatialMemory = (() => {
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 trust = mem.trust != null ? Math.max(0, Math.min(1, mem.trust)) : 0.7;
const size = 0.2 + strength * 0.3;
const tv = _getTrustVisuals(trust, region.color);
const geo = createCrystalGeometry(size);
const mat = new THREE.MeshStandardMaterial({
color: region.color,
emissive: region.color,
emissiveIntensity: 1.5 * strength,
emissive: tv.emissiveColor,
emissiveIntensity: tv.emissiveIntensity,
metalness: 0.6,
roughness: 0.15,
transparent: true,
opacity: 0.5 + strength * 0.4
opacity: tv.opacity
});
const crystal = new THREE.Mesh(geo, mat);
@@ -239,10 +283,12 @@ const SpatialMemory = (() => {
region: mem.category,
pulse: Math.random() * Math.PI * 2,
strength: strength,
trust: trust,
glowDesc: tv.glowDesc,
createdAt: mem.timestamp || new Date().toISOString()
};
const light = new THREE.PointLight(region.color, 0.8 * strength, 5);
const light = new THREE.PointLight(tv.emissiveColor, tv.lightIntensity, 5);
crystal.add(light);
_scene.add(crystal);
@@ -337,8 +383,16 @@ const SpatialMemory = (() => {
mesh.scale.setScalar(pulse);
if (mesh.material) {
const trust = mesh.userData.trust != null ? mesh.userData.trust : 0.7;
const base = mesh.userData.strength || 0.7;
mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base;
if (trust < 0.3) {
// Low trust: pulsing red — visible warning
const pulseAlpha = 0.15 + Math.sin(mesh.userData.pulse * 2.0) * 0.15;
mesh.material.emissiveIntensity = 0.3 + Math.sin(mesh.userData.pulse * 2.0) * 0.3;
mesh.material.opacity = pulseAlpha;
} else {
mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base;
}
}
});
@@ -368,6 +422,42 @@ const SpatialMemory = (() => {
return REGIONS;
}
// ─── UPDATE VISUAL PROPERTIES ────────────────────────
// Re-render crystal when trust/strength change (no position move).
function updateMemoryVisual(memId, updates) {
const obj = _memoryObjects[memId];
if (!obj) return false;
const mesh = obj.mesh;
const region = REGIONS[obj.region] || REGIONS.working;
if (updates.trust != null) {
const trust = Math.max(0, Math.min(1, updates.trust));
mesh.userData.trust = trust;
obj.data.trust = trust;
const tv = _getTrustVisuals(trust, region.color);
mesh.material.emissive = new THREE.Color(tv.emissiveColor);
mesh.material.emissiveIntensity = tv.emissiveIntensity;
mesh.material.opacity = tv.opacity;
mesh.userData.glowDesc = tv.glowDesc;
if (mesh.children.length > 0 && mesh.children[0].isPointLight) {
mesh.children[0].intensity = tv.lightIntensity;
mesh.children[0].color = new THREE.Color(tv.emissiveColor);
}
}
if (updates.strength != null) {
const strength = Math.max(0.05, Math.min(1, updates.strength));
mesh.userData.strength = strength;
obj.data.strength = strength;
}
_dirty = true;
saveToStorage();
console.info('[Mnemosyne] Visual updated:', memId, 'trust:', mesh.userData.trust, 'glow:', mesh.userData.glowDesc);
return true;
}
// ─── QUERY ───────────────────────────────────────────
function getMemoryAtPosition(position, maxDist) {
maxDist = maxDist || 2;
@@ -507,6 +597,7 @@ const SpatialMemory = (() => {
source: o.data.source || 'unknown',
timestamp: o.data.timestamp || o.mesh.userData.createdAt,
strength: o.mesh.userData.strength || 0.7,
trust: o.mesh.userData.trust != null ? o.mesh.userData.trust : 0.7,
connections: o.data.connections || []
}))
};
@@ -653,7 +744,7 @@ const SpatialMemory = (() => {
}
return {
init, placeMemory, removeMemory, update,
init, placeMemory, removeMemory, update, updateMemoryVisual,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, searchNearby, REGIONS,