feat(dashboard): add layer management module for toggle controls

Wire Fresnel zone toggle through Layers module for consistent
state management across toolbar, debug panel, and layer controls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-25 00:15:39 -04:00
parent 294e55cb53
commit 71377b9efc
3 changed files with 203 additions and 10 deletions

View file

@ -2280,17 +2280,19 @@
};
window.toggleFresnelZones = function() {
var btn = document.getElementById('fresnel-toggle-btn');
var isActive = btn && btn.classList.contains('active');
if (isActive) {
// Turn off
Viz3D.toggleFresnelZones(false);
if (btn) btn.classList.remove('active');
if (window.Layers) {
Layers.toggleLayer(Layers.LAYERS.FRESNEL);
} else {
// Turn on
Viz3D.toggleFresnelZones(true);
if (btn) btn.classList.add('active');
// Fallback without Layers module
var btn = document.getElementById('fresnel-toggle-btn');
var isActive = btn && btn.classList.contains('active');
if (isActive) {
Viz3D.toggleFresnelZones(false);
if (btn) btn.classList.remove('active');
} else {
Viz3D.toggleFresnelZones(true);
if (btn) btn.classList.add('active');
}
}
};
@ -2305,6 +2307,18 @@
window.toggleFresnelDebugOverlay = function(visible) {
state.fresnelDebugVisible = visible;
// Sync Viz3D Fresnel zones with debug overlay state
if (window.Viz3D && Viz3D.toggleFresnelZones) {
Viz3D.toggleFresnelZones(visible);
}
// Sync toolbar button
var btn = document.getElementById('fresnel-toggle-btn');
if (btn) {
if (visible) btn.classList.add('active');
else btn.classList.remove('active');
}
if (visible) {
rebuildFresnelDebugEllipsoids();
} else {
@ -2590,6 +2604,17 @@
if (debugSection) {
debugSection.style.display = 'block';
}
// Bind layer controls to Layers module
if (window.Layers) {
var fresnelCheckbox = document.getElementById('fresnel-zones-toggle');
if (fresnelCheckbox) {
Layers.bindCheckbox(Layers.LAYERS.FRESNEL, fresnelCheckbox);
}
Layers.onLayerChange(Layers.LAYERS.FRESNEL, function(visible) {
toggleFresnelDebugOverlay(visible);
});
}
};
// Update Fresnel ellipsoids when links change

166
dashboard/js/layers.js Normal file
View file

@ -0,0 +1,166 @@
/**
* Spaxel Dashboard - Layer Management Module
*
* Centralized management for 3D scene overlay layers.
* Provides toggle state tracking and event dispatch for layer changes.
*
* Layers:
* - Links: TXRX link lines
* - Fresnel Zones: Wireframe ellipsoids for active links (debug)
* - Trails: Blob footprint trails
* - Zones: Zone box meshes and labels
* - Portals: Portal connection meshes
* - Coverage: GDOP quality overlay
* - Crowd Flow: Trajectory flow arrows
*/
(function() {
'use strict';
var LAYERS = {
LINKS: 'links',
FRESNEL: 'fresnel',
TRAILS: 'trails',
ZONES: 'zones',
PORTALS: 'portals',
COVERAGE: 'coverage',
CROWD_FLOW: 'crowd_flow'
};
// Default visibility state
var _state = {};
_state[LAYERS.LINKS] = true;
_state[LAYERS.FRESNEL] = false; // off by default
_state[LAYERS.TRAILS] = true;
_state[LAYERS.ZONES] = true;
_state[LAYERS.PORTALS] = true;
_state[LAYERS.COVERAGE] = false;
_state[LAYERS.CROWD_FLOW] = false;
// Listener registry: layerName -> [callback, ...]
var _listeners = {};
/**
* Register a callback for layer visibility changes.
* @param {string} layer - Layer name (use LAYERS constants)
* @param {function(visible: boolean)} callback
*/
function onLayerChange(layer, callback) {
if (!_listeners[layer]) _listeners[layer] = [];
_listeners[layer].push(callback);
}
/**
* Remove a previously registered callback.
* @param {string} layer
* @param {function} callback
*/
function offLayerChange(layer, callback) {
if (!_listeners[layer]) return;
_listeners[layer] = _listeners[layer].filter(function(fn) {
return fn !== callback;
});
}
/**
* Set layer visibility and notify listeners.
* @param {string} layer - Layer name
* @param {boolean} visible
*/
function setLayerVisible(layer, visible) {
var prev = _state[layer];
_state[layer] = visible;
if (prev !== visible && _listeners[layer]) {
_listeners[layer].forEach(function(cb) {
try { cb(visible); } catch (e) {
console.error('[Layers] Listener error for ' + layer + ':', e);
}
});
}
}
/**
* Toggle layer visibility.
* @param {string} layer
*/
function toggleLayer(layer) {
setLayerVisible(layer, !_state[layer]);
}
/**
* Get current visibility of a layer.
* @param {string} layer
* @returns {boolean}
*/
function isVisible(layer) {
return !!_state[layer];
}
/**
* Sync a checkbox DOM element with layer state.
* Sets up bidirectional binding: checkbox changes -> layer state -> checkbox updates.
* @param {string} layer
* @param {HTMLInputElement} checkbox
*/
function bindCheckbox(layer, checkbox) {
if (!checkbox) return;
// Sync initial state
checkbox.checked = _state[layer];
// Checkbox -> layer
checkbox.addEventListener('change', function() {
setLayerVisible(layer, checkbox.checked);
});
// Layer -> checkbox
onLayerChange(layer, function(visible) {
checkbox.checked = visible;
});
}
/**
* Sync a button DOM element with layer state (active class toggling).
* @param {string} layer
* @param {HTMLElement} button
*/
function bindButton(layer, button) {
if (!button) return;
// Sync initial state
if (_state[layer]) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
// Button -> layer
button.addEventListener('click', function() {
toggleLayer(layer);
});
// Layer -> button
onLayerChange(layer, function(visible) {
if (visible) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
}
// Public API
window.Layers = {
LAYERS: LAYERS,
onLayerChange: onLayerChange,
offLayerChange: offLayerChange,
setLayerVisible: setLayerVisible,
toggleLayer: toggleLayer,
isVisible: isVisible,
bindCheckbox: bindCheckbox,
bindButton: bindButton
};
console.log('[Layers] Module loaded');
})();

View file

@ -3437,6 +3437,8 @@
<script src="js/zone-editor.js"></script>
<!-- Fresnel zone helper (shared with explainability) -->
<script src="js/fresnel.js"></script>
<!-- Layer management -->
<script src="js/layers.js"></script>
<!-- Node placement, GDOP coverage, room editor -->
<script src="js/placement.js"></script>
<!-- Panel Framework (must load before modules that use it) -->