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 && (
+
+ )}