release: bump version to 0.31.0

This commit is contained in:
linkong
2026-04-21 18:35:40 +08:00
parent 0f89372d71
commit b7647379de
21 changed files with 1833 additions and 293 deletions

View File

@@ -6,6 +6,7 @@ import { CONFIG, SATELLITE_CONFIG } from "./constants.js";
import { latLonToVector3 } from "./utils.js";
let satellitePoints = null;
let satelliteBackdropPoints = null;
let satelliteTrails = null;
let satelliteData = [];
let showSatellites = false;
@@ -17,6 +18,7 @@ let lockedRingSprite = null;
let lockedDotSprite = null;
let predictedOrbitLine = null;
let relatedSatelliteSprites = [];
let highlightedSatelliteIndices = null;
let earthObjRef = null;
let sceneRef = null;
let cameraRef = null;
@@ -24,6 +26,7 @@ let lockedSatelliteIndex = null;
let hoveredSatelliteIndex = null;
let positionUpdateAccumulator = 0;
let satelliteCapacity = 0;
let satelliteSatrecCache = new Map();
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const DOT_TEXTURE_SIZE = 32;
@@ -117,6 +120,10 @@ export function updateBreathingPhase(deltaTime = 16) {
breathingPhase += SATELLITE_CONFIG.breathingSpeed * (deltaTime / 16);
}
function getBreathingPulse(phase) {
return 0.5 + 0.5 * Math.sin(phase);
}
export function getSatelliteLegendItems() {
const presentKeys = new Set();
@@ -204,6 +211,37 @@ function createDotTexture() {
return texture;
}
function createBackdropDotTexture() {
const canvas = document.createElement("canvas");
canvas.width = DOT_TEXTURE_SIZE;
canvas.height = DOT_TEXTURE_SIZE;
const ctx = canvas.getContext("2d");
const center = DOT_TEXTURE_SIZE / 2;
const radius = center - 1;
const gradient = ctx.createRadialGradient(
center,
center,
0,
center,
center,
radius,
);
gradient.addColorStop(0, "rgba(7, 14, 27, 0.98)");
gradient.addColorStop(0.55, "rgba(7, 14, 27, 0.88)");
gradient.addColorStop(0.85, "rgba(7, 14, 27, 0.34)");
gradient.addColorStop(1, "rgba(7, 14, 27, 0)");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(center, center, radius, 0, Math.PI * 2);
ctx.fill();
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
const size = DOT_TEXTURE_SIZE * 2;
const canvas = document.createElement("canvas");
@@ -226,8 +264,21 @@ function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
export function createSatellites(scene, earthObj) {
initSatelliteScene(scene, earthObj);
const dotTexture = createDotTexture();
const backdropTexture = createBackdropDotTexture();
const pointsGeometry = new THREE.BufferGeometry();
const backdropGeometry = new THREE.BufferGeometry();
const backdropMaterial = new THREE.PointsMaterial({
size: SATELLITE_CONFIG.dotSize * 1.28,
map: backdropTexture,
color: 0x0b1626,
transparent: true,
opacity: 0.42,
sizeAttenuation: false,
alphaTest: 0.04,
depthWrite: false,
});
const pointsMaterial = new THREE.PointsMaterial({
size: SATELLITE_CONFIG.dotSize,
@@ -237,29 +288,45 @@ export function createSatellites(scene, earthObj) {
opacity: 0.9,
sizeAttenuation: false,
alphaTest: 0.1,
depthWrite: false,
});
satelliteBackdropPoints = new THREE.Points(backdropGeometry, backdropMaterial);
satelliteBackdropPoints.visible = false;
satelliteBackdropPoints.userData = { type: "satelliteBackdropPoints" };
satelliteBackdropPoints.renderOrder = 5;
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
satellitePoints.visible = false;
satellitePoints.userData = { type: "satellitePoints" };
satellitePoints.renderOrder = 6;
const originalScale = { x: 1, y: 1, z: 1 };
satellitePoints.onBeforeRender = () => {
const syncPointScale = () => {
if (earthObj && earthObj.scale.x !== 1) {
satellitePoints.scale.set(
originalScale.x / earthObj.scale.x,
originalScale.y / earthObj.scale.y,
originalScale.z / earthObj.scale.z,
);
const scaleX = originalScale.x / earthObj.scale.x;
const scaleY = originalScale.y / earthObj.scale.y;
const scaleZ = originalScale.z / earthObj.scale.z;
satellitePoints.scale.set(scaleX, scaleY, scaleZ);
if (satelliteBackdropPoints) {
satelliteBackdropPoints.scale.set(scaleX, scaleY, scaleZ);
}
} else {
satellitePoints.scale.set(
originalScale.x,
originalScale.y,
originalScale.z,
);
satellitePoints.scale.set(originalScale.x, originalScale.y, originalScale.z);
if (satelliteBackdropPoints) {
satelliteBackdropPoints.scale.set(
originalScale.x,
originalScale.y,
originalScale.z,
);
}
}
};
satelliteBackdropPoints.onBeforeRender = syncPointScale;
satellitePoints.onBeforeRender = syncPointScale;
earthObj.add(satelliteBackdropPoints);
earthObj.add(satellitePoints);
const trailGeometry = new THREE.BufferGeometry();
@@ -281,7 +348,12 @@ export function createSatellites(scene, earthObj) {
return satellitePoints;
}
function getRequestedSatelliteLimit() {
function getRequestedSatelliteLimit(limitOverride) {
if (limitOverride === null) return null;
if (Number.isFinite(limitOverride) && limitOverride > 0) {
return Math.floor(limitOverride);
}
return SATELLITE_CONFIG.maxCount < 0 ? null : SATELLITE_CONFIG.maxCount;
}
@@ -295,13 +367,50 @@ function createSatellitePositionState() {
}
function ensureSatelliteCapacity(count) {
if (!satellitePoints || !satelliteTrails) return;
if (!satellitePoints || !satelliteBackdropPoints || !satelliteTrails) return;
const nextCapacity = Math.max(count, 0);
if (nextCapacity === satelliteCapacity) return;
const previousPointPositions =
satellitePoints.geometry.attributes.position?.array || null;
const previousBackdropPositions =
satelliteBackdropPoints.geometry.attributes.position?.array || null;
const previousColors = satellitePoints.geometry.attributes.color?.array || null;
const previousTrailPositions =
satelliteTrails.geometry.attributes.position?.array || null;
const previousTrailColors =
satelliteTrails.geometry.attributes.color?.array || null;
const previousSatellitePositions = satellitePositions;
const previousCapacity = satelliteCapacity;
const positions = new Float32Array(nextCapacity * 3);
const backdropPositions = new Float32Array(nextCapacity * 3);
const colors = new Float32Array(nextCapacity * 3);
if (previousPointPositions) {
positions.set(
previousPointPositions.subarray(0, Math.min(previousPointPositions.length, positions.length)),
);
}
if (previousBackdropPositions) {
backdropPositions.set(
previousBackdropPositions.subarray(
0,
Math.min(previousBackdropPositions.length, backdropPositions.length),
),
);
}
if (previousColors) {
colors.set(previousColors.subarray(0, Math.min(previousColors.length, colors.length)));
}
satelliteBackdropPoints.geometry.setAttribute(
"position",
new THREE.BufferAttribute(backdropPositions, 3),
);
satelliteBackdropPoints.geometry.setDrawRange(
0,
Math.min(previousCapacity, nextCapacity),
);
satellitePoints.geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3),
@@ -310,10 +419,26 @@ function ensureSatelliteCapacity(count) {
"color",
new THREE.BufferAttribute(colors, 3),
);
satellitePoints.geometry.setDrawRange(0, 0);
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),
),
);
}
if (previousTrailColors) {
trailColors.set(
previousTrailColors.subarray(
0,
Math.min(previousTrailColors.length, trailColors.length),
),
);
}
satelliteTrails.geometry.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3),
@@ -323,10 +448,19 @@ function ensureSatelliteCapacity(count) {
new THREE.BufferAttribute(trailColors, 3),
);
satellitePositions = Array.from(
{ length: nextCapacity },
createSatellitePositionState,
);
satellitePositions = Array.from({ length: nextCapacity }, (_, index) => {
const previousState = previousSatellitePositions[index];
if (!previousState) {
return createSatellitePositionState();
}
return {
current: previousState.current.clone(),
trail: previousState.trail.slice(),
trailIndex: previousState.trailIndex,
trailCount: previousState.trailCount,
};
});
satelliteCapacity = nextCapacity;
}
@@ -337,7 +471,7 @@ function computeSatellitePosition(satellite, time) {
return null;
}
const satrec = buildSatrecFromProperties(props, time);
const satrec = getOrBuildSatrec(props, time);
if (!satrec || satrec.error) {
return null;
}
@@ -382,6 +516,45 @@ function buildSatrecFromProperties(props, fallbackTime) {
return twoline2satrec(tleLines.line1, tleLines.line2);
}
function getSatelliteSatrecCacheKey(props) {
if (!props?.norad_cat_id) {
return null;
}
if (props.tle_line1 && props.tle_line2) {
return `tle:${props.norad_cat_id}:${props.tle_line1}:${props.tle_line2}`;
}
if (props.epoch) {
return [
"elements",
props.norad_cat_id,
props.epoch,
props.inclination,
props.raan,
props.eccentricity,
props.arg_of_perigee,
props.mean_anomaly,
props.mean_motion,
].join(":");
}
return null;
}
function getOrBuildSatrec(props, fallbackTime) {
const cacheKey = getSatelliteSatrecCacheKey(props);
if (cacheKey && satelliteSatrecCache.has(cacheKey)) {
return satelliteSatrecCache.get(cacheKey);
}
const satrec = buildSatrecFromProperties(props, fallbackTime);
if (cacheKey && satrec && !satrec.error) {
satelliteSatrecCache.set(cacheKey, satrec);
}
return satrec;
}
function computeTleChecksum(line) {
let sum = 0;
@@ -491,8 +664,8 @@ function generateFallbackPosition(satellite, index, total) {
return new THREE.Vector3(x, y, z);
}
export async function loadSatellites() {
const limit = getRequestedSatelliteLimit();
export async function loadSatellites(options = {}) {
const limit = getRequestedSatelliteLimit(options.limit);
const url = new URL(SATELLITE_CONFIG.apiPath, window.location.origin);
if (limit !== null) {
url.searchParams.set("limit", String(limit));
@@ -505,13 +678,17 @@ export async function loadSatellites() {
const data = await response.json();
satelliteData = data.features || [];
satelliteSatrecCache = new Map();
ensureSatelliteCapacity(satelliteData.length);
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return satelliteData.length;
return {
count: satelliteData.length,
requestedLimit: limit,
};
}
export function updateSatellitePositions(deltaTime = 0, force = false) {
if (!satellitePoints || satelliteData.length === 0) return;
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
const shouldUpdateTrails =
showSatellites || showTrails || lockedSatelliteIndex !== null;
@@ -528,6 +705,8 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
positionUpdateAccumulator = 0;
const positions = satellitePoints.geometry.attributes.position.array;
const backdropPositions =
satelliteBackdropPoints.geometry.attributes.position.array;
const colors = satellitePoints.geometry.attributes.color.array;
const trailPositions = satelliteTrails.geometry.attributes.position.array;
const trailColors = satelliteTrails.geometry.attributes.color.array;
@@ -559,13 +738,23 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
positions[i * 3] = pos.x;
positions[i * 3 + 1] = pos.y;
positions[i * 3 + 2] = pos.z;
backdropPositions[i * 3] = pos.x;
backdropPositions[i * 3 + 1] = pos.y;
backdropPositions[i * 3 + 2] = pos.z;
const rule = getSatelliteLegendRule(props);
const { r, g, b } = getSatelliteRuleColor(rule);
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
if (highlightedSatelliteIndices !== null && !highlightedSatelliteIndices.has(i)) {
const lum = r * 0.299 + g * 0.587 + b * 0.114;
colors[i * 3] = lum * 0.75 + r * 0.25;
colors[i * 3 + 1] = lum * 0.75 + g * 0.25;
colors[i * 3 + 2] = lum * 0.75 + b * 0.25;
} else {
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
}
const satPosition = satellitePositions[i];
for (let j = 0; j < TRAIL_LENGTH; j++) {
@@ -601,6 +790,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
positions[i * 3] = 0;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
backdropPositions[i * 3] = 0;
backdropPositions[i * 3 + 1] = 0;
backdropPositions[i * 3 + 2] = 0;
for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
@@ -613,6 +805,8 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
satellitePoints.geometry.attributes.position.needsUpdate = true;
satellitePoints.geometry.attributes.color.needsUpdate = true;
satellitePoints.geometry.setDrawRange(0, count);
satelliteBackdropPoints.geometry.attributes.position.needsUpdate = true;
satelliteBackdropPoints.geometry.setDrawRange(0, count);
satelliteTrails.geometry.attributes.position.needsUpdate = true;
satelliteTrails.geometry.attributes.color.needsUpdate = true;
@@ -631,6 +825,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
export function toggleSatellites(visible) {
showSatellites = visible;
if (satelliteBackdropPoints) {
satelliteBackdropPoints.visible = visible;
}
if (satellitePoints) {
satellitePoints.visible = visible;
}
@@ -821,10 +1018,15 @@ export function hideLockedRing() {
export function updateLockedRingPosition(position) {
if (!position) return;
if (!lockedRingSprite || !lockedDotSprite) {
showHoverRing(position, true);
}
if (lockedRingSprite) {
lockedRingSprite.position.copy(position);
const ringPulse = getBreathingPulse(breathingPhase);
const breathScale =
1 + Math.sin(breathingPhase) * SATELLITE_CONFIG.breathingScaleAmplitude;
1 +
(ringPulse * 2 - 1) * SATELLITE_CONFIG.breathingScaleAmplitude;
lockedRingSprite.scale.set(
SATELLITE_CONFIG.ringSize * breathScale,
SATELLITE_CONFIG.ringSize * breathScale,
@@ -832,20 +1034,21 @@ export function updateLockedRingPosition(position) {
);
lockedRingSprite.material.opacity =
SATELLITE_CONFIG.breathingOpacityMin +
Math.sin(breathingPhase) *
ringPulse *
(SATELLITE_CONFIG.breathingOpacityMax -
SATELLITE_CONFIG.breathingOpacityMin);
}
if (lockedDotSprite) {
lockedDotSprite.position.copy(position);
const dotPulse = getBreathingPulse(breathingPhase);
const dotBreathScale =
1 +
Math.sin(breathingPhase) * SATELLITE_CONFIG.dotBreathingScaleAmplitude;
(dotPulse * 2 - 1) * SATELLITE_CONFIG.dotBreathingScaleAmplitude;
lockedDotSprite.scale.set(4 * dotBreathScale, 4 * dotBreathScale, 1);
lockedDotSprite.material.opacity =
SATELLITE_CONFIG.dotOpacityMin +
Math.sin(breathingPhase) *
dotPulse *
(SATELLITE_CONFIG.dotOpacityMax - SATELLITE_CONFIG.dotOpacityMin);
}
}
@@ -881,6 +1084,15 @@ export function setSatelliteRingState(index, state, position) {
}
}
function applyDimMaterialState(isDimmed) {
if (satellitePoints) {
satellitePoints.material.opacity = isDimmed ? 0.32 : 0.9;
}
if (satelliteBackdropPoints) {
satelliteBackdropPoints.material.opacity = isDimmed ? 0.12 : 0.42;
}
}
export function clearRelatedSatelliteHighlights() {
relatedSatelliteSprites.forEach((item) => {
if (item.sprite) {
@@ -888,12 +1100,16 @@ export function clearRelatedSatelliteHighlights() {
}
});
relatedSatelliteSprites = [];
highlightedSatelliteIndices = null;
applyDimMaterialState(false);
}
export function highlightRelatedSatellites(indices, color = "#7dd3fc") {
clearRelatedSatelliteHighlights();
if (!Array.isArray(indices) || indices.length === 0) return;
highlightedSatelliteIndices = new Set(indices);
applyDimMaterialState(true);
indices.forEach((index) => {
const pos = satellitePositions?.[index]?.current;
if (!pos) return;
@@ -1049,10 +1265,12 @@ export function hidePredictedOrbit() {
export function clearSatelliteData() {
satelliteData = [];
satelliteSatrecCache = new Map();
selectedSatellite = null;
lockedSatelliteIndex = null;
hoveredSatelliteIndex = null;
positionUpdateAccumulator = 0;
breathingPhase = 0;
satellitePositions.forEach((position) => {
position.current.set(0, 0, 0);
@@ -1075,6 +1293,16 @@ export function clearSatelliteData() {
satellitePoints.geometry.setDrawRange(0, 0);
}
if (satelliteBackdropPoints) {
const backdropPositionAttr =
satelliteBackdropPoints.geometry.attributes.position;
if (backdropPositionAttr?.array) {
backdropPositionAttr.array.fill(0);
backdropPositionAttr.needsUpdate = true;
}
satelliteBackdropPoints.geometry.setDrawRange(0, 0);
}
if (satelliteTrails) {
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
const trailColorAttr = satelliteTrails.geometry.attributes.color;
@@ -1097,6 +1325,11 @@ export function clearSatelliteData() {
export function resetSatelliteState() {
clearSatelliteData();
if (satelliteBackdropPoints) {
disposeObject3D(satelliteBackdropPoints);
satelliteBackdropPoints = null;
}
if (satellitePoints) {
disposeObject3D(satellitePoints);
satellitePoints = null;
@@ -1109,6 +1342,7 @@ export function resetSatelliteState() {
satellitePositions = [];
satelliteCapacity = 0;
satelliteSatrecCache = new Map();
showSatellites = false;
showTrails = true;
}