diff --git a/nexus/components/spatial-memory.js b/nexus/components/spatial-memory.js index ab3fdd87..a066440d 100644 --- a/nexus/components/spatial-memory.js +++ b/nexus/components/spatial-memory.js @@ -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,