Compare commits

3 Commits
dev ... main

Author SHA1 Message Date
506402ce16 Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-03-20 21:17:30 +00:00
9d135bf2e1 revert 49a9c33836
revert feat(earth): toolbar and zoom improvements

- Add box-sizing/padding normalization to toolbar buttons
- Remove zoom slider, implement click/hold zoom behavior (+/- buttons)
- Add 10% step on click, 1% continuous on hold
- Fix satellite init: show satellite points immediately, delay trail visibility
- Fix breathing effect: faster pulse, wider opacity range
- Add toggle-cables functionality with visibility state
- Initialize satellites and cables as visible by default
2026-03-20 21:16:45 +00:00
linkong
49a9c33836 feat(earth): toolbar and zoom improvements
- Add box-sizing/padding normalization to toolbar buttons
- Remove zoom slider, implement click/hold zoom behavior (+/- buttons)
- Add 10% step on click, 1% continuous on hold
- Fix satellite init: show satellite points immediately, delay trail visibility
- Fix breathing effect: faster pulse, wider opacity range
- Add toggle-cables functionality with visibility state
- Initialize satellites and cables as visible by default
2026-03-20 17:13:02 +08:00
5 changed files with 47 additions and 432 deletions

View File

@@ -1,136 +0,0 @@
# 卫星预测轨道显示功能
## TL;DR
> 锁定卫星时显示绕地球完整一圈的预测轨道轨迹,从当前位置向外渐变消失
## Context
### 目标
点击锁定卫星 → 显示该卫星绕地球一周的完整预测轨道(而非当前的历史轨迹)
### 当前实现
- `TRAIL_LENGTH = 30` - 历史轨迹点数,每帧 push 当前位置
- 显示最近30帧历史轨迹类似彗星尾巴
### 参考: SatelliteMap.space
- 锁定时显示预测轨道
- 颜色从当前位置向外渐变消失
- 使用 satellite.js与本项目相同
## 实现状态
### ✅ 已完成
- [x] 计算卫星轨道周期(基于 `meanMotion`
- [x] 生成预测轨道点10秒采样间隔
- [x] 创建独立预测轨道渲染对象
- [x] 锁定卫星时显示预测轨道
- [x] 解除锁定时隐藏预测轨道
- [x] 颜色渐变:当前位置(亮) → 轨道终点(暗)
- [x] 页面隐藏时清除轨迹(防止切回时闪现)
### 🚧 进行中
- [ ] 完整圆环轨道(部分卫星因 SGP4 计算问题使用 fallback 圆形轨道)
- [ ] 每颗卫星只显示一条轨道
## 技术细节
### 轨道周期计算
```javascript
function calculateOrbitalPeriod(meanMotion) {
return 86400 / meanMotion;
}
```
### 预测轨道计算
```javascript
function calculatePredictedOrbit(satellite, periodSeconds, sampleInterval = 10) {
const points = [];
const samples = Math.ceil(periodSeconds / sampleInterval);
const now = new Date();
// Full orbit: from now to now+period
for (let i = 0; i <= samples; i++) {
const time = new Date(now.getTime() + i * sampleInterval * 1000);
const pos = computeSatellitePosition(satellite, time);
if (pos) points.push(pos);
}
// Fallback: 如果真实位置计算点太少,使用圆形 fallback
if (points.length < samples * 0.5) {
points.length = 0;
// ... 圆形轨道生成
}
return points;
}
```
### 渲染对象
```javascript
let predictedOrbitLine = null;
export function showPredictedOrbit(satellite) {
hidePredictedOrbit();
// ... 计算并渲染轨道
}
export function hidePredictedOrbit() {
if (predictedOrbitLine) {
earthObjRef.remove(predictedOrbitLine);
predictedOrbitLine.geometry.dispose();
predictedOrbitLine.material.dispose();
predictedOrbitLine = null;
}
}
```
## 已知问题
### 1. TLE 格式问题
`computeSatellitePosition` 使用自行构建的 TLE 格式,对某些卫星返回 null。当前使用 fallback 圆形轨道作为补偿。
### 2. 多条轨道
部分情况下锁定时会显示多条轨道。需要确保 `hidePredictedOrbit()` 被正确调用。
## 性能考虑
### 点数估算
| 卫星类型 | 周期 | 10秒采样 | 点数 |
|---------|------|---------|------|
| LEO | 90分钟 | 540秒 | ~54点 |
| MEO | 12小时 | 4320秒 | ~432点 |
| GEO | 24小时 | 8640秒 | ~864点 |
### 优化策略
- 当前方案(~900点 GEO性能可接受
- 如遇性能问题GEO 降低采样率到 30秒
## 验证方案
### QA Scenarios
**Scenario: 锁定 Starlink 卫星显示预测轨道**
1. 打开浏览器,进入 Earth 页面
2. 显示卫星(点击按钮)
3. 点击一颗 Starlink 卫星(低轨道 LEO
4. 验证:出现黄色预测轨道线,从卫星向外绕行
5. 验证:颜色从亮黄渐变到暗蓝
6. 验证:轨道完整闭环
**Scenario: 锁定 GEO 卫星显示预测轨道**
1. 筛选一颗 GEO 卫星(倾斜角 0-10° 或高轨道)
2. 点击锁定
3. 验证:显示完整 24 小时轨道(或 fallback 圆形轨道)
4. 验证:点数合理(~864点或 fallback
**Scenario: 解除锁定隐藏预测轨道**
1. 锁定一颗卫星,显示预测轨道
2. 点击地球空白处解除锁定
3. 验证:预测轨道消失
**Scenario: 切换页面后轨迹不闪现**
1. 锁定一颗卫星
2. 切换到其他标签页
3. 等待几秒
4. 切回页面
5. 验证:轨迹不突然闪现累积

View File

@@ -54,24 +54,10 @@ export const CABLE_STATE = {
}; };
export const SATELLITE_CONFIG = { export const SATELLITE_CONFIG = {
maxCount: 5000, maxCount: 2000,
trailLength: 10, dotSize: 1.5,
dotSize: 4, trailLength: 30,
ringSize: 0.07, apiPath: '/api/v1/visualization/geo/satellites'
apiPath: '/api/v1/visualization/geo/satellites',
breathingSpeed: 0.08,
breathingScaleAmplitude: 0.15,
breathingOpacityMin: 0.5,
breathingOpacityMax: 0.8,
dotBreathingSpeed: 0.12,
dotBreathingScaleAmplitude: 0.2,
dotOpacityMin: 0.7,
dotOpacityMax: 1.0
};
export const PREDICTED_ORBIT_CONFIG = {
sampleInterval: 10,
opacity: 0.8
}; };
export const GRID_CONFIG = { export const GRID_CONFIG = {

View File

@@ -14,7 +14,7 @@ import {
} from './ui.js'; } from './ui.js';
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js'; import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints, getShowCables } from './cables.js'; import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints, getShowCables } from './cables.js';
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions, showPredictedOrbit, hidePredictedOrbit, updateBreathingPhase } from './satellites.js'; import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js';
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js'; import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js'; import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
@@ -32,12 +32,8 @@ let lockedObject = null;
let lockedObjectType = null; let lockedObjectType = null;
let dragStartTime = 0; let dragStartTime = 0;
let isLongDrag = false; let isLongDrag = false;
let lastSatClickTime = 0;
let lastSatClickIndex = 0;
let lastSatClickPos = { x: 0, y: 0 };
export function clearLockedObject() { export function clearLockedObject() {
hidePredictedOrbit();
hoveredCable = null; hoveredCable = null;
hoveredSatellite = null; hoveredSatellite = null;
hoveredSatelliteIndex = null; hoveredSatelliteIndex = null;
@@ -47,7 +43,6 @@ export function clearLockedObject() {
lockedObjectType = null; lockedObjectType = null;
lockedSatellite = null; lockedSatellite = null;
lockedSatelliteIndex = null; lockedSatelliteIndex = null;
window.lockedSatelliteIndex = null;
cableLockedData = null; cableLockedData = null;
} }
@@ -139,7 +134,6 @@ export function init() {
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = CONFIG.defaultCameraZ; camera.position.z = CONFIG.defaultCameraZ;
window.camera = camera;
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' }); renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
@@ -208,7 +202,6 @@ async function loadData(showWhiteSphere = false) {
(async () => { (async () => {
const satCount = await loadSatellites(); const satCount = await loadSatellites();
console.log(`卫星数据加载完成: ${satCount}`); console.log(`卫星数据加载完成: ${satCount}`);
document.getElementById('satellite-count').textContent = satCount + ' 颗';
updateSatellitePositions(); updateSatellitePositions();
console.log('卫星位置已更新'); console.log('卫星位置已更新');
toggleSatellites(true); toggleSatellites(true);
@@ -435,26 +428,8 @@ function onClick(event, camera, renderer) {
setAutoRotate(false); setAutoRotate(false);
handleCableClick(clickedCable); handleCableClick(clickedCable);
} else if (satIntersects.length > 0) { } else if (satIntersects.length > 0) {
const now = Date.now(); const index = satIntersects[0].index;
const clickX = event.clientX; const sat = selectSatellite(index);
const clickY = event.clientY;
let selectedIndex;
if (satIntersects.length > 1 &&
now - lastSatClickTime < 500 &&
Math.abs(clickX - lastSatClickPos.x) < 30 &&
Math.abs(clickY - lastSatClickPos.y) < 30) {
const currentIdx = satIntersects.findIndex(s => s.index === lastSatClickIndex);
selectedIndex = satIntersects[(currentIdx + 1) % satIntersects.length].index;
} else {
selectedIndex = satIntersects[0].index;
}
lastSatClickTime = now;
lastSatClickIndex = selectedIndex;
lastSatClickPos = { x: clickX, y: clickY };
const sat = selectSatellite(selectedIndex);
if (sat && sat.properties) { if (sat && sat.properties) {
clearLockedObject(); clearLockedObject();
@@ -462,14 +437,12 @@ function onClick(event, camera, renderer) {
lockedObject = sat; lockedObject = sat;
lockedObjectType = 'satellite'; lockedObjectType = 'satellite';
lockedSatellite = sat; lockedSatellite = sat;
lockedSatelliteIndex = selectedIndex; lockedSatelliteIndex = index;
window.lockedSatelliteIndex = selectedIndex;
showPredictedOrbit(sat);
setAutoRotate(false); setAutoRotate(false);
const satPositions = getSatellitePositions(); const satPositions = getSatellitePositions();
if (satPositions && satPositions[selectedIndex]) { if (satPositions && satPositions[index]) {
setSatelliteRingState(selectedIndex, 'locked', satPositions[selectedIndex].current); setSatelliteRingState(index, 'locked', satPositions[index].current);
} }
const props = sat.properties; const props = sat.properties;
@@ -525,9 +498,6 @@ function animate() {
const satPositions = getSatellitePositions(); const satPositions = getSatellitePositions();
// 更新呼吸动画相位
updateBreathingPhase();
if (lockedObjectType === 'satellite' && lockedSatelliteIndex !== null) { if (lockedObjectType === 'satellite' && lockedSatelliteIndex !== null) {
if (satPositions && satPositions[lockedSatelliteIndex]) { if (satPositions && satPositions[lockedSatelliteIndex]) {
updateLockedRingPosition(satPositions[lockedSatelliteIndex].current); updateLockedRingPosition(satPositions[lockedSatelliteIndex].current);

View File

@@ -14,19 +14,13 @@ let selectedSatellite = null;
let satellitePositions = []; let satellitePositions = [];
let hoverRingSprite = null; let hoverRingSprite = null;
let lockedRingSprite = null; let lockedRingSprite = null;
let lockedDotSprite = null;
export let breathingPhase = 0;
export function updateBreathingPhase() {
breathingPhase += SATELLITE_CONFIG.breathingSpeed;
}
const SATELLITE_API = SATELLITE_CONFIG.apiPath + '?limit=' + SATELLITE_CONFIG.maxCount; const SATELLITE_API = SATELLITE_CONFIG.apiPath + '?limit=' + SATELLITE_CONFIG.maxCount;
const MAX_SATELLITES = SATELLITE_CONFIG.maxCount; const MAX_SATELLITES = SATELLITE_CONFIG.maxCount;
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength; const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const DOT_TEXTURE_SIZE = 32; const DOT_TEXTURE_SIZE = 32;
function createDotTexture() { function createCircularDotTexture() {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = DOT_TEXTURE_SIZE; canvas.width = DOT_TEXTURE_SIZE;
canvas.height = DOT_TEXTURE_SIZE; canvas.height = DOT_TEXTURE_SIZE;
@@ -74,7 +68,7 @@ export function createSatellites(scene, earthObj) {
const positions = new Float32Array(MAX_SATELLITES * 3); const positions = new Float32Array(MAX_SATELLITES * 3);
const colors = new Float32Array(MAX_SATELLITES * 3); const colors = new Float32Array(MAX_SATELLITES * 3);
const dotTexture = createDotTexture(); const dotTexture = createCircularDotTexture();
const pointsGeometry = new THREE.BufferGeometry(); const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
@@ -86,27 +80,13 @@ export function createSatellites(scene, earthObj) {
vertexColors: true, vertexColors: true,
transparent: true, transparent: true,
opacity: 0.9, opacity: 0.9,
sizeAttenuation: false, sizeAttenuation: true,
alphaTest: 0.1 alphaTest: 0.1
}); });
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial); satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
satellitePoints.visible = false; satellitePoints.visible = false;
satellitePoints.userData = { type: 'satellitePoints' }; satellitePoints.userData = { type: 'satellitePoints' };
const originalScale = { x: 1, y: 1, z: 1 };
satellitePoints.onBeforeRender = (renderer, scene, camera, geometry, material) => {
if (earthObj && earthObj.scale.x !== 1) {
satellitePoints.scale.set(
originalScale.x / earthObj.scale.x,
originalScale.y / earthObj.scale.y,
originalScale.z / earthObj.scale.z
);
} else {
satellitePoints.scale.set(originalScale.x, originalScale.y, originalScale.z);
}
};
earthObj.add(satellitePoints); earthObj.add(satellitePoints);
const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3); const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
@@ -132,9 +112,7 @@ export function createSatellites(scene, earthObj) {
for (let i = 0; i < MAX_SATELLITES; i++) { for (let i = 0; i < MAX_SATELLITES; i++) {
satellitePositions.push({ satellitePositions.push({
current: new THREE.Vector3(), current: new THREE.Vector3(),
trail: [], trail: []
trailIndex: 0,
trailCount: 0
}); });
} }
@@ -157,19 +135,12 @@ function computeSatellitePosition(satellite, time) {
const meanMotion = props.mean_motion || 15; const meanMotion = props.mean_motion || 15;
const epoch = props.epoch || ''; const epoch = props.epoch || '';
// Simplified epoch calculation const year = epoch && epoch.length >= 4 ? parseInt(epoch.substring(0, 4)) : time.getUTCFullYear();
let epochDate = epoch && epoch.length >= 10 ? new Date(epoch) : time; const month = epoch && epoch.length >= 7 ? parseInt(epoch.substring(5, 7)) : time.getUTCMonth() + 1;
const epochYear = epochDate.getUTCFullYear() % 100; const day = epoch && epoch.length >= 10 ? parseInt(epoch.substring(8, 10)) : time.getUTCDate();
const startOfYear = new Date(Date.UTC(epochDate.getUTCFullYear(), 0, 1));
const dayOfYear = Math.floor((epochDate - startOfYear) / 86400000) + 1;
const msOfDay = epochDate.getUTCHours() * 3600000 + epochDate.getUTCMinutes() * 60000 + epochDate.getUTCSeconds() * 1000 + epochDate.getUTCMilliseconds();
const dayFraction = msOfDay / 86400000;
const epochStr = String(epochYear).padStart(2, '0') + String(dayOfYear).padStart(3, '0') + '.' + dayFraction.toFixed(8).substring(2);
// Format eccentricity as "0.0001652" (7 chars after decimal) const tleLine1 = `1 ${String(noradId).padStart(5, '0')}U 00001A ${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}.00000000 .00000000 00000-0 00000-0 0 9999`;
const eccStr = '0' + eccentricity.toFixed(7); const tleLine2 = `2 ${String(noradId).padStart(5, '0')} ${String(raan.toFixed(4)).padStart(8, ' ')} ${String(inclination.toFixed(4)).padStart(8, ' ')} ${String(eccentricity.toFixed(7)).replace('0.', '.')} ${String(argOfPerigee.toFixed(4)).padStart(8, ' ')} ${String(meanAnomaly.toFixed(4)).padStart(8, ' ')} ${String(meanMotion.toFixed(8)).padStart(11, ' ')} 0 9999`;
const tleLine1 = `1 ${noradId.toString().padStart(5)}U 00001A ${epochStr} .00000000 00000-0 00000-0 0 9999`;
const tleLine2 = `2 ${noradId.toString().padStart(5)} ${raan.toFixed(4).padStart(8)} ${inclination.toFixed(4).padStart(8)} ${eccStr.substring(1)} ${argOfPerigee.toFixed(4).padStart(8)} ${meanAnomaly.toFixed(4).padStart(8)} ${meanMotion.toFixed(8).padStart(11)} 0 9999`;
const satrec = twoline2satrec(tleLine1, tleLine2); const satrec = twoline2satrec(tleLine1, tleLine2);
if (!satrec || satrec.error) { if (!satrec || satrec.error) {
@@ -237,7 +208,7 @@ export async function loadSatellites() {
satelliteData = data.features || []; satelliteData = data.features || [];
console.log(`Loaded ${satelliteData.length} satellites`); console.log(`Loaded ${satelliteData.length} satellites`);
return satelliteData.length; return satelliteData;
} catch (error) { } catch (error) {
console.error('Failed to load satellites:', error); console.error('Failed to load satellites:', error);
return []; return [];
@@ -273,11 +244,9 @@ export function updateSatellitePositions(deltaTime = 0) {
satellitePositions[i].current.copy(pos); satellitePositions[i].current.copy(pos);
const satPos = satellitePositions[i]; satellitePositions[i].trail.push(pos.clone());
if (i !== window.lockedSatelliteIndex) { if (satellitePositions[i].trail.length > TRAIL_LENGTH) {
satPos.trail[satPos.trailIndex] = pos.clone(); satellitePositions[i].trail.shift();
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
} }
positions[i * 3] = pos.x; positions[i * 3] = pos.x;
@@ -307,33 +276,20 @@ export function updateSatellitePositions(deltaTime = 0) {
colors[i * 3 + 1] = g; colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b; colors[i * 3 + 2] = b;
const sp = satellitePositions[i]; const trail = satellitePositions[i].trail;
const trail = sp.trail;
const tc = sp.trailCount;
const ti = sp.trailIndex;
for (let j = 0; j < TRAIL_LENGTH; j++) { for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3; const trailIdx = (i * TRAIL_LENGTH + j) * 3;
if (j < tc) { if (j < trail.length) {
const idx = (ti - tc + j + TRAIL_LENGTH) % TRAIL_LENGTH; const t = trail[j];
const t = trail[idx]; trailPositions[trailIdx] = t.x;
if (t) { trailPositions[trailIdx + 1] = t.y;
trailPositions[trailIdx] = t.x; trailPositions[trailIdx + 2] = t.z;
trailPositions[trailIdx + 1] = t.y;
trailPositions[trailIdx + 2] = t.z; const alpha = j / trail.length;
const alpha = (j + 1) / tc; trailColors[trailIdx] = r * alpha;
trailColors[trailIdx] = r * alpha; trailColors[trailIdx + 1] = g * alpha;
trailColors[trailIdx + 1] = g * alpha; trailColors[trailIdx + 2] = b * alpha;
trailColors[trailIdx + 2] = b * alpha;
} else {
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;
}
} else { } else {
trailPositions[trailIdx] = pos.x; trailPositions[trailIdx] = pos.x;
trailPositions[trailIdx + 1] = pos.y; trailPositions[trailIdx + 1] = pos.y;
@@ -345,7 +301,7 @@ export function updateSatellitePositions(deltaTime = 0) {
} }
} }
for (let i = count; i < MAX_SATELLITES; i++) { for (let i = count; i < 2000; i++) {
positions[i * 3] = 0; positions[i * 3] = 0;
positions[i * 3 + 1] = 0; positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0; positions[i * 3 + 2] = 0;
@@ -426,19 +382,12 @@ export function showHoverRing(position, isLocked = false) {
map: ringTexture, map: ringTexture,
transparent: true, transparent: true,
opacity: 0.8, opacity: 0.8,
depthTest: false, depthTest: false
sizeAttenuation: false
}); });
const ringSize = SATELLITE_CONFIG.ringSize;
const sprite = new THREE.Sprite(spriteMaterial); const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.copy(position); sprite.position.copy(position);
sprite.scale.set(3, 3, 1);
const camera = window.camera;
const cameraDistance = camera ? camera.position.distanceTo(position) : 400;
const scale = ringSize;
sprite.scale.set(scale, scale, 1);
console.log(`[Ring create] ringSize: ${ringSize}, camDist: ${cameraDistance}, scale: ${scale}`);
earthObjRef.add(sprite); earthObjRef.add(sprite);
@@ -447,24 +396,6 @@ export function showHoverRing(position, isLocked = false) {
earthObjRef.remove(lockedRingSprite); earthObjRef.remove(lockedRingSprite);
} }
lockedRingSprite = sprite; lockedRingSprite = sprite;
if (lockedDotSprite) {
earthObjRef.remove(lockedDotSprite);
}
const dotCanvas = createBrighterDotCanvas();
const dotTexture = new THREE.CanvasTexture(dotCanvas);
dotTexture.needsUpdate = true;
const dotMaterial = new THREE.SpriteMaterial({
map: dotTexture,
transparent: true,
opacity: 1.0,
depthTest: false
});
lockedDotSprite = new THREE.Sprite(dotMaterial);
lockedDotSprite.position.copy(position);
lockedDotSprite.scale.set(4 * cameraDistance / 200, 4 * cameraDistance / 200, 1);
earthObjRef.add(lockedDotSprite);
} else { } else {
if (hoverRingSprite) { if (hoverRingSprite) {
earthObjRef.remove(hoverRingSprite); earthObjRef.remove(hoverRingSprite);
@@ -475,23 +406,6 @@ export function showHoverRing(position, isLocked = false) {
return sprite; return sprite;
} }
function createBrighterDotCanvas() {
const size = DOT_TEXTURE_SIZE * 2;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const center = size / 2;
const gradient = ctx.createRadialGradient(center, center, 0, center, center, center);
gradient.addColorStop(0, 'rgba(255, 255, 200, 1)');
gradient.addColorStop(0.3, 'rgba(255, 220, 100, 0.9)');
gradient.addColorStop(0.7, 'rgba(255, 180, 50, 0.5)');
gradient.addColorStop(1, 'rgba(255, 150, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
return canvas;
}
export function hideHoverRings() { export function hideHoverRings() {
if (!earthObjRef) return; if (!earthObjRef) return;
@@ -502,45 +416,20 @@ export function hideHoverRings() {
} }
export function hideLockedRing() { export function hideLockedRing() {
if (!earthObjRef) return; if (!earthObjRef || !lockedRingSprite) return;
if (lockedRingSprite) { earthObjRef.remove(lockedRingSprite);
earthObjRef.remove(lockedRingSprite); lockedRingSprite = null;
lockedRingSprite = null;
}
if (lockedDotSprite) {
earthObjRef.remove(lockedDotSprite);
lockedDotSprite = null;
}
} }
export function updateLockedRingPosition(position) { export function updateLockedRingPosition(position) {
const ringSize = SATELLITE_CONFIG.ringSize;
const camera = window.camera;
const cameraDistance = camera ? camera.position.distanceTo(position) : 400;
if (lockedRingSprite && position) { if (lockedRingSprite && position) {
lockedRingSprite.position.copy(position); lockedRingSprite.position.copy(position);
const breathScale = 1 + Math.sin(breathingPhase) * SATELLITE_CONFIG.breathingScaleAmplitude;
lockedRingSprite.scale.set(ringSize * breathScale, ringSize * breathScale, 1);
const breathOpacity = SATELLITE_CONFIG.breathingOpacityMin + Math.sin(breathingPhase) * (SATELLITE_CONFIG.breathingOpacityMax - SATELLITE_CONFIG.breathingOpacityMin);
lockedRingSprite.material.opacity = breathOpacity;
}
if (lockedDotSprite && position) {
lockedDotSprite.position.copy(position);
const dotBreathScale = 1 + Math.sin(breathingPhase) * SATELLITE_CONFIG.dotBreathingScaleAmplitude;
lockedDotSprite.scale.set(4 * cameraDistance / 200 * dotBreathScale, 4 * cameraDistance / 200 * dotBreathScale, 1);
lockedDotSprite.material.opacity = SATELLITE_CONFIG.dotOpacityMin + Math.sin(breathingPhase) * (SATELLITE_CONFIG.dotOpacityMax - SATELLITE_CONFIG.dotOpacityMin);
} }
} }
export function updateHoverRingPosition(position) { export function updateHoverRingPosition(position) {
const ringSize = SATELLITE_CONFIG.ringSize;
const camera = window.camera;
const cameraDistance = camera ? camera.position.distanceTo(position) : 400;
const scale = ringSize;
if (hoverRingSprite && position) { if (hoverRingSprite && position) {
hoverRingSprite.position.copy(position); hoverRingSprite.position.copy(position);
hoverRingSprite.scale.set(scale, scale, 1);
console.log(`[Hover update] ringSize: ${ringSize}, camDist: ${cameraDistance}, scale: ${scale}`);
} }
} }
@@ -565,91 +454,3 @@ export function initSatelliteScene(scene, earth) {
sceneRef = scene; sceneRef = scene;
earthObjRef = earth; earthObjRef = earth;
} }
let predictedOrbitLine = null;
function calculateOrbitalPeriod(meanMotion) {
return 86400 / meanMotion;
}
function calculatePredictedOrbit(satellite, periodSeconds, sampleInterval = 10) {
const points = [];
const samples = Math.ceil(periodSeconds / sampleInterval);
const now = new Date();
// Full orbit: from now to now+period (complete circle forward)
for (let i = 0; i <= samples; i++) {
const time = new Date(now.getTime() + i * sampleInterval * 1000);
const pos = computeSatellitePosition(satellite, time);
if (pos) points.push(pos);
}
// If we don't have enough points, use fallback orbit
if (points.length < samples * 0.5) {
points.length = 0;
const radius = CONFIG.earthRadius + 5;
const noradId = satellite.properties?.norad_cat_id || 0;
const inclination = satellite.properties?.inclination || 53;
const raan = satellite.properties?.raan || 0;
const meanAnomaly = satellite.properties?.mean_anomaly || 0;
for (let i = 0; i <= samples; i++) {
const theta = (i / samples) * Math.PI * 2;
const phi = (inclination * Math.PI / 180);
const x = radius * Math.sin(phi) * Math.cos(theta + raan * Math.PI / 180);
const y = radius * Math.cos(phi);
const z = radius * Math.sin(phi) * Math.sin(theta + raan * Math.PI / 180);
points.push(new THREE.Vector3(x, y, z));
}
}
return points;
}
export function showPredictedOrbit(satellite) {
hidePredictedOrbit();
const props = satellite.properties;
const meanMotion = props?.mean_motion || 15;
const periodSeconds = calculateOrbitalPeriod(meanMotion);
const points = calculatePredictedOrbit(satellite, periodSeconds);
if (points.length < 2) return;
const positions = new Float32Array(points.length * 3);
const colors = new Float32Array(points.length * 3);
for (let i = 0; i < points.length; i++) {
positions[i * 3] = points[i].x;
positions[i * 3 + 1] = points[i].y;
positions[i * 3 + 2] = points[i].z;
const t = i / (points.length - 1);
colors[i * 3] = 1 - t * 0.4;
colors[i * 3 + 1] = 1 - t * 0.6;
colors[i * 3 + 2] = t;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.LineBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
predictedOrbitLine = new THREE.Line(geometry, material);
earthObjRef.add(predictedOrbitLine);
}
export function hidePredictedOrbit() {
if (predictedOrbitLine) {
earthObjRef.remove(predictedOrbitLine);
predictedOrbitLine.geometry.dispose();
predictedOrbitLine.material.dispose();
predictedOrbitLine = null;
}
}

View File

@@ -25,19 +25,13 @@ start() {
PYTHONPATH="$SCRIPT_DIR/backend" nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 & PYTHONPATH="$SCRIPT_DIR/backend" nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 &
BACKEND_PID=$! BACKEND_PID=$!
echo " 等待后端启动..." sleep 3
for i in {1..10}; do
sleep 2 if ! curl -s http://localhost:8000/health > /dev/null 2>&1; then
if curl -s http://localhost:8000/health > /dev/null 2>&1; then echo -e "${RED}❌ 后端启动失败${NC}"
echo -e " ${GREEN}✅ 后端已就绪${NC}" tail -10 /tmp/planet_backend.log
break exit 1
fi fi
if [ $i -eq 10 ]; then
echo -e "${RED}❌ 后端启动失败${NC}"
tail -10 /tmp/planet_backend.log
exit 1
fi
done
echo -e "${BLUE}🌐 启动前端...${NC}" echo -e "${BLUE}🌐 启动前端...${NC}"
pkill -f "vite" 2>/dev/null || true pkill -f "vite" 2>/dev/null || true