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 (
);
};
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 */}
{/* Main Workspace */}
);
}
// 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 }) => (
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
{/* Left Control Panel */}
{/* Nose Cone */}
handleChange('noseLength', v)} />
handleChange('noseWeight', v)} />
{/* Body Tube */}
handleChange('bodyLength', v)} />
handleChange('bodyDiameter', v)} />
handleChange('bodyWeight', v)} />
{/* Egg Payload */}
handleChange('eggWeight', v)} />
handleChange('eggPosition', v)} />
{/* Fins */}
handleChange('finCount', v)} />
handleChange('finSpan', v)} />
handleChange('finRootChord', v)} />
handleChange('finTipChord', v)} />
handleChange('finSweep', v)} />
handleChange('finWeight', v)} />
{/* Motor */}
handleChange('motorMountWeight', v)} />
{/* Recovery */}
handleChange('recoveryWeight', v)} />
{/* Center Visualizer */}
{/* Right Stats 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
Body Tube
Payload (Egg)
Fins
Propulsion
Recovery
{renderRocketSVG()}
{/* Legend */}
Center of Gravity (CG)
Center of Pressure (CP)
Payload (Egg)
{/* Stability Warning */}
{/* Performance */}
{/* TARC Checklist */}
= 1 ? 'bg-green-900/20 border-green-800' : 'bg-red-900/20 border-red-800'}`}>
{/* Mass Budget */}
{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
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
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 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.
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"
/>