Better accuracy

This commit is contained in:
2026-03-04 19:40:46 +00:00
parent 6885801078
commit 6af56f478f
5 changed files with 371 additions and 71 deletions

View File

@@ -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"
/>
)}

View File

@@ -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, // 01 (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,
}
}

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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="—" />