Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
148 lines
4.2 KiB
JavaScript
148 lines
4.2 KiB
JavaScript
// modules/panels/sovereignty.js — Sovereignty Meter holographic arc gauge
|
|
// Floating arc gauge above the platform showing the current sovereignty score.
|
|
// Reads from state.sovereignty (populated by data/loaders.js via sovereignty-status.json).
|
|
// The assessment is MANUAL — the panel always labels itself as such.
|
|
//
|
|
// Data category: REAL (manual assessment)
|
|
// Data source: state.sovereignty (sovereignty-status.json via data/loaders.js)
|
|
|
|
import * as THREE from 'three';
|
|
import { state } from '../core/state.js';
|
|
import { NEXUS } from '../core/theme.js';
|
|
import { subscribe } from '../core/ticker.js';
|
|
|
|
const FONT = NEXUS.theme.fontMono;
|
|
|
|
// Defaults shown before data loads
|
|
let _score = 85;
|
|
let _label = 'Mostly Sovereign';
|
|
let _assessmentType = 'MANUAL';
|
|
|
|
let _group, _arcMesh, _arcMat, _light, _spriteMat, _scene;
|
|
let _lastSovereignty = null;
|
|
|
|
function _scoreColor(score) {
|
|
if (score >= 80) return NEXUS.theme.sovereignHighHex;
|
|
if (score >= 40) return NEXUS.theme.sovereignMidHex;
|
|
return NEXUS.theme.sovereignLowHex;
|
|
}
|
|
|
|
function _scoreColorStr(score) {
|
|
if (score >= 80) return NEXUS.theme.sovereignHigh;
|
|
if (score >= 40) return NEXUS.theme.sovereignMid;
|
|
return NEXUS.theme.sovereignLow;
|
|
}
|
|
|
|
function _buildArcGeo(score) {
|
|
return new THREE.TorusGeometry(1.6, 0.1, 8, 64, (score / 100) * Math.PI * 2);
|
|
}
|
|
|
|
function _buildMeterTexture(score, label, assessmentType) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 256;
|
|
canvas.height = 128;
|
|
const ctx = canvas.getContext('2d');
|
|
const col = _scoreColorStr(score);
|
|
|
|
ctx.clearRect(0, 0, 256, 128);
|
|
|
|
ctx.font = `bold 52px ${FONT}`;
|
|
ctx.fillStyle = col;
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(`${score}%`, 128, 50);
|
|
|
|
ctx.font = `16px ${FONT}`;
|
|
ctx.fillStyle = '#8899bb';
|
|
ctx.fillText(label.toUpperCase(), 128, 74);
|
|
|
|
ctx.font = `11px ${FONT}`;
|
|
ctx.fillStyle = '#445566';
|
|
ctx.fillText('SOVEREIGNTY', 128, 94);
|
|
|
|
ctx.font = `9px ${FONT}`;
|
|
ctx.fillStyle = '#334455';
|
|
ctx.fillText('MANUAL ASSESSMENT', 128, 112);
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
function _applyScore(score, label, assessmentType) {
|
|
_score = score;
|
|
_label = label;
|
|
_assessmentType = assessmentType;
|
|
|
|
_arcMesh.geometry.dispose();
|
|
_arcMesh.geometry = _buildArcGeo(score);
|
|
|
|
const col = _scoreColor(score);
|
|
_arcMat.color.setHex(col);
|
|
_light.color.setHex(col);
|
|
|
|
if (_spriteMat.map) _spriteMat.map.dispose();
|
|
_spriteMat.map = _buildMeterTexture(score, label, assessmentType);
|
|
_spriteMat.needsUpdate = true;
|
|
}
|
|
|
|
/** @param {THREE.Scene} scene */
|
|
export function init(scene) {
|
|
_scene = scene;
|
|
|
|
_group = new THREE.Group();
|
|
_group.position.set(0, 3.8, 0);
|
|
|
|
// Background ring
|
|
const bgMat = new THREE.MeshBasicMaterial({ color: 0x0a1828, transparent: true, opacity: 0.5 });
|
|
_group.add(new THREE.Mesh(new THREE.TorusGeometry(1.6, 0.1, 8, 64), bgMat));
|
|
|
|
// Score arc
|
|
_arcMat = new THREE.MeshBasicMaterial({
|
|
color: _scoreColor(_score),
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
});
|
|
_arcMesh = new THREE.Mesh(_buildArcGeo(_score), _arcMat);
|
|
_arcMesh.rotation.z = Math.PI / 2; // arc starts at 12 o'clock
|
|
_group.add(_arcMesh);
|
|
|
|
// Glow light
|
|
_light = new THREE.PointLight(_scoreColor(_score), 0.7, 6);
|
|
_group.add(_light);
|
|
|
|
// Sprite label
|
|
_spriteMat = new THREE.SpriteMaterial({
|
|
map: _buildMeterTexture(_score, _label, _assessmentType),
|
|
transparent: true,
|
|
depthWrite: false,
|
|
});
|
|
const sprite = new THREE.Sprite(_spriteMat);
|
|
sprite.scale.set(3.2, 1.6, 1);
|
|
_group.add(sprite);
|
|
|
|
scene.add(_group);
|
|
_group.traverse(obj => {
|
|
if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter';
|
|
});
|
|
|
|
subscribe(update);
|
|
}
|
|
|
|
/**
|
|
* @param {number} _elapsed
|
|
* @param {number} _delta
|
|
*/
|
|
export function update(_elapsed, _delta) {
|
|
if (state.sovereignty && state.sovereignty !== _lastSovereignty) {
|
|
const { score, label, assessment_type } = state.sovereignty;
|
|
const s = Math.max(0, Math.min(100, typeof score === 'number' ? score : _score));
|
|
const l = typeof label === 'string' ? label : _label;
|
|
const t = typeof assessment_type === 'string' ? assessment_type : 'MANUAL';
|
|
_applyScore(s, l, t);
|
|
_lastSovereignty = state.sovereignty;
|
|
}
|
|
}
|
|
|
|
export function dispose() {
|
|
if (_group) _scene.remove(_group);
|
|
if (_spriteMat.map) _spriteMat.map.dispose();
|
|
}
|