import { useRef, useMemo, useState, useEffect, Suspense } from 'react'; import { Mesh, Group } from 'three'; import * as THREE from 'three'; import { useGLTF, useTexture } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import type { CelestialBody as CelestialBodyType } from '../types'; import { fetchBodyResources } from '../utils/api'; import { getCelestialSize } from '../config/celestialSizes'; interface BodyViewerProps { body: CelestialBodyType; } // Reusable component to render just the 3D model/mesh of a celestial body export function BodyViewer({ body }: BodyViewerProps) { const meshRef = useRef(null); const groupRef = useRef(null); const [texturePath, setTexturePath] = useState(undefined); const [modelPath, setModelPath] = useState(undefined); const [modelScale, setModelScale] = useState(1.0); const [loadError, setLoadError] = useState(false); // Determine size and appearance const appearance = useMemo(() => { if (body.type === 'star') { return { size: 0.4, emissive: '#FDB813', emissiveIntensity: 1.5 }; } if (body.type === 'comet') { return { size: getCelestialSize(body.name, body.type), emissive: '#000000', emissiveIntensity: 0 }; } if (body.type === 'satellite') { return { size: getCelestialSize(body.name, body.type), emissive: '#888888', emissiveIntensity: 0.4 }; } return { size: getCelestialSize(body.name, body.type), emissive: '#000000', emissiveIntensity: 0 }; }, [body.name, body.type]); // Fetch resources (texture or model) useEffect(() => { setLoadError(false); setModelPath(undefined); setTexturePath(undefined); const loadResources = async () => { try { const response = await fetchBodyResources(body.id, body.type === 'probe' ? 'model' : 'texture'); if (response.resources.length > 0) { const mainResource = response.resources[0]; const fullPath = `/upload/${mainResource.file_path}`; if (body.type === 'probe') { useGLTF.preload(fullPath); // Preload GLTF setModelPath(fullPath); setModelScale(mainResource.extra_data?.scale || 1.0); } else { setTexturePath(fullPath); } } else { // No resources found if (body.type === 'probe') setModelPath(null); else setTexturePath(null); } } catch (err) { console.error(`Failed to load resource for ${body.name}:`, err); setLoadError(true); if (body.type === 'probe') setModelPath(null); else setTexturePath(null); } }; loadResources(); }, [body.id, body.name, body.type]); // Handle Probe Model rendering if (body.type === 'probe' && modelPath !== undefined) { if (modelPath === null || loadError) { // Fallback sphere for probes if model fails return ( ); } return ; } // Handle Celestial Body (Planet, Star, etc.) rendering if (texturePath !== undefined) { return ( ); } // Show nothing while loading resources return null; } // Sub-component for Probe models function ProbeModelViewer({ modelPath, modelScale }: { modelPath: string; modelScale: number }) { const groupRef = useRef(null); const gltf = useGLTF(modelPath); const scene = gltf.scene; const optimalScale = useMemo(() => { if (!scene) return 1; const box = new THREE.Box3().setFromObject(scene); const size = new THREE.Vector3(); box.getSize(size); const maxDimension = Math.max(size.x, size.y, size.z); const targetSize = 0.35; // Standardize view const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.3; const finalScale = Math.max(0.2, Math.min(1.0, calculatedScale)); return finalScale * modelScale; }, [scene, modelScale]); const configuredScene = useMemo(() => { if (!scene) return null; const clonedScene = scene.clone(); clonedScene.traverse((child: any) => { if (child.isMesh) { if (child.material) { if (Array.isArray(child.material)) { child.material = child.material.map((mat: any) => { const clonedMat = mat.clone(); clonedMat.depthTest = true; clonedMat.depthWrite = true; return clonedMat; }); } else { child.material = child.material.clone(); child.material.depthTest = true; child.material.depthWrite = true; } } } }); return clonedScene; }, [scene]); if (!configuredScene) return null; return ( ); } // Sub-component for Planet models function PlanetModelViewer({ body, size, emissive, emissiveIntensity, texturePath, meshRef }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; texturePath: string | null; meshRef: React.RefObject; }) { const texture = texturePath ? useTexture(texturePath) : null; // Rotation animation useFrame((_, delta) => { if (meshRef.current) { meshRef.current.rotation.y += delta * 0.1; } }); // Irregular Comet Nucleus - potato-like shape function IrregularNucleus({ size, texture }: { size: number; texture: THREE.Texture | null }) { const nucleusRef = useRef(null); // Create irregular geometry by deforming a sphere const geometry = useMemo(() => { const geo = new THREE.SphereGeometry(size, 64, 64); // Higher resolution for detail view const positions = geo.attributes.position; // Deform vertices to create irregular shape for (let i = 0; i < positions.count; i++) { const x = positions.getX(i); const y = positions.getY(i); const z = positions.getZ(i); // Create multiple noise frequencies for complex shape const noise1 = Math.sin(x * 3.5) * Math.cos(y * 2.7) * Math.sin(z * 4.2); const noise2 = Math.cos(x * 5.1) * Math.sin(y * 4.3) * Math.cos(z * 3.8); const noise3 = Math.sin(x * 7.2) * Math.sin(y * 6.1) * Math.cos(z * 5.5); const deformation = 1 + (noise1 * 0.25 + noise2 * 0.15 + noise3 * 0.1); positions.setXYZ(i, x * deformation, y * deformation, z * deformation); } geo.computeVertexNormals(); return geo; }, [size]); // Slow rotation useFrame((_, delta) => { if (nucleusRef.current) { nucleusRef.current.rotation.y += delta * 0.05; nucleusRef.current.rotation.z += delta * 0.02; } }); return ( {texture ? ( ) : ( )} ); } // Enhanced Coma (gas/dust cloud) around comet nucleus function CometComa({ radius }: { radius: number }) { const positions = useMemo(() => { const count = 300; // More particles for detail view const p = new Float32Array(count * 3); for (let i = 0; i < count; i++) { // Denser near center, spreading outward const r = radius * (0.8 + Math.pow(Math.random(), 1.5) * 4); // 0.8x to 4.8x 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]); const pointsRef = useRef(null); // Animate coma useFrame((state, delta) => { if (pointsRef.current) { pointsRef.current.rotation.y += delta * 0.05; // Pulsing effect const scale = 1 + Math.sin(state.clock.elapsedTime * 0.5) * 0.1; pointsRef.current.scale.setScalar(scale); } }); return ( <> {/* Inner bright coma */} {/* Middle glow layer */} {/* Outer diffuse glow */} ); } // 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) */} ); } return ( {/* Use irregular nucleus for comets, regular sphere for others */} {body.type === 'comet' ? ( <> ) : ( {/* Higher resolution for detail view */} {texture ? ( ) : ( )} )} {/* Saturn Rings */} {body.id === '699' && } {/* Sun glow effect */} {body.type === 'star' && ( <> )} ); }