import { useRef, useMemo, useState, useEffect, Suspense } from 'react'; import { Mesh, Group, DoubleSide } 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; disableGlow?: boolean; // 禁用光晕效果(用于详情视图) } // Planetary Rings component - handles texture-based rings function PlanetaryRings({ texturePath, planetRadius }: { texturePath?: string | null, planetRadius: number }) { const texture = texturePath ? useTexture(texturePath) : null; const meshRef = useRef(null); // Dynamic ring dimensions based on planet size // Standard Saturn proportions: Inner ~1.1 R, Outer ~2.3 R const innerRadius = planetRadius * 1.05; const outerRadius = planetRadius * 2.2; // Custom geometry with Polar UV mapping for strip textures const geometry = useMemo(() => { if (!texturePath) return null; const geo = new THREE.RingGeometry(innerRadius, outerRadius, 64); const pos = geo.attributes.position; const uv = geo.attributes.uv; // Manually remap UVs to be polar: // We map normalized radius to BOTH U and V coordinates. // This allows the texture to work whether it's a horizontal strip OR a vertical strip. // - If horizontal strip: U varies with radius (correct), V varies (doesn't matter as strip is uniform vertically) // - If vertical strip: V varies with radius (correct), U varies (doesn't matter as strip is uniform horizontally) for (let i = 0; i < pos.count; i++) { const x = pos.getX(i); const y = pos.getY(i); // Calculate radius from center const r = Math.sqrt(x * x + y * y); // Normalize radius: 0 at inner, 1 at outer // Clamp to ensure floating point errors don't break texture wrapping const normalizedR = Math.max(0, Math.min(1, (r - innerRadius) / (outerRadius - innerRadius))); // Map to UV (same value for both axes to support H/V strips) uv.setXY(i, normalizedR, normalizedR); } geo.attributes.uv.needsUpdate = true; return geo; }, [texturePath, innerRadius, outerRadius]); if (texture && geometry) { return ( ); } return null; } export function BodyViewer({ body, disableGlow = false }: BodyViewerProps) { const meshRef = useRef(null); const [texturePath, setTexturePath] = useState(undefined); const [ringTexturePath, setRingTexturePath] = useState(undefined); const [modelPath, setModelPath] = useState(undefined); const [modelScale, setModelScale] = useState(1.0); const [loadError, setLoadError] = useState(false); // Determine size and appearance - use larger sizes for detail view with a cap const appearance = useMemo(() => { const baseSize = getCelestialSize(body.name, body.type); // Detail view scaling strategy: // - Small bodies (< 0.15): scale up 3x for visibility // - Medium bodies (0.15-0.3): scale up 2x // - Large bodies (> 0.3): use base size with max cap of 1.2 let finalSize: number; if (body.type === 'star') { finalSize = 1.0; // Fixed size for stars } else if (baseSize < 0.15) { // Small bodies: scale up for visibility finalSize = baseSize * 3.0; } else if (baseSize < 0.3) { // Medium bodies: moderate scaling finalSize = baseSize * 2.0; } else { // Large bodies: minimal or no scaling with a cap finalSize = Math.min(baseSize * 1.2, 1.2); } if (body.type === 'star') { // Use database color if available (for interstellar stars), otherwise default to yellow const starColor = body.starSystemData?.color || '#FDB813'; return { size: finalSize, emissive: starColor, emissiveIntensity: 1.5 }; } if (body.type === 'comet') { return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 }; } if (body.type === 'satellite') { return { size: finalSize, emissive: '#888888', emissiveIntensity: 0.4 }; } return { size: finalSize, emissive: '#000000', emissiveIntensity: 0 }; }, [body.name, body.type]); // Fetch resources (texture or model) useEffect(() => { setLoadError(false); setModelPath(undefined); setTexturePath(undefined); setRingTexturePath(undefined); const loadResources = async () => { try { const response = await fetchBodyResources(body.id, body.type === 'probe' ? 'model' : 'texture'); if (response.resources.length > 0) { if (body.type === 'probe') { const mainResource = response.resources[0]; const fullPath = `/upload/${mainResource.file_path}`; useGLTF.preload(fullPath); // Preload GLTF setModelPath(fullPath); setModelScale(mainResource.extra_data?.scale || 1.0); } else { // Find main texture: layer is surface, base, or empty const mainTexture = response.resources.find( (r) => !r.extra_data?.layer || r.extra_data.layer === 'surface' || r.extra_data.layer === 'base' ); // Find ring texture: strictly by layer meta-data const ringTexture = response.resources.find( (r) => r.extra_data?.layer === 'ring' ); if (mainTexture) { setTexturePath(`/upload/${mainTexture.file_path}`); } else { setTexturePath(null); } if (ringTexture) { setRingTexturePath(`/upload/${ringTexture.file_path}`); } else { setRingTexturePath(null); } } } else { // No resources found if (body.type === 'probe') setModelPath(null); else { setTexturePath(null); setRingTexturePath(null); } } } catch (err) { console.error(`Failed to load resource for ${body.name}:`, err); setLoadError(true); if (body.type === 'probe') setModelPath(null); else { setTexturePath(null); setRingTexturePath(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 = 1.2; // Increased from 0.35 to 1.2 for detail view - larger probes const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.8; const finalScale = Math.max(0.5, Math.min(3.0, calculatedScale)); // Increased range for larger display 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, ringTexturePath, meshRef, disableGlow = false }: { body: CelestialBodyType; size: number; emissive: string; emissiveIntensity: number; texturePath: string | null; ringTexturePath?: string | null; meshRef: React.RefObject; disableGlow?: boolean; }) { 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 */} ); } return ( {/* Use irregular nucleus for comets, regular sphere for others */} {body.type === 'comet' ? ( <> ) : ( {/* Higher resolution for detail view */} {texture ? ( ) : ( )} )} {/* Planetary Rings: Render ONLY if texture exists */} {ringTexturePath && ( )} {/* Sun glow effect - multi-layer scattered light (仅在非禁用光晕模式下显示) */} {body.type === 'star' && !disableGlow && ( <> {/* Inner bright corona */} {/* Middle glow layer */} {/* Outer diffuse halo */} {/* Far scattered light */} )} ); }