/** * CelestialBody component - renders a planet or probe with textures */ import { useRef, useMemo, useState, useEffect } from 'react'; import { Mesh, DoubleSide } from 'three'; // Removed AdditiveBlending here import * as THREE from 'three'; // Imported as * to access AdditiveBlending, SpriteMaterial, CanvasTexture import { useFrame } from '@react-three/fiber'; 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 function SaturnRings() { return ( {/* Inner bright ring */} {/* Middle darker band */} {/* Outer bright ring */} {/* Cassini Division (gap) */} {/* A Ring (outer) */} ); } // Planet component with texture 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]; const [texturePath, setTexturePath] = useState(undefined); // Use smart render position calculation const renderPosition = useMemo(() => { return calculateRenderPosition(body, allBodies); }, [position.x, position.y, position.z, body, allBodies]); const scaledPos = { x: renderPosition.x, y: renderPosition.y, z: renderPosition.z }; // Fetch texture from backend API useEffect(() => { fetchBodyResources(body.id, 'texture') .then((response) => { // Find the main texture (not atmosphere or night layers) const mainTexture = response.resources.find( (r) => !r.file_path.includes('atmosphere') && !r.file_path.includes('night') ); if (mainTexture) { // Construct full URL from file_path // file_path is like "texture/2k_sun.jpg", need to add "upload/" prefix const protocol = window.location.protocol; const hostname = window.location.hostname; const port = import.meta.env.VITE_API_BASE_URL ? '' : ':8000'; setTexturePath(`${protocol}//${hostname}${port}/upload/${mainTexture.file_path}`); } else { setTexturePath(null); } }) .catch((err) => { console.error(`Failed to load texture for ${body.name}:`, err); setTexturePath(null); }); }, [body.id, body.name]); // Show nothing while loading if (texturePath === undefined) { return null; } return ; } // Comet Particles Component function CometParticles({ radius, count = 6, color = '#88ccff' }: { radius: number; count?: number; color?: string }) { const positions = useMemo(() => { const p = new Float32Array(count * 3); for (let i = 0; i < count; i++) { // Random spherical distribution const r = radius * (1.2 + Math.random() * 2.0); // Spread: 1.2x to 3.2x radius const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); p[i * 3] = r * Math.sin(phi) * Math.cos(theta); p[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); p[i * 3 + 2] = r * Math.cos(phi); } return p; }, [radius, count]); // Ref for animation const pointsRef = useRef(null); useFrame((_, delta) => { if (pointsRef.current) { // Subtle rotation pointsRef.current.rotation.y += delta * 0.1; pointsRef.current.rotation.z += delta * 0.05; } }); return ( ); } // Separate component to handle texture loading function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies, isSelected = false }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; scaledPos: { x: number; y: number; z: number }; texturePath: string | null; position: { x: number; y: number; z: number }; meshRef: React.RefObject; hasOffset: boolean; allBodies: CelestialBodyType[]; isSelected?: boolean; }) { // Load texture if path is provided const texture = texturePath ? useTexture(texturePath) : null; // Slow rotation for visual effect useFrame((_, delta) => { if (meshRef.current) { meshRef.current.rotation.y += delta * 0.1; } }); // Calculate ACTUAL distance from Sun for display (not scaled) const distance = Math.sqrt(position.x ** 2 + position.y ** 2 + position.z ** 2); // Get offset description if this body has one const offsetDesc = hasOffset ? getOffsetDescription(body, allBodies) : null; return ( {texture ? ( ) : ( )} {/* Saturn Rings */} {body.id === '699' && } {/* Comet Particles */} {body.type === 'comet' && ( )} {/* Sun glow effect */} {body.type === 'star' && ( <> )} {/* Name label */} {body.name_zh || body.name} {offsetDesc && ( <>
{/* 从 9px 减小到 7px */} {offsetDesc} )}
{/* 从 8px 减小到 7px */} {distance.toFixed(2)} AU
); } 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; // Skip probes - they will use 3D models if (body.type === 'probe') { return null; } // Determine size based on body type const appearance = useMemo(() => { if (body.type === 'star') { return { size: 0.4, // Sun size emissive: '#FDB813', emissiveIntensity: 1.5, }; } // Comet - bright core with glow if (body.type === 'comet') { return { size: getCelestialSize(body.name, body.type), emissive: '#000000', // Revert to no special emissive color for texture emissiveIntensity: 0, // Revert to no special emissive intensity }; } // Satellite (natural moons) - small size with slight glow for visibility if (body.type === 'satellite') { return { size: getCelestialSize(body.name, body.type), emissive: '#888888', // Slight glow to make it visible emissiveIntensity: 0.4, }; } // Planet and dwarf planet sizes return { size: getCelestialSize(body.name, body.type), emissive: '#000000', emissiveIntensity: 0, }; }, [body.name, body.type]); return ( ); }