117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
/**
|
|
* OrbitRenderer - Unified orbit rendering component
|
|
* Renders precomputed orbital paths for all celestial bodies (planets and dwarf planets)
|
|
*/
|
|
import { useEffect, useState } from 'react';
|
|
import { Line } from '@react-three/drei';
|
|
import * as THREE from 'three';
|
|
import { scalePosition } from '../utils/scaleDistance';
|
|
import { request } from '../utils/request';
|
|
|
|
interface OrbitData {
|
|
bodyId: string;
|
|
bodyName: string;
|
|
bodyNameZh: string | null;
|
|
points: THREE.Vector3[];
|
|
color: string;
|
|
numPoints: number;
|
|
periodDays: number | null;
|
|
}
|
|
|
|
interface OrbitRendererProps {
|
|
visible?: boolean;
|
|
}
|
|
|
|
export function OrbitRenderer({ visible = true }: OrbitRendererProps) {
|
|
const [orbits, setOrbits] = useState<OrbitData[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchOrbits = async () => {
|
|
try {
|
|
// Fetch precomputed orbits from backend
|
|
const response = await request.get('/celestial/orbits');
|
|
const data = response.data;
|
|
|
|
if (!data.orbits || data.orbits.length === 0) {
|
|
console.warn('⚠️ No orbital data found in database');
|
|
setLoading(false);
|
|
setError('No orbital data available. Please generate orbits first.');
|
|
return;
|
|
}
|
|
|
|
// Convert to Three.js format
|
|
const orbitData: OrbitData[] = data.orbits.map((orbit: any) => {
|
|
// Convert position points to Vector3 with scaling
|
|
const points = orbit.points.map((p: any) => {
|
|
const scaled = scalePosition(p.x, p.y, p.z);
|
|
// Convert to Three.js coordinate system (x, z, y)
|
|
return new THREE.Vector3(scaled.x, scaled.z, scaled.y);
|
|
});
|
|
|
|
// Close the orbit loop if first and last points are close
|
|
if (points.length > 1) {
|
|
const firstPoint = points[0];
|
|
const lastPoint = points[points.length - 1];
|
|
const distance = firstPoint.distanceTo(lastPoint);
|
|
|
|
// If endpoints are close (within 5 units), close the loop
|
|
if (distance < 5) {
|
|
points.push(firstPoint.clone());
|
|
}
|
|
}
|
|
|
|
return {
|
|
bodyId: orbit.body_id,
|
|
bodyName: orbit.body_name,
|
|
bodyNameZh: orbit.body_name_zh,
|
|
points,
|
|
color: orbit.color || '#CCCCCC',
|
|
numPoints: orbit.num_points,
|
|
periodDays: orbit.period_days,
|
|
};
|
|
});
|
|
|
|
setOrbits(orbitData);
|
|
setLoading(false);
|
|
|
|
} catch (err) {
|
|
console.error('❌ Failed to load orbits:', err);
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchOrbits();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return null;
|
|
}
|
|
|
|
if (error) {
|
|
console.error('⚠️ Orbit rendering error:', error);
|
|
return null;
|
|
}
|
|
|
|
if (orbits.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<group visible={visible}>
|
|
{orbits.map((orbit) => (
|
|
<Line
|
|
key={orbit.bodyId}
|
|
points={orbit.points}
|
|
color={orbit.color}
|
|
lineWidth={1.5}
|
|
opacity={0.4}
|
|
transparent
|
|
/>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|