import * as THREE from "three"; const EARTH_RADIUS_KM = 6378.137; const SURFACE_SCALE = 1.003; const SURFACE_OFFSET = 0.72; const CLUSTER_DIAMETER_KM_APPROX = 4500; const CLUSTER_RADIUS_KM_BASE = CLUSTER_DIAMETER_KM_APPROX / 2; const SURFACE_AXIS = new THREE.Vector3(0, 0, 1); function disposeMaterial(material) { if (!material) return; if (Array.isArray(material)) { material.forEach(disposeMaterial); return; } material.dispose(); } function disposeObjectTree(object) { if (!object) return; object.traverse((child) => { if (child.geometry) { child.geometry.dispose(); } if (child.material) { disposeMaterial(child.material); } }); } function createIridiumClusterMaterial() { return new THREE.ShaderMaterial({ transparent: true, side: THREE.DoubleSide, depthTest: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3, blending: THREE.AdditiveBlending, uniforms: { uColor: { value: new THREE.Color(0x5faeff) }, uOpacity: { value: 0.24 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 uColor; uniform float uOpacity; varying vec2 vUv; void main() { vec2 p = vUv * 2.0 - 1.0; float ellipseMetric = p.x * p.x * 0.82 + p.y * p.y * 1.06; float alpha = exp(-ellipseMetric * 1.05) * (1.0 - smoothstep(0.86, 1.24, ellipseMetric)); alpha *= uOpacity; if (alpha <= 0.001) discard; gl_FragColor = vec4(uColor, alpha); } `, }); } function projectOffsetToSurface( centerNormal, alongTrack, crossTrack, alongKm, crossKm, earthRadiusWorld, ) { const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM; const surfaceRadius = earthRadiusWorld * SURFACE_SCALE + SURFACE_OFFSET; return centerNormal .clone() .multiplyScalar(earthRadiusWorld) .addScaledVector(alongTrack, alongKm * worldUnitsPerKm) .addScaledVector(crossTrack, crossKm * worldUnitsPerKm) .normalize() .multiplyScalar(surfaceRadius); } function computeClusterRadiusKm(altitudeKm) { const altitudeScale = THREE.MathUtils.clamp( (Number(altitudeKm) || 780) / 780, 0.88, 1.18, ); return CLUSTER_RADIUS_KM_BASE * altitudeScale; } export function createIridiumFootprintAdapter({ earthObj, earthRadiusWorld, renderOrder, }) { if (!earthObj) return null; const group = new THREE.Group(); group.name = "iridium-footprint-overlay"; group.renderOrder = renderOrder; group.userData = { earthRadiusWorld, clusterGlow: null, }; const clusterGlow = new THREE.Mesh( new THREE.CircleGeometry(1, 72), createIridiumClusterMaterial(), ); clusterGlow.name = "iridium-cluster-glow"; clusterGlow.renderOrder = renderOrder - 1; group.add(clusterGlow); group.userData.clusterGlow = clusterGlow; earthObj.add(group); return group; } export function updateIridiumFootprintAdapter( group, { position, alongTrack, crossTrack, altitudeKm }, ) { if (!group || !position || !alongTrack || !crossTrack) return; const earthRadiusWorld = group.userData?.earthRadiusWorld || EARTH_RADIUS_KM; const centerNormal = position.clone().normalize(); const clusterRadiusKm = computeClusterRadiusKm(altitudeKm); const clusterGlow = group.userData?.clusterGlow || null; const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM; if (clusterGlow) { const clusterCenter = projectOffsetToSurface( centerNormal, alongTrack, crossTrack, 0, 0, earthRadiusWorld, ); const clusterNormal = clusterCenter.clone().normalize(); clusterGlow.position.copy(clusterCenter); clusterGlow.quaternion.setFromUnitVectors(SURFACE_AXIS, clusterNormal); clusterGlow.scale.set( clusterRadiusKm * worldUnitsPerKm * 1.18, clusterRadiusKm * worldUnitsPerKm * 0.96, 1, ); } } export function disposeIridiumFootprintAdapter(group, earthObj) { if (!group) return; if (earthObj) { earthObj.remove(group); } else if (group.parent) { group.parent.remove(group); } disposeObjectTree(group); }