feat(dashboard): add Fresnel zone debug overlay with shared geometry module

Fresnel zone ellipsoids render for all active links when the debug layer
is toggled on. Uses shared fresnel.js helper for geometry computation,
with hover tooltips showing link details and click-to-select. viz3d.js
refactored to use the shared module instead of duplicating calculations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-25 01:42:56 -04:00
parent ebf615a696
commit fb5937af2f
2 changed files with 81 additions and 76 deletions

View file

@ -2290,6 +2290,13 @@
}
};
// Expose for viz3d.js to call when links rebuild
window.rebuildFresnelDebugEllipsoids = function() {
if (state.fresnelDebugVisible) {
rebuildFresnelDebugEllipsoids();
}
};
// ============================================
// Fresnel Zone Debug Overlay
// ============================================

View file

@ -417,9 +417,9 @@ const Viz3D = (function () {
_linkLines.set(id, line);
});
// Update Fresnel zones if visible
if (_fresnelZonesVisible) {
rebuildActiveFresnelZones();
// Notify app.js to rebuild Fresnel debug overlay if visible
if (_fresnelZonesVisible && window.rebuildFresnelDebugEllipsoids) {
rebuildFresnelDebugEllipsoids();
}
}
@ -2890,45 +2890,46 @@ const Viz3D = (function () {
};
let _fresnelZones = []; // Array of THREE.Mesh for explainability Fresnel zones
let _fresnelActiveZones = []; // Array of THREE.Line for active link Fresnel zones (wireframe)
let _fresnelActiveZones = []; // Array of ellipsoid objects from shared Fresnel module
let _fresnelZonesVisible = false; // Toggle state for active link Fresnel zones
/**
* Calculate Fresnel zone ellipsoid geometry for a link.
* Uses the shared Fresnel module from fresnel.js when available.
* Falls back to a local calculation for backward compatibility.
* @param {THREE.Vector3} tx - Transmitter position
* @param {THREE.Vector3} rx - Receiver position
* @param {number} zoneNumber - Fresnel zone number (1-based)
* @returns {Object} Ellipsoid parameters: { center, semiAxes, rotation }
*/
function _calculateFresnelZone(tx, rx, zoneNumber) {
// WiFi wavelength and Fresnel zone constants
const lambda = FRESNEL_CONFIG.wavelength; // ~0.123 m for 2.4 GHz
// For zone 1, use the shared Fresnel module if available
if (window.Fresnel && zoneNumber === 1) {
var channel = 6; // default 2.4 GHz
var params = Fresnel.calculateFresnelEllipsoid(tx, rx, channel);
return {
center: params.center,
semiAxes: params.semiAxes,
rotation: params.rotation,
zoneNumber: 1
};
}
// Fallback for multi-zone or when Fresnel module is unavailable
const lambda = FRESNEL_CONFIG.wavelength;
const n = zoneNumber;
// Direct distance between TX and RX
const d = tx.distanceTo(rx);
// Fresnel zone path difference: n * lambda / 2
const deltaL = n * lambda / 2;
// Ellipsoid semi-axes calculation
// For a prolate spheroid with foci at tx and rx:
// Semi-major axis (a) = (d + deltaL) / 2
// Semi-minor axis (b) = sqrt(deltaL * (2*d + deltaL)) / 2
const a = (d + deltaL) / 2;
const b = Math.sqrt(Math.max(0, deltaL * (2 * d + deltaL))) / 2;
// Center of ellipsoid (midpoint between TX and RX)
const center = new THREE.Vector3().addVectors(tx, rx).multiplyScalar(0.5);
// Rotation: align with TX-RX axis
const direction = new THREE.Vector3().subVectors(rx, tx).normalize();
const up = new THREE.Vector3(0, 1, 0);
const quaternion = new THREE.Quaternion().setFromUnitVectors(up, direction);
const xAxis = new THREE.Vector3(1, 0, 0);
const quaternion = new THREE.Quaternion().setFromUnitVectors(xAxis, direction);
return {
center: center,
semiAxes: new THREE.Vector3(b, b, a), // X, Y, Z semi-axes (Z is along link axis)
semiAxes: new THREE.Vector3(a, b, b),
rotation: quaternion,
zoneNumber: n
};
@ -3004,51 +3005,43 @@ const Viz3D = (function () {
/**
* Create a wireframe Fresnel zone ellipsoid for an active link.
* Uses the shared Fresnel module from fresnel.js for zone 1.
* @param {THREE.Vector3} tx - Transmitter position
* @param {THREE.Vector3} rx - Receiver position
* @param {number} zoneNumber - Fresnel zone number (1-5)
* @param {number} color - Color hex value
* @returns {THREE.LineSegments|null} The created wireframe mesh
* @returns {Object|null} Ellipsoid object { wireframe, fill, data } or null
*/
function _createWireframeFresnelZone(tx, rx, zoneNumber, color) {
if (!_scene) return null;
// Calculate Fresnel zone geometry
// Use the shared Fresnel module for zone 1 (first Fresnel zone)
if (window.Fresnel && zoneNumber === 1) {
var channel = 6; // default 2.4 GHz
var ellipsoid = Fresnel.addFresnelEllipsoid(tx, rx, channel, color, {
wireframeOpacity: 0.4,
fillOpacity: 0.06
});
return ellipsoid;
}
// Fallback: torus-based wireframe for higher zones
var zone = _calculateFresnelZone(tx, rx, zoneNumber);
if (!zone) return null;
// Create wireframe ellipsoid using TorusGeometry (thin tube)
// Torus with tube radius ~0.005m, following the ellipsoid path
var tubeRadius = 0.008; // 8mm tube thickness for visibility
var tubeRadius = 0.008;
var tubularSegments = 64;
var radialSegments = 8;
var geometry = new THREE.TorusGeometry(
zone.semiAxes.z, // major radius (distance from center to ellipsoid surface along Z axis)
zone.semiAxes.x,
tubeRadius,
tubularSegments,
radialSegments
);
// Apply scaling to create ellipsoid instead of torus
// Scale X and Y by semi-minor / semi-major ratio
var scaleRatio = zone.semiAxes.x / zone.semiAxes.z;
geometry.scale(scaleRatio, scaleRatio, 1.0);
var scaleRatio = zone.semiAxes.y / zone.semiAxes.x;
geometry.scale(1, scaleRatio, scaleRatio);
// Position and rotate
var mesh = new THREE.Mesh(geometry);
// Rotate to align with link direction
mesh.position.copy(zone.center);
mesh.quaternion.copy(zone.rotation);
// Orient the torus: rotate 90 degrees so tube lies in correct plane
var orientQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
Math.PI / 2
);
mesh.quaternion.multiply(orientQuat);
// Create wireframe material
var material = new THREE.LineBasicMaterial({
color: color || FRESNEL_CONFIG.color,
transparent: true,
@ -3056,16 +3049,19 @@ const Viz3D = (function () {
depthTest: false
});
// Convert mesh to wireframe
var wireframe = new THREE.LineSegments(
new THREE.WireframeGeometry(geometry),
material
);
wireframe.position.copy(mesh.position);
wireframe.quaternion.copy(mesh.quaternion);
wireframe.position.copy(zone.center);
wireframe.quaternion.copy(zone.rotation);
// Clean up temporary mesh
mesh.geometry.dispose();
// Orient torus to correct plane
var orientQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
Math.PI / 2
);
wireframe.quaternion.multiply(orientQuat);
_scene.add(wireframe);
return wireframe;
@ -3073,15 +3069,13 @@ const Viz3D = (function () {
/**
* Rebuild Fresnel zone visualization for all active links.
* Creates wireframe ellipsoids for the first 3 Fresnel zones of each active link.
* Uses the shared Fresnel module for first Fresnel zone ellipsoids.
*/
function rebuildActiveFresnelZones() {
// Clear existing Fresnel zones
clearActiveFresnelZones();
if (!_fresnelZonesVisible) return;
// Get active links
_activeLinks.forEach(function(link, linkID) {
var txMesh = _nodeMeshes.get(link.node_mac);
var rxMesh = _nodeMeshes.get(link.peer_mac);
@ -3090,50 +3084,54 @@ const Viz3D = (function () {
var tx = txMesh.position;
var rx = rxMesh.position;
// Determine color based on link health
var healthData = _linkHealth.get(linkID);
var healthScore = healthData ? healthData.score : 0.5;
var zoneColor = _getHealthColor(healthScore);
// Create Fresnel zones for first 3 zones
for (var n = 1; n <= 3; n++) {
var wireframe = _createWireframeFresnelZone(tx, rx, n, zoneColor);
if (wireframe) {
_fresnelActiveZones.push(wireframe);
wireframe.userData = {
linkID: linkID,
zoneNumber: n
};
// Create first Fresnel zone using shared module
var ellipsoid = _createWireframeFresnelZone(tx, rx, 1, zoneColor);
if (ellipsoid) {
_fresnelActiveZones.push(ellipsoid);
// Store link metadata for interactions
if (ellipsoid.wireframe) {
ellipsoid.wireframe.userData.linkID = linkID;
ellipsoid.wireframe.userData.healthScore = healthScore;
}
if (ellipsoid.fill) {
ellipsoid.fill.userData.linkID = linkID;
ellipsoid.fill.userData.healthScore = healthScore;
}
}
});
}
/**
* Clear all active Fresnel zone wireframes.
* Clear all active Fresnel zone ellipsoids.
*/
function clearActiveFresnelZones() {
_fresnelActiveZones.forEach(function(wireframe) {
if (_scene) {
_scene.remove(wireframe);
_fresnelActiveZones.forEach(function(item) {
if (window.Fresnel && item && item.wireframe) {
// Shared Fresnel module ellipsoid
Fresnel.removeFresnelEllipsoid(item);
} else if (item && item.geometry) {
// Legacy wireframe object
if (_scene) _scene.remove(item);
item.geometry.dispose();
item.material.dispose();
}
wireframe.geometry.dispose();
wireframe.material.dispose();
});
_fresnelActiveZones = [];
}
/**
* Toggle visibility of Fresnel zone overlays for active links.
* Delegates to the app.js Fresnel debug overlay system.
* @param {boolean} visible - Whether to show Fresnel zones
*/
function toggleFresnelZones(visible) {
_fresnelZonesVisible = visible;
if (visible) {
rebuildActiveFresnelZones();
} else {
clearActiveFresnelZones();
if (window.toggleFresnelDebugOverlay) {
toggleFresnelDebugOverlay(visible);
}
}