init
This commit is contained in:
25
src/components/engine/DesignSection.jsx
Normal file
25
src/components/engine/DesignSection.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
113
src/components/engine/EngineModel3D.jsx
Normal file
113
src/components/engine/EngineModel3D.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user