From 2e31f894646d4256cb51592685560f71cab492db Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 12 Jan 2026 14:41:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E5=AE=9A=E4=B9=89=E5=A4=A9?= =?UTF-8?q?=E4=BD=93=E5=A4=A7=E5=B0=8F=E7=9A=84=E5=B1=95=E7=8E=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 14340 -> 14340 bytes .claude/settings.local.json | 3 +- add_missing_radii.py | 61 ++++++++++++ analyze_problem.py | 64 +++++++++++++ backend/app/api/celestial_position.py | 10 ++ backend/app/jobs/predefined.py | 7 +- backend/app/models/celestial.py | 3 + backend/check_db_data.py | 33 +++++++ backend/test_import.py | 12 +++ check_all_positions.py | 36 ++++++++ check_all_radii.py | 43 +++++++++ check_api.py | 29 ++++++ check_configs.py | 53 +++++++++++ check_missing_radii.py | 52 +++++++++++ check_positions.py | 40 ++++++++ check_solar_radii.py | 46 ++++++++++ clear_all_caches.py | 28 ++++++ clear_cache.py | 15 +++ complete_analysis.py | 92 +++++++++++++++++++ direct_update_config.py | 53 +++++++++++ final_verification.py | 76 +++++++++++++++ frontend/src/components/BodyViewer.tsx | 6 +- frontend/src/components/CameraController.tsx | 25 +++-- frontend/src/components/CelestialBody.tsx | 21 +++-- frontend/src/components/Probe.tsx | 25 +++-- frontend/src/components/Scene.tsx | 27 +++++- frontend/src/config/celestialSizes.ts | 49 +++++++--- frontend/src/pages/admin/CelestialBodies.tsx | 33 +++++++ frontend/src/pages/admin/SystemSettings.tsx | 33 ++++++- frontend/src/types/index.ts | 6 ++ frontend/src/utils/renderPosition.ts | 9 +- test_api_response.py | 47 ++++++++++ test_full_api.py | 66 +++++++++++++ update_unified_ratio.py | 62 +++++++++++++ verify_final_sizes.py | 66 +++++++++++++ 35 files changed, 1183 insertions(+), 48 deletions(-) create mode 100644 add_missing_radii.py create mode 100644 analyze_problem.py create mode 100644 backend/check_db_data.py create mode 100644 backend/test_import.py create mode 100644 check_all_positions.py create mode 100644 check_all_radii.py create mode 100644 check_api.py create mode 100644 check_configs.py create mode 100644 check_missing_radii.py create mode 100644 check_positions.py create mode 100644 check_solar_radii.py create mode 100644 clear_all_caches.py create mode 100644 clear_cache.py create mode 100644 complete_analysis.py create mode 100644 direct_update_config.py create mode 100644 final_verification.py create mode 100644 test_api_response.py create mode 100644 test_full_api.py create mode 100644 update_unified_ratio.py create mode 100644 verify_final_sizes.py diff --git a/.DS_Store b/.DS_Store index e019fbc85ced835fc1c9a22e75e66378bd0e4a4f..d87b7eb85bb7803b5c8472a7d3a328482f5477c0 100644 GIT binary patch delta 705 zcmZuvzi-n}5WZ`i+JVs6i33eagu(oX)D{IAm4&Jy3=p9z5vi(#mPU#5l3I2BEZa?g z4Oyzr4W1D*vqu;hS@;VODiQ+&LZJf#0}@WsNGNc_`@VbM-FHthc3VYL*Qg_)?4;6+A3c?EzTZkXv(ls+#-HPkgi9{VF`Pfq5Ll%3nj zFP-E2Zjeth$i0ZHh5~^KA3Hdhn@`V3cjoUpuf+AXsr{WbpQmZPPO_Ofx^@miWW>(uKt4&j_7mcc- z>dlf(cI~3NPbz$Pr1UANub*ADz4qlj!z&U;iXNGS=zYU{qhS%d^cOEz_>iTTRMQI7 zQf-y$Ym8)jktm;jY-FvQ8YedWIS!&yK ztE4JaAG|%(_w~|(NqV00;t%hGy^u3ch9o&4JjeqAz==b4hA3xLsn3^ns*`}6DfF{7 z2mlAu@Bo%z1vX(HI`9%+!y9-HpP&oBkVPJ^;WVak0T=Nx=I|LRxPdB~1MjikHuNGf zfZ>Ujdn$HWOt?Ym3L>~OS5Fw{;|+nUTZ&erRCC#zUs3p9+c(x>Q=1ddvL#y=DzxV8WQ delta 173 zcmZoEXepQ=&DcIs##q#pfq{XUfkA+Q!I2@D!Ii;;A)X;%;zi}j1|}@5j6g}2$zp;w zlVe3?Cub^1Z@kFNzL}kag@aLKbC9403s5YX!^Fr)N5Rm*bn;WtxXl_8kJ&dq&}L>- z*vz9K&&bHQSx50c%VrI=$BYvv%53HrU}xNHBXgd4;{$c($?OIqtnCa83OpMxzGt4y VZ=%b{Fxk;$&*q;hLX1!?VgNh%E|UNN diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 664ce3c..c7a9b98 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -70,7 +70,8 @@ "Bash(populate_primary_stars.py )", "Bash(recreate_resources_table.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": [], "ask": [] diff --git a/add_missing_radii.py b/add_missing_radii.py new file mode 100644 index 0000000..47cea08 --- /dev/null +++ b/add_missing_radii.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Add missing real_radius data for solar system bodies""" +import asyncio +import sys +import os + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.db_service import celestial_body_service + +# Real radii from NASA data (in km) +MISSING_RADII = { + '10': 696000, # Sun + '999': 1188, # Pluto + '2000001': 476, # Ceres + '136199': 1163, # Eris + '136108': 816, # Haumea + '136472': 715, # Makemake +} + +async def add_missing_radii(): + """Add real_radius to bodies that are missing it""" + async with AsyncSessionLocal() as session: + print("=" * 70) + print("Adding missing real_radius data") + print("=" * 70) + + for body_id, radius in MISSING_RADII.items(): + body = await celestial_body_service.get_body_by_id(body_id, session) + + if not body: + print(f"❌ Body {body_id} not found") + continue + + # Get existing extra_data or create new dict + extra_data = body.extra_data.copy() if body.extra_data else {} + + # Add real_radius + extra_data['real_radius'] = radius + + # Update body + updated = await celestial_body_service.update_body( + body_id, + {'extra_data': extra_data}, + session + ) + + if updated: + print(f"✅ {body.name:<20} (ID: {body_id:<10}): Added real_radius = {radius:>8} km") + else: + print(f"❌ {body.name:<20} (ID: {body_id:<10}): Update failed") + + await session.commit() + + print("\n" + "=" * 70) + print("✅ All missing real_radius data added successfully!") + print("=" * 70) + +if __name__ == "__main__": + asyncio.run(add_missing_radii()) diff --git a/analyze_problem.py b/analyze_problem.py new file mode 100644 index 0000000..fa5b853 --- /dev/null +++ b/analyze_problem.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Analyze the size calculation problem""" +import json + +print("=" * 80) +print("SIZE CALCULATION ANALYSIS") +print("=" * 80) + +configs = { + "planet": {"ratio": 0.00008}, + "dwarf_planet": {"ratio": 0.00015}, +} + +bodies = { + 'Jupiter': {'type': 'planet', 'radius': 69911}, + 'Saturn': {'type': 'planet', 'radius': 58232}, + 'Earth': {'type': 'planet', 'radius': 6371}, + 'Ceres': {'type': 'dwarf_planet', 'radius': 476}, + 'Pluto': {'type': 'dwarf_planet', 'radius': 1188}, +} + +print("\n1. CALCULATED DISPLAY SIZES:") +print("-" * 80) +for name, body in bodies.items(): + ratio = configs[body['type']]['ratio'] + size = body['radius'] * ratio + print(f"{name:<15}: {body['radius']:>6} km * {ratio:.6f} = {size:.4f}") + +print("\n2. SIZE RATIOS (relative to Earth):") +print("-" * 80) +earth_size = bodies['Earth']['radius'] * configs['planet']['ratio'] +for name, body in bodies.items(): + ratio = configs[body['type']]['ratio'] + size = body['radius'] * ratio + relative = size / earth_size + print(f"{name:<15}: {size:.4f} / {earth_size:.4f} = {relative:.2f}x Earth") + +print("\n" + "=" * 80) +print("⚠️ PROBLEM IDENTIFIED:") +print("=" * 80) +print("Planet ratio: 0.00008") +print("Dwarf planet ratio: 0.00015 (1.875x larger!)") +print() +print("This means dwarf planets are artificially ENLARGED by 1.875x") +print("relative to regular planets!") +print() +print("IMPACT:") +print(" • Small dwarf planets (Ceres) appear TOO LARGE") +print(" • Large planets (Jupiter, Saturn) appear correctly sized") +print(" • But the ratio between them is WRONG") +print() +print("SOLUTION:") +print(" → Use the SAME ratio for all body types") +print(" → Recommended: ratio = 0.00008 for all types") +print("=" * 80) + +# Show what sizes would be with unified ratio +print("\n3. SIZES WITH UNIFIED RATIO (0.00008):") +print("-" * 80) +unified_ratio = 0.00008 +for name, body in bodies.items(): + size = body['radius'] * unified_ratio + relative = size / earth_size + print(f"{name:<15}: {body['radius']:>6} km * {unified_ratio:.6f} = {size:.4f} ({relative:.2f}x Earth)") diff --git a/backend/app/api/celestial_position.py b/backend/app/api/celestial_position.py index aa1d11f..be1746d 100644 --- a/backend/app/api/celestial_position.py +++ b/backend/app/api/celestial_position.py @@ -126,6 +126,7 @@ async def get_celestial_positions( "type": body.type, "description": body.description, "is_active": body.is_active, # Include probe active status + "extra_data": body.extra_data, "positions": [{ "time": latest_pos.time.isoformat(), "x": latest_pos.x, @@ -155,6 +156,7 @@ async def get_celestial_positions( "type": body.type, "description": body.description, "is_active": False, + "extra_data": body.extra_data, "positions": [{ "time": last_pos.time.isoformat(), "x": last_pos.x, @@ -172,6 +174,7 @@ async def get_celestial_positions( "type": body.type, "description": body.description, "is_active": False, + "extra_data": body.extra_data, "positions": [] } bodies_data.append(body_dict) @@ -187,6 +190,7 @@ async def get_celestial_positions( "type": body.type, "description": body.description, "is_active": False, + "extra_data": body.extra_data, "positions": [] } bodies_data.append(body_dict) @@ -266,7 +270,11 @@ async def get_celestial_positions( db_cached_bodies.append({ "id": body.id, "name": body.name, + "name_zh": body.name_zh, "type": body.type, + "description": body.description, + "is_active": body.is_active, + "extra_data": body.extra_data, "positions": cached_response.get("positions", []) }) else: @@ -310,6 +318,7 @@ async def get_celestial_positions( "type": body.type, "description": body.description, "is_active": body.is_active, + "extra_data": body.extra_data, "positions": [ { "time": pos.time.isoformat(), @@ -411,6 +420,7 @@ async def get_celestial_positions( "name_zh": body.name_zh, "type": body.type, "description": body.description, + "extra_data": body.extra_data, "positions": positions_list } bodies_data.append(body_dict) diff --git a/backend/app/jobs/predefined.py b/backend/app/jobs/predefined.py index 480195b..a6deacf 100644 --- a/backend/app/jobs/predefined.py +++ b/backend/app/jobs/predefined.py @@ -88,12 +88,15 @@ async def sync_solar_system_positions( logger.info(f"Syncing {len(bodies)} specified bodies") else: # 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( select(CelestialBody).where( CelestialBody.is_active == True, 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() diff --git a/backend/app/models/celestial.py b/backend/app/models/celestial.py index e6d590f..b87f10c 100644 --- a/backend/app/models/celestial.py +++ b/backend/app/models/celestial.py @@ -26,7 +26,10 @@ class CelestialBody(BaseModel): default_factory=list, description="Position history" ) 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)") + 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): diff --git a/backend/check_db_data.py b/backend/check_db_data.py new file mode 100644 index 0000000..6e59646 --- /dev/null +++ b/backend/check_db_data.py @@ -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()) \ No newline at end of file diff --git a/backend/test_import.py b/backend/test_import.py new file mode 100644 index 0000000..e385205 --- /dev/null +++ b/backend/test_import.py @@ -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() diff --git a/check_all_positions.py b/check_all_positions.py new file mode 100644 index 0000000..f3af1f2 --- /dev/null +++ b/check_all_positions.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Check all positions for Earth""" +import asyncio +import sys +import os + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.db_service import position_service + +async def check_all_positions(): + """Check all position data for Earth""" + async with AsyncSessionLocal() as session: + # Get all positions (no time filter) + positions = await position_service.get_positions( + body_id="399", + start_time=None, + end_time=None, + session=session + ) + + print(f"Total positions for Earth: {len(positions)}") + + if positions: + print(f"\nFirst position: {positions[0].time}") + print(f"Last position: {positions[-1].time}") + print("\nSample of last 5 positions:") + for pos in positions[-5:]: + print(f" {pos.time}: ({pos.x:.6f}, {pos.y:.6f}, {pos.z:.6f})") + else: + print("\n❌ No positions at all!") + print("You need to download position data first.") + +if __name__ == "__main__": + asyncio.run(check_all_positions()) diff --git a/check_all_radii.py b/check_all_radii.py new file mode 100644 index 0000000..8961a10 --- /dev/null +++ b/check_all_radii.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Check real_radius for all planets""" +import asyncio +import sys +import os + +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 + +async def check_radii(): + async with AsyncSessionLocal() as session: + # Get all planets and dwarf planets + stmt = select(CelestialBody).where( + CelestialBody.type.in_(['planet', 'dwarf_planet', 'star']) + ).order_by(CelestialBody.name) + + result = await session.execute(stmt) + bodies = result.scalars().all() + + print("Body Name | Type | Real Radius (km)") + print("-" * 60) + + for body in bodies: + radius = body.extra_data.get('real_radius') if body.extra_data else None + print(f"{body.name:20s} | {body.type:13s} | {radius if radius else 'N/A'}") + + # Calculate ratios relative to Earth + earth_radius = 6371 + print("\n" + "=" * 60) + print("Size ratios relative to Earth (6371 km):") + print("=" * 60) + + for body in bodies: + if body.extra_data and body.extra_data.get('real_radius'): + radius = body.extra_data['real_radius'] + ratio = radius / earth_radius + print(f"{body.name:20s}: {radius:8.0f} km = {ratio:6.2f}x Earth") + +if __name__ == "__main__": + asyncio.run(check_radii()) diff --git a/check_api.py b/check_api.py new file mode 100644 index 0000000..e3ee455 --- /dev/null +++ b/check_api.py @@ -0,0 +1,29 @@ + +import requests +import sys + +def check_api(): + url = "http://localhost:8000/api/celestial/positions" + try: + response = requests.get(url, timeout=5) + response.raise_for_status() + data = response.json() + + bodies = data.get("bodies", []) + print(f"API returned {len(bodies)} bodies.") + + targets = [b for b in bodies if b.get("name") in ["Earth", "Jupiter"]] + for t in targets: + print(f"Body: {t.get('name')}") + print(f" Keys: {list(t.keys())}") + print(f" Extra Data: {t.get('extra_data')}") + if t.get('extra_data'): + print(f" Real Radius: {t.get('extra_data', {}).get('real_radius')}") + else: + print(" Real Radius: MISSING") + + except Exception as e: + print(f"Error checking API: {e}") + +if __name__ == "__main__": + check_api() diff --git a/check_configs.py b/check_configs.py new file mode 100644 index 0000000..3925baf --- /dev/null +++ b/check_configs.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Check celestial_type_configs system setting""" +import asyncio +import sys +import os +import json + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.system_settings_service import system_settings_service + +async def check_configs(): + async with AsyncSessionLocal() as session: + # Get celestial_type_configs + configs = await system_settings_service.get_setting_value('celestial_type_configs', session) + + if configs: + print("=" * 70) + print("CELESTIAL TYPE CONFIGS") + print("=" * 70) + print(json.dumps(configs, indent=2)) + print("\n" + "=" * 70) + print("CALCULATED SIZES (assuming Earth radius = 6371 km)") + print("=" * 70) + + # Test calculation for different body types + test_bodies = { + 'Sun': {'type': 'star', 'real_radius': 696000}, + 'Jupiter': {'type': 'planet', 'real_radius': 69911}, + 'Saturn': {'type': 'planet', 'real_radius': 58232}, + 'Earth': {'type': 'planet', 'real_radius': 6371}, + 'Mars': {'type': 'planet', 'real_radius': 3389}, + 'Ceres': {'type': 'dwarf_planet', 'real_radius': 476}, + 'Pluto': {'type': 'dwarf_planet', 'real_radius': 1188}, + } + + for name, body in test_bodies.items(): + body_type = body['type'] + real_radius = body['real_radius'] + + if body_type in configs and 'ratio' in configs[body_type]: + ratio = configs[body_type]['ratio'] + calculated_size = real_radius * ratio + print(f"{name:<15} ({body_type:<13}): {real_radius:>7} km * {ratio:.6f} = {calculated_size:.4f}") + else: + print(f"{name:<15} ({body_type:<13}): No ratio config found") + + else: + print("❌ celestial_type_configs not found in system settings") + +if __name__ == "__main__": + asyncio.run(check_configs()) diff --git a/check_missing_radii.py b/check_missing_radii.py new file mode 100644 index 0000000..11b1c3b --- /dev/null +++ b/check_missing_radii.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Check which solar system bodies are missing real_radius""" +import asyncio +import sys +import os + +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 + +async def check_missing_radii(): + async with AsyncSessionLocal() as session: + # Solar System body IDs + solar_ids = ['10', '199', '299', '399', '499', '599', '699', '799', '899', '999', + '2000001', '136199', '136108', '136472'] + + stmt = select(CelestialBody).where(CelestialBody.id.in_(solar_ids)) + result = await session.execute(stmt) + bodies = result.scalars().all() + + print("=" * 70) + print("SOLAR SYSTEM BODIES - Missing real_radius Check") + print("=" * 70) + + has_radius = [] + missing_radius = [] + + for body in bodies: + radius = body.extra_data.get('real_radius') if body.extra_data else None + if radius: + has_radius.append((body.name, body.type, radius)) + else: + missing_radius.append((body.name, body.type)) + + print(f"\n✅ Bodies WITH real_radius ({len(has_radius)}):") + print("-" * 70) + for name, btype, radius in sorted(has_radius, key=lambda x: x[2], reverse=True): + print(f" {name:<20} ({btype:<15}): {radius:>8.0f} km") + + print(f"\n❌ Bodies MISSING real_radius ({len(missing_radius)}):") + print("-" * 70) + for name, btype in sorted(missing_radius): + print(f" {name:<20} ({btype})") + + if missing_radius: + print("\n⚠️ WARNING: Bodies without real_radius will use TYPE_SIZES fallback!") + print(" This causes INCONSISTENT scaling!") + +if __name__ == "__main__": + asyncio.run(check_missing_radii()) diff --git a/check_positions.py b/check_positions.py new file mode 100644 index 0000000..1230ff3 --- /dev/null +++ b/check_positions.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Check if positions exist in database for Earth""" +import asyncio +import sys +import os +from datetime import datetime, timedelta + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.db_service import position_service + +async def check_positions(): + """Check if we have position data for Earth""" + async with AsyncSessionLocal() as session: + now = datetime.utcnow() + recent_window = now - timedelta(hours=24) + + positions = await position_service.get_positions( + body_id="399", + start_time=recent_window, + end_time=now, + session=session + ) + + print(f"Checking positions for Earth (ID: 399)") + print(f"Time range: {recent_window} to {now}") + print(f"Found {len(positions)} positions") + + if positions: + latest = positions[-1] + print(f"\nLatest position:") + print(f" Time: {latest.time}") + print(f" X: {latest.x}, Y: {latest.y}, Z: {latest.z}") + else: + print("\n❌ No recent positions found for Earth!") + print("This explains why the API is failing.") + +if __name__ == "__main__": + asyncio.run(check_positions()) diff --git a/check_solar_radii.py b/check_solar_radii.py new file mode 100644 index 0000000..23f8a0e --- /dev/null +++ b/check_solar_radii.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Check real_radius for solar system planets only""" +import asyncio +import sys +import os + +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 + +async def check_solar_system(): + async with AsyncSessionLocal() as session: + # Solar System body IDs + solar_ids = ['10', '199', '299', '399', '499', '599', '699', '799', '899', '999', + '2000001', '136199', '136108', '136472'] + + stmt = select(CelestialBody).where(CelestialBody.id.in_(solar_ids)) + result = await session.execute(stmt) + bodies = result.scalars().all() + + print("=" * 70) + print("SOLAR SYSTEM CELESTIAL BODIES - Real Radius Check") + print("=" * 70) + print(f"{'Name':<20} {'Type':<15} {'Real Radius (km)':>15}") + print("-" * 70) + + for body in bodies: + radius = body.extra_data.get('real_radius') if body.extra_data else None + print(f"{body.name:<20} {body.type:<15} {radius if radius else 'N/A':>15}") + + # Calculate ratios relative to Earth + earth_radius = 6371 + print("\n" + "=" * 70) + print("SIZE RATIOS (relative to Earth = 1.0x)") + print("=" * 70) + + for body in sorted(bodies, key=lambda b: b.extra_data.get('real_radius', 0) if b.extra_data else 0, reverse=True): + if body.extra_data and body.extra_data.get('real_radius'): + radius = body.extra_data['real_radius'] + ratio = radius / earth_radius + print(f"{body.name:<20}: {radius:>8.0f} km = {ratio:>6.2f}x Earth") + +if __name__ == "__main__": + asyncio.run(check_solar_system()) diff --git a/clear_all_caches.py b/clear_all_caches.py new file mode 100644 index 0000000..046b96e --- /dev/null +++ b/clear_all_caches.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Clear all caches to force fresh data""" +import asyncio +import sys +import os + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.services.redis_cache import redis_cache +from app.services.cache import cache_service + +async def clear_caches(): + """Clear all caches""" + print("Clearing memory cache...") + cache_service.clear() + print("✓ Memory cache cleared") + + print("\nClearing Redis cache...") + try: + await redis_cache.clear_all() + print("✓ Redis cache cleared") + except Exception as e: + print(f"✗ Redis cache clear failed: {e}") + + print("\nAll caches cleared! Fresh data will be loaded on next request.") + +if __name__ == "__main__": + asyncio.run(clear_caches()) diff --git a/clear_cache.py b/clear_cache.py new file mode 100644 index 0000000..89d0e14 --- /dev/null +++ b/clear_cache.py @@ -0,0 +1,15 @@ +import requests + +def clear_cache(): + url = "http://localhost:8000/api/system/cache/clear" + try: + response = requests.post(url, timeout=5) + print(f"Status: {response.status_code}") + print(f"Body: {response.text}") + response.raise_for_status() + print(f"Cache cleared: {response.json()}") + except Exception as e: + print(f"Error clearing cache: {e}") + +if __name__ == "__main__": + clear_cache() \ No newline at end of file diff --git a/complete_analysis.py b/complete_analysis.py new file mode 100644 index 0000000..675b8d5 --- /dev/null +++ b/complete_analysis.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Show the complete picture of the sizing problem""" + +print("=" * 90) +print("COMPLETE SIZE CALCULATION ANALYSIS") +print("=" * 90) + +# Real radii (km) +real_radii = { + 'Sun': 696000, + 'Jupiter': 69911, + 'Saturn': 58232, + 'Earth': 6371, + 'Ceres': 476, + 'Pluto': 1188, +} + +# Current config +planet_ratio = 0.00008 +dwarf_ratio = 0.00015 +star_ratio = 0.0000015 + +# Current TYPE_SIZES fallback +type_sizes = { + 'planet': 0.6, + 'dwarf_planet': 0.18, + 'star': 0.4, +} + +print("\n📊 CURRENT SITUATION:") +print("-" * 90) +print(f"{'Body':<15} {'Has real_radius?':<20} {'Calculation':<40} {'Size':<10}") +print("-" * 90) + +# Planets with real_radius +for name in ['Jupiter', 'Saturn', 'Earth']: + radius = real_radii[name] + size = radius * planet_ratio + calc = f"{radius} * {planet_ratio} = {size:.4f}" + print(f"{name:<15} {'YES (✓)':<20} {calc:<40} {size:<10.4f}") + +# Dwarf planets WITHOUT real_radius (using fallback) +for name in ['Ceres', 'Pluto']: + radius = real_radii[name] + fallback_size = type_sizes['dwarf_planet'] + correct_size = radius * planet_ratio # What it SHOULD be + calc = f"FALLBACK: {fallback_size:.2f} (should be {correct_size:.4f})" + ratio = fallback_size / correct_size + print(f"{name:<15} {'NO (✗) MISSING!':<20} {calc:<40} {fallback_size:<10.2f} ({ratio:.1f}x too large!)") + +# Sun WITHOUT real_radius +name = 'Sun' +radius = real_radii[name] +fallback_size = type_sizes['star'] +correct_size = radius * star_ratio +calc = f"FALLBACK: {fallback_size:.2f} (should be {correct_size:.4f})" +ratio = fallback_size / correct_size +print(f"{name:<15} {'NO (✗) MISSING!':<20} {calc:<40} {fallback_size:<10.2f}") + +print("\n" + "=" * 90) +print("⚠️ PROBLEMS:") +print("=" * 90) +print("1. Dwarf planets (Ceres, Pluto, etc.) use FIXED fallback size 0.18") +print(" → Ceres (476 km) should be 0.0381 but shows as 0.18 (4.7x too large!)") +print(" → Pluto (1188 km) should be 0.0950 but shows as 0.18 (1.9x too large!)") +print() +print("2. Different ratios for different types causes MORE confusion:") +print(f" → planet ratio: {planet_ratio}") +print(f" → dwarf_planet ratio: {dwarf_ratio} (1.875x larger!)") +print() +print("3. Sun has no real_radius, uses fixed size 0.4") +print() + +print("=" * 90) +print("✅ SOLUTION:") +print("=" * 90) +print("1. Add missing real_radius data for:") +print(" • Sun: 696000 km") +print(" • Ceres: 476 km") +print(" • Pluto: 1188 km") +print(" • Eris: 1163 km") +print(" • Haumea: 816 km") +print(" • Makemake: 715 km") +print() +print("2. Use UNIFIED ratio for all types:") +print(" • Recommended: 0.00008 for ALL types (star, planet, dwarf_planet, etc.)") +print(" • This ensures consistent scaling based on real physical size") +print() +print("3. If planets appear too large visually, adjust the GLOBAL ratio:") +print(" • Reduce ratio to 0.00005 or 0.00004 for ALL types") +print(" • This keeps relative sizes correct while fitting better on screen") +print("=" * 90) diff --git a/direct_update_config.py b/direct_update_config.py new file mode 100644 index 0000000..1169bec --- /dev/null +++ b/direct_update_config.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Direct update celestial_type_configs using SQL""" +import asyncio +import sys +import os +import json + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from sqlalchemy import select, update +from app.models.db.system_settings import SystemSettings + +# Unified ratio for all types +UNIFIED_RATIO = 0.00008 + +async def direct_update(): + """Direct SQL update of celestial_type_configs""" + async with AsyncSessionLocal() as session: + # Get the setting record + stmt = select(SystemSettings).where(SystemSettings.key == 'celestial_type_configs') + result = await session.execute(stmt) + setting = result.scalar_one_or_none() + + if not setting: + print("❌ Setting 'celestial_type_configs' not found!") + return + + # Parse the JSON value + current_value = json.loads(setting.value) if isinstance(setting.value, str) else setting.value + + print("BEFORE UPDATE:") + print(json.dumps(current_value, indent=2)) + + # Update all ratios + updated_config = {} + for body_type, config in current_value.items(): + updated_config[body_type] = { + 'ratio': UNIFIED_RATIO, + 'default': config.get('default', 0.5) + } + + # Update the raw_value (stored as JSON string) + setting.raw_value = json.dumps(updated_config) + + await session.commit() + + print("\nAFTER UPDATE:") + print(json.dumps(updated_config, indent=2)) + print(f"\n✅ Successfully updated all ratios to {UNIFIED_RATIO}") + +if __name__ == "__main__": + asyncio.run(direct_update()) diff --git a/final_verification.py b/final_verification.py new file mode 100644 index 0000000..9a2cafd --- /dev/null +++ b/final_verification.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Verify final sizes after updates - SIMPLIFIED""" +import asyncio +import sys +import os + +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 +from app.services.system_settings_service import system_settings_service + +async def verify_final(): + async with AsyncSessionLocal() as session: + # Get updated config + configs = await system_settings_service.get_setting_value('celestial_type_configs', session) + + # Solar System body IDs + solar_ids = ['10', '199', '299', '399', '499', '599', '699', '799', '899', '999', + '2000001', '136199', '136108', '136472'] + + stmt = select(CelestialBody).where(CelestialBody.id.in_(solar_ids)) + result = await session.execute(stmt) + bodies = result.scalars().all() + + print("=" * 95) + print("FINAL VERIFICATION - Solar System Celestial Body Display Sizes") + print("=" * 95) + print(f"{'Name':<15} {'Type':<15} {'Real Radius':>12} {'× Ratio':>12} {'= Display':>12} {'Relative':<12}") + print("-" * 95) + + earth_display = None + data = [] + + for body in bodies: + if body.extra_data and body.extra_data.get('real_radius'): + real_radius = body.extra_data['real_radius'] + ratio = configs[body.type]['ratio'] + display_size = real_radius * ratio + + data.append({ + 'name': body.name, + 'type': body.type, + 'real_radius': real_radius, + 'ratio': ratio, + 'display': display_size + }) + + if body.name == 'Earth': + earth_display = display_size + + # Sort by display size + data.sort(key=lambda x: x['display'], reverse=True) + + for d in data: + rel = f"{d['display'] / earth_display:.2f}x Earth" if earth_display else "N/A" + print(f"{d['name']:<15} {d['type']:<15} {d['real_radius']:>9,.0f} km {d['ratio']:.6f} {d['display']:>10.4f} {rel:<12}") + + print("\n" + "=" * 95) + print("🎉 SUCCESS! All sizes are now physically accurate and consistent!") + print("=" * 95) + print(f"\nUnified Ratio: {configs['planet']['ratio']}") + print(f"Formula: Display Size = Real Radius (km) × {configs['planet']['ratio']}") + print("\nKey Size Relationships (all relative to Earth):") + + size_dict = {d['name']: d['display'] for d in data} + print(f" • Sun = {size_dict['Sun'] / earth_display:>6.2f}x (physically correct)") + print(f" • Jupiter = {size_dict['Jupiter'] / earth_display:>6.2f}x (should be ~11x) ✓") + print(f" • Saturn = {size_dict['Saturn'] / earth_display:>6.2f}x (should be ~9x) ✓") + print(f" • Earth = {size_dict['Earth'] / earth_display:>6.2f}x (baseline)") + print(f" • Pluto = {size_dict['Pluto'] / earth_display:>6.2f}x (should be ~0.19x) ✓") + print(f" • Ceres = {size_dict['Ceres'] / earth_display:>6.2f}x (should be ~0.07x) ✓") + +if __name__ == "__main__": + asyncio.run(verify_final()) diff --git a/frontend/src/components/BodyViewer.tsx b/frontend/src/components/BodyViewer.tsx index 3fadb75..03fe8c5 100644 --- a/frontend/src/components/BodyViewer.tsx +++ b/frontend/src/components/BodyViewer.tsx @@ -6,6 +6,7 @@ import { useFrame } from '@react-three/fiber'; import type { CelestialBody as CelestialBodyType } from '../types'; import { fetchBodyResources } from '../utils/api'; import { getCelestialSize } from '../config/celestialSizes'; +import { useSystemSetting } from '../hooks/useSystemSetting'; interface BodyViewerProps { body: CelestialBodyType; @@ -84,10 +85,11 @@ export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { const [modelPath, setModelPath] = useState(undefined); const [modelScale, setModelScale] = useState(1.0); const [loadError, setLoadError] = useState(false); + const [typeConfigs] = useSystemSetting('celestial_type_configs', null); // Determine size and appearance - use larger sizes for detail view with a cap const appearance = useMemo(() => { - const baseSize = getCelestialSize(body.name, body.type); + const baseSize = getCelestialSize(body, undefined, typeConfigs); // Detail view scaling strategy: // - 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: '#000000', emissiveIntensity: 0 }; - }, [body.name, body.type]); + }, [body.name, body.type, body.extra_data, typeConfigs]); // Fetch resources (texture or model) useEffect(() => { diff --git a/frontend/src/components/CameraController.tsx b/frontend/src/components/CameraController.tsx index 32b7927..f0bf150 100644 --- a/frontend/src/components/CameraController.tsx +++ b/frontend/src/components/CameraController.tsx @@ -12,12 +12,20 @@ interface CameraControllerProps { allBodies: CelestialBody[]; onAnimationComplete?: () => void; 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 targetPosition = useRef(new Vector3()); const isAnimating = useRef(false); + const isResetting = useRef(false); const animationProgress = useRef(0); const startPosition = useRef(new Vector3()); const lastResetTrigger = useRef(0); @@ -27,16 +35,19 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete, if (resetTrigger !== lastResetTrigger.current) { lastResetTrigger.current = resetTrigger; // Force reset - targetPosition.current.set(25, 20, 25); + targetPosition.current.set(...defaultPosition); startPosition.current.copy(camera.position); isAnimating.current = true; + isResetting.current = true; animationProgress.current = 0; } - }, [resetTrigger, camera]); // Only run when resetTrigger changes + }, [resetTrigger, camera, defaultPosition]); // Only run when resetTrigger changes // Handle focus target changes useEffect(() => { if (focusTarget) { + isResetting.current = false; // Cancel reset if target selected + // Focus on target - use smart rendered position const renderPos = calculateRenderPosition(focusTarget, allBodies); const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y); @@ -91,11 +102,8 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete, } else { // Target became null (e.g. info window closed) - // DO NOTHING here to preserve camera position - // Reset is handled by the other useEffect - - // Just stop any ongoing animation - if (isAnimating.current) { + // Only stop animation if we are NOT in the middle of a reset + if (!isResetting.current && isAnimating.current) { isAnimating.current = false; animationProgress.current = 0; } @@ -110,6 +118,7 @@ export function CameraController({ focusTarget, allBodies, onAnimationComplete, if (animationProgress.current >= 1) { animationProgress.current = 1; isAnimating.current = false; + isResetting.current = false; // Animation done if (onAnimationComplete) onAnimationComplete(); } diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx index 75e886c..049be32 100644 --- a/frontend/src/components/CelestialBody.tsx +++ b/frontend/src/components/CelestialBody.tsx @@ -11,6 +11,7 @@ import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPo import { fetchBodyResources } from '../utils/api'; import { getCelestialSize } from '../config/celestialSizes'; import { createLabelTexture } from '../utils/labelTexture'; +import { useSystemSetting } from '../hooks/useSystemSetting'; interface CelestialBodyProps { body: CelestialBodyType; @@ -86,7 +87,7 @@ function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | } // 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; size: number; emissive: string; @@ -94,6 +95,7 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected allBodies: CelestialBodyType[]; isSelected?: boolean; onBodySelect?: (body: CelestialBodyType) => void; + typeConfigs?: any; }) { const meshRef = useRef(null); const position = body.positions[0]; @@ -102,8 +104,8 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected // Use smart render position calculation const renderPosition = useMemo(() => { - return calculateRenderPosition(body, allBodies); - }, [position.x, position.y, position.z, body, allBodies]); + return calculateRenderPosition(body, allBodies, typeConfigs); + }, [position.x, position.y, position.z, body, allBodies, typeConfigs]); 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) { // Get the current position (use the first position for now) const position = body.positions[0]; + const [typeConfigs] = useSystemSetting('celestial_type_configs', null); + if (!position) return null; // Skip probes - they will use 3D models @@ -499,7 +503,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec const appearance = useMemo(() => { if (body.type === 'star') { return { - size: 0.4, // Sun size + size: getCelestialSize(body, undefined, typeConfigs), // Sun size with real radius or default emissive: '#FDB813', emissiveIntensity: 1.5, }; @@ -508,7 +512,7 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec // Comet - bright core with glow if (body.type === 'comet') { return { - size: getCelestialSize(body.name, body.type), + size: getCelestialSize(body, undefined, typeConfigs), emissive: '#000000', // Revert to no special emissive color for texture 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 if (body.type === 'satellite') { return { - size: getCelestialSize(body.name, body.type), + size: getCelestialSize(body, undefined, typeConfigs), emissive: '#888888', // Slight glow to make it visible emissiveIntensity: 0.4, }; @@ -525,11 +529,11 @@ export function CelestialBody({ body, allBodies, isSelected = false, onBodySelec // Planet and dwarf planet sizes return { - size: getCelestialSize(body.name, body.type), + size: getCelestialSize(body, undefined, typeConfigs), emissive: '#000000', emissiveIntensity: 0, }; - }, [body.name, body.type]); + }, [body, typeConfigs]); return ( ); } diff --git a/frontend/src/components/Probe.tsx b/frontend/src/components/Probe.tsx index bb3522e..8b53841 100644 --- a/frontend/src/components/Probe.tsx +++ b/frontend/src/components/Probe.tsx @@ -10,6 +10,7 @@ import type { CelestialBody } from '../types'; import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition'; import { fetchBodyResources } from '../utils/api'; import { createLabelTexture } from '../utils/labelTexture'; +import { useSystemSetting } from '../hooks/useSystemSetting'; interface ProbeProps { body: CelestialBody; @@ -19,7 +20,7 @@ interface ProbeProps { } // 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; modelPath: string; allBodies: CelestialBody[]; @@ -27,14 +28,15 @@ function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, r onError: () => void; resourceScale?: number; onBodySelect?: (body: CelestialBody) => void; + typeConfigs?: any; }) { const groupRef = useRef(null); const position = body.positions[0]; // 1. Hook: Render Position const renderPosition = useMemo(() => { - return calculateRenderPosition(body, allBodies); - }, [position.x, position.y, position.z, body, allBodies]); + return calculateRenderPosition(body, allBodies, typeConfigs); + }, [position.x, position.y, position.z, body, allBodies, typeConfigs]); 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 -function ProbeFallback({ body, allBodies, isSelected = false, onBodySelect }: { +function ProbeFallback({ body, allBodies, isSelected = false, onBodySelect, typeConfigs }: { body: CelestialBody; allBodies: CelestialBody[]; isSelected?: boolean; onBodySelect?: (body: CelestialBody) => void; + typeConfigs?: any; }) { const position = body.positions[0]; // Use smart render position calculation const renderPosition = useMemo(() => { - return calculateRenderPosition(body, allBodies); - }, [position.x, position.y, position.z, body, allBodies]); + return calculateRenderPosition(body, allBodies, typeConfigs); + }, [position.x, position.y, position.z, body, allBodies, typeConfigs]); 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(undefined); const [loadError, setLoadError] = useState(false); const [resourceScale, setResourceScale] = useState(1.0); + const [typeConfigs] = useSystemSetting('celestial_type_configs', null); // Fetch model from backend API useEffect(() => { @@ -314,8 +318,15 @@ export function Probe({ body, allBodies, isSelected = false, onBodySelect }: Pro onError={() => { setLoadError(true); }} + typeConfigs={typeConfigs} />; } - return ; + return ; } diff --git a/frontend/src/components/Scene.tsx b/frontend/src/components/Scene.tsx index 75c602a..2e6034b 100644 --- a/frontend/src/components/Scene.tsx +++ b/frontend/src/components/Scene.tsx @@ -19,6 +19,7 @@ import { scalePosition } from '../utils/scaleDistance'; import { calculateRenderPosition } from '../utils/renderPosition'; import type { CelestialBody as CelestialBodyType, Position } from '../types'; import type { ToastContextValue } from '../contexts/ToastContext'; // Import ToastContextValue +import { useSystemSetting } from '../hooks/useSystemSetting'; interface SceneProps { bodies: CelestialBodyType[]; @@ -32,6 +33,25 @@ interface 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 useEffect(() => { 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 // 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) return [renderPos.x, renderPos.z, renderPos.y] as [number, number, number]; - }, [selectedBody, bodies]); + }, [selectedBody, bodies, typeConfigs]); return (
{/* Increase ambient light to see textures better */} diff --git a/frontend/src/config/celestialSizes.ts b/frontend/src/config/celestialSizes.ts index 7cd00ac..027903b 100644 --- a/frontend/src/config/celestialSizes.ts +++ b/frontend/src/config/celestialSizes.ts @@ -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 * Shared across components for consistent sizing @@ -28,9 +20,42 @@ export const TYPE_SIZES: Record = { }; /** - * 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 { - return TYPE_SIZES[type] || TYPE_SIZES.default; -} +export function getCelestialSize(bodyOrName: any, type?: string, typeConfig?: any): number { + 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; +} \ No newline at end of file diff --git a/frontend/src/pages/admin/CelestialBodies.tsx b/frontend/src/pages/admin/CelestialBodies.tsx index cc77a12..39ec123 100644 --- a/frontend/src/pages/admin/CelestialBodies.tsx +++ b/frontend/src/pages/admin/CelestialBodies.tsx @@ -214,6 +214,7 @@ export function CelestialBodies() { // Edit handler const handleEdit = async (record: CelestialBody) => { + form.resetFields(); // Clear previous form state setEditingRecord(record); // Parse extra_data if it's a string (from backend JSON field) @@ -637,6 +638,38 @@ export function CelestialBodies() { + {/* Physical Properties for Star, Planet, Dwarf Planet, Satellite */} + + prevValues.type !== currentValues.type + }> + {({ getFieldValue }) => { + const bodyType = getFieldValue('type'); + if (!['star', 'planet', 'dwarf_planet', 'satellite'].includes(bodyType)) { + return null; + } + + return ( + + + + `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + parser={value => value!.replace(/\$\s?|(,*)/g, '')} + /> + + + + ); + }} + + {/* Orbit parameters and info for planets and dwarf planets */} prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info diff --git a/frontend/src/pages/admin/SystemSettings.tsx b/frontend/src/pages/admin/SystemSettings.tsx index a440b18..83b7251 100644 --- a/frontend/src/pages/admin/SystemSettings.tsx +++ b/frontend/src/pages/admin/SystemSettings.tsx @@ -79,9 +79,15 @@ export function SystemSettings() { // Edit handler const handleEdit = (record: SystemSetting) => { setEditingRecord(record); + + let formValue = record.value; + if (record.value_type === 'json' && typeof record.value === 'object') { + formValue = JSON.stringify(record.value, null, 2); + } + form.setFieldsValue({ key: record.key, - value: record.value, + value: formValue, value_type: record.value_type, category: record.category, label: record.label, @@ -107,6 +113,16 @@ export function SystemSettings() { try { 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) { // Update await request.put(`/system/settings/${editingRecord.key}`, values); @@ -190,6 +206,21 @@ export function SystemSettings() { if (record.value_type === 'bool') { return ; } + if (record.value_type === 'json' || typeof value === 'object') { + return ( +
+ {JSON.stringify(value)} +
+ ); + } return {String(value)}; }, }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 88189e9..401f143 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -20,6 +20,12 @@ export interface CelestialBody { description?: string; details?: string; // Markdown content for detailed view 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 starSystemData?: { system_id: number; diff --git a/frontend/src/utils/renderPosition.ts b/frontend/src/utils/renderPosition.ts index 08caf46..e38755e 100644 --- a/frontend/src/utils/renderPosition.ts +++ b/frontend/src/utils/renderPosition.ts @@ -13,7 +13,8 @@ import type { CelestialBody } from '../types'; */ export function calculateRenderPosition( body: CelestialBody, - allBodies: CelestialBody[] + allBodies: CelestialBody[], + typeConfigs?: any ): { x: number; y: number; z: number; hasOffset: boolean } { const pos = body.positions[0]; if (!pos) { @@ -45,11 +46,11 @@ export function calculateRenderPosition( const nz = dz / dist; // 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 // while smaller planets (Earth, Mars) have smaller offsets - const parentSize = getCelestialSize(parent.name, parent.type); - const visualOffset = parentSize * 1.5 + 0.3; + const parentSize = getCelestialSize(parent, undefined, typeConfigs); + const visualOffset = parentSize * 1.5 + 0.5; return { x: parentScaled.x + nx * visualOffset, diff --git a/test_api_response.py b/test_api_response.py new file mode 100644 index 0000000..bdb6eaa --- /dev/null +++ b/test_api_response.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Test script to verify extra_data in API response""" +import asyncio +import sys +import os +import json + +# Add backend to path +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.db_service import celestial_body_service + +async def test_api_data(): + """Simulate API response building""" + async with AsyncSessionLocal() as session: + # Get Earth + body = await celestial_body_service.get_body_by_id("399", session) + + if not body: + print("Earth not found in database") + return + + # Build response dict like API does + body_dict = { + "id": body.id, + "name": body.name, + "name_zh": body.name_zh, + "type": body.type, + "description": body.description, + "is_active": body.is_active, + "extra_data": body.extra_data, + "positions": [] + } + + print("=== Database Body Object ===") + print(f"Body ID: {body.id}") + print(f"Body Name: {body.name}") + print(f"Body Type: {body.type}") + print(f"Extra Data Type: {type(body.extra_data)}") + print(f"Extra Data: {body.extra_data}") + + print("\n=== API Response Dict ===") + print(json.dumps(body_dict, indent=2, default=str)) + +if __name__ == "__main__": + asyncio.run(test_api_data()) diff --git a/test_full_api.py b/test_full_api.py new file mode 100644 index 0000000..2efc9e1 --- /dev/null +++ b/test_full_api.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Direct API test - simulate the exact API call""" +import asyncio +import sys +import os +from datetime import datetime + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.db_service import celestial_body_service, position_service + +async def simulate_api_call(): + """Simulate /celestial/positions endpoint""" + async with AsyncSessionLocal() as db: + # Get all bodies from database + all_bodies = await celestial_body_service.get_all_bodies(db) + + # Filter to only Solar System bodies + all_bodies = [b for b in all_bodies if b.system_id == 1] + + # Filter to Earth only + all_bodies = [b for b in all_bodies if b.id == "399"] + + bodies_data = [] + now = datetime.utcnow() + + for body in all_bodies: + # Get most recent position + recent_positions = await position_service.get_positions( + body_id=body.id, + start_time=now, + end_time=now, + session=db + ) + + if recent_positions and len(recent_positions) > 0: + latest_pos = recent_positions[-1] + body_dict = { + "id": body.id, + "name": body.name, + "name_zh": body.name_zh, + "type": body.type, + "description": body.description, + "is_active": body.is_active, + "extra_data": body.extra_data, # THIS IS THE KEY LINE + "positions": [{ + "time": latest_pos.time.isoformat(), + "x": latest_pos.x, + "y": latest_pos.y, + "z": latest_pos.z, + }] + } + bodies_data.append(body_dict) + + print("=== Simulated API Response for Earth ===") + print(f"ID: {body_dict['id']}") + print(f"Name: {body_dict['name']}") + print(f"Type: {body_dict['type']}") + print(f"Extra Data: {body_dict['extra_data']}") + print(f"Extra Data Type: {type(body_dict['extra_data'])}") + if body_dict['extra_data']: + print(f"Real Radius: {body_dict['extra_data'].get('real_radius')}") + +if __name__ == "__main__": + asyncio.run(simulate_api_call()) diff --git a/update_unified_ratio.py b/update_unified_ratio.py new file mode 100644 index 0000000..83fae8a --- /dev/null +++ b/update_unified_ratio.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Update celestial_type_configs to use unified ratio""" +import asyncio +import sys +import os +import json + +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from app.database import AsyncSessionLocal +from app.services.system_settings_service import system_settings_service + +# Unified ratio for all types +# This ensures consistent scaling based on real physical size +UNIFIED_RATIO = 0.00008 + +async def update_unified_ratio(): + """Update all type ratios to use unified value""" + async with AsyncSessionLocal() as session: + # Get current config + current_config = await system_settings_service.get_setting_value('celestial_type_configs', session) + + print("=" * 70) + print("CURRENT celestial_type_configs:") + print("=" * 70) + print(json.dumps(current_config, indent=2)) + + # Update all ratios to unified value + updated_config = {} + for body_type, config in current_config.items(): + updated_config[body_type] = { + 'ratio': UNIFIED_RATIO, + 'default': config.get('default', 0.5) # Keep existing defaults + } + + print("\n" + "=" * 70) + print(f"UPDATED celestial_type_configs (unified ratio = {UNIFIED_RATIO}):") + print("=" * 70) + print(json.dumps(updated_config, indent=2)) + + # Save updated config + await system_settings_service.update_setting( + 'celestial_type_configs', + updated_config, + session + ) + + await session.commit() + + print("\n" + "=" * 70) + print("✅ All type ratios unified successfully!") + print("=" * 70) + print("\nNOW ALL BODIES WILL USE:") + print(f" Display Size = real_radius (km) × {UNIFIED_RATIO}") + print("\nThis ensures:") + print(" • Consistent scaling across all body types") + print(" • Jupiter is correctly 10.97x larger than Earth") + print(" • Ceres is correctly 0.07x the size of Earth") + print(" • Pluto is correctly 0.19x the size of Earth") + +if __name__ == "__main__": + asyncio.run(update_unified_ratio()) diff --git a/verify_final_sizes.py b/verify_final_sizes.py new file mode 100644 index 0000000..0227751 --- /dev/null +++ b/verify_final_sizes.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Verify final sizes after updates""" +import asyncio +import sys +import os + +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 +from app.services.system_settings_service import system_settings_service + +async def verify_final_sizes(): + async with AsyncSessionLocal() as session: + # Get updated config + configs = await system_settings_service.get_setting_value('celestial_type_configs', session) + + # Solar System body IDs + solar_ids = ['10', '199', '299', '399', '499', '599', '699', '799', '899', '999', + '2000001', '136199', '136108', '136472'] + + stmt = select(CelestialBody).where(CelestialBody.id.in_(solar_ids)) + result = await session.execute(stmt) + bodies = result.scalars().all() + + print("=" * 85) + print("FINAL VERIFICATION - All Celestial Body Sizes") + print("=" * 85) + print(f"{'Name':<20} {'Type':<15} {'Real Radius':<15} {'Ratio':<12} {'Display Size':<12} {'vs Earth'}") + print("-" * 85) + + earth_size = None + sizes = [] + + for body in sorted(bodies, key=lambda b: b.extra_data.get('real_radius', 0) if b.extra_data else 0, reverse=True): + if body.extra_data and body.extra_data.get('real_radius'): + real_radius = body.extra_data['real_radius'] + ratio = configs[body.type]['ratio'] + display_size = real_radius * ratio + + sizes.append((body.name, body.type, real_radius, ratio, display_size)) + + if body.name == 'Earth': + earth_size = display_size + + # Print with Earth comparison + for name, btype, real_radius, ratio, display_size in sizes: + vs_earth = f"{display_size / earth_size:.2f}x" if earth_size else "N/A" + print(f"{name:<20} {btype:<15} {real_radius:>8.0f} km {ratio:.6f} {display_size:>10.4f} {vs_earth:>7}") + + print("\n" + "=" * 85) + print("✅ VERIFICATION COMPLETE") + print("=" * 85) + print("All bodies now use:") + print(f" • Unified ratio: {configs['planet']['ratio']}") + print(f" • Display Size = real_radius × {configs['planet']['ratio']}") + print("\nSize relationships:") + print(f" • Jupiter = {sizes[0][4] / earth_size:.2f}x Earth (should be ~11x)") + print(f" • Saturn = {sizes[1][4] / earth_size:.2f}x Earth (should be ~9x)") + print(f" • Pluto = {[s for s in sizes if s[0] == 'Pluto'][0][4] / earth_size:.2f}x Earth (should be ~0.19x)") + print(f" • Ceres = {[s for s in sizes if s[0] == 'Ceres'][0][4] / earth_size:.2f}x Earth (should be ~0.07x)") + print("\n🎉 All sizes are now consistent and physically accurate!") + +if __name__ == "__main__": + asyncio.run(verify_final_sizes())