Skip to content

RFC-034 — Launch / powered-ascent engine + Scene 0 (the ascent act of /fly)

Status: Draft (epic kickoff) · 2026-07 · Closes: PRD-032 · target: v2.x

Why this is an RFC. Prepending a powered-ascent act to /fly binds architectural commitments that must be right from slice 0 or every later slice forces a rewrite: (1) a new launch-site-relative coordinate frame (Scene 0) — you cannot render a launchpad in the AU-scale heliocentric world without float-precision death; (2) a multi-scale clock that runs seconds-resolution ascent and days-resolution cruise on one continuous scrubbable timeline; (3) an integrated ascent physics engine (equations of motion, not keyframes) that must validate in CI the way fly-physics does today; (4) a per-vehicle LaunchProfile data library keyed on the existing fleet_refs launcher id, with a generic fallback; and (5) a re-basing scene handoff from the ascent frame into the existing heliocentric / cislunar scene at orbit injection. The existing /fly cruise must not regress. UX/HUD detail beyond this doc is captured against PRD-032.

1 · Architecture overview

/fly route — mission timeline extended to one continuous pad→destination arc
 └─ AscentContext (Scene 0) — NEW launch-site-relative frame (ENU, m→km world units)
 │    • integrated planar (2-DOF) ascent EOM from a per-vehicle LaunchProfile
 │    • owns MET 0 → injection; then re-bases (warp) into the helio/cislunar scene
 ├─ MultiScaleClock — seconds-res ascent act ⊕ days-res cruise, ONE scrubber
 ├─ LaunchProfile library — static/data/launch-profiles/<launcher-id>.json
 │    • flagship: hand-authored wet/dry mass, Isp(SL/vac), thrust, mdot, staging,
 │      fairing jettison, pitch program · generic: parameterized 2-stage fallback
 ├─ Ascent engine — src/lib/orbital/ascent-physics.ts (pure fns, CI-validated)
 ├─ AscentCameras — REUSE the #371 montage shot-cutting (flyby-shots.ts pattern):
 │    pad-wide → tower-clear → tracking → onboard-down → staging → MECO/orbit
 └─ Telemetry HUD + Science-Lens ascent layers (TWR · q · gravity/drag/steering loss · force vectors)

Conceptually this is the innermost context below RFC-032's context stack: LaunchSite ⊂ SolarSystem ⊂ …. The re-basing warp at injection is the same move as RFC-032's context boundaries and ADR-058's helio→cislunar SOI handoff — established precedent, not a new pattern.

2 · Design decisions (to lock)

idDecisionRationale
L-ANew launch-site-relative frame (Scene 0), not the AU sceneA launchpad is metres; the heliocentric world is 10¹¹ m. One float space can't hold both. Scene 0 is Earth-surface-relative (ENU), world units m→km, disposed after handoff.
L-BOne continuous timeline via a multi-scale clockPRD-032's core promise is scrubbing pad→destination in one drag. The clock is seconds-dense in the ascent window, days-dense in cruise — a non-linear map over the existing time-control (#351/#387), not two clocks.
L-CFully integrated EOM; hybrid data-anchor as the escape hatchPhysics-first: the trajectory emerges from thrust/gravity/drag on real mass. If integration proves too complex or the ride reads worse than reality, fall back to anchoring the integration to published milestones (MECO/staging alt+vel) — the same philosophy as the shipping tier_1_5_hybrid trajectory data. Decision is reversible per-vehicle.
L-DPer-vehicle LaunchProfile keyed on fleet_refs launcher id; generic 2-stage fallbackAscent shape belongs to the rocket, not the payload. Missions already name their launcher. Flagship profiles hand-authored; every other launcher gets a parameterized generic profile so no mission is launch-less.
L-EReuse the #371 montage engine for the shot sequenceLaunch has the most codified shot vocabulary in spaceflight; flyby-shots.ts (composeShot) + the MET-relative schedule already do cut-on-beats camera work. Author an ascent shot schedule; do not build a second camera system.
L-FScene 0 lazy-loaded; cruise must not regressMirror RFC-032 C-F: the ascent scene + its physics/data are a dynamic import gated on the mission having a launch profile and the clock being in the ascent window. /fly bundle weight and cruise perf are held.
L-GCinematic + scrubber + HUD, not a gamePRD-032 non-goal. Interactive fly-it-yourself is deferred (§9) on the same engine — the EOM core is authored so a control input can drive it later.
L-H2-DOF planar integration first; 3-DOF (yaw/dogleg) deferredThe launch plane carries all the teachable physics. Azimuth steering / doglegs add cost without new pedagogy; defer unless a flagship profile visibly needs it.
L-Iascent-physics.ts is a headless core reused by both /fly and /planSame pattern as the shared fly-physics.ts. The engine ships stepwise integration (animated scene) and summary solvers (ascentToOrbit) so the configurator can plan the take-off (§8 S10) without duplicating physics. Pure fns, no Three.js/DOM.

3 · Coordinate frame + scene handoff

Scene 0 frame. East-North-Up local tangent frame at the launch site, world units km (÷ some scale for render). Integration runs in the launch plane: state = { downrange, altitude, v_downrange, v_up, mass, t }. Earth curvature is applied for the visual (the horizon drops away) and for the orbit-relative velocity readout; the integration itself is planar over a rotating spherical Earth (Coriolis/centrifugal from Earth rotation optional, small, add if a flagship needs it).

