diff --git a/dashboard/js/app.js b/dashboard/js/app.js
index bb81f17..69beed8 100644
--- a/dashboard/js/app.js
+++ b/dashboard/js/app.js
@@ -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
diff --git a/dashboard/js/layers.js b/dashboard/js/layers.js
new file mode 100644
index 0000000..bac1f0a
--- /dev/null
+++ b/dashboard/js/layers.js
@@ -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: TX→RX 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');
+})();
diff --git a/dashboard/live.html b/dashboard/live.html
index 915caba..43be4cf 100644
--- a/dashboard/live.html
+++ b/dashboard/live.html
@@ -3437,6 +3437,8 @@
+
+