release: bump version to 0.40.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rayd1o
2026-04-26 05:03:30 +08:00
parent 5f47ec1659
commit f9c1334365
10 changed files with 406 additions and 163 deletions

View File

@@ -66,6 +66,61 @@ const SATELLITE_CONSTELLATION_LABELS = Object.freeze({
});
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const TRAIL_RIBBON_VERTEX_SHADER = /* glsl */ `
attribute vec3 instanceStart;
attribute vec3 instanceEnd;
attribute vec3 instanceColorStart;
attribute vec3 instanceColorEnd;
uniform vec2 resolution;
uniform float lineWidth;
varying vec3 vColor;
void main() {
float t = position.x;
float side = position.y;
vec4 clipStart = projectionMatrix * modelViewMatrix * vec4(instanceStart, 1.0);
vec4 clipEnd = projectionMatrix * modelViewMatrix * vec4(instanceEnd, 1.0);
vec2 screenStart = (clipStart.xy / clipStart.w * 0.5 + 0.5) * resolution;
vec2 screenEnd = (clipEnd.xy / clipEnd.w * 0.5 + 0.5) * resolution;
vec2 dir = screenEnd - screenStart;
float segLen = length(dir);
vec4 clipPos = mix(clipStart, clipEnd, t);
if (segLen > 0.001) {
dir /= segLen;
vec2 normal = vec2(-dir.y, dir.x);
clipPos.xy += normal * side * lineWidth * 0.5 / resolution * 2.0 * clipPos.w;
vColor = mix(instanceColorStart, instanceColorEnd, t);
} else {
vColor = vec3(0.0);
}
gl_Position = clipPos;
}
`;
const TRAIL_RIBBON_FRAGMENT_SHADER = /* glsl */ `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`;
const TRAIL_INSTANCE_ATTRIBUTE_NAMES = [
"instanceStart",
"instanceEnd",
"instanceColorStart",
"instanceColorEnd",
];
const FALLBACK_ORBIT_DAY_MS = 24 * 60 * 60 * 1000;
const FALLBACK_MIN_MEAN_MOTION = 12;
const FALLBACK_MEAN_MOTION_SPREAD = 4;
const FALLBACK_TRAIL_TIP_LENGTH = 0.004;
const FALLBACK_TRAIL_ALPHA_START = 0.2;
const FALLBACK_TRAIL_ALPHA_END = 0.8;
const DOT_TEXTURE_SIZE = 32;
const POSITION_UPDATE_INTERVAL_MS = 250;
const BACKGROUND_TRAIL_RESET_DELTA_MS = 2000;
@@ -520,16 +575,32 @@ export function createSatellites(scene, earthObj) {
earthObj.add(satelliteBackdropPoints);
earthObj.add(satellitePoints);
const trailGeometry = new THREE.BufferGeometry();
// Instanced screen-space ribbon: one quad instance per trail segment.
// Single mesh / single draw call for all satellite trails.
const ribbonGeometry = new THREE.InstancedBufferGeometry();
// Base quad: position.x = t (0=seg-start, 1=seg-end), position.y = side (-1/+1)
ribbonGeometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array([0, -1, 0, 0, 1, 0, 1, -1, 0, 1, 1, 0]), 3),
);
ribbonGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array([0, 2, 1, 2, 3, 1]), 1));
const trailMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
const trailResolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
const trailMaterial = new THREE.ShaderMaterial({
uniforms: {
lineWidth: { value: SATELLITE_CONFIG.trailLineWidth },
resolution: { value: trailResolution },
},
vertexShader: TRAIL_RIBBON_VERTEX_SHADER,
fragmentShader: TRAIL_RIBBON_FRAGMENT_SHADER,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
satelliteTrails = new THREE.LineSegments(trailGeometry, trailMaterial);
satelliteTrails = new THREE.Mesh(ribbonGeometry, trailMaterial);
satelliteTrails.onBeforeRender = (renderer) => renderer.getSize(trailResolution);
satelliteTrails.frustumCulled = false;
satelliteTrails.visible = false;
satelliteTrails.userData = { type: "satelliteTrails" };
earthObj.add(satelliteTrails);
@@ -568,15 +639,12 @@ function resetSatelliteTrailState() {
function clearSatelliteTrailGeometry() {
if (!satelliteTrails) return;
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
const trailColorAttr = satelliteTrails.geometry.attributes.color;
if (trailPositionAttr?.array) {
trailPositionAttr.array.fill(0);
trailPositionAttr.needsUpdate = true;
}
if (trailColorAttr?.array) {
trailColorAttr.array.fill(0);
trailColorAttr.needsUpdate = true;
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
const attr = satelliteTrails.geometry.attributes[name];
if (attr?.array) {
attr.array.fill(0);
attr.needsUpdate = true;
}
}
}
@@ -599,10 +667,14 @@ function ensureSatelliteCapacity(count) {
const previousPointAlphas = satellitePoints.geometry.attributes.alpha?.array || null;
const previousBackdropAlphas =
satelliteBackdropPoints.geometry.attributes.alpha?.array || null;
const previousTrailPositions =
satelliteTrails.geometry.attributes.position?.array || null;
const previousTrailColors =
satelliteTrails.geometry.attributes.color?.array || null;
const previousInstanceStarts =
satelliteTrails.geometry.attributes.instanceStart?.array || null;
const previousInstanceEnds =
satelliteTrails.geometry.attributes.instanceEnd?.array || null;
const previousInstanceColorStarts =
satelliteTrails.geometry.attributes.instanceColorStart?.array || null;
const previousInstanceColorEnds =
satelliteTrails.geometry.attributes.instanceColorEnd?.array || null;
const previousSatellitePositions = satellitePositions;
const previousCapacity = satelliteCapacity;
@@ -668,32 +740,48 @@ function ensureSatelliteCapacity(count) {
);
satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity));
const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
const trailColors = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
if (previousTrailPositions) {
trailPositions.set(
previousTrailPositions.subarray(
0,
Math.min(previousTrailPositions.length, trailPositions.length),
),
const segCount = nextCapacity * (TRAIL_LENGTH - 1);
const instanceStarts = new Float32Array(segCount * 3);
const instanceEnds = new Float32Array(segCount * 3);
const instanceColorStarts = new Float32Array(segCount * 3);
const instanceColorEnds = new Float32Array(segCount * 3);
if (previousInstanceStarts) {
instanceStarts.set(
previousInstanceStarts.subarray(0, Math.min(previousInstanceStarts.length, instanceStarts.length)),
);
}
if (previousTrailColors) {
trailColors.set(
previousTrailColors.subarray(
0,
Math.min(previousTrailColors.length, trailColors.length),
),
if (previousInstanceEnds) {
instanceEnds.set(
previousInstanceEnds.subarray(0, Math.min(previousInstanceEnds.length, instanceEnds.length)),
);
}
if (previousInstanceColorStarts) {
instanceColorStarts.set(
previousInstanceColorStarts.subarray(0, Math.min(previousInstanceColorStarts.length, instanceColorStarts.length)),
);
}
if (previousInstanceColorEnds) {
instanceColorEnds.set(
previousInstanceColorEnds.subarray(0, Math.min(previousInstanceColorEnds.length, instanceColorEnds.length)),
);
}
satelliteTrails.geometry.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3),
"instanceStart",
new THREE.InstancedBufferAttribute(instanceStarts, 3),
);
satelliteTrails.geometry.setAttribute(
"color",
new THREE.BufferAttribute(trailColors, 3),
"instanceEnd",
new THREE.InstancedBufferAttribute(instanceEnds, 3),
);
satelliteTrails.geometry.setAttribute(
"instanceColorStart",
new THREE.InstancedBufferAttribute(instanceColorStarts, 3),
);
satelliteTrails.geometry.setAttribute(
"instanceColorEnd",
new THREE.InstancedBufferAttribute(instanceColorEnds, 3),
);
satelliteTrails.geometry.instanceCount = segCount;
satellitePositions = Array.from({ length: nextCapacity }, (_, index) => {
const previousState = previousSatellitePositions[index];
@@ -903,7 +991,7 @@ function buildTleLinesFromElements(props, fallbackTime) {
};
}
function generateFallbackPosition(satellite, index, total) {
function generateFallbackPosition(satellite, index, total, time = new Date()) {
const radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
const noradId = satellite.properties?.norad_cat_id || index;
@@ -915,9 +1003,21 @@ function generateFallbackPosition(satellite, index, total) {
.split("")
.reduce((a, b) => a + b.charCodeAt(0), 0);
const randomOffset = (hash % 1000) / 1000;
const rawMeanMotion = Number(satellite.properties?.mean_motion);
const meanMotion =
Number.isFinite(rawMeanMotion) && rawMeanMotion > 0
? rawMeanMotion
: FALLBACK_MIN_MEAN_MOTION + randomOffset * FALLBACK_MEAN_MOTION_SPREAD;
const normalizedIndex = index / total;
const theta = normalizedIndex * Math.PI * 2 * 10 + (raan * Math.PI) / 180;
const elapsedDays = Number.isFinite(time?.getTime?.())
? time.getTime() / FALLBACK_ORBIT_DAY_MS
: Date.now() / FALLBACK_ORBIT_DAY_MS;
const fallbackPhase = elapsedDays * meanMotion * Math.PI * 2;
const theta =
normalizedIndex * Math.PI * 2 * 10 +
(raan * Math.PI) / 180 +
fallbackPhase;
const phi =
(inclination * Math.PI) / 180 + ((meanAnomaly * Math.PI) / 180) * 0.1;
@@ -946,6 +1046,7 @@ export async function loadSatellites(options = {}) {
const data = await response.json();
satelliteData = data.features || [];
satelliteSatrecCache = new Map();
resetSatelliteTrailState();
ensureSatelliteCapacity(satelliteData.length);
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return {
@@ -958,7 +1059,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
const shouldUpdateTrails =
showSatellites || showTrails || lockedSatelliteIndex !== null;
showSatellites ||
showTrails ||
lockedSatelliteIndex !== null;
const shouldResetTrails =
options.resetTrails ||
(!force && deltaTime >= BACKGROUND_TRAIL_RESET_DELTA_MS);
@@ -988,10 +1091,13 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
const colors = satellitePoints.geometry.attributes.color.array;
const pointAlphas = satellitePoints.geometry.attributes.alpha.array;
const backdropAlphas = satelliteBackdropPoints.geometry.attributes.alpha.array;
const trailPositions = satelliteTrails.geometry.attributes.position.array;
const trailColors = satelliteTrails.geometry.attributes.color.array;
const instanceStarts = satelliteTrails.geometry.attributes.instanceStart.array;
const instanceEnds = satelliteTrails.geometry.attributes.instanceEnd.array;
const instanceColorStarts = satelliteTrails.geometry.attributes.instanceColorStart.array;
const instanceColorEnds = satelliteTrails.geometry.attributes.instanceColorEnd.array;
const baseTime = new Date(Date.now() + elapsedMs);
const count = Math.min(satelliteData.length, satelliteCapacity);
let trailSegmentCount = 0;
for (let i = 0; i < count; i++) {
const satellite = satelliteData[i];
@@ -1003,16 +1109,30 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
let pos = computeSatellitePosition(satellite, adjustedTime);
if (!pos) {
pos = generateFallbackPosition(satellite, i, count);
pos = generateFallbackPosition(satellite, i, count, adjustedTime);
}
satellitePositions[i].current.copy(pos);
if (shouldUpdateTrails && i !== lockedSatelliteIndex) {
if (shouldUpdateTrails) {
const satPos = satellitePositions[i];
satPos.trail[satPos.trailIndex] = pos.clone();
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
if (satPos.trailCount === 0 && TRAIL_LENGTH > 1) {
for (let k = 0; k < TRAIL_LENGTH; k++) {
const offsetMs = (TRAIL_LENGTH - 1 - k) * POSITION_UPDATE_INTERVAL_MS;
const pastTime = new Date(adjustedTime.getTime() - offsetMs);
let pastPos = computeSatellitePosition(satellite, pastTime);
if (!pastPos) {
pastPos = generateFallbackPosition(satellite, i, count, pastTime);
}
satPos.trail[satPos.trailIndex] = pastPos;
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
}
satPos.trailCount = TRAIL_LENGTH;
} else {
satPos.trail[satPos.trailIndex] = pos.clone();
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
}
}
positions[i * 3] = pos.x;
@@ -1040,32 +1160,59 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
backdropAlphas[i] = pointAlpha;
const satPosition = satellitePositions[i];
for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
if (j < satPosition.trailCount) {
const idx =
(satPosition.trailIndex - satPosition.trailCount + j + TRAIL_LENGTH) %
TRAIL_LENGTH;
const trailPoint = satPosition.trail[idx];
if (trailPoint) {
trailPositions[trailIdx] = trailPoint.x;
trailPositions[trailIdx + 1] = trailPoint.y;
trailPositions[trailIdx + 2] = trailPoint.z;
const alpha = (j + 1) / satPosition.trailCount;
trailColors[trailIdx] = r * alpha * trailBrightness;
trailColors[trailIdx + 1] = g * alpha * trailBrightness;
trailColors[trailIdx + 2] = b * alpha * trailBrightness;
continue;
const tc = satPosition.trailCount;
let hasVisibleTrail = false;
for (let j = 0; j < TRAIL_LENGTH - 1; j++) {
if (j + 1 < tc) {
const idxA =
(satPosition.trailIndex - tc + j + TRAIL_LENGTH) % TRAIL_LENGTH;
const idxB =
(satPosition.trailIndex - tc + j + 1 + TRAIL_LENGTH) % TRAIL_LENGTH;
const ptA = satPosition.trail[idxA];
const ptB = satPosition.trail[idxB];
if (ptA && ptB && ptA.distanceToSquared(ptB) > 1e-8) {
const base = trailSegmentCount * 3;
instanceStarts[base] = ptA.x;
instanceStarts[base + 1] = ptA.y;
instanceStarts[base + 2] = ptA.z;
instanceEnds[base] = ptB.x;
instanceEnds[base + 1] = ptB.y;
instanceEnds[base + 2] = ptB.z;
const alphaA = (j + 1) / tc;
const alphaB = (j + 2) / tc;
instanceColorStarts[base] = r * alphaA * trailBrightness;
instanceColorStarts[base + 1] = g * alphaA * trailBrightness;
instanceColorStarts[base + 2] = b * alphaA * trailBrightness;
instanceColorEnds[base] = r * alphaB * trailBrightness;
instanceColorEnds[base + 1] = g * alphaB * trailBrightness;
instanceColorEnds[base + 2] = b * alphaB * trailBrightness;
hasVisibleTrail = true;
trailSegmentCount++;
}
}
trailPositions[trailIdx] = pos.x;
trailPositions[trailIdx + 1] = pos.y;
trailPositions[trailIdx + 2] = pos.z;
trailColors[trailIdx] = 0;
trailColors[trailIdx + 1] = 0;
trailColors[trailIdx + 2] = 0;
}
if (!hasVisibleTrail) {
const base = trailSegmentCount * 3;
const dist = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
const nx = pos.x / dist;
const ny = pos.y / dist;
const nz = pos.z / dist;
const tip = FALLBACK_TRAIL_TIP_LENGTH;
instanceStarts[base] = pos.x + nx * tip;
instanceStarts[base + 1] = pos.y + ny * tip;
instanceStarts[base + 2] = pos.z + nz * tip;
instanceEnds[base] = pos.x;
instanceEnds[base + 1] = pos.y;
instanceEnds[base + 2] = pos.z;
const fallbackAlphaStart = FALLBACK_TRAIL_ALPHA_START * trailBrightness;
const fallbackAlphaEnd = FALLBACK_TRAIL_ALPHA_END * trailBrightness;
instanceColorStarts[base] = r * fallbackAlphaStart;
instanceColorStarts[base + 1] = g * fallbackAlphaStart;
instanceColorStarts[base + 2] = b * fallbackAlphaStart;
instanceColorEnds[base] = r * fallbackAlphaEnd;
instanceColorEnds[base + 1] = g * fallbackAlphaEnd;
instanceColorEnds[base + 2] = b * fallbackAlphaEnd;
trailSegmentCount++;
}
}
@@ -1079,12 +1226,23 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
pointAlphas[i] = 0;
backdropAlphas[i] = 0;
for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
trailPositions[trailIdx] = 0;
trailPositions[trailIdx + 1] = 0;
trailPositions[trailIdx + 2] = 0;
}
}
const trailArrayLength = instanceStarts.length / 3;
for (let i = trailSegmentCount; i < trailArrayLength; i++) {
const base = i * 3;
instanceStarts[base] = 0;
instanceStarts[base + 1] = 0;
instanceStarts[base + 2] = 0;
instanceEnds[base] = 0;
instanceEnds[base + 1] = 0;
instanceEnds[base + 2] = 0;
instanceColorStarts[base] = 0;
instanceColorStarts[base + 1] = 0;
instanceColorStarts[base + 2] = 0;
instanceColorEnds[base] = 0;
instanceColorEnds[base + 1] = 0;
instanceColorEnds[base + 2] = 0;
}
satellitePoints.geometry.attributes.position.needsUpdate = true;
@@ -1095,8 +1253,10 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
satelliteBackdropPoints.geometry.attributes.alpha.needsUpdate = true;
satelliteBackdropPoints.geometry.setDrawRange(0, count);
satelliteTrails.geometry.attributes.position.needsUpdate = true;
satelliteTrails.geometry.attributes.color.needsUpdate = true;
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
satelliteTrails.geometry.attributes[name].needsUpdate = true;
}
satelliteTrails.geometry.instanceCount = trailSegmentCount;
// Keep the hover ring synced with the propagated satellite position even
// when the pointer stays still and no new hover event is emitted.
@@ -2044,24 +2204,8 @@ function updateLockedMarkerVisual(isHovered) {
}
}
function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
if (!earthObjRef) return null;
const ringTexture = createRingTexture(7, 11, color);
const spriteMaterial = new THREE.SpriteMaterial({
map: ringTexture,
transparent: true,
opacity: 0.55,
depthTest: false,
sizeAttenuation: false,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.copy(position);
sprite.scale.set(SATELLITE_CONFIG.ringSize * 0.8, SATELLITE_CONFIG.ringSize * 0.8, 1);
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
earthObjRef.add(sprite);
return sprite;
function createRelatedSatelliteSprite(position) {
return createRingSprite(position, false);
}
export function showHoverRing(position, isLocked = false) {
@@ -2259,7 +2403,7 @@ export function highlightRelatedSatellites(indices, color = "#7dd3fc") {
indices.forEach((index) => {
const pos = satellitePositions?.[index]?.current;
if (!pos) return;
const sprite = createRelatedSatelliteSprite(pos, color);
const sprite = createRelatedSatelliteSprite(pos);
if (!sprite) return;
relatedSatelliteSprites.push({ index, sprite, color });
});