Handoff at injection. The ascent ends in a parking orbit / on an injection trajectory, expressed in ECI (km) — the same frame Scene 3 (cislunar) already uses. Two exit paths:

  1. Moon missions → hand into Scene 3 (cislunar ECI) directly; the parking-orbit → TLI already lives there via cislunar_profile. The ascent extends that scene's timeline earlier rather than replacing it.
  2. Interplanetary missions → the ascent zooms out until Earth becomes the AU-scale point, then warps into Scene 2 (heliocentric). The current heliocentric anchor (met_days: 0 at Earth's position) is unchanged in space; the ascent act simply plays first and the helio MET0 is re-labelled "post-injection." No trajectory data changes for existing missions.

The warp is a 1–2 s cinematic re-base (camera + clock), reusing the RFC-032 / ADR-058 handoff choreography.

3.1 · The injection-burn beat (shipped 2026-07-18)

The kick / upper stage that actually leaves parking orbit and builds the transfer speed (S-IVB, Centaur, Blok D, Star 48B, Briz-M…) used to be silent — it happened inside the warp flash. It is now an explicit beat LaunchScene plays after SECO, before the warp, in its existing Earth-local frame (the only frame that can host a km-scale parking orbit — 185 km ≈ 0.0000044 AU, sub-pixel in the heliocentric scene). This closes the gap where interplanetary transfers began already on the escape ellipse with the injection stage unshown.

  • Resolver (pure): src/lib/orbital/injection-burn.tsresolveInjectionBurn(launcherId, vehicle_stage, tli_or_tmi_dv_km_s, dest){ stageName, dvKms|null, burnType } or null (LEO-direct / no injection stage ⇒ no beat). The stage name prefers the mission's authored flight.launch.vehicle_stage (e.g. New Horizons' STAR 48B), else the launcher's standard injection stage from a small map (saturn-v→S-IVB, atlas-v→Centaur, proton-k→Blok D, proton-m→Briz-M, falcon-9→S2, …). burnType is inferred from dest: MOON→TLI, MARS→TMI, else INJECTION. Δv reuses the already-authored flight.totals.tli_or_tmi_dv_km_s.
  • Timing: INJECTION_COAST_S (parking-orbit coast) + INJECTION_BURN_S (kick-stage burn) in ascent-hud.ts, appended to the ascent duration. The unified scrubber's ascent slice ([0, ascentScrubberFraction]) absorbs the extra seconds via launchDurationS; the seam now lands after injection. No ascent-clock.ts change.
  • Render: LaunchScene shows a status ("PARKING ORBIT" → the burn label via injectionPhaseStatus) + an amber callout (stage name + Δv); ascent-scene.ts re-lights the plume on the upper bell for the burn window via setInjectionBurn(on) (clock-driven, since sampleAscentAt freezes the physics state at SECO). Reduced-motion (ADR-025): the beat is fully scrubbable, no extra gating. Absent for missions with no injection stage — zero regression.

4 · The multi-scale clock

Today the /fly clock is days-only (simDay, day/sec speed pills). The ascent act is ~600 s ≈ 0.007 days — invisible at day resolution.

  • Introduce a piecewise timeline map: T_ascent ∈ [0, ~600 s] occupies a fixed fraction of the scrubber; T_cruise ∈ [injection, arrival] occupies the rest. The scrubber position → MET is a monotone non-linear function; the readout shows T+MM:SS in the ascent window and calendar date/T+NNNd in cruise.
  • Speed pills gain a seconds regime (e.g. 1× / 5× / 20× real-time) active in the ascent window; the existing day/sec pills govern cruise. Regime switches at the injection seam.
  • prefers-reduced-motion still hard-freezes per ADR-025.
  • Test hooks extend the ADR-077 pattern: window.__flySetAscentT(seconds) mirrors __flySetSimDay for the ascent window (sets + pauses, skips intro bias) so e2e can pin a beat deterministically.

5 · Ascent physics engine

src/lib/orbital/ascent-physics.ts — pure functions, no Three.js, unit-tested + CI-validated exactly like fly-physics.ts.

Reusability is a hard requirement (L-I). The engine is a headless physics core with two consumers: (1) /fly Scene 0 (renders the integrated trajectory), and (2) /plan — the mission configurator (see §8 S10), which today plans only the transfer and should also plan the take-off: launch-vehicle → Δv-to-orbit, payload-to-orbit, TWR margin, gravity/drag/steering losses feeding the mission's total Δv budget. This mirrors how fly-physics.ts (Tsiolkovsky, transfer math) is already shared between /fly and /plan. So ascent-physics.ts exposes both stepwise integration (for the animated scene) and summary solvers (ascentToOrbit(profile) → { dv_to_leo, losses, payload_margin }) for the configurator — no rendering assumptions, no Three.js, no DOM.

Per integration step (fixed small dt, e.g. RK4 or semi-implicit Euler):

thrust   = stage.thrust(altitude)                 // SL↔vac Isp interpolation on ambient pressure
drag     = ½ · ρ(altitude) · v² · Cd · A          // ρ from a standard atmosphere table (US Std 1976)
weight   = m · g(altitude)                          // g = μ⊕ / r²
a_along  = (thrust − drag)/m − g·sinθ               // θ = flight-path angle (pitch program / gravity turn)
a_perp   = −g·cosθ + (thrust steering component)
m       -= mdot · dt                                // mdot = thrust / (Isp·g0)
q        = ½ · ρ · v²                                // dynamic pressure → Max-Q detection

