This commit is contained in:
2026-03-03 16:43:30 +00:00
commit 03452517b5
58 changed files with 13181 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import { useMemo, useRef, useEffect } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import * as THREE from 'three'
// ── Single body section ────────────────────────────────────────────────
// Renders a CylinderGeometry segment centred at yCenter on the world Y axis.
// rTop = radius at +Y cap, rBot = radius at Y cap.
function Section({ yCenter, yLen, rTop, rBot, color, opacity = 1 }) {
const geo = useMemo(
() => new THREE.CylinderGeometry(
Math.max(rTop, 0.0001),
Math.max(rBot, 0.0001),
yLen, 48, 1, false,
),
[rTop, rBot, yLen],
)
return (
<mesh geometry={geo} position={[0, yCenter, 0]}>
<meshStandardMaterial
color={color}
metalness={0.45}
roughness={0.5}
side={THREE.DoubleSide}
transparent={opacity < 1}
opacity={opacity}
depthWrite={opacity >= 1}
/>
</mesh>
)
}
// ── Rocket composed of stacked vertical sections ───────────────────────
// +Y = nose tip (up) Y = nozzle exit (down / toward ground)
function RocketShape({ geometry: geo }) {
const groupRef = useRef()
// Slow spin around vertical axis
useFrame((_, delta) => {
if (groupRef.current) groupRef.current.rotation.y += delta * 0.18
})
if (!geo) return null
const {
outerRadius: R,
L_nose, L_payload, L_tank, L_tank_fuel, L_tank_ox, L_engine,
totalLength, r_inner, arrangement, innerPropellant,
} = geo
const mid = totalLength / 2
const tankY0 = L_nose + L_payload
const engineY0 = tankY0 + L_tank
// Map rocket-space distance from nose tip → world Y.
// Flips so nose = +Y (top) and engine exit = Y (bottom).
const yc = (y0, h) => mid - y0 - h / 2
return (
<group ref={groupRef}>
{/* Nose cone — tip at +Y, base joins body at Y */}
<Section
yCenter={yc(0, L_nose)} yLen={L_nose}
rTop={0.001} rBot={R}
color="#94a3b8"
/>
{/* Payload bay */}
{L_payload > 0 && (
<Section
yCenter={yc(L_nose, L_payload)} yLen={L_payload}
rTop={R} rBot={R}
color="#6366f1"
/>
)}
{/* Tanks — tandem: oxidizer above fuel */}
{arrangement === 'tandem' && L_tank_ox > 0 && (
<Section
yCenter={yc(tankY0, L_tank_ox)} yLen={L_tank_ox}
rTop={R} rBot={R}
color="#06b6d4"
/>
)}
{arrangement === 'tandem' && L_tank_fuel > 0 && (
<Section
yCenter={yc(tankY0 + L_tank_ox, L_tank_fuel)} yLen={L_tank_fuel}
rTop={R} rBot={R}
color="#f59e0b"
/>
)}
{/* Tanks — coaxial: semi-transparent outer + solid inner */}
{arrangement === 'coaxial' && L_tank > 0 && (
<>
<Section
yCenter={yc(tankY0, L_tank)} yLen={L_tank}
rTop={R} rBot={R}
color={innerPropellant === 'fuel' ? '#06b6d4' : '#f59e0b'}
opacity={0.55}
/>
{r_inner > 0 && (
<Section
yCenter={yc(tankY0, L_tank)} yLen={L_tank}
rTop={r_inner} rBot={r_inner}
color={innerPropellant === 'fuel' ? '#f59e0b' : '#06b6d4'}
/>
)}
</>
)}
{/* Engine — converging (chamber) section: body width → throat */}
{L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.chamberLength > 0 && (
<Section
yCenter={yc(engineY0, geo.chamberLength)} yLen={geo.chamberLength}
rTop={R} rBot={geo.nozzleThroatRadius}
color="#3b82f6"
/>
)}
{/* Engine — diverging (nozzle bell) section: throat → exit (always flares downward) */}
{L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.nozzleLength > 0 && (
<Section
yCenter={yc(engineY0 + geo.chamberLength, geo.nozzleLength)} yLen={geo.nozzleLength}
rTop={geo.nozzleThroatRadius} rBot={geo.nozzleExitRadius ?? R * 1.3}
color="#3b82f6"
/>
)}
{/* Engine — fallback when detailed geometry not available */}
{L_engine > 0 && !(geo.nozzleThroatRadius > 0) && (
<Section
yCenter={yc(engineY0, L_engine)} yLen={L_engine}
rTop={R} rBot={R * 1.3}
color="#3b82f6"
/>
)}
</group>
)
}
// ── Camera auto-fit ────────────────────────────────────────────────────
function CameraRig({ geometry: geo }) {
const { camera, controls } = useThree()
useEffect(() => {
if (!geo) return
const span = Math.max(geo.totalLength, geo.outerRadius * 2)
const dist = span * 2.2
camera.position.set(dist * 0.7, dist * 0.25, dist * 0.7)
camera.near = dist * 0.001
camera.far = dist * 10
camera.updateProjectionMatrix()
if (controls) {
controls.target.set(0, 0, 0)
controls.update()
} else {
camera.lookAt(0, 0, 0)
}
}, [geo, camera, controls])
return null
}
function Scene({ geometry }) {
return (
<>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 8, 5]} intensity={1.4} />
<directionalLight position={[-4, -4, -4]} intensity={0.3} />
<RocketShape geometry={geometry} />
<OrbitControls makeDefault enablePan={false} />
<CameraRig geometry={geometry} />
</>
)
}
export default function RocketModel3D({ geometry }) {
return (
<div className="relative w-full h-full">
{!geometry && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none z-10">
<div className="text-5xl mb-4 opacity-20">🚀</div>
<p className="text-slate-500 text-sm leading-relaxed">
Import an engine and enter an outer diameter<br />to see the 3D rocket
</p>
</div>
)}
<Canvas
camera={{ position: [0.5, 0.3, 0.8], fov: 45 }}
style={{ background: 'transparent' }}
>
<Scene geometry={geometry} />
</Canvas>
</div>
)
}