import React, { useState, useEffect, useMemo } from 'react'; import { Ruler, Rocket, Weight, Wind, AlertTriangle, CheckCircle2, RotateCcw, Target, Info } from 'lucide-react'; // --- Types & Constants --- type Motor = { name: string; totalImpulse: number; // Ns avgThrust: number; // N burnTime: number; // s loadedWeight: number; // g propellantWeight: number; // g }; const MOTORS: Motor[] = [ { name: 'Estes E12', totalImpulse: 27, avgThrust: 12, burnTime: 2.4, loadedWeight: 36, propellantWeight: 21 }, { name: 'Estes F15', totalImpulse: 50, avgThrust: 15, burnTime: 3.4, loadedWeight: 104, propellantWeight: 30 }, { name: 'Aerotech F39', totalImpulse: 50, avgThrust: 39, burnTime: 1.3, loadedWeight: 82, propellantWeight: 35 }, { name: 'Aerotech F67', totalImpulse: 55, avgThrust: 67, burnTime: 0.8, loadedWeight: 90, propellantWeight: 40 }, ]; type RocketState = { targetAlt: number; // ft noseLength: number; // cm noseWeight: number; // g bodyLength: number; // cm bodyDiameter: number; // cm bodyWeight: number; // g finCount: number; finRootChord: number; // cm finTipChord: number; // cm finSpan: number; // cm finSweep: number; // cm (distance from leading edge of root to leading edge of tip) finWeight: number; // g (total for all fins) motorMountWeight: number; // g recoveryWeight: number; // g (parachute + shock cord) eggWeight: number; // g (standard large egg is ~57-60g) eggPosition: number; // cm from nose tip selectedMotorIndex: number; }; const INITIAL_STATE: RocketState = { targetAlt: 820, noseLength: 15, noseWeight: 25, bodyLength: 45, bodyDiameter: 4.1, // standard BT-60 approx bodyWeight: 50, finCount: 3, finRootChord: 8, finTipChord: 4, finSpan: 6, finSweep: 4, finWeight: 15, motorMountWeight: 15, recoveryWeight: 25, eggWeight: 60, eggPosition: 10, selectedMotorIndex: 1, // F15 }; export default function TARCRocketArchitect() { const [config, setConfig] = useState(INITIAL_STATE); const [activeTab, setActiveTab] = useState<'design' | 'analysis'>('design'); // --- Physics Calculations --- const stats = useMemo(() => { const motor = MOTORS[config.selectedMotorIndex]; // 1. Total Mass (g) -> convert to kg for physics const totalMassG = config.noseWeight + config.bodyWeight + config.finWeight + config.motorMountWeight + config.recoveryWeight + config.eggWeight + motor.loadedWeight; const burnoutMassG = totalMassG - motor.propellantWeight; // 2. Center of Gravity (CG) - Weighted Average // Simplified model assuming component CGs are roughly centered in their geometry const moments = [ { w: config.noseWeight, pos: config.noseLength * 0.6 }, { w: config.eggWeight, pos: config.eggPosition }, { w: config.bodyWeight, pos: config.noseLength + (config.bodyLength / 2) }, { w: config.recoveryWeight, pos: config.noseLength + (config.bodyLength / 3) }, // Parachute sits high { w: config.motorMountWeight, pos: config.noseLength + config.bodyLength - 5 }, { w: config.finWeight, pos: config.noseLength + config.bodyLength - (config.finRootChord / 2) }, { w: motor.loadedWeight, pos: config.noseLength + config.bodyLength - 4 } // Motor sits at bottom ]; const totalMoment = moments.reduce((acc, item) => acc + (item.w * item.pos), 0); const cg = totalMoment / totalMassG; // cm from nose tip // 3. Center of Pressure (CP) - Simplified Barrowman Equations // Focusing mostly on nose and fins as they are dominant // Nose CP const cnNose = 2; // For ogive/parabolic const cpNose = 0.466 * config.noseLength; // Approximation for Ogive // Fin CP // Term 1: (Span * Span) / Area // Area of one fin const finArea = 0.5 * (config.finRootChord + config.finTipChord) * config.finSpan; // Barrowman Fin term const cnFins = (1 + (config.bodyDiameter / (2 * config.finSpan + config.bodyDiameter))) * (4 * config.finCount * Math.pow(config.finSpan / config.bodyDiameter, 2)) / (1 + Math.sqrt(1 + Math.pow((2 * config.finSpan) / (config.finRootChord + config.finTipChord), 2))); // Fin CP position relative to leading edge of root chord const xF = (config.finSweep * (config.finRootChord + 2 * config.finTipChord) + (1/3) * (config.finRootChord + config.finTipChord) * (config.finRootChord + 2 * config.finTipChord)) / (config.finRootChord + config.finTipChord); const cpFinsAbsolute = config.noseLength + config.bodyLength - config.finRootChord + xF; // Weighted CP const cp = ((cnNose * cpNose) + (cnFins * cpFinsAbsolute)) / (cnNose + cnFins); // 4. Stability Margin (Calibers) const stabilityMargin = (cp - cg) / config.bodyDiameter; // 5. Apogee Estimation (Metric) // Very simplified energy approximation: h = (I^2) / (2 * m * k) // We will use a more classic kinematic approach with drag // 0.5 * Cd * A * rho * v^2 const g = 9.81; const rho = 1.225; // Air density const cd = 0.75; // Average Cd for student rocket const area = Math.PI * Math.pow((config.bodyDiameter / 100) / 2, 2); // m^2 const avgMassKg = (totalMassG - (motor.propellantWeight / 2)) / 1000; // Velocity at burnout roughly const vBurnout = (motor.totalImpulse / avgMassKg) - (g * motor.burnTime); // Coast height: h = (m * ln(1 + (k * v^2) / (m * g))) / (2 * k) where k = 0.5 * rho * Cd * A // Simplified coast: v^2 / 2g (vacuum) reduced by drag factor const dragFactor = 0.6; // Efficiency factor due to air resistance const vacuumHeight = (Math.pow(vBurnout, 2)) / (2 * g); const coastHeight = vacuumHeight * dragFactor; // Powered flight height (approximate) const poweredHeight = 0.5 * ((motor.avgThrust / avgMassKg) - g) * Math.pow(motor.burnTime, 2); let estimatedApogeeM = poweredHeight + coastHeight; if (estimatedApogeeM < 0) estimatedApogeeM = 0; const estimatedApogeeFt = estimatedApogeeM * 3.28084; return { mass: totalMassG, cg, cp, stability: stabilityMargin, apogee: estimatedApogeeFt, burnoutMass: burnoutMassG }; }, [config]); // --- Helpers --- const handleChange = (field: keyof RocketState, value: number) => { setConfig(prev => ({ ...prev, [field]: value })); }; // --- Renderers --- const renderRocketSVG = () => { const scale = 4; // pixels per cm const w = 400; const h = (config.noseLength + config.bodyLength + 10) * scale; const centerX = w / 2; const noseH = config.noseLength * scale; const bodyH = config.bodyLength * scale; const diameterPx = config.bodyDiameter * scale; const radiusPx = diameterPx / 2; // Fins const rootPx = config.finRootChord * scale; const tipPx = config.finTipChord * scale; const spanPx = config.finSpan * scale; const sweepPx = config.finSweep * scale; const finYStart = noseH + bodyH - rootPx; const finPathRight = ` M ${centerX + radiusPx} ${finYStart} L ${centerX + radiusPx + spanPx} ${finYStart + sweepPx} L ${centerX + radiusPx + spanPx} ${finYStart + sweepPx + tipPx} L ${centerX + radiusPx} ${finYStart + rootPx} Z `; const finPathLeft = ` M ${centerX - radiusPx} ${finYStart} L ${centerX - radiusPx - spanPx} ${finYStart + sweepPx} L ${centerX - radiusPx - spanPx} ${finYStart + sweepPx + tipPx} L ${centerX - radiusPx} ${finYStart + rootPx} Z `; // Center fin (perspective) const finPathCenter = config.finCount > 2 ? ` M ${centerX} ${finYStart} L ${centerX} ${finYStart + rootPx} ` : ''; // CG and CP markers const cgY = stats.cg * scale; const cpY = stats.cp * scale; return ( {/* Fins */} {config.finCount > 2 && } {/* Body Tube */} {/* Nose Cone (Ogive-ish) */} {/* Egg Payload Location */} Egg {/* Markers */} {/* CG */} CG {/* CP */} CP ); }; const getStatusColor = (val: number, target: number, range: number) => { const diff = Math.abs(val - target); if (diff <= range) return 'text-green-500'; if (diff <= range * 2) return 'text-yellow-500'; return 'text-red-500'; }; return (
{/* Header */}

