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 ( = 1} /> ) } // ── Domed tank section ───────────────────────────────────────────────── // Renders a cylindrical tank with hemispherical domes on both ends. // yCenter = center of the entire tank, L_cyl = cylindrical body length, R = radius function TankSection({ yCenter, L_cyl, R, color, opacity = 1 }) { const cylGeo = useMemo( () => new THREE.CylinderGeometry(R, R, L_cyl, 48, 1, false), [R, L_cyl], ) // Top dome: hemisphere pointing upward (positive Y) const topDomeGeo = useMemo( () => new THREE.SphereGeometry(R, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2), [R], ) // Bottom dome: hemisphere pointing downward (negative Y) const botDomeGeo = useMemo( () => new THREE.SphereGeometry(R, 32, 16, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2), [R], ) const material = ( = 1} /> ) return ( <> {/* Cylindrical body */} {material} {/* Top dome */} {material} {/* Bottom dome */} {material} ) } // ── Nose cone with profile (LatheGeometry) ──────────────────────────────── // Uses profile points {x, r} to generate a smooth surface of revolution. // x is mapped to height (y-axis), r is the radius at each point. // Height is inverted so tip (r=0) ends up at the top of the rocket. function NoseCone({ yCenter, profilePoints, color, opacity = 1 }) { const geo = useMemo(() => { if (!profilePoints || profilePoints.length < 2) return null const L = profilePoints[profilePoints.length - 1].x // Map points so height = L - x (inverted), so tip (x=0) becomes height L // This makes tip point upward when positioned in the rocket const curvePoints = profilePoints.map(p => new THREE.Vector2(p.r, L - p.x)) return new THREE.LatheGeometry(curvePoints, 32) }, [profilePoints]) if (!geo) return null const L = profilePoints[profilePoints.length - 1].x return ( = 1} /> ) } // ── 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, noseProfilePoints, } = 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 ( {/* Nose cone — profile-based shape */} {noseProfilePoints && noseProfilePoints.length > 0 ? ( ) : (
)} {/* Payload bay */} {L_payload > 0 && (
)} {/* Tanks — tandem: oxidizer above fuel (with domes) */} {arrangement === 'tandem' && L_tank_ox_cyl > 0 && ( )} {arrangement === 'tandem' && L_tank_fuel_cyl > 0 && ( )} {/* Tanks — coaxial: semi-transparent outer + solid inner */} {arrangement === 'coaxial' && L_tank > 0 && ( <>
{r_inner > 0 && (
)} )} {/* Engine — converging (chamber) section: body width → throat */} {L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.chamberLength > 0 && (
)} {/* Engine — diverging (nozzle bell) section: throat → exit (always flares downward) */} {L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.nozzleLength > 0 && (
)} {/* Engine — fallback when detailed geometry not available */} {L_engine > 0 && !(geo.nozzleThroatRadius > 0) && (
)} ) } // ── 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 ( <> ) } export default function RocketModel3D({ geometry }) { return (
{!geometry && (
🚀

Import an engine and enter an outer diameter
to see the 3D rocket

)}
) }