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,25 @@
import { useState } from 'react'
/**
* Collapsible section wrapper for engine design input groups.
*/
export default function DesignSection({ title, children, defaultOpen = true }) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="border border-slate-700 rounded-lg overflow-hidden mb-4">
<button
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-4 py-3 bg-slate-800 hover:bg-slate-700 text-left transition-colors"
>
<span className="text-sm font-semibold text-slate-200">{title}</span>
<span className="text-slate-400 text-xs select-none">{open ? '▲' : '▼'}</span>
</button>
{open && (
<div className="p-4 bg-slate-900 space-y-3">
{children}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,113 @@
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'
// Builds the 3D mesh from chamber + nozzle geometry
function EngineShape({ chamberGeometry: cg, nozzleGeometry: ng }) {
const meshRef = useRef()
const geometry = useMemo(() => {
if (!cg || !ng) return null
// Radial profile: points are [radius, axialPosition]
// LatheGeometry revolves these around the Y axis
const totalLen = cg.Lc + ng.Ln
// Offset so engine is centered at y=0
const mid = totalLen / 2
const points = [
new THREE.Vector2(0, -mid), // center of back wall
new THREE.Vector2(cg.rc, -mid), // chamber back outer edge
new THREE.Vector2(cg.rc, cg.L_cyl - mid), // end of cylindrical section
new THREE.Vector2(cg.rt, cg.Lc - mid), // throat
new THREE.Vector2(ng.re, cg.Lc + ng.Ln - mid), // nozzle exit
]
return new THREE.LatheGeometry(points, 64)
}, [cg, ng])
// Very slow idle rotation
useFrame((_, delta) => {
if (meshRef.current) {
meshRef.current.rotation.z += delta * 0.15
}
})
if (!geometry) return null
return (
// Rotate 90° around X so engine axis runs horizontally (along Z)
<mesh ref={meshRef} geometry={geometry} rotation={[Math.PI / 2, 0, 0]}>
<meshStandardMaterial
color="#3b82f6"
metalness={0.8}
roughness={0.2}
side={THREE.DoubleSide}
/>
</mesh>
)
}
// Adjusts camera to fit the engine when geometry changes
function CameraRig({ chamberGeometry: cg, nozzleGeometry: ng }) {
const { camera, controls } = useThree()
useEffect(() => {
if (!cg || !ng) return
const totalLength = cg.Lc + ng.Ln
const maxRadius = cg.rc
const dist = Math.max(totalLength, maxRadius * 2) * 2.2
camera.position.set(dist * 0.6, dist * 0.4, dist * 0.8)
camera.near = dist * 0.001
camera.far = dist * 10
camera.updateProjectionMatrix()
// controls is set when OrbitControls uses makeDefault
if (controls) {
controls.target.set(0, 0, 0)
controls.update()
} else {
camera.lookAt(0, 0, 0)
}
}, [cg, ng, camera, controls])
return null
}
function Scene({ chamberGeometry, nozzleGeometry }) {
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 8, 5]} intensity={1.4} />
<directionalLight position={[-4, -4, -4]} intensity={0.25} />
<EngineShape chamberGeometry={chamberGeometry} nozzleGeometry={nozzleGeometry} />
<OrbitControls makeDefault enablePan={false} />
<CameraRig
chamberGeometry={chamberGeometry}
nozzleGeometry={nozzleGeometry}
/>
</>
)
}
export default function EngineModel3D({ chamberGeometry, nozzleGeometry }) {
const hasGeometry = chamberGeometry && nozzleGeometry
return (
<div className="relative w-full h-full">
{!hasGeometry && (
<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">
Enter thermodynamic inputs and<br />chamber config to see the 3D engine
</p>
</div>
)}
<Canvas
camera={{ position: [0.5, 0.3, 0.8], fov: 45 }}
style={{ background: 'transparent' }}
>
<Scene chamberGeometry={chamberGeometry} nozzleGeometry={nozzleGeometry} />
</Canvas>
</div>
)
}