512 lines
14 KiB
Markdown
512 lines
14 KiB
Markdown
# 可视化问题分析与解决方案
|
||
|
||
## 问题 1: 探测器/月球的视觉偏移导致距离失真
|
||
|
||
### 🚨 问题描述
|
||
|
||
当前实现中,为了避免探测器和月球与行星重合,使用了 `renderPosition.ts` 中的智能偏移逻辑:
|
||
|
||
```typescript
|
||
// renderPosition.ts:116-127
|
||
if (realDistance < 0.01) {
|
||
// 月球或表面探测器 - 固定偏移
|
||
visualOffset = planetVisualRadius + 2.0; // 距离表面 2 个单位
|
||
} else if (realDistance < 0.05) {
|
||
visualOffset = planetVisualRadius + 3.0;
|
||
} else {
|
||
visualOffset = planetVisualRadius + 4.0;
|
||
}
|
||
```
|
||
|
||
**问题**:
|
||
- ❌ 月球真实距离地球 0.0026 AU(38万公里)
|
||
- ❌ 但在视觉上被推到距离地球表面 2 个缩放单位
|
||
- ❌ 用户无法判断月球与太阳的真实距离关系
|
||
- ❌ 探测器的真实轨道距离完全失真
|
||
|
||
**影响范围**:
|
||
- 月球(Earth's Moon)
|
||
- 火星探测器(Perseverance, Curiosity)
|
||
- 木星探测器(Juno)
|
||
- 土星探测器(Cassini)
|
||
|
||
---
|
||
|
||
## 问题 2: 矮行星轨道数据加载量过大
|
||
|
||
### 🚨 问题描述
|
||
|
||
当前 `DwarfPlanetOrbits.tsx` 的实现:
|
||
|
||
```typescript
|
||
// DwarfPlanetOrbits.tsx:61-72
|
||
const startDate = new Date('2020-01-01');
|
||
const endDate = new Date('2030-01-01');
|
||
|
||
const response = await fetch(
|
||
`http://localhost:8000/api/celestial/positions?` +
|
||
`body_ids=${bodyIds}&` +
|
||
`start_time=${startDate.toISOString()}&` +
|
||
`end_time=${endDate.toISOString()}&` +
|
||
`step=30d` // 10 年 = 120 个点/天体
|
||
);
|
||
```
|
||
|
||
**问题**:
|
||
- ❌ 矮行星公转周期:冥王星 248 年,阋神星 557 年
|
||
- ❌ 10 年数据只能显示公转轨道的 4-18%
|
||
- ❌ 需要请求完整轨道周期数据(248-557 年)
|
||
- ❌ 数据量巨大:冥王星完整轨道 = 248 年 × 12 月 = 2976 个点
|
||
- ❌ 首次加载会触发大量 NASA API 调用
|
||
|
||
**矮行星轨道周期**:
|
||
| 天体 | 公转周期 | 完整轨道点数(30天/点) | 数据量 |
|
||
|------|----------|----------------------|--------|
|
||
| 冥王星 | 248 年 | 2,976 点 | 71 KB |
|
||
| 阋神星 | 557 年 | 6,684 点 | 160 KB |
|
||
| 妊神星 | 285 年 | 3,420 点 | 82 KB |
|
||
| 鸟神星 | 309 年 | 3,708 点 | 89 KB |
|
||
| 谷神星 | 4.6 年 | 55 点 | 1.3 KB |
|
||
|
||
**总数据量**:~403 KB(单次请求)
|
||
|
||
---
|
||
|
||
## 解决方案对比
|
||
|
||
### 方案 1: 视觉偏移 + 真实距离提示 ⭐⭐⭐
|
||
|
||
**策略**:保持当前视觉偏移,但在 UI 上明确标注真实距离。
|
||
|
||
**实现**:
|
||
|
||
1. **在天体详情卡片中显示真实距离**:
|
||
```typescript
|
||
// ProbeList.tsx 或 CelestialBody 详情
|
||
{hasOffset && (
|
||
<div className="text-yellow-400 text-xs">
|
||
<span>⚠️ 视觉位置已调整便于观察</span>
|
||
<span>真实距离: {realDistance.toFixed(4)} AU</span>
|
||
<span>约 {(realDistance * 149597870.7).toFixed(0)} 千米</span>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
2. **添加真实轨道线(虚线)**:
|
||
```typescript
|
||
// 在 Probe.tsx 中添加真实轨道路径
|
||
{hasOffset && (
|
||
<Line
|
||
points={[
|
||
new Vector3(realPos.x, realPos.y, realPos.z), // 真实位置
|
||
new Vector3(visualPos.x, visualPos.y, visualPos.z) // 视觉位置
|
||
]}
|
||
color="yellow"
|
||
lineWidth={1}
|
||
dashed
|
||
dashSize={0.1}
|
||
gapSize={0.05}
|
||
/>
|
||
)}
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 保持当前的视觉清晰度
|
||
- ✅ 通过文字和虚线提示真实位置
|
||
- ✅ 实现简单,改动小
|
||
|
||
**缺点**:
|
||
- ⚠️ 用户仍然无法直观看到真实距离
|
||
- ⚠️ 需要额外的 UI 提示
|
||
|
||
---
|
||
|
||
### 方案 2: 移除视觉偏移,优化缩放算法 ⭐⭐⭐⭐⭐(推荐)
|
||
|
||
**策略**:改进距离缩放算法,让近距离天体也能清晰显示,无需偏移。
|
||
|
||
**核心思路**:
|
||
- 在 **极近距离**(< 0.01 AU)使用**对数缩放**
|
||
- 让月球和探测器保持真实方向,但有足够的视觉间隔
|
||
|
||
**实现**:
|
||
|
||
```typescript
|
||
// scaleDistance.ts - 新增超近距离缩放
|
||
export function scaleDistance(distanceInAU: number): number {
|
||
// Ultra-close region (< 0.001 AU): extreme expansion for moons/probes
|
||
if (distanceInAU < 0.001) {
|
||
// 对数缩放:0.0001 AU → 0.1, 0.001 AU → 0.5
|
||
return 0.1 + Math.log10(distanceInAU + 0.0001) * 0.4;
|
||
}
|
||
|
||
// Very close region (0.001-0.01 AU): strong expansion
|
||
if (distanceInAU < 0.01) {
|
||
// 0.001 AU → 0.5, 0.01 AU → 1.5
|
||
return 0.5 + (distanceInAU - 0.001) * 100;
|
||
}
|
||
|
||
// Close region (0.01-0.1 AU): moderate expansion
|
||
if (distanceInAU < 0.1) {
|
||
return 1.5 + (distanceInAU - 0.01) * 20;
|
||
}
|
||
|
||
// Inner solar system (0.1-2 AU): expand by 3x
|
||
if (distanceInAU < 2) {
|
||
return 3.3 + (distanceInAU - 0.1) * 3;
|
||
}
|
||
|
||
// Middle region (2-10 AU): normal scale
|
||
if (distanceInAU < 10) {
|
||
return 9 + (distanceInAU - 2) * 1.5;
|
||
}
|
||
|
||
// Outer solar system (10-50 AU): compressed
|
||
if (distanceInAU < 50) {
|
||
return 21 + (distanceInAU - 10) * 0.5;
|
||
}
|
||
|
||
// Very far (> 50 AU): heavily compressed
|
||
return 41 + (distanceInAU - 50) * 0.2;
|
||
}
|
||
```
|
||
|
||
**修改 renderPosition.ts**:
|
||
```typescript
|
||
// 移除视觉偏移逻辑,直接使用缩放位置
|
||
export function calculateRenderPosition(
|
||
body: CelestialBody,
|
||
allBodies: CelestialBody[]
|
||
): { x: number; y: number; z: number } {
|
||
const pos = body.positions[0];
|
||
if (!pos) {
|
||
return { x: 0, y: 0, z: 0 };
|
||
}
|
||
|
||
// 直接使用改进的缩放算法,无需偏移
|
||
const scaled = scalePosition(pos.x, pos.y, pos.z);
|
||
return { x: scaled.x, y: scaled.y, z: scaled.z };
|
||
}
|
||
```
|
||
|
||
**效果对比**:
|
||
|
||
| 天体 | 真实距离 | 旧缩放 | 新缩放 | 改进 |
|
||
|------|----------|--------|--------|------|
|
||
| 月球 | 0.0026 AU | 0.0078 | 0.76 | **98倍** |
|
||
| 火星探测器 | ~1.5 AU | 4.5 | 7.5 | 更清晰 |
|
||
| 地球 | 1.0 AU | 3.0 | 5.7 | 更合理 |
|
||
|
||
**优点**:
|
||
- ✅ 保持真实的空间关系
|
||
- ✅ 月球和探测器仍然可见(足够大)
|
||
- ✅ 用户可以直观理解距离
|
||
- ✅ 无需 UI 提示
|
||
|
||
**缺点**:
|
||
- ⚠️ 需要调整缩放参数,可能需要多次调试
|
||
|
||
---
|
||
|
||
### 方案 3: 双模式切换(真实模式 vs 演示模式)⭐⭐⭐⭐
|
||
|
||
**策略**:提供两种显示模式,用户可以切换。
|
||
|
||
**实现**:
|
||
```typescript
|
||
// App.tsx
|
||
const [visualMode, setVisualMode] = useState<'realistic' | 'demo'>('demo');
|
||
|
||
// Header.tsx 添加切换按钮
|
||
<button onClick={() => setVisualMode(mode === 'realistic' ? 'demo' : 'realistic')}>
|
||
{visualMode === 'realistic' ? '真实距离' : '演示模式'}
|
||
</button>
|
||
|
||
// renderPosition.ts
|
||
export function calculateRenderPosition(
|
||
body: CelestialBody,
|
||
allBodies: CelestialBody[],
|
||
mode: 'realistic' | 'demo'
|
||
): Position {
|
||
if (mode === 'realistic') {
|
||
// 使用改进的缩放,无偏移
|
||
return scalePosition(pos.x, pos.y, pos.z);
|
||
} else {
|
||
// 使用视觉偏移
|
||
return calculateDemoPosition(body, allBodies);
|
||
}
|
||
}
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 灵活性最高
|
||
- ✅ 满足不同用户需求
|
||
- ✅ 教育价值高
|
||
|
||
**缺点**:
|
||
- ⚠️ 实现复杂度较高
|
||
- ⚠️ 需要维护两套逻辑
|
||
|
||
---
|
||
|
||
## 矮行星轨道问题解决方案
|
||
|
||
### 方案 A: 预计算并存储完整轨道数据 ⭐⭐⭐⭐⭐(推荐)
|
||
|
||
**策略**:按照 `ORBIT_OPTIMIZATION.md` 的方案 1A 实现。
|
||
|
||
**实施步骤**:
|
||
|
||
1. **创建 orbits 表**(已在 ORBIT_OPTIMIZATION.md 中定义)
|
||
|
||
2. **后端管理接口生成轨道**:
|
||
```python
|
||
# app/api/routes.py
|
||
@router.post("/admin/orbits/generate")
|
||
async def generate_dwarf_planet_orbits(db: AsyncSession = Depends(get_db)):
|
||
"""为矮行星生成完整轨道数据"""
|
||
dwarf_planets = await celestial_body_service.get_bodies_by_type(db, "dwarf_planet")
|
||
|
||
orbital_periods = {
|
||
"999": 248, # 冥王星
|
||
"136199": 557, # 阋神星
|
||
"136108": 285, # 妊神星
|
||
"136472": 309, # 鸟神星
|
||
"1": 4.6, # 谷神星
|
||
}
|
||
|
||
for planet in dwarf_planets:
|
||
period_years = orbital_periods.get(planet.id, 250)
|
||
|
||
# 计算采样点数:完整周期,每 30 天一个点
|
||
num_points = min(int(period_years * 365 / 30), 1000) # 最多 1000 点
|
||
|
||
# 查询 NASA Horizons
|
||
start = datetime.utcnow()
|
||
end = start + timedelta(days=period_years * 365)
|
||
step_days = int(period_years * 365 / num_points)
|
||
|
||
positions = await horizons_service.get_body_positions(
|
||
planet.id,
|
||
start,
|
||
end,
|
||
f"{step_days}d"
|
||
)
|
||
|
||
# 保存到 orbits 表
|
||
await orbit_service.save_orbit(
|
||
planet.id,
|
||
[{"x": p.x, "y": p.y, "z": p.z} for p in positions],
|
||
num_points,
|
||
period_years * 365
|
||
)
|
||
|
||
return {"message": f"Generated {len(dwarf_planets)} orbits"}
|
||
```
|
||
|
||
3. **前端从 API 读取**:
|
||
```typescript
|
||
// DwarfPlanetOrbits.tsx - 简化版
|
||
useEffect(() => {
|
||
const fetchOrbits = async () => {
|
||
// 直接从后端读取预存的轨道数据
|
||
const response = await fetch('http://localhost:8000/api/celestial/orbits?body_type=dwarf_planet');
|
||
const data = await response.json();
|
||
|
||
const orbitData = data.orbits.map((orbit: any) => ({
|
||
bodyId: orbit.body_id,
|
||
points: orbit.points.map((p: any) => {
|
||
const scaled = scalePosition(p.x, p.y, p.z);
|
||
return new THREE.Vector3(scaled.x, scaled.z, scaled.y);
|
||
}),
|
||
color: orbit.color || getDefaultColor(orbit.body_name)
|
||
}));
|
||
|
||
setOrbits(orbitData);
|
||
};
|
||
|
||
fetchOrbits();
|
||
}, []);
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 完整准确的轨道
|
||
- ✅ 前端加载快(<1秒)
|
||
- ✅ 无需实时 NASA API 调用
|
||
- ✅ 数据量可接受(总共 ~400 KB)
|
||
|
||
**缺点**:
|
||
- ⚠️ 需要数据库迁移
|
||
- ⚠️ 首次生成需要时间(一次性)
|
||
|
||
---
|
||
|
||
### 方案 B: 使用数学模拟轨道 ⭐⭐⭐⭐
|
||
|
||
**策略**:基于轨道六要素(orbital elements)数学计算。
|
||
|
||
**实现**:
|
||
```typescript
|
||
// EllipticalOrbit.tsx
|
||
interface OrbitalElements {
|
||
a: number; // 半长轴 (AU)
|
||
e: number; // 离心率
|
||
i: number; // 轨道倾角 (度)
|
||
omega: number; // 升交点黄经 (度)
|
||
w: number; // 近日点幅角 (度)
|
||
M0: number; // 平近点角 (度)
|
||
period: number; // 轨道周期 (天)
|
||
}
|
||
|
||
// 冥王星轨道要素(来自 NASA JPL)
|
||
const PLUTO_ELEMENTS: OrbitalElements = {
|
||
a: 39.48,
|
||
e: 0.2488,
|
||
i: 17.16,
|
||
omega: 110.30,
|
||
w: 113.77,
|
||
M0: 14.53,
|
||
period: 90560
|
||
};
|
||
|
||
function generateOrbitPoints(elements: OrbitalElements, numPoints = 360): Vector3[] {
|
||
const points: Vector3[] = [];
|
||
|
||
for (let i = 0; i <= numPoints; i++) {
|
||
const M = (i / numPoints) * 2 * Math.PI; // 平近点角
|
||
|
||
// 求解开普勒方程得到偏近点角 E
|
||
let E = M;
|
||
for (let j = 0; j < 10; j++) {
|
||
E = M + elements.e * Math.sin(E);
|
||
}
|
||
|
||
// 计算真近点角
|
||
const v = 2 * Math.atan(
|
||
Math.sqrt((1 + elements.e) / (1 - elements.e)) * Math.tan(E / 2)
|
||
);
|
||
|
||
// 计算轨道平面坐标
|
||
const r = elements.a * (1 - elements.e * Math.cos(E));
|
||
const x_orb = r * Math.cos(v);
|
||
const y_orb = r * Math.sin(v);
|
||
|
||
// 旋转到黄道坐标系
|
||
const point = rotateToEcliptic(x_orb, y_orb, 0, elements);
|
||
|
||
const scaled = scalePosition(point.x, point.y, point.z);
|
||
points.push(new Vector3(scaled.x, scaled.z, scaled.y));
|
||
}
|
||
|
||
return points;
|
||
}
|
||
```
|
||
|
||
**轨道要素来源**:
|
||
- NASA JPL Small-Body Database: https://ssd.jpl.nasa.gov/sbdb.cgi
|
||
- 可以从后端 `celestial_bodies` 表的 `orbital_elements` 字段读取
|
||
|
||
**优点**:
|
||
- ✅ 不需要网络请求
|
||
- ✅ 瞬时生成
|
||
- ✅ 数学上准确
|
||
- ✅ 可以显示任意时间的轨道
|
||
|
||
**缺点**:
|
||
- ⚠️ 实现复杂(轨道力学)
|
||
- ⚠️ 不考虑摄动(其他行星引力影响)
|
||
- ⚠️ 需要获取和验证轨道要素
|
||
|
||
---
|
||
|
||
### 方案 C: 混合方案 - 谷神星用真实数据,其他用模拟 ⭐⭐⭐
|
||
|
||
**策略**:
|
||
- **谷神星**(4.6 年周期):使用真实数据(55 点,1.3 KB)
|
||
- **冥王星、阋神星等**(>200 年周期):使用数学模拟
|
||
|
||
**理由**:
|
||
- 谷神星周期短,数据量小
|
||
- 长周期矮行星用数学模拟足够准确
|
||
|
||
**实现**:
|
||
```typescript
|
||
// DwarfPlanetOrbits.tsx
|
||
const fetchOrbits = async () => {
|
||
// 只请求谷神星(Ceres)的真实数据
|
||
const ceresOrbit = await fetchRealOrbit('1', 5); // 5 年数据
|
||
|
||
// 其他矮行星用数学模拟
|
||
const plutoOrbit = generateOrbitPoints(PLUTO_ELEMENTS);
|
||
const erisOrbit = generateOrbitPoints(ERIS_ELEMENTS);
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 平衡准确性和性能
|
||
- ✅ 减少数据加载
|
||
- ✅ 关键天体(谷神星)使用真实数据
|
||
|
||
**缺点**:
|
||
- ⚠️ 需要维护两套逻辑
|
||
|
||
---
|
||
|
||
## 推荐实施方案
|
||
|
||
### 问题 1(探测器偏移):**方案 2 - 优化缩放算法**
|
||
- 实施优先级:**高**
|
||
- 预计工时:2-3 小时
|
||
- 风险:低(可以逐步调整参数)
|
||
|
||
### 问题 2(矮行星轨道):**方案 A - 预计算轨道数据**
|
||
- 实施优先级:**中**
|
||
- 预计工时:4-6 小时(包括数据库迁移)
|
||
- 风险:低(数据生成是一次性的)
|
||
|
||
**备选方案**:如果时间紧张,可以先用 **方案 B(数学模拟)** 作为临时解决方案。
|
||
|
||
---
|
||
|
||
## 实施步骤(建议顺序)
|
||
|
||
### 第一阶段:修复探测器偏移问题
|
||
1. 修改 `scaleDistance.ts`,添加超近距离缩放
|
||
2. 简化 `renderPosition.ts`,移除偏移逻辑
|
||
3. 测试月球、火星探测器等的显示效果
|
||
4. 微调缩放参数
|
||
|
||
### 第二阶段:优化矮行星轨道
|
||
1. 创建 `orbits` 表(数据库迁移)
|
||
2. 实现后端轨道生成 API
|
||
3. 在管理后台添加"生成轨道"按钮
|
||
4. 修改 `DwarfPlanetOrbits.tsx` 从 API 读取
|
||
5. 首次生成所有矮行星轨道数据
|
||
|
||
---
|
||
|
||
## 后续优化建议
|
||
|
||
1. **添加缩放级别指示器**:
|
||
- 显示当前视图的缩放比例
|
||
- "内太阳系视图(0-2 AU,放大 3x)"
|
||
|
||
2. **添加距离标尺**:
|
||
- 在场景中显示距离参考线
|
||
- "1 AU = X 屏幕单位"
|
||
|
||
3. **轨道数据自动更新**:
|
||
- 定期(每月)重新生成轨道数据
|
||
- 保持数据时效性
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**创建时间**: 2025-11-29
|
||
**相关文件**:
|
||
- `frontend/src/utils/scaleDistance.ts`
|
||
- `frontend/src/utils/renderPosition.ts`
|
||
- `frontend/src/components/DwarfPlanetOrbits.tsx`
|
||
- `ORBIT_OPTIMIZATION.md`
|