Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,56 @@ multi-screen sims, see [`doc/multi-screen.md`](doc/multi-screen.md).
| File | Purpose |
|---|---|
| `src/RigidBodyPrecessionColors.ts` | All `ProfileColorProperty` instances |
| `src/SimConstants.ts` | Named numeric constants (layout px, physics SI units) |
| `src/RigidBodyPrecessionConstants.ts` | Named numeric constants (layout px, physics SI units) |
| `src/RigidBodyPrecessionNamespace.ts` | Namespace for color property names |
| `src/i18n/StringManager.ts` | Singleton localized string accessor |
| `src/precession-screen/RigidBodyPrecessionScreen.ts` | Screen wrapper |
| `src/precession-screen/model/RigidBodyPrecessionModel.ts` | Simulation state and logic |
| `src/precession-screen/view/RigidBodyPrecessionScreenView.ts` | Visual nodes, layout, `screenSummaryContent` + `pdomOrder` |
| `src/precession-screen/view/RigidBodyPrecessionScreenSummaryContent.ts` | Accessible screen summary (reference a11y pattern) |
| `src/precession-screen/view/RigidBodyPrecessionKeyboardHelpContent.ts` | Keyboard-help dialog content |
| `src/steady-precession-screen/` | Screen 1 — idealized Ω = τ/(Iω) gyroscope |
| `src/nutation-screen/` | Screen 2 — heavy symmetric top integrated from its full Lagrangian |
| `src/torque-free-screen/` | Screen 3 — Euler/Poinsot tumbling (placeholder) |
| `src/common/rigid-body/SteadyPrecessionPhysics.ts` | Screen 1's closed-form relations |
| `src/common/rigid-body/HeavySymmetricTopPhysics.ts` | Screen 2's RK4 integrator, invariants, turning points |
| `src/common/rigid-body/TopTipTrace.ts` | Ring buffer of (t, θ, φ) samples behind the tip path and θ(t) graph |
| `src/common/view/TopProjection.ts` | Euler angles → oblique 2-D projection (axis, wheel rim, tilt circles) |
| `src/common/view/PlayAreaPanel.ts` | Titled panel wrapper shared by every screen's play area |
| `src/common/SimPanel.ts` | Pre-themed `Panel` wrapper (uses `RigidBodyPrecessionColors` automatically) |
| `src/common/SimButtonOptions.ts` | Flat button-appearance option bundles + light-control-surface combo-box options |
| `src/common/TimeModel.ts` | Composable play/pause + elapsed-time model for animated sims |
| `scripts/generate-icons.ts` | PNG icons from `public/icons/icon.svg` |
| `scripts/rename-sim.ts` | Automated fork/rename across all files and folders |

## Physics

### Screen 1 — steady precession

Closed form only: Ω = τ / (I ω), with the tilt held fixed. Deliberately idealized so the
gyroscopic relation stands alone.

### Screen 2 — nutation (`HeavySymmetricTopPhysics.ts`)

The full heavy-symmetric-top Lagrangian in Euler angles (θ, φ, ψ), integrated with RK4 at
a fixed 0.5 ms internal substep. θ is a dynamical variable, so releasing the top produces
real nutation. Key entry points:

- `stepHeavyTop` — the integrator; substeps `dt`, wraps ψ, applies the tilt limits
- `createReleaseState` — the four release modes, which differ only in φ̇(0): `cusp` (0),
`loop` (−Ω_slow), `smooth` (½Ω_slow), `steady` (Ω_slow)
- `nutationTurningPoints` — bisects the turning-point cubic u̇² = f(cos θ) for the band
the axis is confined to; drawn as the two dashed circles and the graph's reference lines
- `steadyPrecessionRates` / `criticalSpinRate` — roots of I₁cos θ Ω² − I₃ω₃Ω + Mgl = 0,
and the spin below which no steady precession exists
- `totalEnergy`, `verticalAngularMomentum`, `spinAngularMomentum` — invariants, exact
without friction; the test suite asserts they hold to 6+ decimal places over 10 s

