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
PhysicsStateholding ship restitution, ship friction, ship CCD flag, terrain restitution, terrain friction. - Local React state
debugof typeRapierDebugOptionswithshowColliders,showContacts,showVelocitybooleans. - Module-private
DEFAULTSconstant defining baseline collider material values (shipRestitution: 0.09,shipFriction: 0.01,shipCcd: true,terrainRestitution: 0.09,terrainFriction: 0.01). - Local
ToggleRowpresentational component for binary on/off buttons. - The
updatecallback that mutates local state and live-applies it to the Rapier facades. - The
toggleDebugcallback that updates debug options and enables/disables the debug renderer based on whether any overlay flag is on.
READS FROM
./PlaygroundSharedforStatRow,SECTION_HEADER_STYLE,LABEL_STYLE, and theTabPropstype (including theside: PanelSidediscriminator).../../engine/physics/rapier-ship(RapierShip) — invoked for setters only; no read access exposed here.../../engine/physics/rapier-terrain(RapierTerrain) — callsgetTerrainCount()andgetFloaterCount()for the status block; calls setters inupdate.../../engine/physics/rapier-debug-render(RapierDebugplus theRapierDebugOptionstype) — read for setting the local debug state shape; not read for current options.../../engine/physics/rapier-world(RapierWorld) — callsisReady()to gate the status block andgetLastStepCount()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 byshowColliders || showContacts || showVelocityso 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,RapierDebugfacades. - Does not create, destroy, or query individual collider handles.
- Does not persist tuning values across mounts, sessions, or reloads — all state resets to
DEFAULTSwhenever 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
TabPropsfields beyondside. - Does not render any UI for the
baseside outside parameter sliders, nor for theinstanceside 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
StatRow→update(key, DEFAULTS[key]), same pathway as a manual change. - Debug toggle click (
Colliders/Contacts/Velocity) →toggleDebug(key)→ flips the corresponding flag, callsRapierDebug.setOptions(next)andRapierDebug.setEnabled(any). - Re-render of the
instanceside readsRapierWorld.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 thePlaygroundTabunion (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 withside === 'base'); the two instances hold independent React state.
Pattern notes
- Split-panel tab pattern: a single tab component branches on
sideto 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
updatemirrors immediately to a Rapier facade setter via aswitchon 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 fromDEFAULTS. - Debug-renderer enable rule:
RapierDebug.setEnabledis 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:
updateacceptsnumber | booleanand casts within eachswitcharm 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). ToggleRowstyling is hardcoded inline (cyan#00e5ffON state, translucent OFF state) and is intentionally local rather than promoted toPlaygroundShared— it exists only becauseStatRowcovers numeric sliders, not boolean toggles.