TARC ARCHITECT

American Rocketry Challenge Design Studio

Est. Altitude {Math.round(stats.apogee)} ft
Stability = 1 ? 'text-green-400' : 'text-red-500'}`}> {stats.stability.toFixed(2)} cal
{/* Main Workspace */}
{/* Left Control Panel */}
handleChange('targetAlt', parseFloat(e.target.value))} className="w-20 bg-slate-900 border border-slate-600 rounded px-2 py-1 text-right text-sm" />
{/* Nose Cone */}

Nose Cone

handleChange('noseLength', v)} /> handleChange('noseWeight', v)} />
{/* Body Tube */}

Body Tube

handleChange('bodyLength', v)} /> handleChange('bodyDiameter', v)} /> handleChange('bodyWeight', v)} />
{/* Egg Payload */}

Payload (Egg)

handleChange('eggWeight', v)} /> handleChange('eggPosition', v)} />
{/* Fins */}

Fins

handleChange('finCount', v)} /> handleChange('finSpan', v)} /> handleChange('finRootChord', v)} /> handleChange('finTipChord', v)} /> handleChange('finSweep', v)} /> handleChange('finWeight', v)} />
{/* Motor */}

Propulsion

handleChange('motorMountWeight', v)} />
{/* Recovery */}

Recovery

handleChange('recoveryWeight', v)} />
{/* Center Visualizer */}
{renderRocketSVG()}
{/* Legend */}
Center of Gravity (CG)
Center of Pressure (CP)
Payload (Egg)
{/* Right Stats Panel */}
{/* Stability Warning */}
= 1 ? 'bg-green-900/20 border-green-800' : 'bg-red-900/20 border-red-800'}`}>
{stats.stability >= 1 ? : }

