0.9.9
parent
e796eceaf8
commit
7037628831
|
|
@ -96,7 +96,7 @@ function App() {
|
||||||
// Filter probes and planets from all bodies
|
// Filter probes and planets from all bodies
|
||||||
const probes = bodies.filter((b) => b.type === 'probe');
|
const probes = bodies.filter((b) => b.type === 'probe');
|
||||||
const planets = bodies.filter((b) =>
|
const planets = bodies.filter((b) =>
|
||||||
b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite'
|
b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite' || b.type === 'comet'
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBodySelect = (body: CelestialBody | null) => {
|
const handleBodySelect = (body: CelestialBody | null) => {
|
||||||
|
|
@ -138,7 +138,7 @@ function App() {
|
||||||
<div className="w-full h-full relative">
|
<div className="w-full h-full relative">
|
||||||
{/* Header with simplified branding and User Auth */}
|
{/* Header with simplified branding and User Auth */}
|
||||||
<Header
|
<Header
|
||||||
bodyCount={bodies.length}
|
bodyCount={bodies.filter(b => b.is_active !== false).length}
|
||||||
selectedBodyName={selectedBody?.name}
|
selectedBodyName={selectedBody?.name}
|
||||||
user={user}
|
user={user}
|
||||||
onOpenAuth={() => setShowAuthModal(true)}
|
onOpenAuth={() => setShowAuthModal(true)}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import { useTexture, Html } from '@react-three/drei';
|
||||||
import type { CelestialBody as CelestialBodyType } from '../types';
|
import type { CelestialBody as CelestialBodyType } from '../types';
|
||||||
import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition';
|
import { calculateRenderPosition, getOffsetDescription } from '../utils/renderPosition';
|
||||||
import { fetchBodyResources } from '../utils/api';
|
import { fetchBodyResources } from '../utils/api';
|
||||||
|
import { PLANET_SIZES, SATELLITE_SIZES, getCelestialSize } from '../config/celestialSizes';
|
||||||
|
|
||||||
interface CelestialBodyProps {
|
interface CelestialBodyProps {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
allBodies: CelestialBodyType[];
|
allBodies: CelestialBodyType[];
|
||||||
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saturn Rings component - multiple rings for band effect
|
// Saturn Rings component - multiple rings for band effect
|
||||||
|
|
@ -73,12 +75,13 @@ function SaturnRings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet component with texture
|
// Planet component with texture
|
||||||
function Planet({ body, size, emissive, emissiveIntensity, allBodies }: {
|
function Planet({ body, size, emissive, emissiveIntensity, allBodies, isSelected = false }: {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
size: number;
|
size: number;
|
||||||
emissive: string;
|
emissive: string;
|
||||||
emissiveIntensity: number;
|
emissiveIntensity: number;
|
||||||
allBodies: CelestialBodyType[];
|
allBodies: CelestialBodyType[];
|
||||||
|
isSelected?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const meshRef = useRef<Mesh>(null);
|
const meshRef = useRef<Mesh>(null);
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
@ -132,11 +135,12 @@ function Planet({ body, size, emissive, emissiveIntensity, allBodies }: {
|
||||||
meshRef={meshRef}
|
meshRef={meshRef}
|
||||||
hasOffset={renderPosition.hasOffset}
|
hasOffset={renderPosition.hasOffset}
|
||||||
allBodies={allBodies}
|
allBodies={allBodies}
|
||||||
|
isSelected={isSelected}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate component to handle texture loading
|
// Separate component to handle texture loading
|
||||||
function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies }: {
|
function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, texturePath, position, meshRef, hasOffset, allBodies, isSelected = false }: {
|
||||||
body: CelestialBodyType;
|
body: CelestialBodyType;
|
||||||
size: number;
|
size: number;
|
||||||
emissive: string;
|
emissive: string;
|
||||||
|
|
@ -147,6 +151,7 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
|
||||||
meshRef: React.RefObject<Mesh>;
|
meshRef: React.RefObject<Mesh>;
|
||||||
hasOffset: boolean;
|
hasOffset: boolean;
|
||||||
allBodies: CelestialBodyType[];
|
allBodies: CelestialBodyType[];
|
||||||
|
isSelected?: boolean;
|
||||||
}) {
|
}) {
|
||||||
// Load texture if path is provided
|
// Load texture if path is provided
|
||||||
const texture = texturePath ? useTexture(texturePath) : null;
|
const texture = texturePath ? useTexture(texturePath) : null;
|
||||||
|
|
@ -212,25 +217,27 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
|
||||||
distanceFactor={10}
|
distanceFactor={10}
|
||||||
style={{
|
style={{
|
||||||
color: body.type === 'star' ? '#FDB813' : '#ffffff',
|
color: body.type === 'star' ? '#FDB813' : '#ffffff',
|
||||||
fontSize: '11px',
|
fontSize: '9px', // 从 11px 减小到 9px
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textShadow: '0 0 4px rgba(0,0,0,0.8)',
|
textShadow: '0 0 4px rgba(0,0,0,0.8)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡
|
||||||
|
transition: 'opacity 0.3s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{body.name_zh || body.name}
|
{body.name_zh || body.name}
|
||||||
{offsetDesc && (
|
{offsetDesc && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '9px', color: '#ffaa00', opacity: 0.9 }}>
|
<span style={{ fontSize: '7px', color: '#ffaa00', opacity: isSelected ? 0.9 : 0.5 }}> {/* 从 9px 减小到 7px */}
|
||||||
{offsetDesc}
|
{offsetDesc}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '8px', opacity: 0.7 }}>
|
<span style={{ fontSize: '7px', opacity: isSelected ? 0.7 : 0.3 }}> {/* 从 8px 减小到 7px */}
|
||||||
{distance.toFixed(2)} AU
|
{distance.toFixed(2)} AU
|
||||||
</span>
|
</span>
|
||||||
</Html>
|
</Html>
|
||||||
|
|
@ -238,7 +245,7 @@ function PlanetMesh({ body, size, emissive, emissiveIntensity, scaledPos, textur
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CelestialBody({ body, allBodies }: CelestialBodyProps) {
|
export function CelestialBody({ body, allBodies, isSelected = false }: CelestialBodyProps) {
|
||||||
// Get the current position (use the first position for now)
|
// Get the current position (use the first position for now)
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
|
|
@ -252,7 +259,7 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) {
|
||||||
const appearance = useMemo(() => {
|
const appearance = useMemo(() => {
|
||||||
if (body.type === 'star') {
|
if (body.type === 'star') {
|
||||||
return {
|
return {
|
||||||
size: 0.4, // Revert to original size
|
size: 0.4, // Sun size
|
||||||
emissive: '#FDB813',
|
emissive: '#FDB813',
|
||||||
emissiveIntensity: 1.5,
|
emissiveIntensity: 1.5,
|
||||||
};
|
};
|
||||||
|
|
@ -260,32 +267,16 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) {
|
||||||
|
|
||||||
// Satellite (natural moons) - small size with slight glow for visibility
|
// Satellite (natural moons) - small size with slight glow for visibility
|
||||||
if (body.type === 'satellite') {
|
if (body.type === 'satellite') {
|
||||||
const satelliteSizes: Record<string, number> = {
|
|
||||||
Moon: 0.15, // Small but visible
|
|
||||||
// Add other satellites here as needed
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
size: satelliteSizes[body.name] || 0.12,
|
size: getCelestialSize(body.name, body.type),
|
||||||
emissive: '#888888', // Slight glow to make it visible
|
emissive: '#888888', // Slight glow to make it visible
|
||||||
emissiveIntensity: 0.4,
|
emissiveIntensity: 0.4,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planet sizes - balanced for visibility with smaller probes
|
// Planet and dwarf planet sizes
|
||||||
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
|
|
||||||
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
|
|
||||||
Pluto: 0.2, // Dwarf planet, smaller than Moon
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
size: planetSizes[body.name] || 0.5,
|
size: getCelestialSize(body.name, body.type),
|
||||||
emissive: '#000000',
|
emissive: '#000000',
|
||||||
emissiveIntensity: 0,
|
emissiveIntensity: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -298,6 +289,7 @@ export function CelestialBody({ body, allBodies }: CelestialBodyProps) {
|
||||||
emissive={appearance.emissive}
|
emissive={appearance.emissive}
|
||||||
emissiveIntensity={appearance.emissiveIntensity}
|
emissiveIntensity={appearance.emissiveIntensity}
|
||||||
allBodies={allBodies}
|
allBodies={allBodies}
|
||||||
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export function Constellations() {
|
||||||
{constellation.starPositions.map((pos, idx) => (
|
{constellation.starPositions.map((pos, idx) => (
|
||||||
<mesh key={`${constellation.name}-star-${idx}`} position={pos}>
|
<mesh key={`${constellation.name}-star-${idx}`} position={pos}>
|
||||||
<sphereGeometry args={[15, 8, 8]} />
|
<sphereGeometry args={[15, 8, 8]} />
|
||||||
<meshBasicMaterial color="#FFFFFF" transparent opacity={0.9} />
|
<meshBasicMaterial color="#FFFFFF" transparent opacity={0.6} />
|
||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -104,9 +104,9 @@ export function Constellations() {
|
||||||
key={`${constellation.name}-line-${idx}`}
|
key={`${constellation.name}-line-${idx}`}
|
||||||
points={[segment.start, segment.end]}
|
points={[segment.start, segment.end]}
|
||||||
color="#4488FF"
|
color="#4488FF"
|
||||||
lineWidth={1.5}
|
lineWidth={1}
|
||||||
transparent
|
transparent
|
||||||
opacity={0.5}
|
opacity={0.3}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ export function Constellations() {
|
||||||
<Text
|
<Text
|
||||||
fontSize={120}
|
fontSize={120}
|
||||||
color="#6699FF"
|
color="#6699FF"
|
||||||
fillOpacity={0.8}
|
fillOpacity={0.6}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="middle"
|
anchorY="middle"
|
||||||
outlineWidth={0}
|
outlineWidth={0}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,17 @@ import { fetchBodyResources } from '../utils/api';
|
||||||
interface ProbeProps {
|
interface ProbeProps {
|
||||||
body: CelestialBody;
|
body: CelestialBody;
|
||||||
allBodies: CelestialBody[];
|
allBodies: CelestialBody[];
|
||||||
|
isSelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate component for each probe type to properly use hooks
|
// Separate component for each probe type to properly use hooks
|
||||||
function ProbeModel({ body, modelPath, allBodies, onError }: {
|
function ProbeModel({ body, modelPath, allBodies, isSelected = false, onError, resourceScale = 1.0 }: {
|
||||||
body: CelestialBody;
|
body: CelestialBody;
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
allBodies: CelestialBody[];
|
allBodies: CelestialBody[];
|
||||||
|
isSelected?: boolean;
|
||||||
onError: () => void;
|
onError: () => void;
|
||||||
|
resourceScale?: number;
|
||||||
}) {
|
}) {
|
||||||
const groupRef = useRef<Group>(null);
|
const groupRef = useRef<Group>(null);
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
@ -52,16 +55,17 @@ function ProbeModel({ body, modelPath, allBodies, onError }: {
|
||||||
const maxDimension = Math.max(size.x, size.y, size.z);
|
const maxDimension = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
// Target size for display (consistent visual size)
|
// Target size for display (consistent visual size)
|
||||||
const targetSize = 0.5; // Target visual size in scene units
|
const targetSize = 0.35; // Balanced size for all probes
|
||||||
|
|
||||||
// Calculate scale factor
|
// Calculate scale factor
|
||||||
const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.2;
|
const calculatedScale = maxDimension > 0 ? targetSize / maxDimension : 0.3;
|
||||||
|
|
||||||
// Clamp scale to reasonable range
|
// Clamp scale to reasonable range - tighter range for consistency
|
||||||
const finalScale = Math.max(0.1, Math.min(2.0, calculatedScale));
|
const finalScale = Math.max(0.2, Math.min(1.0, calculatedScale));
|
||||||
|
|
||||||
return finalScale;
|
// Apply custom scale from resource metadata
|
||||||
}, [scene, body.name]);
|
return finalScale * resourceScale;
|
||||||
|
}, [scene, body.name, resourceScale]);
|
||||||
|
|
||||||
// 4. Hook: Configured Scene
|
// 4. Hook: Configured Scene
|
||||||
const configuredScene = useMemo(() => {
|
const configuredScene = useMemo(() => {
|
||||||
|
|
@ -128,25 +132,27 @@ function ProbeModel({ body, modelPath, allBodies, onError }: {
|
||||||
distanceFactor={15}
|
distanceFactor={15}
|
||||||
style={{
|
style={{
|
||||||
color: '#00ffff',
|
color: '#00ffff',
|
||||||
fontSize: '12px',
|
fontSize: '9px', // 从 12px 减小到 9px
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textShadow: '0 0 6px rgba(0,255,255,0.8)',
|
textShadow: '0 0 6px rgba(0,255,255,0.8)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡
|
||||||
|
transition: 'opacity 0.3s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🛰️ {body.name_zh || body.name}
|
{body.name_zh || body.name}
|
||||||
{offsetDesc && (
|
{offsetDesc && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '10px', color: '#ffaa00', opacity: 0.9 }}>
|
<span style={{ fontSize: '7px', color: '#ffaa00', opacity: isSelected ? 0.9 : 0.5 }}> {/* 从 10px 减小到 7px */}
|
||||||
{offsetDesc}
|
{offsetDesc}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '10px', opacity: 0.8 }}>
|
<span style={{ fontSize: '7px', opacity: isSelected ? 0.8 : 0.3 }}> {/* 从 10px 减小到 7px */}
|
||||||
{distance.toFixed(2)} AU
|
{distance.toFixed(2)} AU
|
||||||
</span>
|
</span>
|
||||||
</Html>
|
</Html>
|
||||||
|
|
@ -155,7 +161,7 @@ function ProbeModel({ body, modelPath, allBodies, onError }: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback component when model is not available
|
// Fallback component when model is not available
|
||||||
function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: CelestialBody[] }) {
|
function ProbeFallback({ body, allBodies, isSelected = false }: { body: CelestialBody; allBodies: CelestialBody[]; isSelected?: boolean }) {
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
|
|
||||||
// Use smart render position calculation
|
// Use smart render position calculation
|
||||||
|
|
@ -185,25 +191,27 @@ function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: Ce
|
||||||
distanceFactor={15}
|
distanceFactor={15}
|
||||||
style={{
|
style={{
|
||||||
color: '#ff6666',
|
color: '#ff6666',
|
||||||
fontSize: '12px',
|
fontSize: '9px', // 从 12px 减小到 9px
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textShadow: '0 0 6px rgba(255,0,0,0.8)',
|
textShadow: '0 0 6px rgba(255,0,0,0.8)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
opacity: isSelected ? 1 : 0.4, // 未选中时更暗淡
|
||||||
|
transition: 'opacity 0.3s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🛰️ {body.name_zh || body.name}
|
{body.name_zh || body.name}
|
||||||
{offsetDesc && (
|
{offsetDesc && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '10px', color: '#ffaa00', opacity: 0.9 }}>
|
<span style={{ fontSize: '7px', color: '#ffaa00', opacity: isSelected ? 0.9 : 0.5 }}> {/* 从 10px 减小到 7px */}
|
||||||
{offsetDesc}
|
{offsetDesc}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
<span style={{ fontSize: '10px', opacity: 0.8 }}>
|
<span style={{ fontSize: '7px', opacity: isSelected ? 0.8 : 0.3 }}> {/* 从 10px 减小到 7px */}
|
||||||
{distance.toFixed(2)} AU
|
{distance.toFixed(2)} AU
|
||||||
</span>
|
</span>
|
||||||
</Html>
|
</Html>
|
||||||
|
|
@ -211,10 +219,11 @@ function ProbeFallback({ body, allBodies }: { body: CelestialBody; allBodies: Ce
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Probe({ body, allBodies }: ProbeProps) {
|
export function Probe({ body, allBodies, isSelected = false }: ProbeProps) {
|
||||||
const position = body.positions[0];
|
const position = body.positions[0];
|
||||||
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
const [modelPath, setModelPath] = useState<string | null | undefined>(undefined);
|
||||||
const [loadError, setLoadError] = useState<boolean>(false);
|
const [loadError, setLoadError] = useState<boolean>(false);
|
||||||
|
const [resourceScale, setResourceScale] = useState<number>(1.0);
|
||||||
|
|
||||||
// Fetch model from backend API
|
// Fetch model from backend API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -227,6 +236,12 @@ export function Probe({ body, allBodies }: ProbeProps) {
|
||||||
if (response.resources.length > 0) {
|
if (response.resources.length > 0) {
|
||||||
// Get the first model resource
|
// Get the first model resource
|
||||||
const modelResource = response.resources[0];
|
const modelResource = response.resources[0];
|
||||||
|
|
||||||
|
// Get scale from extra_data
|
||||||
|
const scale = modelResource.extra_data?.scale || 1.0;
|
||||||
|
setResourceScale(scale);
|
||||||
|
console.log(`[Probe ${body.name}] Scale from resource:`, scale);
|
||||||
|
|
||||||
// Construct full URL from file_path
|
// Construct full URL from file_path
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
|
|
@ -265,11 +280,11 @@ export function Probe({ body, allBodies }: ProbeProps) {
|
||||||
|
|
||||||
// Use model if available and no load error, otherwise use fallback
|
// Use model if available and no load error, otherwise use fallback
|
||||||
if (modelPath && !loadError) {
|
if (modelPath && !loadError) {
|
||||||
return <ProbeModel body={body} modelPath={modelPath} allBodies={allBodies} onError={() => {
|
return <ProbeModel body={body} modelPath={modelPath} allBodies={allBodies} isSelected={isSelected} resourceScale={resourceScale} onError={() => {
|
||||||
console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`);
|
console.error(`[Probe ${body.name}] ProbeModel rendering failed, switching to fallback`);
|
||||||
setLoadError(true);
|
setLoadError(true);
|
||||||
}} />;
|
}} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProbeFallback body={body} allBodies={allBodies} />;
|
return <ProbeFallback body={body} allBodies={allBodies} isSelected={isSelected} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk, Sparkles } from 'lucide-react';
|
||||||
import type { CelestialBody } from '../types';
|
import type { CelestialBody } from '../types';
|
||||||
|
|
||||||
interface ProbeListProps {
|
interface ProbeListProps {
|
||||||
|
|
@ -11,7 +11,7 @@ interface ProbeListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) {
|
export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(true); // 默认关闭
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null); // 只允许一个分组展开
|
const [expandedGroup, setExpandedGroup] = useState<string | null>(null); // 只允许一个分组展开
|
||||||
|
|
||||||
|
|
@ -42,6 +42,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
||||||
const dwarfPlanetList = processedBodies.filter(({ body }) => body.type === 'dwarf_planet');
|
const dwarfPlanetList = processedBodies.filter(({ body }) => body.type === 'dwarf_planet');
|
||||||
const satelliteList = processedBodies.filter(({ body }) => body.type === 'satellite');
|
const satelliteList = processedBodies.filter(({ body }) => body.type === 'satellite');
|
||||||
const probeList = processedBodies.filter(({ body }) => body.type === 'probe');
|
const probeList = processedBodies.filter(({ body }) => body.type === 'probe');
|
||||||
|
const cometList = processedBodies.filter(({ body }) => body.type === 'comet');
|
||||||
|
|
||||||
const toggleGroup = (groupName: string) => {
|
const toggleGroup = (groupName: string) => {
|
||||||
// 如果点击的是当前展开的分组,则收起;否则切换到新分组
|
// 如果点击的是当前展开的分组,则收起;否则切换到新分组
|
||||||
|
|
@ -147,6 +148,20 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Comets Group */}
|
||||||
|
{cometList.length > 0 && (
|
||||||
|
<BodyGroup
|
||||||
|
title="彗星"
|
||||||
|
icon={<Sparkles size={12} />}
|
||||||
|
count={cometList.length}
|
||||||
|
bodies={cometList}
|
||||||
|
isExpanded={expandedGroup === 'comet'}
|
||||||
|
onToggle={() => toggleGroup('comet')}
|
||||||
|
selectedBody={selectedBody}
|
||||||
|
onBodySelect={onBodySelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* No results message */}
|
{/* No results message */}
|
||||||
{processedBodies.length === 0 && (
|
{processedBodies.length === 0 && (
|
||||||
<div className="text-center py-8 text-gray-500 text-xs">
|
<div className="text-center py-8 text-gray-500 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { OrbitControls, Stars as BackgroundStars, Html } from '@react-three/drei';
|
import { OrbitControls, Stars as BackgroundStars, Html } from '@react-three/drei';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { CelestialBody } from './CelestialBody';
|
import { CelestialBody } from './CelestialBody';
|
||||||
import { Probe } from './Probe';
|
import { Probe } from './Probe';
|
||||||
import { CameraController } from './CameraController';
|
import { CameraController } from './CameraController';
|
||||||
|
|
@ -29,6 +29,16 @@ interface SceneProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0 }: SceneProps) {
|
export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbits = true, onBodySelect, resetTrigger = 0 }: SceneProps) {
|
||||||
|
// State to control info panel visibility (independent of selection)
|
||||||
|
const [showInfoPanel, setShowInfoPanel] = useState(true);
|
||||||
|
|
||||||
|
// Reset info panel visibility when selected body changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedBody) {
|
||||||
|
setShowInfoPanel(true);
|
||||||
|
}
|
||||||
|
}, [selectedBody]);
|
||||||
|
|
||||||
// Separate natural celestial bodies (planets/dwarf planets/satellites/stars) from probes
|
// Separate natural celestial bodies (planets/dwarf planets/satellites/stars) from probes
|
||||||
const celestialBodies = bodies.filter((b) => b.type !== 'probe');
|
const celestialBodies = bodies.filter((b) => b.type !== 'probe');
|
||||||
const probes = bodies.filter((b) => b.type === 'probe');
|
const probes = bodies.filter((b) => b.type === 'probe');
|
||||||
|
|
@ -116,7 +126,12 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
|
||||||
|
|
||||||
{/* Render all celestial bodies: planets, dwarf planets, satellites, and stars */}
|
{/* Render all celestial bodies: planets, dwarf planets, satellites, and stars */}
|
||||||
{celestialBodies.map((body) => (
|
{celestialBodies.map((body) => (
|
||||||
<CelestialBody key={body.id} body={body} allBodies={bodies} />
|
<CelestialBody
|
||||||
|
key={body.id}
|
||||||
|
body={body}
|
||||||
|
allBodies={bodies}
|
||||||
|
isSelected={selectedBody?.id === body.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Unified orbit renderer for all celestial bodies (planets and dwarf planets) */}
|
{/* Unified orbit renderer for all celestial bodies (planets and dwarf planets) */}
|
||||||
|
|
@ -124,7 +139,12 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
|
||||||
|
|
||||||
{/* Render visible probes with 3D models */}
|
{/* Render visible probes with 3D models */}
|
||||||
{visibleProbes.map((body) => (
|
{visibleProbes.map((body) => (
|
||||||
<Probe key={body.id} body={body} allBodies={bodies} />
|
<Probe
|
||||||
|
key={body.id}
|
||||||
|
body={body}
|
||||||
|
allBodies={bodies}
|
||||||
|
isSelected={selectedBody?.id === body.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Render trajectory for selected probe */}
|
{/* Render trajectory for selected probe */}
|
||||||
|
|
@ -148,9 +168,9 @@ export function Scene({ bodies, selectedBody, trajectoryPositions = [], showOrbi
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dynamic Focus Info Label */}
|
{/* Dynamic Focus Info Label */}
|
||||||
{selectedBody && onBodySelect && (
|
{selectedBody && showInfoPanel && (
|
||||||
<Html position={focusInfoPosition} center zIndexRange={[100, 0]}>
|
<Html position={focusInfoPosition} center zIndexRange={[100, 0]}>
|
||||||
<FocusInfo body={selectedBody} onClose={() => onBodySelect(null)} />
|
<FocusInfo body={selectedBody} onClose={() => setShowInfoPanel(false)} />
|
||||||
</Html>
|
</Html>
|
||||||
)}
|
)}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Celestial body rendering sizes configuration
|
||||||
|
* Shared across components for consistent sizing
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planet rendering sizes (radius in scene units)
|
||||||
|
*/
|
||||||
|
export const PLANET_SIZES: Record<string, number> = {
|
||||||
|
Mercury: 0.35,
|
||||||
|
Venus: 0.55,
|
||||||
|
Earth: 0.6,
|
||||||
|
Mars: 0.45,
|
||||||
|
Jupiter: 1.4,
|
||||||
|
Saturn: 1.2,
|
||||||
|
Uranus: 0.8,
|
||||||
|
Neptune: 0.8,
|
||||||
|
Pluto: 0.2,
|
||||||
|
// Default for unknown planets
|
||||||
|
default: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Satellite rendering sizes (radius in scene units)
|
||||||
|
*/
|
||||||
|
export const SATELLITE_SIZES: Record<string, number> = {
|
||||||
|
Moon: 0.15,
|
||||||
|
// Default for unknown satellites
|
||||||
|
default: 0.12,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Star rendering sizes (radius in scene units)
|
||||||
|
*/
|
||||||
|
export const STAR_SIZES: Record<string, number> = {
|
||||||
|
Sun: 0.4,
|
||||||
|
// Default for unknown stars
|
||||||
|
default: 0.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comet rendering sizes (radius in scene units)
|
||||||
|
* Comets are typically small with a bright nucleus
|
||||||
|
*/
|
||||||
|
export const COMET_SIZES: Record<string, number> = {
|
||||||
|
// Famous comets
|
||||||
|
Halley: 0.15,
|
||||||
|
// Default for unknown comets
|
||||||
|
default: 0.12,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rendering size for a celestial body by name and type
|
||||||
|
*/
|
||||||
|
export function getCelestialSize(name: string, type: string): number {
|
||||||
|
switch (type) {
|
||||||
|
case 'planet':
|
||||||
|
case 'dwarf_planet':
|
||||||
|
return PLANET_SIZES[name] || PLANET_SIZES.default;
|
||||||
|
case 'satellite':
|
||||||
|
return SATELLITE_SIZES[name] || SATELLITE_SIZES.default;
|
||||||
|
case 'star':
|
||||||
|
return STAR_SIZES[name] || STAR_SIZES.default;
|
||||||
|
case 'comet':
|
||||||
|
return COMET_SIZES[name] || COMET_SIZES.default;
|
||||||
|
default:
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
* Celestial Bodies Management Page
|
* Celestial Bodies Management Page
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert } from 'antd';
|
import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col } from 'antd';
|
||||||
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import { DataTable } from '../../components/admin/DataTable';
|
import { DataTable } from '../../components/admin/DataTable';
|
||||||
import { request } from '../../utils/request';
|
import { request } from '../../utils/request';
|
||||||
|
|
||||||
|
|
@ -35,6 +36,8 @@ export function CelestialBodies() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [refreshResources, setRefreshResources] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
@ -185,6 +188,50 @@ export function CelestialBodies() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle resource upload
|
||||||
|
const handleResourceUpload = async (file: File, resourceType: string) => {
|
||||||
|
if (!editingRecord) {
|
||||||
|
message.error('请先选择要编辑的天体');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request.post(
|
||||||
|
`/celestial/resources/upload?body_id=${editingRecord.id}&resource_type=${resourceType}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
message.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`);
|
||||||
|
setRefreshResources(prev => prev + 1); // Trigger reload
|
||||||
|
return false; // Prevent default upload behavior
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.detail || '上传失败');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle resource delete
|
||||||
|
const handleResourceDelete = async (resourceId: number) => {
|
||||||
|
try {
|
||||||
|
await request.delete(`/celestial/resources/${resourceId}`);
|
||||||
|
message.success('删除成功');
|
||||||
|
setRefreshResources(prev => prev + 1); // Trigger reload
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.response?.data?.detail || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ColumnsType<CelestialBody> = [
|
const columns: ColumnsType<CelestialBody> = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
|
|
@ -214,6 +261,7 @@ export function CelestialBodies() {
|
||||||
{ text: '卫星', value: 'satellite' },
|
{ text: '卫星', value: 'satellite' },
|
||||||
{ text: '探测器', value: 'probe' },
|
{ text: '探测器', value: 'probe' },
|
||||||
{ text: '矮行星', value: 'dwarf_planet' },
|
{ text: '矮行星', value: 'dwarf_planet' },
|
||||||
|
{ text: '彗星', value: 'comet' },
|
||||||
],
|
],
|
||||||
onFilter: (value, record) => record.type === value,
|
onFilter: (value, record) => record.type === value,
|
||||||
render: (type: string) => {
|
render: (type: string) => {
|
||||||
|
|
@ -223,6 +271,7 @@ export function CelestialBodies() {
|
||||||
dwarf_planet: '矮行星',
|
dwarf_planet: '矮行星',
|
||||||
satellite: '卫星',
|
satellite: '卫星',
|
||||||
probe: '探测器',
|
probe: '探测器',
|
||||||
|
comet: '彗星',
|
||||||
};
|
};
|
||||||
return typeMap[type] || type;
|
return typeMap[type] || type;
|
||||||
},
|
},
|
||||||
|
|
@ -319,6 +368,8 @@ export function CelestialBodies() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="id"
|
name="id"
|
||||||
label="JPL Horizons ID"
|
label="JPL Horizons ID"
|
||||||
|
|
@ -326,22 +377,8 @@ export function CelestialBodies() {
|
||||||
>
|
>
|
||||||
<Input disabled={!!editingRecord} placeholder="例如:-31 (Voyager 1) 或 399 (Earth)" />
|
<Input disabled={!!editingRecord} placeholder="例如:-31 (Voyager 1) 或 399 (Earth)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
<Form.Item
|
<Col span={12}>
|
||||||
name="name"
|
|
||||||
label="英文名"
|
|
||||||
rules={[{ required: true, message: '请输入英文名' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:Voyager 1" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="name_zh"
|
|
||||||
label="中文名"
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:旅行者1号" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="type"
|
name="type"
|
||||||
label="类型"
|
label="类型"
|
||||||
|
|
@ -353,56 +390,182 @@ export function CelestialBodies() {
|
||||||
<Select.Option value="satellite">卫星</Select.Option>
|
<Select.Option value="satellite">卫星</Select.Option>
|
||||||
<Select.Option value="probe">探测器</Select.Option>
|
<Select.Option value="probe">探测器</Select.Option>
|
||||||
<Select.Option value="star">恒星</Select.Option>
|
<Select.Option value="star">恒星</Select.Option>
|
||||||
|
<Select.Option value="comet">彗星</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="is_active"
|
name="name"
|
||||||
label="状态"
|
label="英文名"
|
||||||
valuePropName="checked"
|
rules={[{ required: true, message: '请输入英文名' }]}
|
||||||
>
|
>
|
||||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
<Input placeholder="例如:Voyager 1" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="name_zh"
|
||||||
|
label="中文名"
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:旅行者1号" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="description"
|
name="description"
|
||||||
label="描述"
|
label="描述"
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={4} />
|
<Input.TextArea rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{editingRecord && editingRecord.has_resources && (
|
{editingRecord && (
|
||||||
<Form.Item label="已配置资源">
|
<ResourceManager
|
||||||
<Descriptions bordered size="small" column={1}>
|
bodyId={editingRecord.id}
|
||||||
{Object.entries(editingRecord.resources || {}).map(([type, resources]) => (
|
bodyType={editingRecord.type}
|
||||||
<Descriptions.Item
|
resources={editingRecord.resources}
|
||||||
key={type}
|
hasResources={editingRecord.has_resources}
|
||||||
label={
|
onUpload={handleResourceUpload}
|
||||||
type === 'texture' ? '纹理' :
|
onDelete={handleResourceDelete}
|
||||||
type === 'model' ? '模型' :
|
uploading={uploading}
|
||||||
type === 'icon' ? '图标' :
|
refreshTrigger={refreshResources}
|
||||||
type === 'thumbnail' ? '缩略图' :
|
/>
|
||||||
type === 'data' ? '数据' : type
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{resources.map((res: any, idx: number) => (
|
|
||||||
<div key={res.id} style={{ marginBottom: idx < resources.length - 1 ? 8 : 0 }}>
|
|
||||||
<Tag color="blue">{res.file_path}</Tag>
|
|
||||||
<span style={{ fontSize: 12, color: '#888' }}>
|
|
||||||
({(res.file_size / 1024).toFixed(2)} KB)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Descriptions.Item>
|
|
||||||
))}
|
|
||||||
</Descriptions>
|
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: '#888' }}>
|
|
||||||
提示:资源文件管理功能即将推出,敬请期待
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resource Manager Component
|
||||||
|
function ResourceManager({
|
||||||
|
bodyId,
|
||||||
|
bodyType,
|
||||||
|
resources,
|
||||||
|
hasResources,
|
||||||
|
onUpload,
|
||||||
|
onDelete,
|
||||||
|
uploading,
|
||||||
|
refreshTrigger,
|
||||||
|
}: {
|
||||||
|
bodyId: string;
|
||||||
|
bodyType: string;
|
||||||
|
resources?: any;
|
||||||
|
hasResources?: boolean;
|
||||||
|
onUpload: (file: File, resourceType: string) => Promise<boolean>;
|
||||||
|
onDelete: (resourceId: number) => Promise<void>;
|
||||||
|
uploading: boolean;
|
||||||
|
refreshTrigger: number;
|
||||||
|
}) {
|
||||||
|
const [currentResources, setCurrentResources] = useState(resources);
|
||||||
|
|
||||||
|
// Load resources when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
request.get(`/celestial/resources/${bodyId}`)
|
||||||
|
.then(({ data }) => {
|
||||||
|
const grouped: any = {};
|
||||||
|
data.resources.forEach((res: any) => {
|
||||||
|
if (!grouped[res.resource_type]) {
|
||||||
|
grouped[res.resource_type] = [];
|
||||||
|
}
|
||||||
|
grouped[res.resource_type].push(res);
|
||||||
|
});
|
||||||
|
setCurrentResources(grouped);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('加载资源列表失败');
|
||||||
|
});
|
||||||
|
}, [refreshTrigger, bodyId]);
|
||||||
|
|
||||||
|
const resourceTypes = [
|
||||||
|
{ key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)' },
|
||||||
|
{ key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item label="资源配置">
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
{resourceTypes.map(({ key, label }) => (
|
||||||
|
<div key={key}>
|
||||||
|
<div style={{ marginBottom: 8, fontWeight: 500 }}>{label}</div>
|
||||||
|
|
||||||
|
<Upload
|
||||||
|
beforeUpload={(file) => onUpload(file, key)}
|
||||||
|
showUploadList={false}
|
||||||
|
disabled={uploading}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />} loading={uploading} size="small">
|
||||||
|
上传 {label.split(' ')[0]}
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
|
||||||
|
{currentResources?.[key] && currentResources[key].length > 0 && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
{currentResources[key].map((res: any) => (
|
||||||
|
<div key={res.id} style={{ marginBottom: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
|
<Tag color="blue">{res.file_path}</Tag>
|
||||||
|
<span style={{ fontSize: 12, color: '#888' }}>
|
||||||
|
({(res.file_size / 1024).toFixed(2)} KB)
|
||||||
|
</span>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除?"
|
||||||
|
onConfirm={() => onDelete(res.id)}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
{key === 'model' && (
|
||||||
|
<div style={{ marginLeft: 8 }}>
|
||||||
|
<Space size="small">
|
||||||
|
<span style={{ fontSize: 12, color: '#666' }}>显示缩放:</span>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={0.1}
|
||||||
|
max={5}
|
||||||
|
step={0.1}
|
||||||
|
defaultValue={res.extra_data?.scale || 1.0}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
placeholder="1.0"
|
||||||
|
onChange={(value) => {
|
||||||
|
// Update scale in resource
|
||||||
|
const newScale = value || 1.0;
|
||||||
|
request.put(`/celestial/resources/${res.id}`, {
|
||||||
|
extra_data: { ...res.extra_data, scale: newScale }
|
||||||
|
}).then(() => {
|
||||||
|
message.success('缩放参数已更新');
|
||||||
|
}).catch(() => {
|
||||||
|
message.error('更新失败');
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 11, color: '#999' }}>
|
||||||
|
(推荐: Webb=0.3, 旅行者=1.5)
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* TypeScript type definitions for Cosmo application
|
* TypeScript type definitions for Cosmo application
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type CelestialBodyType = 'planet' | 'probe' | 'star' | 'dwarf_planet' | 'satellite';
|
export type CelestialBodyType = 'planet' | 'probe' | 'star' | 'dwarf_planet' | 'satellite' | 'comet';
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
time: string;
|
time: string;
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ export async function fetchBodyResources(bodyId: string, resourceType?: string):
|
||||||
file_size: number;
|
file_size: number;
|
||||||
mime_type: string;
|
mime_type: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
extra_data?: Record<string, any>;
|
||||||
}>;
|
}>;
|
||||||
}> {
|
}> {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { scalePosition } from './scaleDistance';
|
import { scalePosition } from './scaleDistance';
|
||||||
|
import { getCelestialSize } from '../config/celestialSizes';
|
||||||
import type { CelestialBody } from '../types';
|
import type { CelestialBody } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate rendering position using true scaled coordinates
|
* Calculate rendering position using true scaled coordinates
|
||||||
* For satellites, add a radial offset relative to PARENT PLANET
|
* For satellites and probes near planets, add a radial offset relative to PARENT PLANET
|
||||||
*/
|
*/
|
||||||
export function calculateRenderPosition(
|
export function calculateRenderPosition(
|
||||||
body: CelestialBody,
|
body: CelestialBody,
|
||||||
|
|
@ -22,16 +23,15 @@ export function calculateRenderPosition(
|
||||||
// 1. Calculate base scaled position (fallback)
|
// 1. Calculate base scaled position (fallback)
|
||||||
const scaled = scalePosition(pos.x, pos.y, pos.z);
|
const scaled = scalePosition(pos.x, pos.y, pos.z);
|
||||||
|
|
||||||
// 2. Special handling for satellites (like Moon)
|
// 2. Special handling for satellites (like Moon) and probes near planets
|
||||||
if (body.type === 'satellite') {
|
if (body.type === 'satellite' || body.type === 'probe') {
|
||||||
const parent = findParentPlanet(body, allBodies);
|
const parent = findParentPlanet(body, allBodies);
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const parentPos = parent.positions[0];
|
const parentPos = parent.positions[0];
|
||||||
// Calculate parent's scaled position (where it is rendered)
|
// Calculate parent's scaled position (where it is rendered)
|
||||||
// Note: Recursion is possible if parent is also a satellite, but unlikely for planets
|
|
||||||
const parentScaled = scalePosition(parentPos.x, parentPos.y, parentPos.z);
|
const parentScaled = scalePosition(parentPos.x, parentPos.y, parentPos.z);
|
||||||
|
|
||||||
// Calculate vector from Parent to Satellite (in True AU coordinates)
|
// Calculate vector from Parent to body (in True AU coordinates)
|
||||||
const dx = pos.x - parentPos.x;
|
const dx = pos.x - parentPos.x;
|
||||||
const dy = pos.y - parentPos.y;
|
const dy = pos.y - parentPos.y;
|
||||||
const dz = pos.z - parentPos.z;
|
const dz = pos.z - parentPos.z;
|
||||||
|
|
@ -44,11 +44,12 @@ export function calculateRenderPosition(
|
||||||
const ny = dy / dist;
|
const ny = dy / dist;
|
||||||
const nz = dz / dist;
|
const nz = dz / dist;
|
||||||
|
|
||||||
// Set a fixed visual offset distance
|
// Calculate dynamic offset based on parent planet's rendering size
|
||||||
// Earth radius is 0.6, Moon is 0.15.
|
// Formula: planetRadius × 1.5 + 0.3 (fixed gap)
|
||||||
// A distance of 1.5 units ensures they are close but distinct and don't overlap.
|
// This ensures larger planets (Jupiter, Saturn) have larger offsets
|
||||||
// This simulates a "mini-system" view at the planet's location.
|
// while smaller planets (Earth, Mars) have smaller offsets
|
||||||
const visualOffset = 1.5;
|
const parentSize = getCelestialSize(parent.name, parent.type);
|
||||||
|
const visualOffset = parentSize * 1.5 + 0.3;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: parentScaled.x + nx * visualOffset,
|
x: parentScaled.x + nx * visualOffset,
|
||||||
|
|
@ -106,7 +107,31 @@ export function findParentPlanet(
|
||||||
* Get description for bodies that are close to planets
|
* Get description for bodies that are close to planets
|
||||||
*/
|
*/
|
||||||
export function getOffsetDescription(body: CelestialBody, allBodies: CelestialBody[]): string | null {
|
export function getOffsetDescription(body: CelestialBody, allBodies: CelestialBody[]): string | null {
|
||||||
// This function is kept for compatibility but no longer calculates offsets
|
if (body.type !== 'satellite' && body.type !== 'probe') {
|
||||||
// Could be used to show proximity information in the future
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = findParentPlanet(body, allBodies);
|
||||||
|
if (!parent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = body.positions[0];
|
||||||
|
const parentPos = parent.positions[0];
|
||||||
|
if (!pos || !parentPos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual distance
|
||||||
|
const dx = pos.x - parentPos.x;
|
||||||
|
const dy = pos.y - parentPos.y;
|
||||||
|
const dz = pos.z - parentPos.z;
|
||||||
|
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
|
||||||
|
|
||||||
|
// Return proximity description
|
||||||
|
const distanceStr = dist < 0.01
|
||||||
|
? `${(dist * 149597870.7).toFixed(0)} km` // Convert AU to km for very close objects
|
||||||
|
: `${dist.toFixed(4)} AU`;
|
||||||
|
|
||||||
|
return `近${parent.name_zh || parent.name} (${distanceStr})`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue