- Fixed onFresnelMouseMove to properly iterate through arrays of ellipsoids - Fixed showFresnelTooltip to access ellipsoid data from array correctly - Enhanced tooltip to show zone count and improved units display Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
341 lines
13 KiB
JavaScript
341 lines
13 KiB
JavaScript
/**
|
|
* Spaxel Dashboard - Fresnel Zone Helper Module
|
|
*
|
|
* Shared Fresnel zone ellipsoid geometry computation for:
|
|
* - Debug overlay (all active links)
|
|
* - Explainability overlay (contributing links for a specific blob)
|
|
*
|
|
* Provides FresnelEllipsoid() function that returns Three.js meshes
|
|
* ready for scene insertion.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ============================================
|
|
// Configuration
|
|
// ============================================
|
|
const CONFIG = {
|
|
// WiFi wavelengths (c/f in meters)
|
|
wavelength_5ghz: 0.06, // 5 GHz
|
|
wavelength_2_4ghz: 0.125, // 2.4 GHz
|
|
// Geometry
|
|
sphereSegments: 32, // Sphere geometry segments (reduced on mobile)
|
|
sphereHeightSegments: 16,
|
|
wireframeOpacity: 0.6, // Line opacity for wireframe
|
|
fillOpacity: 0.08, // Fill opacity
|
|
mobileViewportWidth: 768, // Width threshold for mobile
|
|
mobileSegments: 16, // Reduced segments for mobile
|
|
mobileHeightSegments: 8
|
|
};
|
|
|
|
// ============================================
|
|
// Private State
|
|
// ============================================
|
|
let _scene = null;
|
|
|
|
/**
|
|
* Initialize the Fresnel module with the Three.js scene.
|
|
* @param {THREE.Scene} scene - The Three.js scene
|
|
*/
|
|
function init(scene) {
|
|
_scene = scene;
|
|
}
|
|
|
|
/**
|
|
* Get WiFi wavelength based on channel number.
|
|
* @param {number} channel - WiFi channel (1-14 for 2.4 GHz, 36-165 for 5 GHz)
|
|
* @returns {number} Wavelength in meters
|
|
*/
|
|
function getWavelengthForChannel(channel) {
|
|
if (!channel || channel < 1) return CONFIG.wavelength_2_4ghz;
|
|
|
|
// 2.4 GHz channels: 1-14
|
|
if (channel <= 14) {
|
|
return CONFIG.wavelength_2_4ghz;
|
|
}
|
|
|
|
// 5 GHz channels: 36 and above
|
|
return CONFIG.wavelength_5ghz;
|
|
}
|
|
|
|
/**
|
|
* Calculate Fresnel zone ellipsoid parameters for a link.
|
|
* Based on the first Fresnel zone geometry.
|
|
*
|
|
* For a link with TX at position P1 and RX at position P2:
|
|
* - Link distance d = |P1 - P2|
|
|
* - WiFi wavelength lambda: 5 GHz -> lambda = 0.06m, 2.4 GHz -> lambda = 0.125m
|
|
* - Semi-major axis: a = (d + lambda/2) / 2
|
|
* - Semi-minor axis: b = sqrt(a^2 - (d/2)^2)
|
|
* - Ellipsoid centre: midpoint(P1, P2)
|
|
* - Ellipsoid orientation: major axis along the P1->P2 unit vector
|
|
*
|
|
* @param {THREE.Vector3} tx - Transmitter position
|
|
* @param {THREE.Vector3} rx - Receiver position
|
|
* @param {number} channel - WiFi channel number (for wavelength)
|
|
* @returns {Object} Ellipsoid parameters: { center, semiAxes, rotation, lambda, d, a, b }
|
|
*/
|
|
function calculateFresnelEllipsoid(tx, rx, channel) {
|
|
return calculateFresnelEllipsoidForZone(tx, rx, channel, 1);
|
|
}
|
|
|
|
/**
|
|
* Calculate Fresnel zone ellipsoid parameters for a specific zone number.
|
|
* Based on the nth Fresnel zone geometry.
|
|
*
|
|
* For the nth Fresnel zone:
|
|
* - Semi-major axis: a = (d + n*lambda/2) / 2
|
|
* - Semi-minor axis: b = sqrt(a^2 - (d/2)^2)
|
|
*
|
|
* @param {THREE.Vector3} tx - Transmitter position
|
|
* @param {THREE.Vector3} rx - Receiver position
|
|
* @param {number} channel - WiFi channel number (for wavelength)
|
|
* @param {number} zoneNumber - Fresnel zone number (1-based, typically 1-5)
|
|
* @returns {Object} Ellipsoid parameters: { center, semiAxes, rotation, lambda, d, a, b, zoneNumber }
|
|
*/
|
|
function calculateFresnelEllipsoidForZone(tx, rx, channel, zoneNumber) {
|
|
// Get wavelength based on channel
|
|
const lambda = getWavelengthForChannel(channel);
|
|
|
|
// Direct distance between TX and RX
|
|
const d = tx.distanceTo(rx);
|
|
|
|
// nth Fresnel zone ellipsoid parameters
|
|
// Semi-major axis: a = (d + n*lambda/2) / 2
|
|
const n = Math.max(1, zoneNumber);
|
|
const a = (d + n * lambda / 2) / 2;
|
|
|
|
// Semi-minor axis: b = sqrt(a^2 - (d/2)^2)
|
|
// Using the property that for a prolate spheroid with foci at tx and rx:
|
|
// b^2 = a^2 - (d/2)^2
|
|
const b = Math.sqrt(Math.max(0, a * a - (d / 2) * (d / 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
|
|
// X-axis is the major axis of the ellipsoid (a), Y and Z are minor (b)
|
|
const direction = new THREE.Vector3().subVectors(rx, tx).normalize();
|
|
const xAxis = new THREE.Vector3(1, 0, 0);
|
|
const quaternion = new THREE.Quaternion().setFromUnitVectors(xAxis, direction);
|
|
|
|
return {
|
|
center: center,
|
|
semiAxes: new THREE.Vector3(a, b, b), // X (major along link), Y, Z (minor)
|
|
rotation: quaternion,
|
|
lambda: lambda,
|
|
d: d,
|
|
a: a,
|
|
b: b,
|
|
zoneNumber: n,
|
|
channel: channel
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a Three.js Mesh for a Fresnel zone ellipsoid.
|
|
* Creates both wireframe and fill meshes for proper visualization.
|
|
*
|
|
* @param {THREE.Vector3} tx - Transmitter position
|
|
* @param {THREE.Vector3} rx - Receiver position
|
|
* @param {number} channel - WiFi channel number
|
|
* @param {number} color - Color hex value (e.g., 0x4FC3F7 for blue)
|
|
* @param {Object} options - Optional settings { wireframeOpacity, fillOpacity, zoneNumber }
|
|
* @returns {Object} Object containing { wireframe, fill, data } meshes
|
|
*/
|
|
function FresnelEllipsoid(tx, rx, channel, color, options) {
|
|
if (!_scene) {
|
|
console.warn('[Fresnel] Scene not initialized. Call Fresnel.init(scene) first.');
|
|
return null;
|
|
}
|
|
|
|
options = options || {};
|
|
const wireframeOpacity = options.wireframeOpacity !== undefined ? options.wireframeOpacity : CONFIG.wireframeOpacity;
|
|
const fillOpacity = options.fillOpacity !== undefined ? options.fillOpacity : CONFIG.fillOpacity;
|
|
const zoneNumber = options.zoneNumber || 1;
|
|
|
|
// Calculate ellipsoid geometry for the specified zone
|
|
const ellipsoid = calculateFresnelEllipsoidForZone(tx, rx, channel, zoneNumber);
|
|
|
|
// Determine segment count based on viewport (mobile optimization)
|
|
const isMobile = window.innerWidth < CONFIG.mobileViewportWidth;
|
|
const segments = isMobile ? CONFIG.mobileSegments : CONFIG.sphereSegments;
|
|
const heightSegments = isMobile ? CONFIG.mobileHeightSegments : CONFIG.sphereHeightSegments;
|
|
|
|
// Create unit sphere geometry — will be scaled via mesh.scale
|
|
const geometry = new THREE.SphereGeometry(1, segments, heightSegments);
|
|
|
|
// Create wireframe using EdgesGeometry for crisp edges
|
|
const edgesGeometry = new THREE.EdgesGeometry(geometry);
|
|
const wireframeMaterial = new THREE.LineBasicMaterial({
|
|
color: color,
|
|
transparent: true,
|
|
opacity: wireframeOpacity,
|
|
depthTest: true,
|
|
depthWrite: false
|
|
});
|
|
const wireframe = new THREE.LineSegments(edgesGeometry, wireframeMaterial);
|
|
|
|
// Create fill mesh
|
|
const fillMaterial = new THREE.MeshBasicMaterial({
|
|
color: color,
|
|
transparent: true,
|
|
opacity: fillOpacity,
|
|
depthWrite: false,
|
|
side: THREE.DoubleSide
|
|
});
|
|
const fill = new THREE.Mesh(geometry, fillMaterial);
|
|
|
|
// Apply non-uniform scaling: X = semi-major (a), Y = semi-minor (b), Z = semi-minor (b)
|
|
wireframe.scale.copy(ellipsoid.semiAxes);
|
|
fill.scale.copy(ellipsoid.semiAxes);
|
|
|
|
// Position at ellipsoid center
|
|
wireframe.position.copy(ellipsoid.center);
|
|
fill.position.copy(ellipsoid.center);
|
|
|
|
// Apply rotation to align with link axis
|
|
wireframe.quaternion.copy(ellipsoid.rotation);
|
|
fill.quaternion.copy(ellipsoid.rotation);
|
|
|
|
// Store metadata for raycasting and interactions
|
|
const data = {
|
|
tx: tx.clone(),
|
|
rx: rx.clone(),
|
|
channel: channel,
|
|
zoneNumber: ellipsoid.zoneNumber,
|
|
lambda: ellipsoid.lambda,
|
|
d: ellipsoid.d,
|
|
a: ellipsoid.a,
|
|
b: ellipsoid.b,
|
|
semiAxes: ellipsoid.semiAxes.clone(),
|
|
center: ellipsoid.center.clone(),
|
|
rotation: ellipsoid.rotation.clone()
|
|
};
|
|
|
|
wireframe.userData = { fresnelEllipsoid: data };
|
|
fill.userData = { fresnelEllipsoid: data };
|
|
|
|
return {
|
|
wireframe: wireframe,
|
|
fill: fill,
|
|
data: data
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add a Fresnel ellipsoid to the scene.
|
|
* Convenience function that creates and adds the mesh.
|
|
*
|
|
* @param {THREE.Vector3} tx - Transmitter position
|
|
* @param {THREE.Vector3} rx - Receiver position
|
|
* @param {number} channel - WiFi channel number
|
|
* @param {number} color - Color hex value
|
|
* @param {Object} options - Optional settings
|
|
* @returns {Object} Object containing { wireframe, fill, data }
|
|
*/
|
|
function addFresnelEllipsoid(tx, rx, channel, color, options) {
|
|
const ellipsoid = FresnelEllipsoid(tx, rx, channel, color, options);
|
|
if (!ellipsoid) return null;
|
|
|
|
if (_scene) {
|
|
_scene.add(ellipsoid.wireframe);
|
|
_scene.add(ellipsoid.fill);
|
|
}
|
|
|
|
return ellipsoid;
|
|
}
|
|
|
|
/**
|
|
* Remove a Fresnel ellipsoid from the scene.
|
|
*
|
|
* @param {Object} ellipsoid - Object returned from addFresnelEllipsoid or FresnelEllipsoid
|
|
*/
|
|
function removeFresnelEllipsoid(ellipsoid) {
|
|
if (!ellipsoid) return;
|
|
|
|
if (ellipsoid.wireframe) {
|
|
if (_scene) _scene.remove(ellipsoid.wireframe);
|
|
if (ellipsoid.wireframe.geometry) ellipsoid.wireframe.geometry.dispose();
|
|
if (ellipsoid.wireframe.material) ellipsoid.wireframe.material.dispose();
|
|
}
|
|
|
|
if (ellipsoid.fill) {
|
|
if (_scene) _scene.remove(ellipsoid.fill);
|
|
if (ellipsoid.fill.geometry) ellipsoid.fill.geometry.dispose();
|
|
if (ellipsoid.fill.material) ellipsoid.fill.material.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add multiple Fresnel zone ellipsoids (zones 1-maxZone) for a single link.
|
|
* Creates wireframe-only ellipsoids for zones 2-5 to avoid visual clutter.
|
|
*
|
|
* @param {THREE.Vector3} tx - Transmitter position
|
|
* @param {THREE.Vector3} rx - Receiver position
|
|
* @param {number} channel - WiFi channel number
|
|
* @param {number} maxZone - Maximum zone number to create (default 5)
|
|
* @param {Object} options - Optional settings { zone1Color, zoneColors, wireframeOpacity }
|
|
* @returns {Array} Array of ellipsoid objects, one per zone
|
|
*/
|
|
function addFresnelEllipsoidMultiZone(tx, rx, channel, maxZone, options) {
|
|
if (!_scene) {
|
|
console.warn('[Fresnel] Scene not initialized. Call Fresnel.init(scene) first.');
|
|
return [];
|
|
}
|
|
|
|
maxZone = maxZone || 5;
|
|
options = options || {};
|
|
|
|
// Zone 1 is green (most sensitive)
|
|
const zone1Color = options.zone1Color || 0x66bb6a;
|
|
|
|
// Colors for zones 2-5 (gradient from cyan to blue)
|
|
const zoneColors = options.zoneColors || [0x4dd0e1, 0x26c6da, 0x00bcd4, 0x0097a7];
|
|
|
|
const wireframeOpacity = options.wireframeOpacity !== undefined ? options.wireframeOpacity : CONFIG.wireframeOpacity;
|
|
const fillOpacity = options.fillOpacity !== undefined ? options.fillOpacity : CONFIG.fillOpacity;
|
|
|
|
const ellipsoids = [];
|
|
|
|
for (let n = 1; n <= maxZone; n++) {
|
|
const color = (n === 1) ? zone1Color : (zoneColors[n - 2] || 0x0097a7);
|
|
|
|
// For zones 2+, use wireframe only to reduce visual clutter
|
|
const zoneOptions = {
|
|
wireframeOpacity: wireframeOpacity,
|
|
fillOpacity: (n === 1) ? fillOpacity : 0, // No fill for zones 2+
|
|
zoneNumber: n
|
|
};
|
|
|
|
const ellipsoid = FresnelEllipsoid(tx, rx, channel, color, zoneOptions);
|
|
if (ellipsoid) {
|
|
if (_scene) {
|
|
_scene.add(ellipsoid.wireframe);
|
|
if (ellipsoid.fill) _scene.add(ellipsoid.fill);
|
|
}
|
|
ellipsoids.push(ellipsoid);
|
|
}
|
|
}
|
|
|
|
return ellipsoids;
|
|
}
|
|
|
|
// ============================================
|
|
// Public API
|
|
// ============================================
|
|
window.Fresnel = {
|
|
init: init,
|
|
calculateFresnelEllipsoid: calculateFresnelEllipsoid,
|
|
calculateFresnelEllipsoidForZone: calculateFresnelEllipsoidForZone,
|
|
FresnelEllipsoid: FresnelEllipsoid,
|
|
addFresnelEllipsoid: addFresnelEllipsoid,
|
|
addFresnelEllipsoidMultiZone: addFresnelEllipsoidMultiZone,
|
|
removeFresnelEllipsoid: removeFresnelEllipsoid,
|
|
// Configuration access
|
|
CONFIG: CONFIG
|
|
};
|
|
|
|
console.log('[Fresnel] Module loaded');
|
|
})();
|