// modules/panels/dual-brain.js — Dual-Brain Status holographic panel // Shows the Brain Gap Scorecard with two glowing brain orbs. // Displayed as HONEST-OFFLINE: the dual-brain system is not yet deployed. // Brain pulse particles are set to ZERO — will flow when system comes online. // // Data category: HONEST-OFFLINE // Data source: — (dual-brain system not deployed; shows "AWAITING DEPLOYMENT") import * as THREE from 'three'; import { NEXUS } from '../core/theme.js'; import { subscribe } from '../core/ticker.js'; const ORIGIN = new THREE.Vector3(10, 3, -8); const OFFLINE_COLOR = NEXUS.theme.agentDormantHex; // dim blue — system offline const ACCENT = NEXUS.theme.accentStr; const FONT = NEXUS.theme.fontMono; let _group, _sprite, _scanSprite, _scanCanvas, _scanCtx, _scanTexture; let _cloudOrb, _localOrb; let _scene; function _buildPanelTexture() { const W = 512, H = 512; const canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H; const ctx = canvas.getContext('2d'); ctx.fillStyle = NEXUS.theme.panelBg; ctx.fillRect(0, 0, W, H); ctx.strokeStyle = ACCENT; ctx.lineWidth = 2; ctx.strokeRect(1, 1, W - 2, H - 2); ctx.strokeStyle = '#223366'; ctx.lineWidth = 1; ctx.strokeRect(5, 5, W - 10, H - 10); // Title ctx.font = `bold 22px ${FONT}`; ctx.fillStyle = '#88ccff'; ctx.textAlign = 'center'; ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40); ctx.strokeStyle = NEXUS.theme.panelBorderFaint; ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(W - 20, 52); ctx.stroke(); // Section header ctx.font = `11px ${FONT}`; ctx.fillStyle = NEXUS.theme.panelDim; ctx.textAlign = 'left'; ctx.fillText('BRAIN GAP SCORECARD', 20, 74); const categories = ['Triage', 'Tool Use', 'Code Gen', 'Planning', 'Communication', 'Reasoning']; const barX = 20, barW = W - 130, barH = 20; let y = 90; for (const cat of categories) { ctx.font = `13px ${FONT}`; ctx.fillStyle = NEXUS.theme.agentDormant; ctx.textAlign = 'left'; ctx.fillText(cat, barX, y + 14); ctx.font = `bold 13px ${FONT}`; ctx.fillStyle = NEXUS.theme.panelVeryDim; ctx.textAlign = 'right'; ctx.fillText('\u2014', W - 20, y + 14); // em dash — no data y += 22; ctx.fillStyle = 'rgba(255, 255, 255, 0.06)'; ctx.fillRect(barX, y, barW, barH); // empty bar background only y += barH + 12; } ctx.strokeStyle = NEXUS.theme.panelBorderFaint; ctx.beginPath(); ctx.moveTo(20, y + 4); ctx.lineTo(W - 20, y + 4); ctx.stroke(); y += 22; // Honest offline status ctx.font = `bold 18px ${FONT}`; ctx.fillStyle = NEXUS.theme.panelVeryDim; ctx.textAlign = 'center'; ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10); ctx.font = `11px ${FONT}`; ctx.fillStyle = '#223344'; ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32); // Brain indicators — offline dim y += 52; ctx.beginPath(); ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2); ctx.fillStyle = NEXUS.theme.panelVeryDim; ctx.fill(); ctx.font = `11px ${FONT}`; ctx.fillStyle = NEXUS.theme.panelVeryDim; ctx.textAlign = 'left'; ctx.fillText('CLOUD', W / 2 - 48, y + 12); ctx.beginPath(); ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2); ctx.fillStyle = NEXUS.theme.panelVeryDim; ctx.fill(); ctx.fillText('LOCAL', W / 2 + 42, y + 12); return new THREE.CanvasTexture(canvas); } /** @param {THREE.Scene} scene */ export function init(scene) { _scene = scene; _group = new THREE.Group(); _group.position.copy(ORIGIN); _group.lookAt(0, 3, 0); scene.add(_group); // Static panel sprite const texture = _buildPanelTexture(); const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.92, depthWrite: false }); _sprite = new THREE.Sprite(material); _sprite.scale.set(5.0, 5.0, 1); _sprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' }; _group.add(_sprite); // Accent light const light = new THREE.PointLight(NEXUS.theme.accent, 0.6, 10); light.position.set(0, 0.5, 1); _group.add(light); // Offline brain orbs — dim const orbGeo = new THREE.SphereGeometry(0.35, 32, 32); const orbMat = (color) => new THREE.MeshStandardMaterial({ color, emissive: new THREE.Color(color), emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, transparent: true, opacity: 0.85, }); _cloudOrb = new THREE.Mesh(orbGeo, orbMat(OFFLINE_COLOR)); _cloudOrb.position.set(-2.0, 3.0, 0); _cloudOrb.userData.zoomLabel = 'Cloud Brain'; _group.add(_cloudOrb); _localOrb = new THREE.Mesh(orbGeo.clone(), orbMat(OFFLINE_COLOR)); _localOrb.position.set(2.0, 3.0, 0); _localOrb.userData.zoomLabel = 'Local Brain'; _group.add(_localOrb); // Brain pulse particles — ZERO count (system offline) const particleGeo = new THREE.BufferGeometry(); particleGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3)); const particleMat = new THREE.PointsMaterial({ color: 0x44ddff, size: 0.08, sizeAttenuation: true, transparent: true, opacity: 0.8, depthWrite: false, }); _group.add(new THREE.Points(particleGeo, particleMat)); // Scan line overlay _scanCanvas = document.createElement('canvas'); _scanCanvas.width = 512; _scanCanvas.height = 512; _scanCtx = _scanCanvas.getContext('2d'); _scanTexture = new THREE.CanvasTexture(_scanCanvas); const scanMat = new THREE.SpriteMaterial({ map: _scanTexture, transparent: true, opacity: 0.18, depthWrite: false, }); _scanSprite = new THREE.Sprite(scanMat); _scanSprite.scale.set(5.0, 5.0, 1); _scanSprite.position.set(0, 0, 0.01); _group.add(_scanSprite); subscribe(update); } /** * @param {number} elapsed * @param {number} _delta */ export function update(elapsed, _delta) { // Gentle float animation const ud = _sprite.userData; _sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.08; // Scan line — horizontal sweep const W = 512, H = 512; _scanCtx.clearRect(0, 0, W, H); const scanY = ((elapsed * 60) % H); const grad = _scanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20); grad.addColorStop(0, 'rgba(68, 136, 255, 0)'); grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.4)'); grad.addColorStop(1, 'rgba(68, 136, 255, 0)'); _scanCtx.fillStyle = grad; _scanCtx.fillRect(0, scanY - 20, W, 40); _scanTexture.needsUpdate = true; } export function dispose() { if (_group) _scene.remove(_group); if (_scanTexture) _scanTexture.dispose(); }