Stability Analysis

Margin: {stats.stability.toFixed(2)} cal

{stats.stability < 1 ? "DANGER: Rocket is unstable. Increase fin size or add nose weight." : "Rocket is stable. Ensure CG is ahead of CP."}

{/* Mass Budget */}

Mass Budget

Structure {(stats.mass - config.eggWeight - MOTORS[config.selectedMotorIndex].loadedWeight).toFixed(0)} g
Egg {config.eggWeight} g
Motor {MOTORS[config.selectedMotorIndex].loadedWeight} g
Total {stats.mass.toFixed(0)} g
{/* Performance */}

Flight Performance

Target Alt {config.targetAlt} ft
Est. Apogee {Math.round(stats.apogee)} ft
Deviation {Math.abs(Math.round(stats.apogee - config.targetAlt))} ft
{/* TARC Checklist */}

TARC Rules Check

  • Egg protection installed (bubble wrap/foam)
  • Rail buttons aligned
  • Vent holes for altimeter
  • Contact info on body tube

Calculations are estimates based on Barrowman equations and standard drag coefficients. Actual flight data will vary.

); } // Sub-component for inputs const InputGroup = ({ label, value, min, max, step=1, onChange }: { label: string, value: number, min: number, max: number, step?: number, onChange: (val: number) => void }) => (
onChange(parseFloat(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-orange-500" /> onChange(parseFloat(e.target.value))} className="w-12 bg-slate-900 border border-slate-600 rounded px-1 py-0.5 text-xs text-right" />
);