Better accuracy
This commit is contained in:
@@ -30,6 +30,91 @@ function Section({ yCenter, yLen, rTop, rBot, color, opacity = 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 = (
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
metalness={0.45}
|
||||
roughness={0.5}
|
||||
side={THREE.DoubleSide}
|
||||
transparent={opacity < 1}
|
||||
opacity={opacity}
|
||||
depthWrite={opacity >= 1}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Cylindrical body */}
|
||||
<mesh geometry={cylGeo} position={[0, yCenter, 0]}>
|
||||
{material}
|
||||
</mesh>
|
||||
{/* Top dome */}
|
||||
<mesh geometry={topDomeGeo} position={[0, yCenter + L_cyl / 2 + R / 2, 0]}>
|
||||
{material}
|
||||
</mesh>
|
||||
{/* Bottom dome */}
|
||||
<mesh geometry={botDomeGeo} position={[0, yCenter - L_cyl / 2 - R / 2, 0]}>
|
||||
{material}
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<mesh geometry={geo} position={[0, yCenter - L / 2, 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 }) {
|
||||
@@ -45,7 +130,7 @@ function RocketShape({ geometry: geo }) {
|
||||
const {
|
||||
outerRadius: R,
|
||||
L_nose, L_payload, L_tank, L_tank_fuel, L_tank_ox, L_engine,
|
||||
totalLength, r_inner, arrangement, innerPropellant,
|
||||
totalLength, r_inner, arrangement, innerPropellant, noseProfilePoints,
|
||||
} = geo
|
||||
|
||||
const mid = totalLength / 2
|
||||
@@ -59,12 +144,20 @@ function RocketShape({ geometry: geo }) {
|
||||
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"
|
||||
/>
|
||||
{/* Nose cone — profile-based shape */}
|
||||
{noseProfilePoints && noseProfilePoints.length > 0 ? (
|
||||
<NoseCone
|
||||
yCenter={yc(0, L_nose)}
|
||||
profilePoints={noseProfilePoints}
|
||||
color="#94a3b8"
|
||||
/>
|
||||
) : (
|
||||
<Section
|
||||
yCenter={yc(0, L_nose)} yLen={L_nose}
|
||||
rTop={0.001} rBot={R}
|
||||
color="#94a3b8"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Payload bay */}
|
||||
{L_payload > 0 && (
|
||||
@@ -75,18 +168,18 @@ function RocketShape({ geometry: geo }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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}
|
||||
{/* Tanks — tandem: oxidizer above fuel (with domes) */}
|
||||
{arrangement === 'tandem' && L_tank_ox_cyl > 0 && (
|
||||
<TankSection
|
||||
yCenter={yc(tankY0, L_tank_ox)} L_cyl={L_tank_ox_cyl}
|
||||
R={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}
|
||||
{arrangement === 'tandem' && L_tank_fuel_cyl > 0 && (
|
||||
<TankSection
|
||||
yCenter={yc(tankY0 + L_tank_ox, L_tank_fuel)} L_cyl={L_tank_fuel_cyl}
|
||||
R={R}
|
||||
color="#f59e0b"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,46 @@
|
||||
// Pure calculation functions for rocket vehicle design (no React)
|
||||
|
||||
import { STRUCTURAL_MATERIALS } from './knowledgebaseData.js'
|
||||
|
||||
const G0 = 9.80665 // m/s² standard gravity
|
||||
const R_HE = 2077 // J/kg/K — specific gas constant for helium
|
||||
const T_PRESSURANT = 293 // K — standard pressurant storage temperature
|
||||
|
||||
/**
|
||||
* Generate nose cone profile points (r vs x) for a given shape.
|
||||
* All shapes share L = 2R.
|
||||
*
|
||||
* @param {string} shape — 'conical' | 'tangentOgive' | 'vonKarman'
|
||||
* @param {number} R — base radius (m)
|
||||
* @param {number} L — nose length (m)
|
||||
* @param {number} n — number of points (default 24)
|
||||
* @returns {Array<{x, r}>} profile points from tip (x=0) to base (x=L)
|
||||
*/
|
||||
function noseConeProfile(shape, R, L, n = 24) {
|
||||
const points = []
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = (i / (n - 1)) * L
|
||||
let r
|
||||
|
||||
if (shape === 'tangentOgive') {
|
||||
// Tangent ogive: ρ = (R² + L²) / (2R)
|
||||
const rho = (R * R + L * L) / (2 * R)
|
||||
r = Math.sqrt(rho * rho - (L - x) * (L - x)) - (rho - R)
|
||||
} else if (shape === 'vonKarman') {
|
||||
// Von Kármán (LD-Haack): θ = acos(1 − 2x/L), r = (R/√π) * √(θ − sin(2θ)/2)
|
||||
const theta = Math.acos(1 - 2 * x / L)
|
||||
r = (R / Math.sqrt(Math.PI)) * Math.sqrt(theta - Math.sin(2 * theta) / 2)
|
||||
} else {
|
||||
// Default conical: r(x) = R * (x / L)
|
||||
r = R * (x / L)
|
||||
}
|
||||
|
||||
points.push({ x, r: Math.max(0, r) })
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate full rocket geometry and performance from engine data and rocket inputs.
|
||||
@@ -15,7 +55,13 @@ const G0 = 9.80665 // m/s² standard gravity
|
||||
* rhoOx, // kg/m³
|
||||
* payloadMass, // kg
|
||||
* payloadBayLength, // m
|
||||
* structMassFraction, // 0–1 (dry mass fraction of propellant mass)
|
||||
* tankMaterialId, // 'al_6061_t6' | 'ss_304' | ... (structural material)
|
||||
* tankSafetyFactor, // safety factor for hoop stress
|
||||
* feedSystem, // 'pressure_fed' | 'pump_fed'
|
||||
* ullagePercent, // % extra volume above propellant for gases
|
||||
* otherStructFraction, // fraction of propellant mass for non-tank structure
|
||||
* engineDryMass, // kg (from engine structural results, null if not available)
|
||||
* noseConeShape, // 'conical' | 'tangentOgive' | 'vonKarman'
|
||||
* }
|
||||
* @returns {object|null}
|
||||
*/
|
||||
@@ -31,13 +77,20 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
rhoOx,
|
||||
payloadMass,
|
||||
payloadBayLength,
|
||||
structMassFraction,
|
||||
tankMaterialId,
|
||||
tankSafetyFactor,
|
||||
feedSystem,
|
||||
ullagePercent,
|
||||
otherStructFraction,
|
||||
engineDryMass,
|
||||
noseConeShape,
|
||||
} = rocketInputs
|
||||
|
||||
const Isp = engineData?.allThermo?.Isp
|
||||
const F = engineData?.allThermo?.F
|
||||
const mdot = engineData?.allThermo?.mdot
|
||||
const OF = engineData?.allThermo?.OF
|
||||
const p0 = engineData?.allThermo?.p0 ?? 6.9e6 // Chamber pressure, default 6.9 MPa
|
||||
|
||||
// Flow rates: use direct values if solved, otherwise derive from mdot + OF
|
||||
let mdot_f = engineData?.allThermo?.mdot_f
|
||||
@@ -65,8 +118,9 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
const V_ox = m_ox / rhoOx
|
||||
const V_prop = V_fuel + V_ox
|
||||
|
||||
// ── Nose cone (ogive approximation: height = 2R) ────────────────────
|
||||
// ── Nose cone (height = 2R) ───────────────────────────────────────────
|
||||
const L_nose = 2 * R
|
||||
const noseProfilePoints = noseConeProfile(noseConeShape ?? 'conical', R, L_nose, 24)
|
||||
|
||||
// ── Payload bay ─────────────────────────────────────────────────────
|
||||
const L_payload = payloadBayLength ?? 0
|
||||
@@ -76,33 +130,90 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
const cg = engineData?.chamberGeometry
|
||||
const L_engine = (ng?.Ln ?? 0) + (cg?.Lc ?? 0)
|
||||
|
||||
// ── Tank geometry ───────────────────────────────────────────────────
|
||||
let L_tank_fuel, L_tank_ox, L_tank, r_inner
|
||||
// ── Tank geometry with domes and ullage ──────────────────────────────
|
||||
let L_tank_fuel, L_tank_fuel_cyl, L_tank_ox, L_tank_ox_cyl, L_tank, r_inner, m_tank_fuel, m_tank_ox, m_tank_total
|
||||
let t_wall = null
|
||||
|
||||
const ullage = ullagePercent ?? 5
|
||||
const materialId = tankMaterialId ?? 'al_6061_t6'
|
||||
const material = STRUCTURAL_MATERIALS.find(m => m.id === materialId) || STRUCTURAL_MATERIALS[0]
|
||||
const SF = tankSafetyFactor ?? 2.0
|
||||
|
||||
// Tank pressure (Pa)
|
||||
const p_tank = feedSystem === 'pump_fed' ? 2e6 : p0 * 1.2
|
||||
|
||||
// Hoop stress wall thickness (same for all tanks at same pressure and radius)
|
||||
t_wall = (p_tank * R) / (material.yieldStrength / SF)
|
||||
|
||||
if (arrangement === 'coaxial') {
|
||||
// Both tanks share the same axial section; total volume in annulus + inner cylinder
|
||||
// Inner tank radius from the smaller propellant volume
|
||||
// Outer tank occupies the remaining annular area
|
||||
// Coaxial: hemispherical domes only on outer tank; inner tank is annular (flat bulkheads)
|
||||
const V_inner = innerPropellant === 'fuel' ? V_fuel : V_ox
|
||||
const V_outer = innerPropellant === 'fuel' ? V_ox : V_fuel
|
||||
|
||||
// Solve for tank length using outer volume first (conservative — outer is usually larger)
|
||||
// V_outer = π (R² - r_inner²) L_tank and V_inner = π r_inner² L_tank
|
||||
// From V_inner / (V_inner + V_outer) = r_inner² / R²
|
||||
// → r_inner = R √(V_inner / V_prop_total)
|
||||
r_inner = R * Math.sqrt(V_inner / V_prop)
|
||||
// Guard: inner radius must be smaller than outer
|
||||
if (r_inner >= R) r_inner = R * 0.7
|
||||
|
||||
L_tank = V_prop / (Math.PI * R * R)
|
||||
L_tank_fuel = arrangement === 'coaxial' ? L_tank : null
|
||||
L_tank_ox = arrangement === 'coaxial' ? L_tank : null
|
||||
// Outer tank: account for dome volume
|
||||
const V_dome_outer = (4 / 3) * Math.PI * R * R * R
|
||||
const V_eff_outer = V_outer * (1 + ullage / 100)
|
||||
const L_tank_cyl_outer = Math.max(0, (V_eff_outer - V_dome_outer) / (Math.PI * R * R))
|
||||
|
||||
// Inner annular tank (no domes): full volume in cylindrical section
|
||||
const A_annulus = Math.PI * (R * R - r_inner * r_inner)
|
||||
const L_tank_cyl_inner = V_inner / A_annulus
|
||||
|
||||
// Total outer tank length
|
||||
const L_tank_outer = L_tank_cyl_outer + 2 * R
|
||||
|
||||
// Both tanks are coaxial, so effective length is the longer one
|
||||
L_tank = Math.max(L_tank_outer, L_tank_cyl_inner)
|
||||
|
||||
// For compatibility with tandem display
|
||||
L_tank_fuel = L_tank
|
||||
L_tank_fuel_cyl = L_tank_cyl_outer
|
||||
L_tank_ox = L_tank
|
||||
L_tank_ox_cyl = L_tank_cyl_inner
|
||||
|
||||
// Coaxial tank mass: outer tank with domes + inner annular section
|
||||
const m_cyl_outer = 2 * Math.PI * R * t_wall * L_tank_cyl_outer * material.density
|
||||
const m_dome_outer = 4 * Math.PI * R * R * t_wall * material.density
|
||||
const m_tank_outer = m_cyl_outer + m_dome_outer
|
||||
|
||||
const m_cyl_inner = 2 * Math.PI * r_inner * t_wall * L_tank_cyl_inner * material.density
|
||||
const m_tank_inner = m_cyl_inner // Inner annular tank, no domes
|
||||
|
||||
m_tank_total = m_tank_outer + m_tank_inner
|
||||
} else {
|
||||
// Tandem: stacked cylinders
|
||||
L_tank_fuel = V_fuel / (Math.PI * R * R)
|
||||
L_tank_ox = V_ox / (Math.PI * R * R)
|
||||
// Tandem: both tanks have hemispherical domes
|
||||
// Effective volumes include ullage
|
||||
const V_eff_fuel = V_fuel * (1 + ullage / 100)
|
||||
const V_eff_ox = V_ox * (1 + ullage / 100)
|
||||
|
||||
// Dome volume for each tank
|
||||
const V_dome_one = (4 / 3) * Math.PI * R * R * R
|
||||
|
||||
// Cylindrical sections
|
||||
L_tank_fuel_cyl = Math.max(0, (V_eff_fuel - V_dome_one) / (Math.PI * R * R))
|
||||
L_tank_ox_cyl = Math.max(0, (V_eff_ox - V_dome_one) / (Math.PI * R * R))
|
||||
|
||||
// Total tank lengths including domes
|
||||
L_tank_fuel = L_tank_fuel_cyl + 2 * R
|
||||
L_tank_ox = L_tank_ox_cyl + 2 * R
|
||||
L_tank = L_tank_fuel + L_tank_ox
|
||||
r_inner = null
|
||||
|
||||
// Tank mass: cylindrical + dome shells (two hemispheres = full sphere surface area)
|
||||
m_tank_fuel = (2 * Math.PI * R * t_wall * L_tank_fuel_cyl * material.density) + (4 * Math.PI * R * R * t_wall * material.density)
|
||||
m_tank_ox = (2 * Math.PI * R * t_wall * L_tank_ox_cyl * material.density) + (4 * Math.PI * R * R * t_wall * material.density)
|
||||
m_tank_total = m_tank_fuel + m_tank_ox
|
||||
}
|
||||
|
||||
// ── Pressurant system (pressure-fed only) ─────────────────────────────
|
||||
let m_pressurant = 0
|
||||
if (feedSystem === 'pressure_fed') {
|
||||
const m_He = (p_tank * V_prop) / (R_HE * T_PRESSURANT)
|
||||
const m_bottle = m_He * 4 // Conservative bottle mass estimate
|
||||
m_pressurant = m_He + m_bottle
|
||||
}
|
||||
|
||||
// ── Total vehicle length ────────────────────────────────────────────
|
||||
@@ -110,9 +221,11 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
|
||||
// ── Mass budget ─────────────────────────────────────────────────────
|
||||
const m_prop = m_fuel + m_ox
|
||||
const m_struct = m_prop * (structMassFraction ?? 0.10)
|
||||
const m_other_struct = m_prop * (otherStructFraction ?? 0.05)
|
||||
const m_struct = m_tank_total + m_other_struct
|
||||
const m_payload = payloadMass ?? 0
|
||||
const m_dry = m_struct + m_payload
|
||||
const m_engine = engineDryMass ?? 0
|
||||
const m_dry = m_struct + m_payload + m_engine + m_pressurant
|
||||
const m_wet = m_prop + m_dry
|
||||
|
||||
const massRatio = m_wet / m_dry
|
||||
@@ -135,6 +248,8 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
L_tank,
|
||||
L_tank_fuel,
|
||||
L_tank_ox,
|
||||
L_tank_fuel_cyl,
|
||||
L_tank_ox_cyl,
|
||||
L_engine,
|
||||
totalLength,
|
||||
r_inner,
|
||||
@@ -146,8 +261,13 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
m_prop,
|
||||
m_struct,
|
||||
m_payload,
|
||||
m_engine,
|
||||
m_dry,
|
||||
m_wet,
|
||||
m_tank_total,
|
||||
m_pressurant,
|
||||
// Tank structure
|
||||
t_wall,
|
||||
// Performance
|
||||
massRatio,
|
||||
deltaV,
|
||||
@@ -160,6 +280,8 @@ export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
nozzleThroatRadius: ng?.rt ?? null,
|
||||
chamberLength: cg?.Lc ?? 0,
|
||||
nozzleLength: ng?.Ln ?? 0,
|
||||
// Nose cone profile for 3D model
|
||||
noseProfilePoints,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export { downloadBlob }
|
||||
* Build a rocket design JSON Blob for download.
|
||||
* Schema version 1: inputs section is re-importable; results is reference-only.
|
||||
*/
|
||||
export function exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry }) {
|
||||
export function exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry, noseConeShape }) {
|
||||
const payload_ = {
|
||||
version: 1,
|
||||
type: 'rocket_design',
|
||||
@@ -17,6 +17,7 @@ export function exportRocketJSON({ outerRadius, tankConfig, propDensities, paylo
|
||||
propDensities,
|
||||
payload,
|
||||
structure,
|
||||
noseConeShape,
|
||||
},
|
||||
engineData: engineData ?? null,
|
||||
results: geometry ?? null,
|
||||
@@ -43,7 +44,7 @@ export function parseRocketImport(jsonString) {
|
||||
throw new Error(`Unsupported export version: ${data.version}`)
|
||||
}
|
||||
|
||||
const { outerRadius, tankConfig, propDensities, payload, structure } = data.inputs ?? {}
|
||||
const { outerRadius, tankConfig, propDensities, payload, structure, noseConeShape } = data.inputs ?? {}
|
||||
|
||||
return {
|
||||
outerRadius: outerRadius ?? null,
|
||||
@@ -51,6 +52,7 @@ export function parseRocketImport(jsonString) {
|
||||
propDensities: propDensities ?? null,
|
||||
payload: payload ?? null,
|
||||
structure: structure ?? null,
|
||||
noseConeShape: noseConeShape ?? null,
|
||||
engineData: data.engineData ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,17 @@ export function useRocketDesign() {
|
||||
|
||||
// Structure
|
||||
const [structure, setStructure] = useState({
|
||||
structMassFraction: 0.10,
|
||||
burnTime: null, // null → use engine feedSystem.burnTime if available
|
||||
tankMaterialId: 'al_6061_t6',
|
||||
tankSafetyFactor: 2.0,
|
||||
feedSystem: 'pressure_fed', // 'pressure_fed' | 'pump_fed'
|
||||
ullagePercent: 5,
|
||||
otherStructFraction: 0.05,
|
||||
burnTime: null, // null → use engine feedSystem.burnTime if available
|
||||
})
|
||||
|
||||
// Nose cone shape
|
||||
const [noseConeShape, setNoseConeShape] = useState('conical')
|
||||
|
||||
// Effective burn time (prefer explicit override, fallback to engine data)
|
||||
const effectiveBurnTime = useMemo(() => {
|
||||
if (structure.burnTime != null && structure.burnTime > 0) return structure.burnTime
|
||||
@@ -46,28 +53,35 @@ export function useRocketDesign() {
|
||||
return null
|
||||
}, [engineData])
|
||||
|
||||
const rocketInputs = useMemo(() => ({
|
||||
outerRadius: outerRadius ?? suggestedRadius,
|
||||
burnTime: effectiveBurnTime,
|
||||
arrangement: tankConfig.arrangement,
|
||||
innerPropellant: tankConfig.innerPropellant,
|
||||
rhoFuel: propDensities.rhoFuel,
|
||||
rhoOx: propDensities.rhoOx,
|
||||
payloadMass: payload.mass,
|
||||
payloadBayLength: payload.bayLength,
|
||||
structMassFraction: structure.structMassFraction,
|
||||
}), [outerRadius, suggestedRadius, effectiveBurnTime, tankConfig, propDensities, payload, structure])
|
||||
|
||||
// Flatten engine results for calcs
|
||||
const engineCalcData = useMemo(() => {
|
||||
if (!engineData) return null
|
||||
return {
|
||||
allThermo: engineData.results?.thermodynamics ?? {},
|
||||
nozzleGeometry: engineData.results?.nozzleGeometry ?? null,
|
||||
chamberGeometry: engineData.results?.chamberGeometry ?? null,
|
||||
allThermo: engineData.results?.thermodynamics ?? {},
|
||||
nozzleGeometry: engineData.results?.nozzleGeometry ?? null,
|
||||
chamberGeometry: engineData.results?.chamberGeometry ?? null,
|
||||
structureResults: engineData.results?.structureResults ?? null,
|
||||
}
|
||||
}, [engineData])
|
||||
|
||||
const rocketInputs = useMemo(() => ({
|
||||
outerRadius: outerRadius ?? suggestedRadius,
|
||||
burnTime: effectiveBurnTime,
|
||||
arrangement: tankConfig.arrangement,
|
||||
innerPropellant: tankConfig.innerPropellant,
|
||||
rhoFuel: propDensities.rhoFuel,
|
||||
rhoOx: propDensities.rhoOx,
|
||||
payloadMass: payload.mass,
|
||||
payloadBayLength: payload.bayLength,
|
||||
tankMaterialId: structure.tankMaterialId,
|
||||
tankSafetyFactor: structure.tankSafetyFactor,
|
||||
feedSystem: structure.feedSystem,
|
||||
ullagePercent: structure.ullagePercent,
|
||||
otherStructFraction: structure.otherStructFraction,
|
||||
engineDryMass: engineCalcData?.structureResults?.m_total ?? null,
|
||||
noseConeShape: noseConeShape,
|
||||
}), [outerRadius, suggestedRadius, effectiveBurnTime, tankConfig, propDensities, payload, structure, engineCalcData, noseConeShape])
|
||||
|
||||
const geometry = useMemo(
|
||||
() => calcRocketGeometry(engineCalcData, rocketInputs),
|
||||
[engineCalcData, rocketInputs],
|
||||
@@ -79,13 +93,23 @@ export function useRocketDesign() {
|
||||
)
|
||||
|
||||
/** Bulk-restore all state from a parsed rocket design import. */
|
||||
function loadRocketDesign({ outerRadius, tankConfig, propDensities, payload, structure, engineData }) {
|
||||
function loadRocketDesign({ outerRadius, tankConfig, propDensities, payload, structure, engineData, noseConeShape: ncs }) {
|
||||
if (outerRadius != null) setOuterRadius(outerRadius)
|
||||
if (tankConfig) setTankConfig(tankConfig)
|
||||
if (propDensities) setPropDensities(propDensities)
|
||||
if (payload) setPayload(payload)
|
||||
if (structure) setStructure(structure)
|
||||
if (structure) {
|
||||
// Backward compatibility: old exports have structMassFraction, new have otherStructFraction
|
||||
setStructure(prev => ({
|
||||
...prev,
|
||||
...(structure.structMassFraction != null && structure.otherStructFraction == null
|
||||
? { otherStructFraction: structure.structMassFraction }
|
||||
: {}),
|
||||
...structure,
|
||||
}))
|
||||
}
|
||||
if (engineData) setEngineData(engineData)
|
||||
if (ncs != null) setNoseConeShape(ncs)
|
||||
}
|
||||
|
||||
/** Parse and store an engine JSON (the full exported object). */
|
||||
@@ -139,6 +163,8 @@ export function useRocketDesign() {
|
||||
// Structure
|
||||
structure, setStructure,
|
||||
effectiveBurnTime,
|
||||
// Nose cone
|
||||
noseConeShape, setNoseConeShape,
|
||||
// Output
|
||||
geometry,
|
||||
diagnosis,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { PropellantModal } from '../components/PropellantModal.jsx'
|
||||
import { useRocketDesign } from '../hooks/useRocketDesign.js'
|
||||
import { STRUCTURAL_MATERIALS } from '../engine/knowledgebaseData.js'
|
||||
import DesignSection from '../components/engine/DesignSection.jsx'
|
||||
import RocketModel3D from '../components/rocket/RocketModel3D.jsx'
|
||||
import ErrorBoundary from '../components/ErrorBoundary.jsx'
|
||||
@@ -133,6 +134,7 @@ export default function RocketPage() {
|
||||
payload, setPayload,
|
||||
structure, setStructure,
|
||||
effectiveBurnTime,
|
||||
noseConeShape, setNoseConeShape,
|
||||
geometry,
|
||||
diagnosis,
|
||||
loadRocketDesign,
|
||||
@@ -163,7 +165,7 @@ export default function RocketPage() {
|
||||
}
|
||||
|
||||
function handleExportJSON() {
|
||||
const blob = exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry })
|
||||
const blob = exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry, noseConeShape })
|
||||
downloadBlob(blob, 'rocket-design.json')
|
||||
}
|
||||
|
||||
@@ -277,6 +279,16 @@ export default function RocketPage() {
|
||||
Enter outer diameter to generate model
|
||||
</p>
|
||||
)}
|
||||
<SelectInput
|
||||
label="Nose Cone Shape"
|
||||
value={noseConeShape}
|
||||
onChange={v => setNoseConeShape(v)}
|
||||
options={[
|
||||
{ value: 'conical', label: 'Conical' },
|
||||
{ value: 'tangentOgive', label: 'Tangent Ogive' },
|
||||
{ value: 'vonKarman', label: 'Von Kármán' },
|
||||
]}
|
||||
/>
|
||||
</DesignSection>
|
||||
|
||||
<DesignSection title="Tank Configuration">
|
||||
@@ -370,14 +382,48 @@ export default function RocketPage() {
|
||||
</DesignSection>
|
||||
|
||||
<DesignSection title="Structure">
|
||||
<NumInput
|
||||
label="Structural Mass Fraction"
|
||||
value={structure.structMassFraction}
|
||||
onChange={v => setStructure(s => ({ ...s, structMassFraction: v ?? 0.10 }))}
|
||||
units="—"
|
||||
step="0.01"
|
||||
placeholder="0.10"
|
||||
<SelectInput
|
||||
label="Tank Material"
|
||||
value={structure.tankMaterialId}
|
||||
onChange={v => setStructure(s => ({ ...s, tankMaterialId: v }))}
|
||||
options={STRUCTURAL_MATERIALS.map(m => ({ value: m.id, label: m.name }))}
|
||||
/>
|
||||
<NumInput
|
||||
label="Tank Safety Factor"
|
||||
value={structure.tankSafetyFactor}
|
||||
onChange={v => setStructure(s => ({ ...s, tankSafetyFactor: v ?? 2.0 }))}
|
||||
units="—"
|
||||
step="0.1"
|
||||
placeholder="2.0"
|
||||
/>
|
||||
<SelectInput
|
||||
label="Feed System"
|
||||
value={structure.feedSystem}
|
||||
onChange={v => setStructure(s => ({ ...s, feedSystem: v }))}
|
||||
options={[
|
||||
{ value: 'pressure_fed', label: 'Pressure-Fed' },
|
||||
{ value: 'pump_fed', label: 'Pump-Fed' },
|
||||
]}
|
||||
/>
|
||||
<NumInput
|
||||
label="Ullage %"
|
||||
value={structure.ullagePercent}
|
||||
onChange={v => setStructure(s => ({ ...s, ullagePercent: v ?? 5 }))}
|
||||
units="%"
|
||||
step="0.1"
|
||||
placeholder="5"
|
||||
/>
|
||||
<NumInput
|
||||
label="Other Structure %"
|
||||
value={structure.otherStructFraction * 100}
|
||||
onChange={v => setStructure(s => ({ ...s, otherStructFraction: (v ?? 5) / 100 }))}
|
||||
units="%"
|
||||
step="0.1"
|
||||
placeholder="5"
|
||||
/>
|
||||
<p className="text-[11px] text-slate-500 pl-[11.5rem]">
|
||||
Nose cone, bay, interstage, thrust structure
|
||||
</p>
|
||||
</DesignSection>
|
||||
</div>
|
||||
|
||||
@@ -443,8 +489,10 @@ export default function RocketPage() {
|
||||
<ResultSection title="Tank Geometry">
|
||||
{geometry?.arrangement === 'tandem' ? (
|
||||
<>
|
||||
<ResultRow label="Fuel Tank Length" value={geometry?.L_tank_fuel != null ? geometry.L_tank_fuel * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Oxidizer Tank Length" value={geometry?.L_tank_ox != null ? geometry.L_tank_ox * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Fuel Tank Cyl. Length" value={geometry?.L_tank_fuel_cyl != null ? geometry.L_tank_fuel_cyl * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Oxidizer Tank Cyl. Length" value={geometry?.L_tank_ox_cyl != null ? geometry.L_tank_ox_cyl * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Fuel Tank Total Length" value={geometry?.L_tank_fuel != null ? geometry.L_tank_fuel * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Oxidizer Tank Total Length" value={geometry?.L_tank_ox != null ? geometry.L_tank_ox * 1000 : null} unit="mm" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -452,12 +500,21 @@ export default function RocketPage() {
|
||||
<ResultRow label="Inner Tank Radius" value={geometry?.r_inner != null ? geometry.r_inner * 1000 : null} unit="mm" />
|
||||
</>
|
||||
)}
|
||||
<ResultRow label="Total Tank Length" value={geometry?.L_tank != null ? geometry.L_tank * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Tank Wall Thickness" value={geometry?.t_wall != null ? geometry.t_wall * 1000 : null} unit="mm" />
|
||||
<ResultRow label="Total Tank Length" value={geometry?.L_tank != null ? geometry.L_tank * 1000 : null} unit="mm" />
|
||||
</ResultSection>
|
||||
|
||||
<ResultSection title="Mass Budget">
|
||||
<ResultRow label="Structural Mass" value={geometry?.m_struct} unit="kg" />
|
||||
<ResultRow label="Payload Mass" value={geometry?.m_payload} unit="kg" />
|
||||
<ResultRow label="Tank Mass" value={geometry?.m_tank_total} unit="kg" />
|
||||
<ResultRow label="Other Structure Mass" value={geometry?.m_struct != null && geometry?.m_tank_total != null ? geometry.m_struct - geometry.m_tank_total : null} unit="kg" />
|
||||
<ResultRow label="Structural Mass" value={geometry?.m_struct} unit="kg" />
|
||||
<ResultRow label="Payload Mass" value={geometry?.m_payload} unit="kg" />
|
||||
{geometry?.m_engine > 0 && (
|
||||
<ResultRow label="Engine Dry Mass" value={geometry?.m_engine} unit="kg" />
|
||||
)}
|
||||
{geometry?.m_pressurant > 0 && (
|
||||
<ResultRow label="Pressurant System" value={geometry?.m_pressurant} unit="kg" />
|
||||
)}
|
||||
<ResultRow label="Dry Mass" value={geometry?.m_dry} unit="kg" />
|
||||
<ResultRow label="Wet Mass" value={geometry?.m_wet} unit="kg" />
|
||||
<ResultRow label="Mass Ratio (Wet/Dry)" value={geometry?.massRatio} unit="—" />
|
||||
|
||||
Reference in New Issue
Block a user