cosmo/frontend/src/components/Constellations.tsx

131 lines
3.8 KiB
TypeScript

/**
* Constellations component - renders major constellations with connecting lines
*/
import { useEffect, useState, useMemo } from 'react';
import { Line, Text, Billboard } from '@react-three/drei';
import * as THREE from 'three';
import { fetchStaticData } from '../utils/api';
interface ConstellationStar {
name: string;
ra: number; // Right Ascension in degrees
dec: number; // Declination in degrees
}
interface Constellation {
name: string;
name_zh: string;
stars: ConstellationStar[];
lines: [number, number][]; // Indices of stars to connect
}
/**
* Convert RA/Dec to Cartesian coordinates
* Use fixed distance for constellation stars to create celestial sphere effect
*/
function raDecToCartesian(ra: number, dec: number, distance: number = 100) {
const raRad = (ra * Math.PI) / 180;
const decRad = (dec * Math.PI) / 180;
const x = distance * Math.cos(decRad) * Math.cos(raRad);
const y = distance * Math.cos(decRad) * Math.sin(raRad);
const z = distance * Math.sin(decRad);
return new THREE.Vector3(x, y, z);
}
export function Constellations() {
const [constellations, setConstellations] = useState<Constellation[]>([]);
useEffect(() => {
// Load constellation data from API
fetchStaticData('constellation')
.then((response) => {
// Convert API response to Constellation format
const constellationData = response.items.map((item) => ({
name: item.name,
name_zh: item.name_zh,
stars: item.data.stars,
lines: item.data.lines,
}));
setConstellations(constellationData);
})
.catch((err) => console.error('Failed to load constellations:', err));
}, []);
const constellationLines = useMemo(() => {
return constellations.map((constellation) => {
// Convert all stars to 3D positions
const starPositions = constellation.stars.map((star) =>
raDecToCartesian(star.ra, star.dec)
);
// Create line segments based on connection indices
const lineSegments = constellation.lines.map(([startIdx, endIdx]) => ({
start: starPositions[startIdx],
end: starPositions[endIdx],
}));
// Calculate center position for label (average of all stars)
const center = starPositions.reduce(
(acc, pos) => acc.add(pos),
new THREE.Vector3()
).divideScalar(starPositions.length);
return {
name: constellation.name,
nameZh: constellation.name_zh,
starPositions,
lineSegments,
center,
};
});
}, [constellations]);
if (constellationLines.length === 0) {
return null;
}
return (
<group>
{constellationLines.map((constellation) => (
<group key={constellation.name}>
{/* Render constellation stars */}
{constellation.starPositions.map((pos, idx) => (
<mesh key={`${constellation.name}-star-${idx}`} position={pos}>
<sphereGeometry args={[0.3, 8, 8]} />
<meshBasicMaterial color="#FFFFFF" transparent opacity={0.8} />
</mesh>
))}
{/* Render connecting lines */}
{constellation.lineSegments.map((segment, idx) => (
<Line
key={`${constellation.name}-line-${idx}`}
points={[segment.start, segment.end]}
color="#4488FF"
lineWidth={1}
transparent
opacity={0.5}
/>
))}
{/* Constellation name label */}
<Billboard position={constellation.center}>
<Text
fontSize={2}
color="#88AAFF"
anchorX="center"
anchorY="middle"
outlineWidth={0.1}
outlineColor="#000000"
>
{constellation.nameZh}
</Text>
</Billboard>
</group>
))}
</group>
);
}