Friction is a phenomenological viscous model (`tipDrag` on the center of mass, `spinDrag`
on the spin), off by default. `maxTilt` is an inelastic mechanical stop where the axle
rests against its mount — the nutation screen sets it to 90°.

Apparatus constants (`NUTATION_*` in `RigidBodyPrecessionConstants.ts`) are a
demonstration gyroscope wheel. Note that ω_nut · Ω_slow = M g l / I₁ is fixed by the
apparatus alone: a hand-sized top nutates at several hertz no matter how it is spun, so
the wheel is deliberately large and short-armed to bring both timescales on screen at
once (≈1.6 Hz nutation, ≈3 s per precession revolution), with a slow-motion option.

## Common components

### SimPanel
Expand Down Expand Up @@ -110,6 +146,8 @@ Fleet-standard Vitest layout (keep when forking):
| `vitest.config.ts` | `happy-dom` environment; `setupFiles: ["./tests/setup.ts"]`; `execArgv: ["--expose-gc"]` |
| `tests/setup.ts` | Canvas / AudioContext mocks + `init({ name: "…" })` before SceneryStack imports |
| `tests/TimeModel.test.ts` | Sample model unit tests — replace with real physics tests |
| `tests/HeavySymmetricTopPhysics.test.ts` | Screen 2 integrator vs. analytic results (invariants, turning points, fast-top limits) |
| `tests/NutationModel.test.ts` | Screen 2 model wiring: release, re-release, trace bounds, friction |
| `tests/memory-leak.test.ts` | WeakRef + `forceGC` dispose regression (fleet pattern) |
| `tests/fuzz/fuzz.spec.ts` | Optional Playwright fuzz smoke via joist `?fuzz` |
| `playwright.config.ts` | Chromium project + Vite webServer for fuzz |
Expand Down
24 changes: 24 additions & 0 deletions src/RigidBodyPrecessionColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,30 @@ const RigidBodyPrecessionColors = {
projector: new Color(153, 153, 153, 0.35),
}),

/** Path traced by the axle tip on the nutation screen. */
tipTraceColorProperty: new ProfileColorProperty(RigidBodyPrecessionNamespace, "tipTrace", {
default: "#ffb74d",
projector: "#e65100",
}),

/** Turning-point circles bounding the nutation band. */
nutationBandColorProperty: new ProfileColorProperty(RigidBodyPrecessionNamespace, "nutationBand", {
default: "#ce93d8",
projector: "#6a1b9a",
}),

/** Face of the gyroscope wheel on the nutation screen (translucent so the trace shows through). */
wheelFillColorProperty: new ProfileColorProperty(RigidBodyPrecessionNamespace, "wheelFill", {
default: new Color(79, 195, 247, 0.55),
projector: new Color(2, 119, 189, 0.4),
}),

/** Readout color for a state that is unstable or out of range (spin below critical). */
warningColorProperty: new ProfileColorProperty(RigidBodyPrecessionNamespace, "warning", {
default: "#ff8a65",
projector: "#bf360c",
}),