Discrete events fire when their condition trips: staging at mass ≤ stage_dry (or scheduled MET), fairing jettison at altitude/q threshold, MECO/SECO at target orbital velocity. Loss accounting integrates ∫g·sinθ dt (gravity), ∫(drag/m) dt (drag), and the steering component (steering) across the flight for the HUD.

Validation (the CI gate). Mirror the fly-physics-validation harness: each flagship profile ships committed expected milestones (MECO altitude & velocity, staging MET, orbit insertion velocity) with tolerances; a regression in the math or the profile data fails CI. This is how "physics first, prototype is ground truth" stays honest.

Hybrid escape hatch (L-C). If a vehicle's pure integration drifts from reality or reads poorly, a per-profile anchor block supplies target milestones and the integrator tunes the pitch program / a small thrust-fudge to hit them — logged and captioned, never silent.

5.1 · Insertion guidance — two-phase steering (#415)

The open-loop pitch_program alone reaches a genuine orbit only when the table happens to place the vehicle horizontal at apoapsis at circular speed; higher-energy stacks loft and cross circular speed on the way down — a suborbital lob, not an orbit (staging mass bookkeeping is correct; this is a guidance problem). So the integrator steers in two phases (commandedPitchRad):

  • Atmospheric phase (altitude < GUIDANCE.handoverAltM, ≈ 55 km): the pitch_program drives the aero-safe gravity turn (low angle-of-attack in dense air).
  • Guidance phase (above handover, powered): an altitude-hold autopilot — the real technique (Saturn V IGM, Shuttle/PEG) in miniature. It commands the vertical acceleration a_des = kAlt·(target−alt) − kVy·vy and solves for the thrust pitch that delivers it after gravity and the centrifugal relief of horizontal speed: pitch = asin((a_des + g − vₓ²/r) / (T/m)). As horizontal speed builds toward orbital, vₓ²/r cancels gravity and the commanded pitch falls to zero on its own — the vehicle arrives level at circular speed. The two phases blend over GUIDANCE.blendM. The orbit gate additionally requires |vy| < GUIDANCE.orbitVyMaxMs, so a residual lob can't trip it while descending.

True 2-body dynamics (#415 Track 2). The integrator works in a launch-local Earth-centred frame — position from the centre is (x, y+R⊕) — with gravity always pulling toward the centre (−μ/r³·P) and altitude = |P| − R⊕. Orbits are therefore real curved arcs: at circular speed the ground falls away as fast as gravity pulls, so a vehicle holds its orbit after cutoff instead of lobbing back (an earlier flat-frame + vx²/r patch approximated this; the real 2-body is the honest model). The orbit gate is now perigee-based — SECO fires when the osculating perigee reaches a stable altitude — identical for a high-TWR direct ascent and a low-TWR upper stage that circularises over a long burn. This lands saturn-v (S-IVB) and every other adequately-powered serial stack in a stable orbit.

One shared gain set now flies every adequately-powered serial stack to a stable orbit — falcon-9, titan-ii-glv, saturn-ib, proton-k, saturn-v — with no per-vehicle tuning. The flagship-profiles.test.ts GENUINE_ORBIT tier is the regression guard.

5.2 · Parallel-boost stage (#415 Track 3)

Strap-on boosters ignite at liftoff and burn in parallel with the core (stage 0), then jettison when spent — the core fires throughout. LaunchProfile.boosters gives the per-booster masses/thrust/Isp and a count; the integrator combines them (combinedBoosterStage), adds their thrust to the core's during the boost phase, carries their mass, burns their propellant on their own Isp, and drops them (a staging beat) at depletion. Covers Ariane 5 EAP, H-IIA SRB-A, the R-7 strap-ons, Delta II GEM-40 solids, the Atlas half-stage. vostok-k (R-7 + 4 strap-ons + Blok-E) now reaches a genuine orbit.

5.3 · Low-TWR upper stages + the Earth-rotation credit — all 14 fly (#416)

Closing the last low-TWR-upper-stage vehicles (atlas-v Centaur ~0.3 TWR, ariane-5 ESC-A ~0.19, h-iia LE-5B ~0.5, space-shuttle-stack OMS) turned up missing physics, not a guidance gap:

  • Earth-rotation launch credit (the fix). The integrator launched from a non-rotating Earth (pad at rest) yet charged full inertial circular speed — every vehicle silently taxed the ~0.4–0.46 km/s the pad's eastward rotation gives for free (§3's "optional, small" note was wrong; it's essential). The pad now starts with 465.1·cos(lat) m/s eastward (latitude from launchSite.lat), and — because the atmosphere co-rotates — drag and steering loss are computed on air-relative velocity, not inertial (otherwise a phantom dynamic pressure / steering loss appears on the pad). This alone put the two thinnest-margin, most-equatorial vehicles over the line: ariane-5 @ Kourou (5.2°) was being denied 463 m/s — the largest credit on Earth, the reason ESA's spaceport is equatorial — and h-iia @ Tanegashima (30.4°) 401 m/s. Losses dropped to realistic values (total ~1.2–1.8 km/s across the fleet).

  • PEG + lofted-boost guidance for a genuinely low-TWR upper stage (gated by the loftBoost profile flag; adequately-powered vehicles keep the §5.1 altitude-hold, untouched). A low-TWR stage can't circularise from apoapsis (it must dip, and dips into the ground from a bare-target apoapsis). Real vehicles fly a lofted direct injection: the boost phase raises apoapsis past target — steering the time-to-apoapsis so the upper stage ignites still climbing with apoapsis a runway ahead — then a linear-tangent PEG solver (pegSolve/pegPitchRad, gravity carried as a predicted integral) circularises as it coasts up. The honest osculating-perigee gate still decides SECO, so guidance can never fabricate an orbit.

  • Spec corrections (the data was wrong, the operator's instinct): Shuttle Orbiter empty mass 92→78 t (real); atlas-v modelled as the 541 heavy variant (4 solids) its science missions flew; ariane-5 at a representative 10 t dual-GTO payload (16 t was the catalog-max LEO paper number). Shuttle pitch program re-tuned for the corrected rotation physics.

Result: all 14 flagships reach a genuine stable orbit; flagship-profiles.test.ts GENUINE_ORBIT now guards the full set (perigee ≥ 130 km + osculating-eccentricity bound). (The Delta IV / Titan III–IV upper stages in #414 are the same low-TWR class and will reuse loftBoost + PEG.)

6 · LaunchProfile data schema

static/data/launch-profiles/<launcher-id>.json, <launcher-id> = the fleet_refs[].id with role: "launcher".

jsonc
{
  "id": "falcon-9",
  "name": "Falcon 9 Block 5",
  "agency": "SpaceX",
  "source_tier": "flagship",              // "flagship" | "generic"
  "launch_site": { "lat": 28.56, "lon": -80.58, "name": "SLC-40, Cape Canaveral" },
  "stages": [
    { "name": "S1", "wet_kg": 433100, "dry_kg": 25600,
      "thrust_sl_kN": 7607, "thrust_vac_kN": 8227, "isp_sl_s": 283, "isp_vac_s": 312,
      "burn_s": 162 },
    { "name": "S2", "wet_kg": 111500, "dry_kg": 4000,
      "thrust_vac_kN": 981, "isp_vac_s": 348, "burn_s": 397 }
  ],
  "fairing": { "jettison_alt_km": 110, "mass_kg": 1900 },
  "pitch_program": [ [0, 90], [12, 88], [40, 70], [120, 45], [180, 20] ],  // [t_s, flight-path-angle_deg]
  "events": [ { "t_s": 72, "type": "max_q" }, { "t_s": 162, "type": "meco" },
              { "t_s": 165, "type": "staging" }, { "t_s": 559, "type": "seco" } ],
  "anchor": { "meco_alt_km": 66, "meco_vel_kms": 2.35 },   // optional, hybrid mode
  "provenance": [ { "l": "SpaceX Falcon 9 Users Guide", "u": "…" } ]
}

Validated fail-closed by a schema (like existing mission data), provenance-tracked, i18n-overlaid for user-facing strings like /science. Generic fallback: when a launcher has no flagship profile, source_tier: "generic" is synthesized from published per-stage Δv / mass fractions and a canned pitch program, captioned as representative.

7 · Rendering (Scene 0)

  • Vehicle: a stylised procedural multi-stage body is the S1 placeholder; the target is a per-vehicle accurate 3D model so each rocket looks like the real thing (Falcon 9 ≠ Saturn V ≠ Long March 5 ≠ Soyuz), exactly as the spacecraft fleet is individually modelled. Each flagship gets its own GLB via the fleet 3D-model pipeline (RFC-016 / ADR-052) — real silhouette, stage divisions, engine count, fairing, livery — with stages that visibly separate, a fairing that splits, and an engine plume that scales with throttle. This is a dedicated slice (§8 S11); the procedural body carries every earlier slice so the physics/camera/HUD work isn't blocked on modelling.
  • World: launch pad + tower (simple), curved lit Earth surface that recedes, sky gradient darkening to black as altitude climbs, star field fading in. Atmosphere shell for the visual ρ cue.
  • Launch site as a place (candidate slice S8): rather than a bare pad geometry, the T-minus scene can reuse the surface-scene panorama renderer (RFC-017 / ADR-072) — the same zoomable/panoramic treatment /moon and /mars landing sites get — to make each flagship's pad a recognizable, explorable place (LC-39A, SLC-40, Baikonur, Sriharikota, Kourou, Wenchang). The pipeline already exists; this points it at Earth launch complexes. A stylised pad ships first (S1); the full per-site panorama is a candidate later slice so it never gates the ascent physics.
  • Broadcast-grade bar: the render + camera + plume + sound target a live-launch webcast look (PRD-032 north star), not a schematic. Direction is locked in S0 before any of this is built.
  • Budget: governed by the same detect-gpu tiering as the rest of the 3D routes; Scene 0 is short-lived and disposed on handoff (forceContextLoss per the surface-perf lesson) so it never competes with the cruise scene.

8 · Slice plan (each slice ships something watchable)

SliceLocksDeliverable
S0visual direction (gate) — ✅ LOCKED 2026-07-16Art-directed concept mockups for the key beats — direction approved. Reference frames + the locked art-direction spec live in docs/wip/2026-07-16-launch-mockups/: photoreal-leaning 3D scene + restrained cyan/monospace broadcast-register HUD + a constant bottom timeline scrubber + on-vehicle force vectors. Broadcast-grade is the bar (PRD-032 north star).
S1frame + clock + engine skeletonScene 0 renders a pad; the multi-scale clock scrubs; a single hard-coded profile integrates and the vehicle rises. No camera/HUD polish.
S2ascent-physics.ts + validation harnessFull EOM (thrust/gravity/drag/staging/losses) + CI-validated milestones for one flagship (Falcon 9).
S3LaunchProfile schema + loader + 2–3 flagshipsFalcon 9 + Saturn V + one non-US flagship; schema + fail-closed validation.
S4camera montage (reuse #371)pad → tower-clear → tracking → onboard → staging → MECO shot sequence, cut to the S0-locked direction.
S5telemetry HUD + Science-Lens ascent layerslive TWR/q/losses + force vectors + per-beat captions & ScienceChips.
S6scene handoffinjection → warp into helio (interplanetary) and into cislunar (Moon missions); one continuous timeline end-to-end.
S7flagship tier complete + generic fallbackremaining hero vehicles; generic 2-stage model for every other launcher.
S8launch-site panorama (candidate)per-flagship explorable zoomable pad scene via the surface-scene renderer (RFC-017/ADR-072) — LC-39A / SLC-40 / Baikonur / Sriharikota / Kourou / Wenchang. Deferrable behind a stylised pad if it stretches the epic.
S9i18n + a11y + polish14-locale strings, reduced-motion, keyboard/D-pad, mobile (applies across S1–S10).
S10/plan take-off planningExtend the mission configurator to plan the launch, not just the transfer — reuse ascent-physics.ts summary solvers to surface launch-vehicle → Δv-to-orbit, payload-to-orbit, TWR margin, and ascent losses as part of the mission's Δv budget. Gated on the engine being cleanly reusable (L-I); no new physics, only a new consumer of the S2 core.
S11per-vehicle 3D modelsAccurate GLB per flagship rocket via the fleet model pipeline (RFC-016 / ADR-052) — real silhouette, stage divisions, engines, fairing, livery — replacing the procedural placeholder, so a launch shows the actual vehicle (the spacecraft-fleet treatment applied to launchers). Data-authoring effort, sequenced after the physics + render + wiring are solid.

9 · Deferred (explicit non-goals for this epic)

  • Interactive fly-it-yourself mode — throttle/pitch control on the same EOM core (a later epic; L-G keeps the engine control-input-ready).
  • Landing / booster recovery act — RTLS / droneship return.
  • 3-DOF azimuth steering / doglegs (L-H) — add only if a flagship visibly needs it.
  • Abort / failure modes — pad abort, MaxQ abort, engine-out; rich but out of v1 scope.

Forward-compatibility (the sibling epic). This epic is the ascent bookend; the planned sibling is descent & landing (Moon/Mars EDL → the existing surface scenes) + orbital rendezvous / docking (Earth & Moon orbit insertions, separations, joinings — ISS/Tiangong/Apollo/Gateway). The full arc is pad → orbit → cruise → arrival → descent → surface. Three architecture choices here are made to extend downward, not just ship ascent: (a) the integrated EOM core (§5) is a general powered-flight integrator, not ascent-specific — a descent is the same equations with the thrust vector reversed and a target of zero velocity at the surface; (b) the multi-scale clock (§4) already models a seconds-dense window inside a days-dense timeline — landings are another seconds-dense window; (c) the re-basing scene handoff (§3) is the general mechanism for stitching an Earth/Moon/Mars-relative frame into the cruise frame — descent is the inverse handoff (cruise → surface-relative). Building these general now avoids a rewrite when the sibling epic lands.

9.1 · Concrete extension seams (shipped by the launch-consolidation epic, 2026-07-18)

The launch-consolidation work turned the three "extend-downward" intentions above into named, tested contracts. A future descent/arrival epic implements against these seams — it does not re-architect them. The seams (in scope now); the descent feature is out of scope (build the seams + docs, not the feature).

  1. The phase-scene contract — src/lib/three/flight-phase-scene.ts. interface FlightPhaseScene<TState> is the structural contract every /fly flight-phase render obeys: scene / camera / setState(s) / setAspect / setForceVisible(force, on) / setForcesVisible / snapCamera / reset / dispose, plus type ForceKey = 'thrust'|'weight'|'drag'|'velocity'. AscentScene implements it today; the broadcast composer (ascent-renderer.ts, createAscentRenderer) consumes only the render slice (scene/camera/setAspect), so it drives any FlightPhaseScene unchanged. A future DescentScene implements the same interface and is rendered by the same composer — no renderer change.
  2. The headless physics core — src/lib/orbital/ascent-physics.ts. integrateAscent(profile) → AscentSummary is a pure, browser-free powered-flight integrator (fully unit-tested; excluded from nothing). A future descent-physics.ts is the same EOM with the thrust vector reversed and a zero-velocity-at-surface target — it produces the same AscentState-shaped samples the scene already consumes.
  3. The clock Phase union — src/lib/orbital/ascent-clock.ts. type Phase = 'ascent' | 'cruise' today. Widen to 'ascent' | 'cruise' | 'arrival' | 'descent'; makeTimeline / scrubberToPoint / advanceClock / defaultRegimeFor already localise every phase-specific branch, so widening is additive (add the new arms; the exhaustiveness of the Phase-switch return types flags every site that must handle the new members).

Named future files (do not exist yet — this is the map, not a promise): src/lib/orbital/descent-physics.ts (the reversed-thrust integrator), src/lib/three/descent-scene.ts (DescentScene implements FlightPhaseScene<DescentState>), src/lib/components/DescentScene.svelte (clock-driven exactly as LaunchScenet/playing/speed fed by the master clock, showDescent = pt.phase === 'descent'). The descent hands the arrival frame back to the existing surface scenes (SurfaceScene), the inverse of the ascent → cruise re-basing warp.

10 · Open questions

  1. Timeline seam feel — does the seconds→days regime switch at injection want a hard cut, an eased ramp, or a "warp" pause? Prototype in S5.
  2. Vehicle model sourcing — procedural stylised bodies vs. per-flagship GLBs; how far the RFC-016 fleet-model pipeline covers launchers vs. spacecraft.
  3. Atmosphere/ρ model — US Standard 1976 table is ample for Earth; is any non-Earth launch (none today) ever in scope? Assume Earth-only.
  4. Where flagship numbers come from — per-vehicle users' guides / Spaceflight101 / agency press kits; provenance gate as with all Orrery data.

11 · Stitch into /fly — event vocabulary · Science Lens · transition

The launch act is not a separate feature bolted onto /fly; it's the opening of the same mission on the same clock. Three contracts make it continuous.

11.1 · One event vocabulary

/fly's mission schema already carries a closed FlightEventType union (fly-event-labels.ts). The launch beats reuse what exists and add only the genuinely-new ones:

  • launch — reuse verbatim; the ascent act is this event, expanded from a zero-duration dot into ~8 minutes.
  • separation — reuse verbatim for stage separation (do NOT introduce a parallel staging).
  • parking_orbit_exit / tli_or_tmi — reuse; these ARE the launch→flight handoff (the injection burn).
  • ⚠️ ascent already means lunar-surface ascent (Apollo LM). The launch act must NOT reuse it.
  • Add: max_q, meco, fairing_jettison, seco (a.k.a. orbit_insertion). The engine's AscentEvent maps 1:1 onto the widened union.

Result: pad beats → cruise events → arrival are one FlightEvent stream on one MET clock; the seconds-vs-days unit gap is bridged by the multi-scale clock (§4). Widening the union touches the schema + defaultEventLabel + 14-locale message keys (same pattern as any event addition).

11.2 · Science Lens layers (the forces free-body diagram)

The lens is a registry (science-layers.ts: LayerKey + LAYER_ORDER + LAYER_DEFAULTS + metaFor() + overlay builders keyed on userData.layerKey). Launch reuses gravity (weight vector), velocity, and atmosphere (density/q shell), and adds launch-phase layers, gated the way coast/conics are "/fly only":

LayerNew/reuseDraws in Scene 0
gravity · velocity · atmosphere♻️ reuseweight arrow · velocity arrow · density shell
thrust➕ newthrust vector up the body axis
drag➕ newdrag vector opposing velocity + Max-Q emphasis
ascent-losses➕ newrunning gravity/drag/steering Δv ledger (panel)

Each new layer adds a LAYER_ORDER + LAYER_DEFAULTS + metaFor() entry (the exhaustiveness tests enforce completeness) and a ?name= ScienceChip cross-link to new /science articles: thrust-to-weight, gravity-turn, max-q, tsiolkovsky / rocket-equation, staging & mass-ratio. The teachable core is the thrust-vs-weight-vs-drag free-body diagram on the vehicle.

Status (2026-07-18, launch-consolidation epic).Shipped: the thrust + drag layers (science-layers.ts, 31 layers), weight via the reused gravity layer and velocity via velocity — wired to the scene's force arrows through the pure LAUNCH_FORCE_LAYERS map (src/lib/orbital/launch-force-layers.ts) which LaunchScene subscribes to via onLayerChange and drives setForceVisible(force, on) per FlightPhaseScene. The lens panel is the force legend (no separate legend). ⏳ Pending: the ascent-losses Δv-ledger layer + panel, the atmosphere density-shell reuse, and the five /science articles + ScienceChip cross-links (that content runs through the /science translate toolchain — largest remaining piece).

11.3 · The transition (launch → flight)

  • Where (the seam): the injection / parking_orbit_exit event — the vehicle established in a parking orbit / on its escape trajectory (~T+9 min, the engine's seco/orbit beat). This is exactly where /fly begins today (its MET-0 ≡ post-injection). It is already modelled as the ascent-clock seam where phase flips 'ascent' → 'cruise'; that flip is the transition trigger.
  • How (the mechanism): reuse the ADR-058 scene-handoff (helio→cislunar-at-SOI) plus a view mode param. Scene 0 plays pad → SECO/parking orbit; at the seam the camera does the re-basing warp (§3) — zoom out until Earth collapses to the AU-scale point — and cross-fades into the next scene: view=ascentview=helio (interplanetary) or view=cislunar (Moon). The heliocentric scene picks up at its existing MET-0 anchor, unchanged, now merely preceded by the ascent act.
  • Edge case: a LEO-only mission has no interplanetary cruise, so it hands to an Earth-orbit view or simply ends at orbit. The Orrery corpus is mostly lunar/interplanetary, where the helio/cislunar handoff applies.

12 · Descent & Landing — the closing bookend (§9 · Track D, shipped 2026-07-18)

The flight ended at destination orbit. For the 37 Moon / Mars / Venus landers it now continues through Entry, Descent & Landing to the surface, closing the arc pad → orbit → cruise → arrival → **descent → surface**. This is the inverse of the launch act, built on the three "extend-downward" seams (§9): the FlightPhaseScene contract, the multi-scale clock, and the re-basing scene handoff — implemented against them, not re-architecting them.

1:1 mirror of the ascent stack (the ascent counterpart → the descent equivalent):

AscentDescent
ascent-physics.ts · integrateAscentdescent-physics.ts · integrateDescent
ascent-physics-constants.tsdescent-physics-constants.ts (per-body μ / R / atmosphere)
launch-profile-registry.ts + static/data/launch-profiles/descent-profile-registry.ts + static/data/descent-profiles/
launcher-models.ts · LauncherModeldescent-models.ts · DescentModel
ascent-scene.tsdescent-scene.ts
ascent-renderer.tsreused unchanged (drives any FlightPhaseScene)
LaunchScene.svelteDescentScene.svelte
ascent-clock Phase='ascent'|'cruise'widened → +'descent' (the scrubber TAIL)
/fly showLaunch/launchTshowDescent/descentT
  • Physics (integrated EOM + EDL phases). integrateDescent(profile) is a 1-DOF along-track integrator: per-body gravity + exponential drag + a per-phase flight-path angle. Aerodynamic phases (entry / parachute / aeroshell / coast / airbag) are force-integrated; powered phases (powered_retro / skycrane) are throttle-guided — the descent rate tracks gain·altitude down to a soft terminal velocity, and the retro thrust to fly that schedule is back-solved for the fuel gauge + felt-g. Honest telemetry (altitude, velocity, deceleration-g, Mach, dynamic pressure) without a 3-DOF trajectory the scene never renders. flightPathAngleDeg is the effective descent (collapse) angle, tuned so the speed-bleed + timeline match the published EDL (Mars ~7–8° → Mach ~2 at chute deploy, ~6-min entry; Venus steep ~65° → ~160 g peak, ~50-min descent; Moon models the terminal powered descent). Per-body atmospheres: Moon vacuum (no drag), Mars ~0.02 kg·m⁻³ / 11 km scale height, Venus ~65 kg·m⁻³ / 15.9 km.
  • Registry + 6 EDL archetypes. Thin per-mission JSON (<missionId>.json) carries the numbers; six embedded TS archetype templates supply the phase sequence: LUNAR_POWERED (Apollo / Luna / Chang'e / Chandrayaan / SLIM / Beresheet / Artemis / Blue Moon), LUNA_DIRECT_IMPACT (Luna 9), MARS_PARACHUTE_RETRO (Viking / Phoenix / InSight / Mars-3 / Schiaparelli / Zhurong), MARS_AIRBAG (Pathfinder / Spirit / Opportunity), MARS_SKYCRANE (Curiosity / Perseverance), VENUS_AEROSHELL (Venera-13 / Vega-1 / Vega-2). hasDescentProfile(missionId) gates entry — orbiters never descend. Crash reconstructions (Beresheet / Schiaparelli / Mars 3) model a propellant cutout → honest hard impact (touchdownSuccess:false, HUD reads "IMPACT").
  • Model + scene. buildDescentModel(siteId, body, vehLen) composes the EDL stack (aeroshell = heat-shield + backshell, parachute, skycrane / retro / airbag descent stage) wrapped around the mission's real landed model (buildMoon/MarsLanderModel / buildVenera13) as the terminal part — so a descent and its /moon /mars surface view show the same craft. descent-scene.ts inverts ascent-scene.ts: the destination body (textured) grows below the descending lander; a per-body sky (airless black / Martian salmon / Venusian amber); EDL separations (heat-shield jettison, chute deploy + cut, backshell sep, skycrane lower, airbag inflate) are scrub-safe pure functions of the event METs; a retro plume; a smooth per-phase camera.
  • Clock + scrubber. ascent-clock Phase widened to 'ascent'|'cruise'|'descent' with descentDurationS / descentScrubberFraction (default 0 ⇒ orbiters keep the byte-identical 2-segment bar). The descent owns the tail [1−fD, 1]; E+MM:SS readout; DESCENT_SPEED_MULTIPLIERS = [1,3,10]. In /fly, at arrival a landing mission enters showDescent instead of looping the cruise; ?descent=1 deep-links straight to the EDL act.
  • Surface handoff. At touchdown DescentScene flashes and fires onComplete/fly navigates to /${body}?site=${siteId}&from=descent. SurfaceScene already reads ?site= and rotates the body so the landing site faces the camera — the circle closes ON the site.
  • Science Lens. Reuses the four generic lens layers (thrust / gravity / drag / velocity) via descent-force-layers.ts — no new registration. Pedagogically rich: under the parachute drag dominates; in the skycrane thrust ≈ weight; in the airbag bounce only weight remains.

Source-of-truth decision (supersedes the planned mission-JSON backfill). The descent-profile registry is the single source of truth for EDL data (entry state, system, duration). The /fly descent dossier reads entry velocity + EDL system from the profile, NOT from a duplicated flight.arrival.edl_system / entry_velocity_km_s in the 37 mission JSONs — duplicating would risk divergence. The FlightArrival schema is left unchanged; a future external consumer can derive EDL facts from the profile registry.

Status (2026-07-18).Shipped: descent-physics (+ constants + tests), descent-profile-registry (+ 6 archetypes), 37 descent-profile JSONs, descent-models, descent-scene, DescentScene.svelte, the 3-segment clock, the /fly wiring + ?descent=1 deep-link, the surface handoff, and the descent force-lens mapping. Verified end-to-end in-browser (Apollo 11 Moon → /moon; Curiosity Mars EDL). ⏳ Pending / deferred: asteroid / comet / Jupiter-probe / Titan landers (out of scope this epic); a dedicated /venus surface route (Venus touchdown currently rests on the landed frame, no surface handoff); /science EDL articles.

12.1 · Phase 2 — outer + small-body EDL (shipped 2026-07-18)

Phase 1 (above) covers the 37 Moon / Mars / Venus landers. Phase 2 extends the same engine to four body regimes that Phase 1 left out, each requiring distinct constants and archetypes: micro-g airless small bodies, comet nuclei, Titan's thick cold N₂ atmosphere, and Jupiter's gas column with no solid surface.

New body constants. descent-physics-constants.ts gains real μ / R / atmospheric parameters for each regime (Itokawa, Ryugu, Bennu, Eros, comet 67P, Titan, Jupiter). bodyAirDensity no longer clamps at the body's 1-bar datum — Jupiter probes sink well below it into denser gas, so the integrator follows the real exponential column until the end-trigger fires.

Four new archetypes added to descent-profile-registry.ts:

ArchetypeRegimeSequence
ASTEROID_TOUCH_AND_GOmicro-g airlessguided micro-g descent → sample contact (1–5 s) → departure burn; no landing — terminal state SAMPLE COLLECTED
COMET_HARPOONmicro-g airlessunpowered coast → harpoon deploy → bounce / settle sequence; terminal state SETTLED
TITAN_PARACHUTEthick cold N₂aeroshell → drogue → stabiliser chute; ~2.5 h descent; terminal state TOUCHDOWN
JUPITER_PROBEgas column / no surfacehypersonic entry → chute → passive pressure descent; ends when ambient pressure exceeds pressure_pa end-trigger (probe_signal_lost at ~22 bar); terminal state SIGNAL LOST

Jupiter-probe physics. A new pressure_pa end-trigger fires when bodyAmbientPressurePa(altitude, body) crosses the profile's crush limit — the probe does not "land", it is overwhelmed by increasing pressure. The SIGNAL LOST terminal state is honest: the final screen reads "PROBE SIGNAL LOST", matching the Galileo mission record. Integration accumulates to ~230 g peak deceleration, matching the published ~228 g measurement. No surface handoff is attempted.

Terminal states by regime — none of the Phase 2 endings hand off to a surface route (they rest on the landed frame as Venus does):

RegimeTerminal stateHUD reads
Asteroid touch-and-goSAMPLE COLLECTED"SAMPLE COLLECTED — DEPARTURE"
Comet harpoonSETTLED"SETTLED ON SURFACE"
Titan parachuteTOUCHDOWN"TOUCHDOWN"
Jupiter probeSIGNAL LOST"PROBE SIGNAL LOST"

Shipped missions (5 of 7, all browser-verified): hayabusa1 (Itokawa), hayabusa2 (Ryugu), osiris-rex (Bennu), rosetta / Philae (67P), galileo (Jupiter). Not yet shipped (2 of 7): Cassini / Huygens (Titan) — Titan is not currently a flyable /fly destination and must be added as one first. NEAR Shoemaker (Eros) — no Eros mission data in the repo at all. Both gaps tracked in GH #418; plan in docs/wip/2026-07-18-descent-phase2-outer-bodies.md.

Reference: archetype implementations in src/lib/orbital/descent-profile-registry.ts; body constants + bodyAmbientPressurePa / bodyAirDensity in src/lib/orbital/descent-physics-constants.ts; WebGL builders in src/lib/three/descent-models.ts; per-mission profile JSONs in static/data/descent-profiles/.

13 · Tier-1 Earth-orbit re-entry — closing the return bookend (2026-07-19)

The launch + planetary-descent acts model craft that go somewhere else. The ~31 Earth-orbit crewed capsules (Mercury / Gemini / Vostok / Voskhod / Apollo-CM / Skylab / Soyuz / Dragon / Shenzhou) were flight:false catalog stubs. This makes them fully flyable pad → orbit-coast → re-entry → recovery, on the same unified /fly timeline, and is the MVP template for later tiers (lunar-return, sample-return SRCs, X-37B runway).

Earth as a descent body. DescentBody gains 'earth' (μ/R/ρ₀/scale-height/ sound-speed in descent-physics-constants.ts, a blue-limb re-entry sky in descent-scene.ts) and one new archetype EARTH_CAPSULE_REENTRY — ballistic entry → drogue → main canopies → splashdown/ground. Orbital and suborbital differ only in the JSON entry state, not the EDL hardware, so one archetype covers both. A fouled main (soyuz-1) is a low-Cd·A profile that busts the survivable limit → honest touchdownSuccess:false.

The orbit-coast act. New fly-leo-coast-scene.ts (Earth-centred, km scene units — the cislunar 1/10000 scale renders LEO sub-pixel) + CoastScene.svelte: the capsule loops its orbit ring above a rotating Earth with a building ground-track. Hybrid loop rule: render min(realRevs, LOOP_CAP=3) loops — 1/2/3-orbit missions render exactly that many; marathons (Gemini-7 = 206, Skylab-4 = 1214) render ~3 representative loops while the REV n/N + MET/date counters carry the real scale.

Unified master scrubber. The coast is the "cruise" act of /fly's single pad→orbit→re-entry timeline. CoastScene is externalClock-driven; the one scrubber floats over every overlay and drags continuously through all three acts. masterTimeline uses the coast duration as its cruise; onMasterScrub routes the cruise band to the coast for Earth-orbit missions; the rAF advances coastMetDays and crosses the deorbit seam into re-entry. The re-entry ends on a recovery card (splashdown / ground / honest IMPACT) — Earth has no surface-explore route.

Data + reference. Coast descriptors: src/lib/orbital/earth-orbit-registry.ts. Capsule models (8 families): src/lib/three/capsule-models.ts. Per-mission profiles: static/data/descent-profiles/*.json (FPA=4 + mass-scaled chute Cd·A). Launchers added (E7): mercury-redstone, voskhod-11a57, soyuz, long-march-2f. Handover + open follow-ups (descent-playback time-compression, in-scene re-entry HUD, science-lens): docs/wip/2026-07-19-tier1-earth-orbit-reentry.md.

Orrery — architecture documentation · MIT · No tracking