diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bd10dab..fbb3ab0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -96,7 +96,7 @@ function App() { // Filter probes and planets from all bodies const probes = bodies.filter((b) => b.type === 'probe'); const planets = bodies.filter((b) => - b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite' + b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite' || b.type === 'comet' ); const handleBodySelect = (body: CelestialBody | null) => { @@ -138,7 +138,7 @@ function App() {
{/* Header with simplified branding and User Auth */}
b.is_active !== false).length} selectedBodyName={selectedBody?.name} user={user} onOpenAuth={() => setShowAuthModal(true)} diff --git a/frontend/src/components/CelestialBody.tsx b/frontend/src/components/CelestialBody.tsx index ea4b006..046b07e 100644 --- a/frontend/src/components/CelestialBody.tsx +++ b/frontend/src/components/CelestialBody.tsx @@ -8,10 +8,12 @@ import { useTexture, Html } from '@react-three/drei'; import type { CelestialBody as CelestialBodyType } from '../types'; import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition'; import { fetchBodyResources } from '../utils/api'; +import { PLANET_SIZES, SATELLITE_SIZES, getCelestialSize } from '../config/celestialSizes'; interface CelestialBodyProps { body: CelestialBodyType; allBodies: CelestialBodyType[]; + isSelected?: boolean; } // Saturn Rings component - multiple rings for band effect @@ -73,12 +75,13 @@ function SaturnRings() { } // Planet component with texture -function Planet({ body, size, emissive, emissiveIntensity, allBodies }: { +function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; allBodies: CelestialBodyType[]; + isSelected?: boolean; }) { const meshRef = useRef(null); const position = body.positions[0]; @@ -132,11 +135,12 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies }: { meshRef={meshRef} hasOffset={renderPosition.hasOffset} allBodies={allBodies} + isSelected={isSelected} />; } // Separate component to handle texture loading -function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies }: { +function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies, isSelected = false }: { body: CelestialBodyType; size: number; emissive: string; @@ -147,6 +151,7 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur meshRef: React.RefObject; hasOffset: boolean; allBodies: CelestialBodyType[]; + isSelected?: boolean; }) { // Load texture if path is provided const texture = texturePath ? useTexture(texturePath) : null; @@ -212,25 +217,27 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur distanceFactor={10} style={{ color: body.type === 'star' ? '#FDB813' : '#ffffff', - fontSize: '11px', + fontSize: '9px', // 从 11px 减小到 9px fontWeight: 'bold', textShadow: '0 0 4px rgba(0,0,0,0.8)', pointerEvents: 'none', userSelect: 'none', whiteSpace: 'nowrap', + opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡 + transition: 'opacity 0.3s ease', }} > {body.name_zh || body.name} {offsetDesc && ( <>
- + {/* 从 9px 减小到 7px */} {offsetDesc} )}
- + {/* 从 8px 减小到 7px */} {distance.toFixed(2)} AU @@ -238,7 +245,7 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur ); } -export function CelestialBody({ body, allBodies }: CelestialBodyProps) { +export function CelestialBody({ body, allBodies, isSelected = false }: CelestialBodyProps) { // Get the current position (use the first position for now) const position = body.positions[0]; if (!position) return null; @@ -252,7 +259,7 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) { const appearance = useMemo(() => { if (body.type === 'star') { return { - size: 0.4, // Revert to original size + size: 0.4, // Sun size emissive: '#FDB813', emissiveIntensity: 1.5, }; @@ -260,32 +267,16 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) { // Satellite (natural moons) - small size with slight glow for visibility if (body.type === 'satellite') { - const satelliteSizes: Record = { - Moon: 0.15, // Small but visible - // Add other satellites here as needed - }; return { - size: satelliteSizes[body.name] || 0.12, + size: getCelestialSize(body.name, body.type), emissive: '#888888', // Slight glow to make it visible emissiveIntensity: 0.4, }; } - // Planet sizes - balanced for visibility with smaller probes - const planetSizes: Record = { - Mercury: 0.35, // Slightly larger for visibility - Venus: 0.55, // Slightly larger for visibility - Earth: 0.6, // Slightly larger for visibility - Mars: 0.45, // Slightly larger for visibility - Jupiter: 1.4, // Larger gas giant - Saturn: 1.2, // Larger gas giant - Uranus: 0.8, // Medium outer planet - Neptune: 0.8, // Medium outer planet - Pluto: 0.2, // Dwarf planet, smaller than Moon - }; - + // Planet and dwarf planet sizes return { - size: planetSizes[body.name] || 0.5, + size: getCelestialSize(body.name, body.type), emissive: '#000000', emissiveIntensity: 0, }; @@ -298,6 +289,7 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) { emissive={appearance.emissive} emissiveIntensity={appearance.emissiveIntensity} allBodies={allBodies} + isSelected={isSelected} /> ); } diff --git a/frontend/src/components/Constellations.tsx b/frontend/src/components/Constellations.tsx index c8c395f..3197039 100644 --- a/frontend/src/components/Constellations.tsx +++ b/frontend/src/components/Constellations.tsx @@ -94,7 +94,7 @@ export function Constellations() { {constellation.starPositions.map((pos, idx) => ( - + ))} @@ -104,9 +104,9 @@ export function Constellations() { key={`${constellation.name}-line-${idx}`} points={[segment.start, segment.end]} color="#4488FF" - lineWidth={1.5} + lineWidth={1} transparent - opacity={0.5} + opacity={0.3} /> ))} @@ -115,7 +115,7 @@ export function Constellations() { void; + resourceScale?: number; }) { const groupRef = useRef(null); const position = body.positions[0]; @@ -52,16 +55,17 @@ function ProbeModel({ body, modelPath, allBodies, onError }: { const maxDimension = Math.max(size.x, size.y, size.z); // Target size for display (consistent visual size) - const targetSize = 0.5; // Target visual size in scene units + const targetSize = 0.35; // Balanced size for all probes // Calculate scale factor - const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.2; + const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.3; - // Clamp scale to reasonable range - const finalScale = Math.max(0.1, Math.min(2.0, calculatedScale)); + // Clamp scale to reasonable range - tighter range for consistency + const finalScale = Math.max(0.2, Math.min(1.0, calculatedScale)); - return finalScale; - }, [scene, body.name]); + // Apply custom scale from resource metadata + return finalScale * resourceScale; + }, [scene, body.name, resourceScale]); // 4. Hook: Configured Scene const configuredScene = useMemo(() => { @@ -128,25 +132,27 @@ function ProbeModel({ body, modelPath, allBodies, onError }: { distanceFactor={15} style={{ color: '#00ffff', - fontSize: '12px', + fontSize: '9px', // 从 12px 减小到 9px fontWeight: 'bold', textShadow: '0 0 6px rgba(0,255,255,0.8)', pointerEvents: 'none', userSelect: 'none', whiteSpace: 'nowrap', + opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡 + transition: 'opacity 0.3s ease', }} > - 🛰️ {body.name_zh || body.name} + {body.name_zh || body.name} {offsetDesc && ( <>
- + {/* 从 10px 减小到 7px */} {offsetDesc} )}
- + {/* 从 10px 减小到 7px */} {distance.toFixed(2)} AU @@ -155,7 +161,7 @@ function ProbeModel({ body, modelPath, allBodies, onError }: { } // Fallback component when model is not available -function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: CelestialBody[] }) { +function ProbeFallback({ body, allBodies, isSelected = false }: { body: CelestialBody; allBodies: CelestialBody[]; isSelected?: boolean }) { const position = body.positions[0]; // Use smart render position calculation @@ -185,25 +191,27 @@ function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: Ce distanceFactor={15} style={{ color: '#ff6666', - fontSize: '12px', + fontSize: '9px', // 从 12px 减小到 9px fontWeight: 'bold', textShadow: '0 0 6px rgba(255,0,0,0.8)', pointerEvents: 'none', userSelect: 'none', whiteSpace: 'nowrap', + opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡 + transition: 'opacity 0.3s ease', }} > - 🛰️ {body.name_zh || body.name} + {body.name_zh || body.name} {offsetDesc && ( <>
- + {/* 从 10px 减小到 7px */} {offsetDesc} )}
- + {/* 从 10px 减小到 7px */} {distance.toFixed(2)} AU @@ -211,10 +219,11 @@ function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: Ce ); } -export function Probe({ body, allBodies }: ProbeProps) { +export function Probe({ body, allBodies, isSelected = false }: ProbeProps) { const position = body.positions[0]; const [modelPath, setModelPath] = useState(undefined); const [loadError, setLoadError] = useState(false); + const [resourceScale, setResourceScale] = useState(1.0); // Fetch model from backend API useEffect(() => { @@ -227,6 +236,12 @@ export function Probe({ body, allBodies }: ProbeProps) { if (response.resources.length > 0) { // Get the first model resource const modelResource = response.resources[0]; + + // Get scale from extra_data + const scale = modelResource.extra_data?.scale || 1.0; + setResourceScale(scale); + console.log(`[Probe ${body.name}] Scale from resource:`, scale); + // Construct full URL from file_path const protocol = window.location.protocol; const hostname = window.location.hostname; @@ -265,11 +280,11 @@ export function Probe({ body, allBodies }: ProbeProps) { // Use model if available and no load error, otherwise use fallback if (modelPath && !loadError) { - return { + return { console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`); setLoadError(true); }} />; } - return ; + return ; } diff --git a/frontend/src/components/ProbeList.tsx b/frontend/src/components/ProbeList.tsx index 3dd3c89..0001b5f 100644 --- a/frontend/src/components/ProbeList.tsx +++ b/frontend/src/components/ProbeList.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk } from 'lucide-react'; +import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk, Sparkles } from 'lucide-react'; import type { CelestialBody } from '../types'; interface ProbeListProps { @@ -11,7 +11,7 @@ interface ProbeListProps { } export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) { - const [isCollapsed, setIsCollapsed] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(true); // 默认关闭 const [searchTerm, setSearchTerm] = useState(''); const [expandedGroup, setExpandedGroup] = useState(null); // 只允许一个分组展开 @@ -42,6 +42,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset const dwarfPlanetList = processedBodies.filter(({ body }) => body.type === 'dwarf_planet'); const satelliteList = processedBodies.filter(({ body }) => body.type === 'satellite'); const probeList = processedBodies.filter(({ body }) => body.type === 'probe'); + const cometList = processedBodies.filter(({ body }) => body.type === 'comet'); const toggleGroup = (groupName: string) => { // 如果点击的是当前展开的分组,则收起;否则切换到新分组 @@ -147,6 +148,20 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset /> )} + {/* Comets Group */} + {cometList.length > 0 && ( + } + count={cometList.length} + bodies={cometList} + isExpanded={expandedGroup === 'comet'} + onToggle={() => toggleGroup('comet')} + selectedBody={selectedBody} + onBodySelect={onBodySelect} + /> + )} + {/* No results message */} {processedBodies.length === 0 && (
diff --git a/frontend/src/components/Scene.tsx b/frontend/src/components/Scene.tsx index 36520f1..3dcff0c 100644 --- a/frontend/src/components/Scene.tsx +++ b/frontend/src/components/Scene.tsx @@ -3,7 +3,7 @@ */ import { Canvas } from '@react-three/fiber'; import { OrbitControls, Stars as BackgroundStars, Html } from '@react-three/drei'; -import { useMemo } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { CelestialBody } from './CelestialBody'; import { Probe } from './Probe'; import { CameraController } from './CameraController'; @@ -29,6 +29,16 @@ interface SceneProps { } export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0 }: SceneProps) { + // State to control info panel visibility (independent of selection) + const [showInfoPanel, setShowInfoPanel] = useState(true); + + // Reset info panel visibility when selected body changes + useEffect(() => { + if (selectedBody) { + setShowInfoPanel(true); + } + }, [selectedBody]); + // Separate natural celestial bodies (planets/dwarf planets/satellites/stars) from probes const celestialBodies = bodies.filter((b) => b.type !== 'probe'); const probes = bodies.filter((b) => b.type === 'probe'); @@ -116,7 +126,12 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi {/* Render all celestial bodies: planets, dwarf planets, satellites, and stars */} {celestialBodies.map((body) => ( - + ))} {/* Unified orbit renderer for all celestial bodies (planets and dwarf planets) */} @@ -124,7 +139,12 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi {/* Render visible probes with 3D models */} {visibleProbes.map((body) => ( - + ))} {/* Render trajectory for selected probe */} @@ -148,9 +168,9 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi /> {/* Dynamic Focus Info Label */} - {selectedBody && onBodySelect && ( + {selectedBody && showInfoPanel && ( - onBodySelect(null)} /> + setShowInfoPanel(false)} /> )} diff --git a/frontend/src/config/celestialSizes.ts b/frontend/src/config/celestialSizes.ts new file mode 100644 index 0000000..2446b5c --- /dev/null +++ b/frontend/src/config/celestialSizes.ts @@ -0,0 +1,69 @@ +/** + * Celestial body rendering sizes configuration + * Shared across components for consistent sizing + */ + +/** + * Planet rendering sizes (radius in scene units) + */ +export const PLANET_SIZES: Record = { + Mercury: 0.35, + Venus: 0.55, + Earth: 0.6, + Mars: 0.45, + Jupiter: 1.4, + Saturn: 1.2, + Uranus: 0.8, + Neptune: 0.8, + Pluto: 0.2, + // Default for unknown planets + default: 0.5, +}; + +/** + * Satellite rendering sizes (radius in scene units) + */ +export const SATELLITE_SIZES: Record = { + Moon: 0.15, + // Default for unknown satellites + default: 0.12, +}; + +/** + * Star rendering sizes (radius in scene units) + */ +export const STAR_SIZES: Record = { + Sun: 0.4, + // Default for unknown stars + default: 0.4, +}; + +/** + * Comet rendering sizes (radius in scene units) + * Comets are typically small with a bright nucleus + */ +export const COMET_SIZES: Record = { + // Famous comets + Halley: 0.15, + // Default for unknown comets + default: 0.12, +}; + +/** + * Get the rendering size for a celestial body by name and type + */ +export function getCelestialSize(name: string, type: string): number { + switch (type) { + case 'planet': + case 'dwarf_planet': + return PLANET_SIZES[name] || PLANET_SIZES.default; + case 'satellite': + return SATELLITE_SIZES[name] || SATELLITE_SIZES.default; + case 'star': + return STAR_SIZES[name] || STAR_SIZES.default; + case 'comet': + return COMET_SIZES[name] || COMET_SIZES.default; + default: + return 0.5; + } +} diff --git a/frontend/src/pages/admin/CelestialBodies.tsx b/frontend/src/pages/admin/CelestialBodies.tsx index 61dd974..4fce50d 100644 --- a/frontend/src/pages/admin/CelestialBodies.tsx +++ b/frontend/src/pages/admin/CelestialBodies.tsx @@ -2,9 +2,10 @@ * Celestial Bodies Management Page */ import { useState, useEffect } from 'react'; -import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert } from 'antd'; -import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons'; +import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col } from 'antd'; +import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; +import type { UploadFile } from 'antd/es/upload/interface'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; @@ -35,6 +36,8 @@ export function CelestialBodies() { const [form] = Form.useForm(); const [searching, setSearching] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [uploading, setUploading] = useState(false); + const [refreshResources, setRefreshResources] = useState(0); useEffect(() => { loadData(); @@ -166,7 +169,7 @@ export function CelestialBodies() { const handleModalOk = async () => { try { const values = await form.validateFields(); - + if (editingRecord) { // Update await request.put(`/celestial/${editingRecord.id}`, values); @@ -176,7 +179,7 @@ export function CelestialBodies() { await request.post('/celestial/', values); message.success('创建成功'); } - + setIsModalOpen(false); loadData(); } catch (error) { @@ -185,6 +188,50 @@ export function CelestialBodies() { } }; + // Handle resource upload + const handleResourceUpload = async (file: File, resourceType: string) => { + if (!editingRecord) { + message.error('请先选择要编辑的天体'); + return false; + } + + setUploading(true); + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await request.post( + `/celestial/resources/upload?body_id=${editingRecord.id}&resource_type=${resourceType}`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + + message.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`); + setRefreshResources(prev => prev + 1); // Trigger reload + return false; // Prevent default upload behavior + } catch (error: any) { + message.error(error.response?.data?.detail || '上传失败'); + return false; + } finally { + setUploading(false); + } + }; + + // Handle resource delete + const handleResourceDelete = async (resourceId: number) => { + try { + await request.delete(`/celestial/resources/${resourceId}`); + message.success('删除成功'); + setRefreshResources(prev => prev + 1); // Trigger reload + } catch (error: any) { + message.error(error.response?.data?.detail || '删除失败'); + } + }; + const columns: ColumnsType = [ { title: 'ID', @@ -214,6 +261,7 @@ export function CelestialBodies() { { text: '卫星', value: 'satellite' }, { text: '探测器', value: 'probe' }, { text: '矮行星', value: 'dwarf_planet' }, + { text: '彗星', value: 'comet' }, ], onFilter: (value, record) => record.type === value, render: (type: string) => { @@ -223,6 +271,7 @@ export function CelestialBodies() { dwarf_planet: '矮行星', satellite: '卫星', probe: '探测器', + comet: '彗星', }; return typeMap[type] || type; }, @@ -319,90 +368,204 @@ export function CelestialBodies() { )} - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + - + - {editingRecord && editingRecord.has_resources && ( - - - {Object.entries(editingRecord.resources || {}).map(([type, resources]) => ( - - {resources.map((res: any, idx: number) => ( -
- {res.file_path} - - ({(res.file_size / 1024).toFixed(2)} KB) - -
- ))} -
- ))} -
-
- 提示:资源文件管理功能即将推出,敬请期待 -
-
+ {editingRecord && ( + )} ); +} + +// Resource Manager Component +function ResourceManager({ + bodyId, + bodyType, + resources, + hasResources, + onUpload, + onDelete, + uploading, + refreshTrigger, +}: { + bodyId: string; + bodyType: string; + resources?: any; + hasResources?: boolean; + onUpload: (file: File, resourceType: string) => Promise; + onDelete: (resourceId: number) => Promise; + uploading: boolean; + refreshTrigger: number; +}) { + const [currentResources, setCurrentResources] = useState(resources); + + // Load resources when modal opens + useEffect(() => { + request.get(`/celestial/resources/${bodyId}`) + .then(({ data }) => { + const grouped: any = {}; + data.resources.forEach((res: any) => { + if (!grouped[res.resource_type]) { + grouped[res.resource_type] = []; + } + grouped[res.resource_type].push(res); + }); + setCurrentResources(grouped); + }) + .catch(() => { + message.error('加载资源列表失败'); + }); + }, [refreshTrigger, bodyId]); + + const resourceTypes = [ + { key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)' }, + { key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)' }, + ]; + + return ( + + + + {resourceTypes.map(({ key, label }) => ( +
+
{label}
+ + onUpload(file, key)} + showUploadList={false} + disabled={uploading} + > + + + + {currentResources?.[key] && currentResources[key].length > 0 && ( +
+ {currentResources[key].map((res: any) => ( +
+
+ {res.file_path} + + ({(res.file_size / 1024).toFixed(2)} KB) + + onDelete(res.id)} + okText="删除" + cancelText="取消" + > + + +
+ {key === 'model' && ( +
+ + 显示缩放: + { + // Update scale in resource + const newScale = value || 1.0; + request.put(`/celestial/resources/${res.id}`, { + extra_data: { ...res.extra_data, scale: newScale } + }).then(() => { + message.success('缩放参数已更新'); + }).catch(() => { + message.error('更新失败'); + }); + }} + /> + + (推荐: Webb=0.3, 旅行者=1.5) + + +
+ )} +
+ ))} +
+ )} +
+ ))} +
+
+ ); } \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index eb7aa89..bd0c0d2 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -2,7 +2,7 @@ * TypeScript type definitions for Cosmo application */ -export type CelestialBodyType = 'planet' | 'probe' | 'star' | 'dwarf_planet' | 'satellite'; +export type CelestialBodyType = 'planet' | 'probe' | 'star' | 'dwarf_planet' | 'satellite' | 'comet'; export interface Position { time: string; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 808cc45..e2cee08 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -138,6 +138,7 @@ export async function fetchBodyResources(bodyId: string, resourceType?: string): file_size: number; mime_type: string; created_at: string; + extra_data?: Record; }>; }> { const params: Record = {}; diff --git a/frontend/src/utils/renderPosition.ts b/frontend/src/utils/renderPosition.ts index f522d4f..08caf46 100644 --- a/frontend/src/utils/renderPosition.ts +++ b/frontend/src/utils/renderPosition.ts @@ -4,11 +4,12 @@ */ import { scalePosition } from './scaleDistance'; +import { getCelestialSize } from '../config/celestialSizes'; import type { CelestialBody } from '../types'; /** * Calculate rendering position using true scaled coordinates - * For satellites, add a radial offset relative to PARENT PLANET + * For satellites and probes near planets, add a radial offset relative to PARENT PLANET */ export function calculateRenderPosition( body: CelestialBody, @@ -22,33 +23,33 @@ export function calculateRenderPosition( // 1. Calculate base scaled position (fallback) const scaled = scalePosition(pos.x, pos.y, pos.z); - // 2. Special handling for satellites (like Moon) - if (body.type === 'satellite') { + // 2. Special handling for satellites (like Moon) and probes near planets + if (body.type === 'satellite' || body.type === 'probe') { const parent = findParentPlanet(body, allBodies); if (parent) { const parentPos = parent.positions[0]; // Calculate parent's scaled position (where it is rendered) - // Note: Recursion is possible if parent is also a satellite, but unlikely for planets const parentScaled = scalePosition(parentPos.x, parentPos.y, parentPos.z); - - // Calculate vector from Parent to Satellite (in True AU coordinates) + + // Calculate vector from Parent to body (in True AU coordinates) const dx = pos.x - parentPos.x; const dy = pos.y - parentPos.y; const dz = pos.z - parentPos.z; - + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); - + if (dist > 0) { // Normalize the direction vector const nx = dx / dist; const ny = dy / dist; const nz = dz / dist; - // Set a fixed visual offset distance - // Earth radius is 0.6, Moon is 0.15. - // A distance of 1.5 units ensures they are close but distinct and don't overlap. - // This simulates a "mini-system" view at the planet's location. - const visualOffset = 1.5; + // Calculate dynamic offset based on parent planet's rendering size + // Formula: planetRadius × 1.5 + 0.3 (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; return { x: parentScaled.x + nx * visualOffset, @@ -106,7 +107,31 @@ export function findParentPlanet( * Get description for bodies that are close to planets */ export function getOffsetDescription(body: CelestialBody, allBodies: CelestialBody[]): string | null { - // This function is kept for compatibility but no longer calculates offsets - // Could be used to show proximity information in the future - return null; + if (body.type !== 'satellite' && body.type !== 'probe') { + return null; + } + + const parent = findParentPlanet(body, allBodies); + if (!parent) { + return null; + } + + const pos = body.positions[0]; + const parentPos = parent.positions[0]; + if (!pos || !parentPos) { + return null; + } + + // Calculate actual distance + const dx = pos.x - parentPos.x; + const dy = pos.y - parentPos.y; + const dz = pos.z - parentPos.z; + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + + // Return proximity description + const distanceStr = dist < 0.01 + ? `${(dist * 149597870.7).toFixed(0)} km` // Convert AU to km for very close objects + : `${dist.toFixed(4)} AU`; + + return `近${parent.name_zh || parent.name} (${distanceStr})`; }