Compare commits
2 Commits
00df13c070
...
14fb922cd2
| Author | SHA1 | Date |
|---|---|---|
|
|
14fb922cd2 | |
|
|
2e31f89464 |
|
|
@ -70,7 +70,8 @@
|
||||||
"Bash(populate_primary_stars.py )",
|
"Bash(populate_primary_stars.py )",
|
||||||
"Bash(recreate_resources_table.py )",
|
"Bash(recreate_resources_table.py )",
|
||||||
"Bash(reset_positions.py )",
|
"Bash(reset_positions.py )",
|
||||||
"Bash(test_pluto.py )"
|
"Bash(test_pluto.py )",
|
||||||
|
"Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend backend/venv/bin/python:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
# Scheduled Jobs System - Code Review Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document summarizes the code review and cleanup performed on the scheduled jobs system.
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. Backend - Removed Debug Logs with Emojis
|
|
||||||
|
|
||||||
#### `app/jobs/predefined.py`
|
|
||||||
- Removed emoji icons from log messages (🌍, 📋, 🔄, ✅, ❌, 🎉, ⚠️)
|
|
||||||
- Changed `logger.info` to `logger.debug` for detailed operation logs
|
|
||||||
- Kept `logger.info` only for high-level operation summaries
|
|
||||||
- Kept `logger.error` and `logger.warning` for error conditions
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```python
|
|
||||||
logger.info(f"🌍 Starting solar system position sync: days={days}")
|
|
||||||
logger.info(f"🔄 Fetching positions for {body.name}")
|
|
||||||
logger.info(f"✅ Saved {count} positions for {body.name}")
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```python
|
|
||||||
logger.info(f"Starting solar system position sync: days={days}")
|
|
||||||
logger.debug(f"Fetching positions for {body.name}")
|
|
||||||
logger.debug(f"Saved {count} positions for {body.name}")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `app/jobs/registry.py`
|
|
||||||
- Changed task registration log from `logger.info` to `logger.debug`
|
|
||||||
- Changed task execution logs from `logger.info` to `logger.debug`
|
|
||||||
- Removed emoji icons (📋, 🚀, ✅)
|
|
||||||
|
|
||||||
#### `app/services/scheduler_service.py`
|
|
||||||
- Removed emoji icons from all log messages (⏰, ❌, ✅)
|
|
||||||
- Kept important lifecycle logs as `logger.info` (start, stop, job scheduling)
|
|
||||||
- Changed detailed execution logs to `logger.debug`
|
|
||||||
|
|
||||||
### 2. Backend - Removed Unused Imports
|
|
||||||
|
|
||||||
#### `app/api/scheduled_job.py`
|
|
||||||
- Removed unused imports: `update`, `delete` from sqlalchemy
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```python
|
|
||||||
from sqlalchemy import select, update, delete
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```python
|
|
||||||
from sqlalchemy import select
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Frontend - Removed Debug Console Logs
|
|
||||||
|
|
||||||
#### `pages/admin/ScheduledJobs.tsx`
|
|
||||||
- Removed `console.log` statements from `loadAvailableTasks()`
|
|
||||||
- Removed `console.error` statements from `loadAvailableTasks()`
|
|
||||||
- Removed `console.log` statements from `handleEdit()`
|
|
||||||
- Removed `console.error` from error handling (kept only toast messages)
|
|
||||||
|
|
||||||
**Removed:**
|
|
||||||
```typescript
|
|
||||||
console.log('Loaded available tasks:', result);
|
|
||||||
console.error('Failed to load available tasks:', error);
|
|
||||||
console.log('Editing record:', record);
|
|
||||||
console.log('Available tasks:', availableTasks);
|
|
||||||
console.error(error);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Quality Improvements
|
|
||||||
|
|
||||||
### 1. Consistent Logging Levels
|
|
||||||
- **ERROR**: For failures that prevent operations
|
|
||||||
- **WARNING**: For non-critical issues (e.g., "No bodies found")
|
|
||||||
- **INFO**: For high-level operation summaries
|
|
||||||
- **DEBUG**: For detailed operation traces
|
|
||||||
|
|
||||||
### 2. Clean User-Facing Messages
|
|
||||||
- All user-facing error messages use toast notifications
|
|
||||||
- No console output in production frontend code
|
|
||||||
- Backend logs are professional and parseable
|
|
||||||
|
|
||||||
### 3. Transaction Safety
|
|
||||||
- Using SQLAlchemy savepoints (`begin_nested()`) for isolated error handling
|
|
||||||
- Proper rollback and commit patterns
|
|
||||||
- Error messages include full traceback for debugging
|
|
||||||
|
|
||||||
## Testing Results
|
|
||||||
|
|
||||||
### Import Test
|
|
||||||
✓ All backend imports successful
|
|
||||||
✓ Task registry properly initialized
|
|
||||||
✓ 2 tasks registered:
|
|
||||||
- sync_solar_system_positions
|
|
||||||
- sync_celestial_events
|
|
||||||
|
|
||||||
### Task Schema Test
|
|
||||||
✓ Task parameters properly defined:
|
|
||||||
- body_ids (array, optional, default=None)
|
|
||||||
- days (integer, optional, default=7)
|
|
||||||
- source (string, optional, default=nasa_horizons_cron)
|
|
||||||
|
|
||||||
### Integration Test
|
|
||||||
✓ Position constraint fixed (nasa_horizons_cron added to CHECK constraint)
|
|
||||||
✓ Manual job execution successful
|
|
||||||
✓ 26 celestial bodies synced with 52 positions
|
|
||||||
✓ Task record properly created and updated
|
|
||||||
✓ No failures during execution
|
|
||||||
|
|
||||||
## Remaining Console Logs (Other Admin Pages)
|
|
||||||
|
|
||||||
The following console logs exist in other admin pages but were left unchanged as they're outside the scope of this scheduled jobs feature:
|
|
||||||
|
|
||||||
- `SystemSettings.tsx`: 1 console.error
|
|
||||||
- `Users.tsx`: 2 console.error
|
|
||||||
- `Dashboard.tsx`: 1 console.error
|
|
||||||
- `StaticData.tsx`: 1 console.error
|
|
||||||
- `CelestialBodies.tsx`: 2 (1 error, 1 for JSON parsing)
|
|
||||||
- `NASADownload.tsx`: 3 (2 debug logs, 1 error)
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
1. `/backend/app/jobs/predefined.py` - Removed emoji logs, adjusted log levels
|
|
||||||
2. `/backend/app/jobs/registry.py` - Changed to debug logging
|
|
||||||
3. `/backend/app/services/scheduler_service.py` - Removed emojis, adjusted log levels
|
|
||||||
4. `/backend/app/api/scheduled_job.py` - Removed unused imports
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
1. `/frontend/src/pages/admin/ScheduledJobs.tsx` - Removed all console logs
|
|
||||||
|
|
||||||
### Database
|
|
||||||
1. `/backend/scripts/fix_position_source_constraint.py` - Fixed CHECK constraint
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
All scheduled jobs related code has been reviewed and cleaned:
|
|
||||||
- ✅ No emoji icons in production logs
|
|
||||||
- ✅ Appropriate logging levels (ERROR/WARNING/INFO/DEBUG)
|
|
||||||
- ✅ No console.log/console.error in frontend
|
|
||||||
- ✅ No unused imports
|
|
||||||
- ✅ All imports and registrations working
|
|
||||||
- ✅ Database constraints fixed
|
|
||||||
- ✅ Integration tests passing
|
|
||||||
|
|
||||||
The code is now production-ready with clean, professional logging suitable for monitoring and debugging.
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
# 恒星系统架构改造 - 进度报告
|
|
||||||
|
|
||||||
## ✅ 已完成工作
|
|
||||||
|
|
||||||
### 1. 数据库架构改造
|
|
||||||
- ✅ 创建 `star_systems` 表
|
|
||||||
- ✅ 添加太阳系初始记录(id=1)
|
|
||||||
- ✅ 扩展 `celestial_bodies` 表(添加 `system_id` 字段)
|
|
||||||
- ✅ 更新所有太阳系天体 `system_id = 1`(30个天体)
|
|
||||||
|
|
||||||
### 2. ORM 模型
|
|
||||||
- ✅ 创建 `StarSystem` ORM 模型
|
|
||||||
- ✅ 更新 `CelestialBody` ORM 模型(添加 system_id 关系)
|
|
||||||
- ✅ 在 `__init__.py` 中注册 StarSystem
|
|
||||||
|
|
||||||
### 3. 数据迁移
|
|
||||||
- ✅ 编写完整的数据迁移脚本(`scripts/migrate_interstellar_data.py`)
|
|
||||||
- ✅ 实现自动中文名翻译功能
|
|
||||||
- ✅ 实现行星数据去重逻辑
|
|
||||||
- ✅ 成功迁移 578 个系外恒星系统
|
|
||||||
- ✅ 成功迁移 898 颗系外行星(去重后)
|
|
||||||
|
|
||||||
### 4. 后端服务层
|
|
||||||
- ✅ 创建 `StarSystemService`(`app/services/star_system_service.py`)
|
|
||||||
- 支持 CRUD 操作
|
|
||||||
- 支持搜索和分页
|
|
||||||
- 支持获取恒星系及其所有天体
|
|
||||||
- 支持统计功能
|
|
||||||
|
|
||||||
- ✅ 创建 Pydantic 模型(`app/models/star_system.py`)
|
|
||||||
- StarSystemBase
|
|
||||||
- StarSystemCreate
|
|
||||||
- StarSystemUpdate
|
|
||||||
- StarSystemResponse
|
|
||||||
- StarSystemWithBodies
|
|
||||||
- StarSystemStatistics
|
|
||||||
|
|
||||||
### 5. 迁移数据统计
|
|
||||||
```
|
|
||||||
恒星系统总数: 579
|
|
||||||
- 太阳系: 1
|
|
||||||
- 系外恒星系: 578
|
|
||||||
|
|
||||||
天体总数: 928
|
|
||||||
- 太阳系天体: 30(含太阳、行星、矮行星、卫星、探测器、彗星)
|
|
||||||
- 系外行星: 898(已去重)
|
|
||||||
|
|
||||||
数据质量:
|
|
||||||
- 去重前行星记录: ~3000+
|
|
||||||
- 去重后行星记录: 898
|
|
||||||
- 去重率: ~70%
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚧 剩余工作
|
|
||||||
|
|
||||||
### 1. 后端 API 开发
|
|
||||||
- [ ] 创建 StarSystem API 路由
|
|
||||||
- GET /api/star-systems(获取所有恒星系统)
|
|
||||||
- GET /api/star-systems/{id}(获取单个恒星系统)
|
|
||||||
- GET /api/star-systems/{id}/bodies(获取恒星系及其天体)
|
|
||||||
- POST /api/admin/star-systems(创建恒星系统)
|
|
||||||
- PUT /api/admin/star-systems/{id}(更新恒星系统)
|
|
||||||
- DELETE /api/admin/star-systems/{id}(删除恒星系统)
|
|
||||||
- GET /api/star-systems/statistics(获取统计信息)
|
|
||||||
|
|
||||||
- [ ] 更新 CelestialBody API
|
|
||||||
- 添加 `system_id` 查询参数
|
|
||||||
- 添加 `include_no_system` 参数(用于包含探测器等)
|
|
||||||
|
|
||||||
### 2. 后台管理界面(Admin Frontend)
|
|
||||||
- [ ] 创建恒星系统管理页面(`/admin/star-systems`)
|
|
||||||
- 列表展示(支持搜索、分页)
|
|
||||||
- 新增恒星系统
|
|
||||||
- 编辑恒星系统
|
|
||||||
- 删除恒星系统(不可删除太阳系)
|
|
||||||
- 查看恒星系详情(含所有行星)
|
|
||||||
|
|
||||||
- [ ] 改造天体管理页面(`/admin/celestial-bodies`)
|
|
||||||
- **关键改动**:先选择恒星系,再列出该恒星系的天体
|
|
||||||
- 添加恒星系选择器(下拉框)
|
|
||||||
- 根据选中的恒星系过滤天体列表
|
|
||||||
- 新增天体时自动设置 `system_id`
|
|
||||||
- 支持在恒星系之间移动天体
|
|
||||||
|
|
||||||
### 3. 前端界面更新
|
|
||||||
- [ ] 更新 GalaxyScene 组件
|
|
||||||
- 使用新的 `/api/star-systems` API
|
|
||||||
- 移除前端行星去重代码
|
|
||||||
- 优化恒星点击事件(使用后端返回的完整数据)
|
|
||||||
|
|
||||||
- [ ] 更新 App.tsx 查询逻辑
|
|
||||||
- Solar 视图:查询 `system_id=1` 的天体
|
|
||||||
- Galaxy 视图:查询所有恒星系统
|
|
||||||
|
|
||||||
### 4. 菜单配置
|
|
||||||
- [ ] 在后台管理菜单中添加"恒星系统管理"入口
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 数据模型关系图
|
|
||||||
|
|
||||||
```
|
|
||||||
star_systems (579条记录)
|
|
||||||
├── id=1: Solar System (太阳系)
|
|
||||||
│ └── celestial_bodies (30条)
|
|
||||||
│ ├── Sun (star)
|
|
||||||
│ ├── Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune (planet)
|
|
||||||
│ ├── Pluto, Ceres, Haumea, Makemake, Eris (dwarf_planet)
|
|
||||||
│ ├── Moon, Io, Europa, Ganymede, Callisto (satellite)
|
|
||||||
│ ├── Voyager 1, Voyager 2, Parker Solar Probe... (probe)
|
|
||||||
│ └── Halley, NEOWISE, C/2020 F3 (comet)
|
|
||||||
│
|
|
||||||
├── id=2: Proxima Cen System (比邻星系统)
|
|
||||||
│ └── celestial_bodies (2条)
|
|
||||||
│ ├── Proxima Cen b (比邻星 b)
|
|
||||||
│ └── Proxima Cen d (比邻星 d)
|
|
||||||
│
|
|
||||||
├── id=3: TRAPPIST-1 System
|
|
||||||
│ └── celestial_bodies (7条)
|
|
||||||
│ └── TRAPPIST-1 b/c/d/e/f/g/h
|
|
||||||
│
|
|
||||||
└── ... (575 more systems)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 下一步行动
|
|
||||||
|
|
||||||
**立即可做:**
|
|
||||||
1. 完成 StarSystem API 路由
|
|
||||||
2. 测试 API 端点
|
|
||||||
3. 开发后台管理界面
|
|
||||||
|
|
||||||
**预计工作量:**
|
|
||||||
- 后端 API:1-2小时
|
|
||||||
- 后台界面:3-4小时
|
|
||||||
- 前端更新:1-2小时
|
|
||||||
- 测试验证:1小时
|
|
||||||
|
|
||||||
**总计:6-9小时**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 技术要点
|
|
||||||
|
|
||||||
### 中文名翻译规则
|
|
||||||
```python
|
|
||||||
# 恒星名翻译示例
|
|
||||||
Proxima Cen → 比邻星
|
|
||||||
Kepler-442 → 开普勒-442
|
|
||||||
TRAPPIST-1 → TRAPPIST-1
|
|
||||||
HD 40307 → HD 40307
|
|
||||||
|
|
||||||
# 行星名翻译示例
|
|
||||||
Proxima Cen b → 比邻星 b
|
|
||||||
Kepler-442 b → 开普勒-442 b
|
|
||||||
```
|
|
||||||
|
|
||||||
### 去重逻辑
|
|
||||||
- 按行星名称(name)去重
|
|
||||||
- 保留字段最完整的记录(非NULL字段最多的)
|
|
||||||
- 平均每个恒星系从5.2条记录减少到1.6条(效率提升70%)
|
|
||||||
|
|
||||||
### 查询优化
|
|
||||||
```sql
|
|
||||||
-- Solar 视图
|
|
||||||
SELECT * FROM celestial_bodies WHERE system_id = 1;
|
|
||||||
|
|
||||||
-- Galaxy 视图
|
|
||||||
SELECT * FROM star_systems WHERE id > 1;
|
|
||||||
|
|
||||||
-- 恒星系详情
|
|
||||||
SELECT * FROM celestial_bodies WHERE system_id = ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档版本**: v1.0
|
|
||||||
**更新时间**: 2025-12-05 19:10
|
|
||||||
**状态**: 数据迁移完成,API开发进行中
|
|
||||||
|
|
@ -126,6 +126,7 @@ async def get_celestial_positions(
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
"is_active": body.is_active, # Include probe active status
|
"is_active": body.is_active, # Include probe active status
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": [{
|
"positions": [{
|
||||||
"time": latest_pos.time.isoformat(),
|
"time": latest_pos.time.isoformat(),
|
||||||
"x": latest_pos.x,
|
"x": latest_pos.x,
|
||||||
|
|
@ -155,6 +156,7 @@ async def get_celestial_positions(
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": [{
|
"positions": [{
|
||||||
"time": last_pos.time.isoformat(),
|
"time": last_pos.time.isoformat(),
|
||||||
"x": last_pos.x,
|
"x": last_pos.x,
|
||||||
|
|
@ -172,6 +174,7 @@ async def get_celestial_positions(
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": []
|
"positions": []
|
||||||
}
|
}
|
||||||
bodies_data.append(body_dict)
|
bodies_data.append(body_dict)
|
||||||
|
|
@ -187,6 +190,7 @@ async def get_celestial_positions(
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": []
|
"positions": []
|
||||||
}
|
}
|
||||||
bodies_data.append(body_dict)
|
bodies_data.append(body_dict)
|
||||||
|
|
@ -266,7 +270,11 @@ async def get_celestial_positions(
|
||||||
db_cached_bodies.append({
|
db_cached_bodies.append({
|
||||||
"id": body.id,
|
"id": body.id,
|
||||||
"name": body.name,
|
"name": body.name,
|
||||||
|
"name_zh": body.name_zh,
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
|
"description": body.description,
|
||||||
|
"is_active": body.is_active,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": cached_response.get("positions", [])
|
"positions": cached_response.get("positions", [])
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
|
@ -310,6 +318,7 @@ async def get_celestial_positions(
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
"is_active": body.is_active,
|
"is_active": body.is_active,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
"time": pos.time.isoformat(),
|
"time": pos.time.isoformat(),
|
||||||
|
|
@ -411,6 +420,7 @@ async def get_celestial_positions(
|
||||||
"name_zh": body.name_zh,
|
"name_zh": body.name_zh,
|
||||||
"type": body.type,
|
"type": body.type,
|
||||||
"description": body.description,
|
"description": body.description,
|
||||||
|
"extra_data": body.extra_data,
|
||||||
"positions": positions_list
|
"positions": positions_list
|
||||||
}
|
}
|
||||||
bodies_data.append(body_dict)
|
bodies_data.append(body_dict)
|
||||||
|
|
|
||||||
|
|
@ -88,12 +88,15 @@ async def sync_solar_system_positions(
|
||||||
logger.info(f"Syncing {len(bodies)} specified bodies")
|
logger.info(f"Syncing {len(bodies)} specified bodies")
|
||||||
else:
|
else:
|
||||||
# Get all active solar system bodies
|
# Get all active solar system bodies
|
||||||
# Typically solar system bodies include planets, dwarf planets, and major satellites
|
# Typically solar system bodies include planets, dwarf planets, major satellites, comets, and probes
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(CelestialBody).where(
|
select(CelestialBody).where(
|
||||||
CelestialBody.is_active == True,
|
CelestialBody.is_active == True,
|
||||||
CelestialBody.system_id == 1,
|
CelestialBody.system_id == 1,
|
||||||
CelestialBody.type.in_(['planet', 'dwarf_planet', 'satellite'])
|
CelestialBody.type.in_([
|
||||||
|
'planet', 'dwarf_planet', 'satellite',
|
||||||
|
'comet', 'probe', 'asteroid','star'
|
||||||
|
])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bodies = result.scalars().all()
|
bodies = result.scalars().all()
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ class CelestialBody(BaseModel):
|
||||||
default_factory=list, description="Position history"
|
default_factory=list, description="Position history"
|
||||||
)
|
)
|
||||||
description: str | None = Field(None, description="Description")
|
description: str | None = Field(None, description="Description")
|
||||||
|
details: str | None = Field(None, description="Details markdown")
|
||||||
is_active: bool | None = Field(None, description="Active status (for probes: True=active, False=inactive)")
|
is_active: bool | None = Field(None, description="Active status (for probes: True=active, False=inactive)")
|
||||||
|
extra_data: dict | None = Field(None, description="Extra metadata (e.g. real_radius, orbit_color)")
|
||||||
|
system_id: int | None = Field(None, description="Star system ID")
|
||||||
|
|
||||||
|
|
||||||
class CelestialDataResponse(BaseModel):
|
class CelestialDataResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add backend directory to path
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), "backend"))
|
||||||
|
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.models.db.celestial_body import CelestialBody
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def check_data():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
# Check Earth and Jupiter
|
||||||
|
stmt = select(CelestialBody).where(CelestialBody.name.in_(['Earth', 'Jupiter']))
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
bodies = result.scalars().all()
|
||||||
|
|
||||||
|
print(f"Found {len(bodies)} bodies.")
|
||||||
|
for body in bodies:
|
||||||
|
print(f"Body: {body.name} (ID: {body.id})")
|
||||||
|
print(f" Extra Data (Raw): {body.extra_data}")
|
||||||
|
if body.extra_data:
|
||||||
|
print(f" Real Radius: {body.extra_data.get('real_radius')}")
|
||||||
|
else:
|
||||||
|
print(" Extra Data is None/Empty")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(check_data())
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), "backend"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.api import celestial_position
|
||||||
|
print("Import successful")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Import failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
@ -6,6 +6,7 @@ import { useFrame } from '@react-three/fiber';
|
||||||
import type { CelestialBody as CelestialBodyType } from '../types';
|
import type { CelestialBody as CelestialBodyType } from '../types';
|
||||||
import { fetchBodyResources } from '../utils/api';
|
import { fetchBodyResources } from '../utils/api';
|
||||||
import { getCelestialSize } from '../config/celestialSizes';
|
import { getCelestialSize } from '../config/celestialSizes';
|
||||||
|
import { useSystemSetting } from '../hooks/useSystemSetting';
|
||||||
|
|
||||||
interface BodyViewerProps {
|
interface BodyViewerProps {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
|
|
@ -84,10 +85,11 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
|
||||||
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
||||||
const [modelScale, setModelScale] = useState<number>(1.0);
|
const [modelScale, setModelScale] = useState<number>(1.0);
|
||||||
const [loadError, setLoadError] = useState<boolean>(false);
|
const [loadError, setLoadError] = useState<boolean>(false);
|
||||||
|
const [typeConfigs] = useSystemSetting('celestial_type_configs', null);
|
||||||
|
|
||||||
// Determine size and appearance - use larger sizes for detail view with a cap
|
// Determine size and appearance - use larger sizes for detail view with a cap
|
||||||
const appearance = useMemo(() => {
|
const appearance = useMemo(() => {
|
||||||
const baseSize = getCelestialSize(body.name, body.type);
|
const baseSize = getCelestialSize(body, undefined, typeConfigs);
|
||||||
|
|
||||||
// Detail view scaling strategy:
|
// Detail view scaling strategy:
|
||||||
// - Small bodies (< 0.15): scale up 3x for visibility
|
// - Small bodies (< 0.15): scale up 3x for visibility
|
||||||
|
|
@ -120,7 +122,7 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) {
|
||||||
return { size: finalSize, emissive: '#888888', emissiveIntensity: 0.4 };
|
return { size: finalSize, emissive: '#888888', emissiveIntensity: 0.4 };
|
||||||
}
|
}
|
||||||
return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 };
|
return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 };
|
||||||
}, [body.name, body.type]);
|
}, [body.name, body.type, body.extra_data, typeConfigs]);
|
||||||
|
|
||||||
// Fetch resources (texture or model)
|
// Fetch resources (texture or model)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,20 @@ interface CameraControllerProps {
|
||||||
allBodies: CelestialBody[];
|
allBodies: CelestialBody[];
|
||||||
onAnimationComplete?: () => void;
|
onAnimationComplete?: () => void;
|
||||||
resetTrigger?: number;
|
resetTrigger?: number;
|
||||||
|
defaultPosition?: [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CameraController({ focusTarget, allBodies, onAnimationComplete, resetTrigger = 0 }: CameraControllerProps) {
|
export function CameraController({
|
||||||
|
focusTarget,
|
||||||
|
allBodies,
|
||||||
|
onAnimationComplete,
|
||||||
|
resetTrigger = 0,
|
||||||
|
defaultPosition = [10, 8, 10] // Default closer to inner solar system
|
||||||
|
}: CameraControllerProps) {
|
||||||
const { camera } = useThree();
|
const { camera } = useThree();
|
||||||
const targetPosition = useRef(new Vector3());
|
const targetPosition = useRef(new Vector3());
|
||||||
const isAnimating = useRef(false);
|
const isAnimating = useRef(false);
|
||||||
|
const isResetting = useRef(false);
|
||||||
const animationProgress = useRef(0);
|
const animationProgress = useRef(0);
|
||||||
const startPosition = useRef(new Vector3());
|
const startPosition = useRef(new Vector3());
|
||||||
const lastResetTrigger = useRef(0);
|
const lastResetTrigger = useRef(0);
|
||||||
|
|
@ -27,16 +35,19 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete,
|
||||||
if (resetTrigger !== lastResetTrigger.current) {
|
if (resetTrigger !== lastResetTrigger.current) {
|
||||||
lastResetTrigger.current = resetTrigger;
|
lastResetTrigger.current = resetTrigger;
|
||||||
// Force reset
|
// Force reset
|
||||||
targetPosition.current.set(25, 20, 25);
|
targetPosition.current.set(...defaultPosition);
|
||||||
startPosition.current.copy(camera.position);
|
startPosition.current.copy(camera.position);
|
||||||
isAnimating.current = true;
|
isAnimating.current = true;
|
||||||
|
isResetting.current = true;
|
||||||
animationProgress.current = 0;
|
animationProgress.current = 0;
|
||||||
}
|
}
|
||||||
}, [resetTrigger, camera]); // Only run when resetTrigger changes
|
}, [resetTrigger, camera, defaultPosition]); // Only run when resetTrigger changes
|
||||||
|
|
||||||
// Handle focus target changes
|
// Handle focus target changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusTarget) {
|
if (focusTarget) {
|
||||||
|
isResetting.current = false; // Cancel reset if target selected
|
||||||
|
|
||||||
// Focus on target - use smart rendered position
|
// Focus on target - use smart rendered position
|
||||||
const renderPos = calculateRenderPosition(focusTarget, allBodies);
|
const renderPos = calculateRenderPosition(focusTarget, allBodies);
|
||||||
const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
|
const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
|
||||||
|
|
@ -91,11 +102,8 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete,
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Target became null (e.g. info window closed)
|
// Target became null (e.g. info window closed)
|
||||||
// DO NOTHING here to preserve camera position
|
// Only stop animation if we are NOT in the middle of a reset
|
||||||
// Reset is handled by the other useEffect
|
if (!isResetting.current && isAnimating.current) {
|
||||||
|
|
||||||
// Just stop any ongoing animation
|
|
||||||
if (isAnimating.current) {
|
|
||||||
isAnimating.current = false;
|
isAnimating.current = false;
|
||||||
animationProgress.current = 0;
|
animationProgress.current = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +118,7 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete,
|
||||||
if (animationProgress.current >= 1) {
|
if (animationProgress.current >= 1) {
|
||||||
animationProgress.current = 1;
|
animationProgress.current = 1;
|
||||||
isAnimating.current = false;
|
isAnimating.current = false;
|
||||||
|
isResetting.current = false; // Animation done
|
||||||
if (onAnimationComplete) onAnimationComplete();
|
if (onAnimationComplete) onAnimationComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPo
|
||||||
import { fetchBodyResources } from '../utils/api';
|
import { fetchBodyResources } from '../utils/api';
|
||||||
import { getCelestialSize } from '../config/celestialSizes';
|
import { getCelestialSize } from '../config/celestialSizes';
|
||||||
import { createLabelTexture } from '../utils/labelTexture';
|
import { createLabelTexture } from '../utils/labelTexture';
|
||||||
|
import { useSystemSetting } from '../hooks/useSystemSetting';
|
||||||
|
|
||||||
interface CelestialBodyProps {
|
interface CelestialBodyProps {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
|
|
@ -86,7 +87,7 @@ function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string |
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet component with texture
|
// Planet component with texture
|
||||||
function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false, onBodySelect }: {
|
function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false, onBodySelect, typeConfigs }: {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
size: number;
|
size: number;
|
||||||
emissive: string;
|
emissive: string;
|
||||||
|
|
@ -94,6 +95,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
|
||||||
allBodies: CelestialBodyType[];
|
allBodies: CelestialBodyType[];
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onBodySelect?: (body: CelestialBodyType) => void;
|
onBodySelect?: (body: CelestialBodyType) => void;
|
||||||
|
typeConfigs?: any;
|
||||||
}) {
|
}) {
|
||||||
const meshRef = useRef<Mesh>(null);
|
const meshRef = useRef<Mesh>(null);
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
@ -102,8 +104,8 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected
|
||||||
|
|
||||||
// Use smart render position calculation
|
// Use smart render position calculation
|
||||||
const renderPosition = useMemo(() => {
|
const renderPosition = useMemo(() => {
|
||||||
return calculateRenderPosition(body, allBodies);
|
return calculateRenderPosition(body, allBodies, typeConfigs);
|
||||||
}, [position.x, position.y, position.z, body, allBodies]);
|
}, [position.x, position.y, position.z, body, allBodies, typeConfigs]);
|
||||||
|
|
||||||
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
||||||
|
|
||||||
|
|
@ -488,6 +490,8 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
|
||||||
export function CelestialBody({ body, allBodies, isSelected = false, onBodySelect }: CelestialBodyProps) {
|
export function CelestialBody({ body, allBodies, isSelected = false, onBodySelect }: CelestialBodyProps) {
|
||||||
// Get the current position (use the first position for now)
|
// Get the current position (use the first position for now)
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
const [typeConfigs] = useSystemSetting('celestial_type_configs', null);
|
||||||
|
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
|
|
||||||
// Skip probes - they will use 3D models
|
// Skip probes - they will use 3D models
|
||||||
|
|
@ -499,7 +503,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
|
||||||
const appearance = useMemo(() => {
|
const appearance = useMemo(() => {
|
||||||
if (body.type === 'star') {
|
if (body.type === 'star') {
|
||||||
return {
|
return {
|
||||||
size: 0.4, // Sun size
|
size: getCelestialSize(body, undefined, typeConfigs), // Sun size with real radius or default
|
||||||
emissive: '#FDB813',
|
emissive: '#FDB813',
|
||||||
emissiveIntensity: 1.5,
|
emissiveIntensity: 1.5,
|
||||||
};
|
};
|
||||||
|
|
@ -508,7 +512,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
|
||||||
// Comet - bright core with glow
|
// Comet - bright core with glow
|
||||||
if (body.type === 'comet') {
|
if (body.type === 'comet') {
|
||||||
return {
|
return {
|
||||||
size: getCelestialSize(body.name, body.type),
|
size: getCelestialSize(body, undefined, typeConfigs),
|
||||||
emissive: '#000000', // Revert to no special emissive color for texture
|
emissive: '#000000', // Revert to no special emissive color for texture
|
||||||
emissiveIntensity: 0, // Revert to no special emissive intensity
|
emissiveIntensity: 0, // Revert to no special emissive intensity
|
||||||
};
|
};
|
||||||
|
|
@ -517,7 +521,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
|
||||||
// Satellite (natural moons) - small size with slight glow for visibility
|
// Satellite (natural moons) - small size with slight glow for visibility
|
||||||
if (body.type === 'satellite') {
|
if (body.type === 'satellite') {
|
||||||
return {
|
return {
|
||||||
size: getCelestialSize(body.name, body.type),
|
size: getCelestialSize(body, undefined, typeConfigs),
|
||||||
emissive: '#888888', // Slight glow to make it visible
|
emissive: '#888888', // Slight glow to make it visible
|
||||||
emissiveIntensity: 0.4,
|
emissiveIntensity: 0.4,
|
||||||
};
|
};
|
||||||
|
|
@ -525,11 +529,11 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
|
||||||
|
|
||||||
// Planet and dwarf planet sizes
|
// Planet and dwarf planet sizes
|
||||||
return {
|
return {
|
||||||
size: getCelestialSize(body.name, body.type),
|
size: getCelestialSize(body, undefined, typeConfigs),
|
||||||
emissive: '#000000',
|
emissive: '#000000',
|
||||||
emissiveIntensity: 0,
|
emissiveIntensity: 0,
|
||||||
};
|
};
|
||||||
}, [body.name, body.type]);
|
}, [body, typeConfigs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Planet
|
<Planet
|
||||||
|
|
@ -540,6 +544,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec
|
||||||
allBodies={allBodies}
|
allBodies={allBodies}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onBodySelect={onBodySelect}
|
onBodySelect={onBodySelect}
|
||||||
|
typeConfigs={typeConfigs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { CelestialBody } from '../types';
|
||||||
import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition';
|
import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition';
|
||||||
import { fetchBodyResources } from '../utils/api';
|
import { fetchBodyResources } from '../utils/api';
|
||||||
import { createLabelTexture } from '../utils/labelTexture';
|
import { createLabelTexture } from '../utils/labelTexture';
|
||||||
|
import { useSystemSetting } from '../hooks/useSystemSetting';
|
||||||
|
|
||||||
interface ProbeProps {
|
interface ProbeProps {
|
||||||
body: CelestialBody;
|
body: CelestialBody;
|
||||||
|
|
@ -19,7 +20,7 @@ interface ProbeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate component for each probe type to properly use hooks
|
// Separate component for each probe type to properly use hooks
|
||||||
function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, resourceScale = 1.0, onBodySelect }: {
|
function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, resourceScale = 1.0, onBodySelect, typeConfigs }: {
|
||||||
body: CelestialBody;
|
body: CelestialBody;
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
allBodies: CelestialBody[];
|
allBodies: CelestialBody[];
|
||||||
|
|
@ -27,14 +28,15 @@ function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, r
|
||||||
onError: () => void;
|
onError: () => void;
|
||||||
resourceScale?: number;
|
resourceScale?: number;
|
||||||
onBodySelect?: (body: CelestialBody) => void;
|
onBodySelect?: (body: CelestialBody) => void;
|
||||||
|
typeConfigs?: any;
|
||||||
}) {
|
}) {
|
||||||
const groupRef = useRef<Group>(null);
|
const groupRef = useRef<Group>(null);
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
||||||
// 1. Hook: Render Position
|
// 1. Hook: Render Position
|
||||||
const renderPosition = useMemo(() => {
|
const renderPosition = useMemo(() => {
|
||||||
return calculateRenderPosition(body, allBodies);
|
return calculateRenderPosition(body, allBodies, typeConfigs);
|
||||||
}, [position.x, position.y, position.z, body, allBodies]);
|
}, [position.x, position.y, position.z, body, allBodies, typeConfigs]);
|
||||||
|
|
||||||
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
||||||
|
|
||||||
|
|
@ -176,18 +178,19 @@ function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback component when model is not available
|
// Fallback component when model is not available
|
||||||
function ProbeFallback({ body, allBodies, isSelected = false, onBodySelect }: {
|
function ProbeFallback({ body, allBodies, isSelected = false, onBodySelect, typeConfigs }: {
|
||||||
body: CelestialBody;
|
body: CelestialBody;
|
||||||
allBodies: CelestialBody[];
|
allBodies: CelestialBody[];
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onBodySelect?: (body: CelestialBody) => void;
|
onBodySelect?: (body: CelestialBody) => void;
|
||||||
|
typeConfigs?: any;
|
||||||
}) {
|
}) {
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
||||||
// Use smart render position calculation
|
// Use smart render position calculation
|
||||||
const renderPosition = useMemo(() => {
|
const renderPosition = useMemo(() => {
|
||||||
return calculateRenderPosition(body, allBodies);
|
return calculateRenderPosition(body, allBodies, typeConfigs);
|
||||||
}, [position.x, position.y, position.z, body, allBodies]);
|
}, [position.x, position.y, position.z, body, allBodies, typeConfigs]);
|
||||||
|
|
||||||
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z };
|
||||||
|
|
||||||
|
|
@ -257,6 +260,7 @@ export function Probe({ body, allBodies, isSelected = false, onBodySelect }: Pro
|
||||||
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
||||||
const [loadError, setLoadError] = useState<boolean>(false);
|
const [loadError, setLoadError] = useState<boolean>(false);
|
||||||
const [resourceScale, setResourceScale] = useState<number>(1.0);
|
const [resourceScale, setResourceScale] = useState<number>(1.0);
|
||||||
|
const [typeConfigs] = useSystemSetting('celestial_type_configs', null);
|
||||||
|
|
||||||
// Fetch model from backend API
|
// Fetch model from backend API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -314,8 +318,15 @@ export function Probe({ body, allBodies, isSelected = false, onBodySelect }: Pro
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setLoadError(true);
|
setLoadError(true);
|
||||||
}}
|
}}
|
||||||
|
typeConfigs={typeConfigs}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProbeFallback body={body} allBodies={allBodies} isSelected={isSelected} onBodySelect={onBodySelect} />;
|
return <ProbeFallback
|
||||||
|
body={body}
|
||||||
|
allBodies={allBodies}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onBodySelect={onBodySelect}
|
||||||
|
typeConfigs={typeConfigs}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { scalePosition } from '../utils/scaleDistance';
|
||||||
import { calculateRenderPosition } from '../utils/renderPosition';
|
import { calculateRenderPosition } from '../utils/renderPosition';
|
||||||
import type { CelestialBody as CelestialBodyType, Position } from '../types';
|
import type { CelestialBody as CelestialBodyType, Position } from '../types';
|
||||||
import type { ToastContextValue } from '../contexts/ToastContext'; // Import ToastContextValue
|
import type { ToastContextValue } from '../contexts/ToastContext'; // Import ToastContextValue
|
||||||
|
import { useSystemSetting } from '../hooks/useSystemSetting';
|
||||||
|
|
||||||
interface SceneProps {
|
interface SceneProps {
|
||||||
bodies: CelestialBodyType[];
|
bodies: CelestialBodyType[];
|
||||||
|
|
@ -32,6 +33,25 @@ interface SceneProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0, toast, onViewDetails }: SceneProps) {
|
export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0, toast, onViewDetails }: SceneProps) {
|
||||||
|
const [typeConfigs] = useSystemSetting('celestial_type_configs', null);
|
||||||
|
const [rawCameraPos] = useSystemSetting('default_camera_position', [10, 8, 10]);
|
||||||
|
|
||||||
|
// Parse camera position if it's a string (from system settings)
|
||||||
|
const defaultCameraPos = useMemo(() => {
|
||||||
|
if (typeof rawCameraPos === 'string') {
|
||||||
|
try {
|
||||||
|
// Handle strings like "[10, 8, 10]"
|
||||||
|
const parsed = JSON.parse(rawCameraPos);
|
||||||
|
if (Array.isArray(parsed) && parsed.length === 3) {
|
||||||
|
return parsed as [number, number, number];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse default_camera_position:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rawCameraPos as [number, number, number];
|
||||||
|
}, [rawCameraPos]);
|
||||||
|
|
||||||
// Debug: log what Scene receives
|
// Debug: log what Scene receives
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[Scene] Received bodies:', {
|
console.log('[Scene] Received bodies:', {
|
||||||
|
|
@ -81,17 +101,17 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
|
||||||
|
|
||||||
// We need to use the EXACT same logic as CelestialBody/Probe components
|
// We need to use the EXACT same logic as CelestialBody/Probe components
|
||||||
// to ensure the label sticks to the object
|
// to ensure the label sticks to the object
|
||||||
const renderPos = calculateRenderPosition(selectedBody, bodies);
|
const renderPos = calculateRenderPosition(selectedBody, bodies, typeConfigs);
|
||||||
|
|
||||||
// Convert to Three.js coordinates (x, z, y)
|
// Convert to Three.js coordinates (x, z, y)
|
||||||
return [renderPos.x, renderPos.z, renderPos.y] as [number, number, number];
|
return [renderPos.x, renderPos.z, renderPos.y] as [number, number, number];
|
||||||
}, [selectedBody, bodies]);
|
}, [selectedBody, bodies, typeConfigs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="cosmo-scene-container" className="w-full h-full bg-black">
|
<div id="cosmo-scene-container" className="w-full h-full bg-black">
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{
|
camera={{
|
||||||
position: [25, 20, 25], // Closer view to make solar system appear larger
|
position: defaultCameraPos, // Dynamic default position
|
||||||
fov: 60, // Slightly narrower FOV for less distortion
|
fov: 60, // Slightly narrower FOV for less distortion
|
||||||
far: 20000, // Increased far plane for distant stars and constellations
|
far: 20000, // Increased far plane for distant stars and constellations
|
||||||
}}
|
}}
|
||||||
|
|
@ -110,6 +130,7 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
|
||||||
focusTarget={selectedBody}
|
focusTarget={selectedBody}
|
||||||
allBodies={bodies}
|
allBodies={bodies}
|
||||||
resetTrigger={resetTrigger}
|
resetTrigger={resetTrigger}
|
||||||
|
defaultPosition={defaultCameraPos}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Increase ambient light to see textures better */}
|
{/* Increase ambient light to see textures better */}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
||||||
/**
|
|
||||||
* Celestial body rendering sizes configuration
|
|
||||||
* Shared across components for consistent sizing
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Planet rendering sizes (radius in scene units)
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Celestial body rendering sizes configuration
|
* Celestial body rendering sizes configuration
|
||||||
* Shared across components for consistent sizing
|
* Shared across components for consistent sizing
|
||||||
|
|
@ -28,9 +20,42 @@ export const TYPE_SIZES: Record<string, number> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the rendering size for a celestial body by type only
|
* Get the rendering size for a celestial body
|
||||||
|
* Supports dynamic scaling via system config and real radius
|
||||||
|
*
|
||||||
|
* @param bodyOrName CelestialBody object OR name string (legacy)
|
||||||
|
* @param type Body type string (required if bodyOrName is string)
|
||||||
|
* @param typeConfig System configuration object (optional)
|
||||||
*/
|
*/
|
||||||
export function getCelestialSize(name: string, type: string): number {
|
export function getCelestialSize(bodyOrName: any, type?: string, typeConfig?: any): number {
|
||||||
return TYPE_SIZES[type] || TYPE_SIZES.default;
|
let bodyType = type;
|
||||||
}
|
let realRadius = undefined;
|
||||||
|
|
||||||
|
// Handle object input (preferred)
|
||||||
|
if (typeof bodyOrName === 'object' && bodyOrName !== null) {
|
||||||
|
bodyType = bodyOrName.type;
|
||||||
|
realRadius = bodyOrName.extra_data?.real_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure type is a string
|
||||||
|
const safeType = bodyType || 'default';
|
||||||
|
|
||||||
|
// 1. Try system configuration first
|
||||||
|
if (typeConfig && typeConfig[safeType]) {
|
||||||
|
const config = typeConfig[safeType];
|
||||||
|
|
||||||
|
// If we have a real radius and a ratio, calculate exact size
|
||||||
|
if (realRadius && config.ratio) {
|
||||||
|
const finalSize = realRadius * config.ratio;
|
||||||
|
return finalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to configured default for this type
|
||||||
|
if (config.default) {
|
||||||
|
return config.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to hardcoded type sizes
|
||||||
|
return TYPE_SIZES[safeType] || TYPE_SIZES.default;
|
||||||
|
}
|
||||||
|
|
@ -214,6 +214,7 @@ export function CelestialBodies() {
|
||||||
|
|
||||||
// Edit handler
|
// Edit handler
|
||||||
const handleEdit = async (record: CelestialBody) => {
|
const handleEdit = async (record: CelestialBody) => {
|
||||||
|
form.resetFields(); // Clear previous form state
|
||||||
setEditingRecord(record);
|
setEditingRecord(record);
|
||||||
|
|
||||||
// Parse extra_data if it's a string (from backend JSON field)
|
// Parse extra_data if it's a string (from backend JSON field)
|
||||||
|
|
@ -637,6 +638,38 @@ export function CelestialBodies() {
|
||||||
<Input.TextArea rows={2} />
|
<Input.TextArea rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Physical Properties for Star, Planet, Dwarf Planet, Satellite */}
|
||||||
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
|
||||||
|
prevValues.type !== currentValues.type
|
||||||
|
}>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const bodyType = getFieldValue('type');
|
||||||
|
if (!['star', 'planet', 'dwarf_planet', 'satellite'].includes(bodyType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name={['extra_data', 'real_radius']}
|
||||||
|
label="真实半径 (km)"
|
||||||
|
tooltip="天体的物理半径,用于精确计算显示比例。若不填则使用类型默认值。"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
min={0}
|
||||||
|
placeholder="例如:6371 (地球)"
|
||||||
|
formatter={value => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
|
||||||
|
parser={value => value!.replace(/\$\s?|(,*)/g, '')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
{/* Orbit parameters and info for planets and dwarf planets */}
|
{/* Orbit parameters and info for planets and dwarf planets */}
|
||||||
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
|
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
|
||||||
prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info
|
prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,15 @@ export function SystemSettings() {
|
||||||
// Edit handler
|
// Edit handler
|
||||||
const handleEdit = (record: SystemSetting) => {
|
const handleEdit = (record: SystemSetting) => {
|
||||||
setEditingRecord(record);
|
setEditingRecord(record);
|
||||||
|
|
||||||
|
let formValue = record.value;
|
||||||
|
if (record.value_type === 'json' && typeof record.value === 'object') {
|
||||||
|
formValue = JSON.stringify(record.value, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
key: record.key,
|
key: record.key,
|
||||||
value: record.value,
|
value: formValue,
|
||||||
value_type: record.value_type,
|
value_type: record.value_type,
|
||||||
category: record.category,
|
category: record.category,
|
||||||
label: record.label,
|
label: record.label,
|
||||||
|
|
@ -107,6 +113,16 @@ export function SystemSettings() {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
// Parse JSON if needed
|
||||||
|
if (values.value_type === 'json' && typeof values.value === 'string') {
|
||||||
|
try {
|
||||||
|
values.value = JSON.parse(values.value);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('JSON 格式错误');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (editingRecord) {
|
if (editingRecord) {
|
||||||
// Update
|
// Update
|
||||||
await request.put(`/system/settings/${editingRecord.key}`, values);
|
await request.put(`/system/settings/${editingRecord.key}`, values);
|
||||||
|
|
@ -190,6 +206,21 @@ export function SystemSettings() {
|
||||||
if (record.value_type === 'bool') {
|
if (record.value_type === 'bool') {
|
||||||
return <Badge status={value ? 'success' : 'default'} text={value ? '是' : '否'} />;
|
return <Badge status={value ? 'success' : 'default'} text={value ? '是' : '否'} />;
|
||||||
}
|
}
|
||||||
|
if (record.value_type === 'json' || typeof value === 'object') {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
maxWidth: 300,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
{JSON.stringify(value)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return <span style={{ fontWeight: 500 }}>{String(value)}</span>;
|
return <span style={{ fontWeight: 500 }}>{String(value)}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ export interface CelestialBody {
|
||||||
description?: string;
|
description?: string;
|
||||||
details?: string; // Markdown content for detailed view
|
details?: string; // Markdown content for detailed view
|
||||||
is_active?: boolean; // Probe status: true = active, false = inactive
|
is_active?: boolean; // Probe status: true = active, false = inactive
|
||||||
|
extra_data?: {
|
||||||
|
real_radius?: number; // Real radius in km
|
||||||
|
orbit_color?: string; // Orbit line color
|
||||||
|
orbit_period_days?: number; // Orbital period in days
|
||||||
|
[key: string]: any; // Allow additional metadata
|
||||||
|
};
|
||||||
// Star system specific data
|
// Star system specific data
|
||||||
starSystemData?: {
|
starSystemData?: {
|
||||||
system_id: number;
|
system_id: number;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ import type { CelestialBody } from '../types';
|
||||||
*/
|
*/
|
||||||
export function calculateRenderPosition(
|
export function calculateRenderPosition(
|
||||||
body: CelestialBody,
|
body: CelestialBody,
|
||||||
allBodies: CelestialBody[]
|
allBodies: CelestialBody[],
|
||||||
|
typeConfigs?: any
|
||||||
): { x: number; y: number; z: number; hasOffset: boolean } {
|
): { x: number; y: number; z: number; hasOffset: boolean } {
|
||||||
const pos = body.positions[0];
|
const pos = body.positions[0];
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
|
|
@ -45,11 +46,11 @@ export function calculateRenderPosition(
|
||||||
const nz = dz / dist;
|
const nz = dz / dist;
|
||||||
|
|
||||||
// Calculate dynamic offset based on parent planet's rendering size
|
// Calculate dynamic offset based on parent planet's rendering size
|
||||||
// Formula: planetRadius × 1.5 + 0.3 (fixed gap)
|
// Formula: planetRadius × 1.5 + 0.5 (fixed gap)
|
||||||
// This ensures larger planets (Jupiter, Saturn) have larger offsets
|
// This ensures larger planets (Jupiter, Saturn) have larger offsets
|
||||||
// while smaller planets (Earth, Mars) have smaller offsets
|
// while smaller planets (Earth, Mars) have smaller offsets
|
||||||
const parentSize = getCelestialSize(parent.name, parent.type);
|
const parentSize = getCelestialSize(parent, undefined, typeConfigs);
|
||||||
const visualOffset = parentSize * 1.5 + 0.3;
|
const visualOffset = parentSize * 1.5 + 0.5;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: parentScaled.x + nx * visualOffset,
|
x: parentScaled.x + nx * visualOffset,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue