cosmo/frontend/src/components/CelestialBody.tsx

246 lines
7.1 KiB
TypeScript
Raw Normal View History

/**
* CelestialBody component - renders a planet or probe with textures
*/
import { useRef, useMemo } from 'react';
import { Mesh, DoubleSide } from 'three';
import { useFrame } from '@react-three/fiber';
import { useTexture, Html } from '@react-three/drei';
import type { CelestialBody as CelestialBodyType } from '../types';
import { scalePosition } from '../utils/scaleDistance';
interface CelestialBodyProps {
body: CelestialBodyType;
}
// Saturn Rings component - multiple rings for band effect
function SaturnRings() {
return (
<group rotation={[Math.PI / 2, 0, 0]}>
{/* Inner bright ring */}
<mesh>
<ringGeometry args={[1.4, 1.6, 64]} />
<meshBasicMaterial
color="#D4B896"
transparent
opacity={0.7}
side={DoubleSide}
/>
</mesh>
{/* Middle darker band */}
<mesh>
<ringGeometry args={[1.6, 1.75, 64]} />
<meshBasicMaterial
color="#8B7355"
transparent
opacity={0.5}
side={DoubleSide}
/>
</mesh>
{/* Outer bright ring */}
<mesh>
<ringGeometry args={[1.75, 2.0, 64]} />
<meshBasicMaterial
color="#C4A582"
transparent
opacity={0.6}
side={DoubleSide}
/>
</mesh>
{/* Cassini Division (gap) */}
<mesh>
<ringGeometry args={[2.0, 2.05, 64]} />
<meshBasicMaterial
color="#000000"
transparent
opacity={0.2}
side={DoubleSide}
/>
</mesh>
{/* A Ring (outer) */}
<mesh>
<ringGeometry args={[2.05, 2.2, 64]} />
<meshBasicMaterial
color="#B89968"
transparent
opacity={0.5}
side={DoubleSide}
/>
</mesh>
</group>
);
}
// Planet component with texture
function Planet({ body, size, emissive, emissiveIntensity }: {
body: CelestialBodyType;
size: number;
emissive: string;
emissiveIntensity: number;
}) {
const meshRef = useRef<Mesh>(null);
const position = body.positions[0];
// Apply non-linear distance scaling for better visualization
const scaledPos = useMemo(() => {
// Special handling for Moon - display it relative to Earth with visible offset
if (body.name === 'Moon') {
const moonScaled = scalePosition(position.x, position.y, position.z);
// Add a visual offset to make Moon visible next to Earth (2 units away)
// Moon orbits Earth at ~0.00257 AU, we'll give it a 2-unit offset from Earth's scaled position
const angle = Math.atan2(position.y, position.x);
const offset = 2.0; // Visual offset in scaled units
return {
x: moonScaled.x + Math.cos(angle) * offset,
y: moonScaled.y + Math.sin(angle) * offset,
z: moonScaled.z,
};
}
return scalePosition(position.x, position.y, position.z);
}, [position.x, position.y, position.z, body.name]);
// Texture mapping for planets
const texturePath = useMemo(() => {
const textureMap: Record<string, string> = {
Sun: '/textures/2k_sun.jpg',
Mercury: '/textures/2k_mercury.jpg',
Venus: '/textures/2k_venus_surface.jpg',
Earth: '/textures/2k_earth_daymap.jpg',
Moon: '/textures/2k_moon.jpg',
Mars: '/textures/2k_mars.jpg',
Jupiter: '/textures/2k_jupiter.jpg',
Saturn: '/textures/2k_saturn.jpg',
Uranus: '/textures/2k_uranus.jpg',
Neptune: '/textures/2k_neptune.jpg',
};
return textureMap[body.name] || null;
}, [body.name]);
// Load texture - this must be at the top level, not in try-catch
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);
return (
<group position={[scaledPos.x, scaledPos.z, scaledPos.y]}>
<mesh ref={meshRef} renderOrder={0}>
<sphereGeometry args={[size, 64, 64]} />
{texture ? (
<meshStandardMaterial
map={texture}
emissive={emissive}
emissiveIntensity={emissiveIntensity}
roughness={body.type === 'star' ? 0 : 0.7}
metalness={0.1}
depthTest={true}
depthWrite={true}
/>
) : (
<meshStandardMaterial
color="#888888"
emissive={emissive}
emissiveIntensity={emissiveIntensity}
roughness={0.7}
metalness={0.1}
depthTest={true}
depthWrite={true}
/>
)}
</mesh>
{/* Saturn Rings */}
{body.name === 'Saturn' && <SaturnRings />}
{/* Sun glow effect */}
{body.type === 'star' && (
<>
<pointLight intensity={10} distance={400} color="#fff8e7" />
<mesh>
<sphereGeometry args={[size * 1.8, 32, 32]} />
<meshBasicMaterial color="#FDB813" transparent opacity={0.35} />
</mesh>
</>
)}
{/* Name label */}
<Html
position={[0, size + 0.3, 0]}
center
distanceFactor={10}
style={{
color: body.type === 'star' ? '#FDB813' : '#ffffff',
fontSize: '11px',
fontWeight: 'bold',
textShadow: '0 0 4px rgba(0,0,0,0.8)',
pointerEvents: 'none',
userSelect: 'none',
whiteSpace: 'nowrap',
}}
>
2025-11-27 10:14:25 +00:00
{body.name_zh || body.name}
<br />
<span style={{ fontSize: '8px', opacity: 0.7 }}>
{distance.toFixed(2)} AU
</span>
</Html>
</group>
);
}
export function CelestialBody({ body }: 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, // Slightly larger sun for better visibility
emissive: '#FDB813',
emissiveIntensity: 1.5,
};
}
// Planet sizes - balanced for visibility with smaller probes
const planetSizes: Record<string, number> = {
Mercury: 0.35, // Slightly larger for visibility
Venus: 0.55, // Slightly larger for visibility
Earth: 0.6, // Slightly larger for visibility
Moon: 0.25, // Smaller than Earth
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
};
return {
size: planetSizes[body.name] || 0.5,
emissive: '#000000',
emissiveIntensity: 0,
};
}, [body.name, body.type]);
return (
<Planet
body={body}
size={appearance.size}
emissive={appearance.emissive}
emissiveIntensity={appearance.emissiveIntensity}
/>
);
}