PURPOSE

Tab component inside the Ship Playground UI for live Rapier2D physics tuning. Lets the developer adjust ship and terrain collider material parameters (restitution, friction), toggle continuous collision detection (CCD) on the ship, and turn on Rapier debug overlays (colliders, contacts, velocity vectors). Splits across a two-panel playground layout: the instance side renders debug toggles and a Rapier status readout; the base side renders the parameter sliders for ship and terrain colliders.

OWNS

  • Local React state PhysicsState holding ship restitution, ship friction, ship CCD flag, terrain restitution, terrain friction.
  • Local React state debug of type RapierDebugOptions with showColliders, showContacts, showVelocity booleans.
  • Module-private DEFAULTS constant defining baseline collider material values (shipRestitution: 0.09, shipFriction: 0.01, shipCcd: true, terrainRestitution: 0.09, terrainFriction: 0.01).
  • Local ToggleRow presentational component for binary on/off buttons.
  • The update callback that mutates local state and live-applies it to the Rapier facades.
  • The toggleDebug callback that updates debug options and enables/disables the debug renderer based on whether any overlay flag is on.

READS FROM

  • ./PlaygroundShared for StatRow, SECTION_HEADER_STYLE, LABEL_STYLE, and the TabProps type (including the side: PanelSide discriminator).
  • ../../engine/physics/rapier-ship (RapierShip) — invoked for setters only; no read access exposed here.
  • ../../engine/physics/rapier-terrain (RapierTerrain) — calls getTerrainCount() and getFloaterCount() for the status block; calls setters in update.
  • ../../engine/physics/rapier-debug-render (RapierDebug plus the RapierDebugOptions type) — read for setting the local debug state shape; not read for current options.
  • ../../engine/physics/rapier-world (RapierWorld) — calls isReady() to gate the status block and getLastStepCount() to display steps per frame.

PUSHES TO

  • RapierShip.setRestitution(number) when the ship restitution slider changes.
  • RapierShip.setFriction(number) when the ship friction slider changes.
  • RapierShip.setCcdEnabled(boolean) when the CCD toggle flips.
  • RapierTerrain.setAllRestitution(number) when the terrain restitution slider changes. Applies to every terrain collider in the world.
  • RapierTerrain.setAllFriction(number) when the terrain friction slider changes. Applies to every terrain collider in the world.
  • RapierDebug.setOptions(RapierDebugOptions) whenever any debug toggle flips.
  • RapierDebug.setEnabled(boolean) driven by showColliders || showContacts || showVelocity so the debug renderer is off when no overlays are selected.

DOES NOT

  • Does not own or step the Rapier simulation; mutation only flows through RapierShip, RapierTerrain, RapierDebug facades.
  • Does not create, destroy, or query individual collider handles.
  • Does not persist tuning values across mounts, sessions, or reloads — all state resets to DEFAULTS whenever the component remounts.
  • Does not write to Zustand stores, telemetry sinks, Supabase, or anywhere outside the Rapier facades.
  • Does not gate writes behind RapierWorld.isReady(); setters are called regardless of init state, so the underlying facades must tolerate pre-init calls.
  • Does not read live values back from the Rapier facades to populate the UI — the sliders reflect local React state only and can drift from world truth if anything else mutates the colliders.
  • Does not handle restart, build mode, or any other TabProps fields beyond side.
  • Does not render any UI for the base side outside parameter sliders, nor for the instance side outside debug toggles and status.

Signals

  • Slider change on ship restitution → update('shipRestitution', v) → state set + RapierShip.setRestitution(v).
  • Slider change on ship friction → update('shipFriction', v) → state set + RapierShip.setFriction(v).
  • CCD toggle click → update('shipCcd', !state.shipCcd) → state set + RapierShip.setCcdEnabled(v).
  • Slider change on terrain restitution → update('terrainRestitution', v) → state set + RapierTerrain.setAllRestitution(v).
  • Slider change on terrain friction → update('terrainFriction', v) → state set + RapierTerrain.setAllFriction(v).
  • Reset button on any StatRowupdate(key, DEFAULTS[key]), same pathway as a manual change.
  • Debug toggle click (Colliders / Contacts / Velocity) → toggleDebug(key) → flips the corresponding flag, calls RapierDebug.setOptions(next) and RapierDebug.setEnabled(any).
  • Re-render of the instance side reads RapierWorld.isReady(), RapierWorld.getLastStepCount(), RapierTerrain.getTerrainCount(), RapierTerrain.getFloaterCount() synchronously each frame React re-renders the panel.

Entry points

  • Default export shape: named export PhysicsTab({ side }: TabProps). Imported and mounted by the Ship Playground tab host as one of the entries in the PlaygroundTab union (the 'physics' discriminant).
  • Internal helper ToggleRow({ label, on, onClick }) — file-private; not exported.
  • The component is rendered twice by the playground shell for split-panel tabs (once with side === 'instance', once with side === 'base'); the two instances hold independent React state.

Pattern notes

  • Split-panel tab pattern: a single tab component branches on side to render two distinct UIs in the playground’s left/right panels. Because state is local, the parameter side and the debug side do not share React state; the debug toggles in one panel cannot read or write the slider values in the other and vice versa.
  • Live-tuning pattern: every state mutation in update mirrors immediately to a Rapier facade setter via a switch on the property key. There is no debounce, no batching, and no useEffect-based sync — the side-effect runs inside the state setter callback path.
  • Initial-state pattern: useState<PhysicsState>({ ...DEFAULTS }) does not call the facade setters on mount, so live Rapier values are whatever the engine initialized them to until the user moves a slider. This means the displayed slider value can be a lie on first render if the engine’s defaults differ from DEFAULTS.
  • Debug-renderer enable rule: RapierDebug.setEnabled is driven by the OR of all three overlay flags rather than tracked separately. Turning the last overlay off automatically disables the renderer; turning the first overlay on automatically enables it.
  • Type-cast pattern: update accepts number | boolean and casts within each switch arm rather than overloading or splitting into typed setters — a deliberate concession to keep the call sites and the JSX uniform.
  • Facade boundary: this component is the consumer side of the Rapier facade contract. All Rapier-specific knowledge stays in engine/physics/rapier-*; the tab knows only the public surface (typed setters, getTerrainCount, getFloaterCount, isReady, getLastStepCount, setOptions, setEnabled).
  • ToggleRow styling is hardcoded inline (cyan #00e5ff ON state, translucent OFF state) and is intentionally local rather than promoted to PlaygroundShared — it exists only because StatRow covers numeric sliders, not boolean toggles.