/** Top-view inset card fill (graph-background hue @ 70%). */
sceneInsetCardColorProperty: new ProfileColorProperty(RigidBodyPrecessionNamespace, "sceneInsetCard", {
default: new Color(15, 26, 46, 0.7),
Expand Down
70 changes: 70 additions & 0 deletions src/RigidBodyPrecessionConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const STEADY_PRECESSION_PANEL_WIDTH = 300;
/** Height of the precession-angle graph. */
export const PRECESSION_GRAPH_HEIGHT = 160;

/** Width of the control panel on the nutation screen. */
export const NUTATION_PANEL_WIDTH = 300;

/** Height of the nutation-angle graph. */
export const NUTATION_GRAPH_HEIGHT = 130;

// ── Physics defaults (SI units) ───────────────────────────────────────────────

/** Gravitational acceleration (m/s²). */
Expand Down Expand Up @@ -52,12 +58,76 @@ export const DEFAULT_ARM_MASS_KG = 0.2;
/** Default pivot-to-mass distance (m). */
export const DEFAULT_PIVOT_TO_MASS_DISTANCE_M = 0.35;

// ── Nutation screen: heavy symmetric top (SI units) ───────────────────────────
//
// A pivoted gyroscope wheel, sized so both timescales are watchable. The product
// ω_nut · Ω_slow = M g l / I₁ is fixed by the apparatus alone, so a hand-sized top
// (which nutates at several hertz) cannot show both motions at once; this wheel is
// the classroom demonstration gyroscope, heavy and slow enough that nutation runs
// near 1.6 Hz while one precession revolution takes about 3 s.

/** Mass of the gyroscope wheel (kg). */
export const NUTATION_WHEEL_MASS_KG = 3.0;

/** Rim radius of the gyroscope wheel (m). */
export const NUTATION_WHEEL_RADIUS_M = 0.33;

/** Distance from pivot to the wheel's center of mass along the axle (m). */
export const NUTATION_COM_DISTANCE_M = 0.14;

/** I₃ — spin-axis moment of inertia of a thin rim, M R² (kg·m²). */
export const NUTATION_SPIN_INERTIA_KG_M2 = NUTATION_WHEEL_MASS_KG * NUTATION_WHEEL_RADIUS_M ** 2;

/** I₁ — transverse moment about the pivot: ½ M R² for the rim plus the parallel-axis M l² (kg·m²). */
export const NUTATION_TRANSVERSE_INERTIA_KG_M2 =
0.5 * NUTATION_WHEEL_MASS_KG * NUTATION_WHEEL_RADIUS_M ** 2 + NUTATION_WHEEL_MASS_KG * NUTATION_COM_DISTANCE_M ** 2;

/** Default spin about the symmetry axis (rad/s) — about 1.4× the critical spin at 45°. */
export const DEFAULT_NUTATION_SPIN_RAD_S = 7;

/** Default release tilt from the vertical (rad) — 45°. */
export const DEFAULT_NUTATION_TILT_RAD = Math.PI / 4;

/** Viscous drag on the center of mass when friction is enabled (N·m·s). */
export const NUTATION_TIP_DRAG_N_M_S = 0.055;

/** Spin friction at the pivot when friction is enabled (N·m·s). */
export const NUTATION_SPIN_DRAG_N_M_S = 0.008;

/** Tip-trace samples retained (8 s at 60 Hz), which also backs the θ(t) graph. */
export const NUTATION_TRACE_CAPACITY = 480;

/**
* Samples actually drawn as the tip path (about 4 s, a little over one precession
* revolution). Drawing the whole buffer overlays several revolutions and turns the
* cusps into a mesh.
*/
export const NUTATION_TRACE_DRAW_SAMPLES = 260;

/** Sample interval for the tip trace and θ(t) graph (s). */
export const NUTATION_SAMPLE_INTERVAL_S = 1 / 60;

/** Visible time window of the θ(t) graph (s). */
export const NUTATION_GRAPH_WINDOW_S = 8;

// ── Ranges ────────────────────────────────────────────────────────────────────

export const SPIN_RATE_RANGE = { min: 5 * 2 * Math.PI, max: 80 * 2 * Math.PI };
export const ARM_MASS_RANGE = { min: 0.05, max: 0.5 };
export const PIVOT_DISTANCE_RANGE = { min: 0.15, max: 0.5 };

/** Spin range for the nutation screen (rad/s); spans the critical spin at 45°. */
export const NUTATION_SPIN_RANGE = { min: 2, max: 20 };

/**
* Tilt at which the axle bottoms out on its mount (rad) — horizontal. A top that loses
* its spin falls only this far; past it the axle would swing through the support.
*/
export const NUTATION_MAX_TILT_RAD = Math.PI / 2;

/** Release-tilt range for the nutation screen (rad) — 15° to 80°. */
export const NUTATION_TILT_RANGE = { min: (15 * Math.PI) / 180, max: (80 * Math.PI) / 180 };

RigidBodyPrecessionNamespace.register("RigidBodyPrecessionConstants", {
SCREEN_VIEW_MARGIN,
PANEL_CORNER_RADIUS,
Expand Down
Loading
Loading