From 6af56f478f7bc241ce82a40e28bdf9a3e6dce597 Mon Sep 17 00:00:00 2001 From: JimmyBinoculars Date: Wed, 4 Mar 2026 19:40:46 +0000 Subject: [PATCH] Better accuracy --- src/components/rocket/RocketModel3D.jsx | 125 +++++++++++++++--- src/engine/rocketDesignCalcs.js | 164 +++++++++++++++++++++--- src/engine/rocketExportImport.js | 6 +- src/hooks/useRocketDesign.js | 64 ++++++--- src/pages/RocketPage.jsx | 83 ++++++++++-- 5 files changed, 371 insertions(+), 71 deletions(-) diff --git a/src/components/rocket/RocketModel3D.jsx b/src/components/rocket/RocketModel3D.jsx index f91f43b..8334f15 100644 --- a/src/components/rocket/RocketModel3D.jsx +++ b/src/components/rocket/RocketModel3D.jsx @@ -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 = ( + = 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 }) { @@ -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 ( - {/* Nose cone — tip at +Y, base joins body at −Y */} -
+ {/* Nose cone — profile-based shape */} + {noseProfilePoints && noseProfilePoints.length > 0 ? ( + + ) : ( +
+ )} {/* Payload bay */} {L_payload > 0 && ( @@ -75,18 +168,18 @@ function RocketShape({ geometry: geo }) { /> )} - {/* Tanks — tandem: oxidizer above fuel */} - {arrangement === 'tandem' && L_tank_ox > 0 && ( -
0 && ( + )} - {arrangement === 'tandem' && L_tank_fuel > 0 && ( -
0 && ( + )} diff --git a/src/engine/rocketDesignCalcs.js b/src/engine/rocketDesignCalcs.js index 5efb878..caeb93a 100644 --- a/src/engine/rocketDesignCalcs.js +++ b/src/engine/rocketDesignCalcs.js @@ -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, } } diff --git a/src/engine/rocketExportImport.js b/src/engine/rocketExportImport.js index 7a2fd6b..24f2a26 100644 --- a/src/engine/rocketExportImport.js +++ b/src/engine/rocketExportImport.js @@ -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, } } diff --git a/src/hooks/useRocketDesign.js b/src/hooks/useRocketDesign.js index 0ce319a..2fd678f 100644 --- a/src/hooks/useRocketDesign.js +++ b/src/hooks/useRocketDesign.js @@ -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, diff --git a/src/pages/RocketPage.jsx b/src/pages/RocketPage.jsx index 7295559..b6795ee 100644 --- a/src/pages/RocketPage.jsx +++ b/src/pages/RocketPage.jsx @@ -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

)} + setNoseConeShape(v)} + options={[ + { value: 'conical', label: 'Conical' }, + { value: 'tangentOgive', label: 'Tangent Ogive' }, + { value: 'vonKarman', label: 'Von Kármán' }, + ]} + /> @@ -370,14 +382,48 @@ export default function RocketPage() { - setStructure(s => ({ ...s, structMassFraction: v ?? 0.10 }))} - units="—" - step="0.01" - placeholder="0.10" + setStructure(s => ({ ...s, tankMaterialId: v }))} + options={STRUCTURAL_MATERIALS.map(m => ({ value: m.id, label: m.name }))} /> + setStructure(s => ({ ...s, tankSafetyFactor: v ?? 2.0 }))} + units="—" + step="0.1" + placeholder="2.0" + /> + setStructure(s => ({ ...s, feedSystem: v }))} + options={[ + { value: 'pressure_fed', label: 'Pressure-Fed' }, + { value: 'pump_fed', label: 'Pump-Fed' }, + ]} + /> + setStructure(s => ({ ...s, ullagePercent: v ?? 5 }))} + units="%" + step="0.1" + placeholder="5" + /> + setStructure(s => ({ ...s, otherStructFraction: (v ?? 5) / 100 }))} + units="%" + step="0.1" + placeholder="5" + /> +

+ Nose cone, bay, interstage, thrust structure +

@@ -443,8 +489,10 @@ export default function RocketPage() { {geometry?.arrangement === 'tandem' ? ( <> - - + + + + ) : ( <> @@ -452,12 +500,21 @@ export default function RocketPage() { )} - + + - - + + + + + {geometry?.m_engine > 0 && ( + + )} + {geometry?.m_pressurant > 0 && ( + + )}