init
This commit is contained in:
197
src/components/rocket/RocketModel3D.jsx
Normal file
197
src/components/rocket/RocketModel3D.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user