rapier-debug-render
Rapier2D debug visualization overlay. Draws collision shapes, contact points/normals, and ship velocity vectors directly onto the game canvas in world space. Toggled from the ship playground PhysicsTab.
Purpose
Visual diagnostics for the Rapier physics integration. Renders into the camera-transformed canvas so overlays align with world geometry. When disabled or when RapierWorld is not ready, render() short-circuits.
Dependencies
rapier-world— readiness check, world handle, step-count statrapier-ship— ship rigid body and collider for velocity arrow + contact iterationrapier-terrain— terrain/floater counts for stats stringrapier-enemies— enemy sensor count for stats string
Public API
Single exported object RapierDebug (singleton-style namespace).
| Member | Signature | Purpose |
|---|---|---|
setEnabled | (value: boolean) => void | Master on/off toggle |
isEnabled | () => boolean | Current enabled state |
setOptions | (opts: Partial<RapierDebugOptions>) => void | Patch which overlays render |
getOptions | () => RapierDebugOptions | Returns a shallow copy of current options |
render | (ctx, camX, camY, camScale) => void | Draws active overlays |
getStats | () => string | One-line HUD string |
RapierDebugOptions
interface RapierDebugOptions {
showColliders: boolean; // default true
showContacts: boolean; // default true
showVelocity: boolean; // default true
}Module-local state: enabled (default false) and options (initialized from defaultOptions).
Render Pipeline
render(ctx, camX, camY, camScale) calls ctx.save() then conditionally draws three layers, then ctx.restore().
Colliders (showColliders)
Uses Rapier’s built-in world.debugRender() which returns a buffer of vertex pairs (line segments) and a parallel colors array (rgba floats 0..1).
- Iterates in groups of 4 floats per segment (x1,y1,x2,y2).
- Pulls per-vertex color from
colors[i..i+2], multiplies by 255, buildsrgb(r,g,b). Falls back to#00ffff(cyan) if colors array is exhausted. - Line width:
1.5 / camScale(constant on-screen thickness regardless of zoom). - Each segment is offset by
-camX, -camYto convert world to screen coordinates.
Velocity Arrow (showVelocity)
Drawn only when the ship body exists and Math.hypot(vel.x, vel.y) > 1 (units/sec).
- White (
#ffffff) line from ship position topos + vel * 0.3(arrowScale = 0.3). - Line width
2 / camScale. - Two-leg arrowhead at the tip: each leg length
8 / camScale, splayed at ±0.3 rad from the velocity angle (Math.atan2(vel.y, vel.x)). - Speed label:
${speed.toFixed(0)} u/sin${10 / camScale}px monospace, offset (+10, −10) screen-pixels-equivalent from the ship.
Contact Points (showContacts)
Iterates contact pairs involving the ship collider.
world.contactPairsWith(RapierShip.getCollider()!, otherCollider => ...)walks each other collider in contact.- For each pair,
world.contactPair(shipCollider, otherCollider, (manifold, flipped) => ...)yields the manifold. - For every solver contact point (
manifold.numSolverContacts()/manifold.solverContactPoint(i)): draws a red (#ff0000) filled circle, radius3 / camScale. - Contact normal: uses
manifold.localNormal1()plus the first solver contact point; draws a yellow (#ffff00) line of length20 / camScalealong the normal, width1.5 / camScale.
The flipped manifold parameter is captured but not used.
Constants (extracted from code)
| Constant | Value | Used for |
|---|---|---|
| Collider line width | 1.5 / camScale | Shape outlines |
| Collider fallback color | #00ffff | When color array runs out |
| Velocity arrow scale | 0.3 | Multiplier on vel for arrow tip |
| Velocity min speed | 1 | Below this, no arrow drawn |
| Velocity line width | 2 / camScale | Arrow body |
| Velocity color | #ffffff | Arrow + label |
| Arrowhead length | 8 / camScale | Each splay leg |
| Arrowhead splay | ±0.3 rad | Angle off velocity vector |
| Speed label font | ${10/camScale}px monospace | Label rendering |
| Contact dot color | #ff0000 | Solver contact point |
| Contact dot radius | 3 / camScale | Per-point circle |
| Normal color | #ffff00 | Contact normal line |
| Normal length | 20 / camScale | Normal arrow |
| Normal line width | 1.5 / camScale | Normal line |
getStats()
Returns one of:
'Rapier: not initialized'ifRapierWorld.isReady()is false.'Rapier: <N> steps/frame | Terrain: <T> + <F> floaters | Enemies: <E> sensors'otherwise, joined with|.
Pulls from RapierWorld.getLastStepCount(), RapierTerrain.getTerrainCount(), RapierTerrain.getFloaterCount(), RapierEnemies.getCount().
Caller Contract
ctx is expected to already be camera-transformed by the caller — the renderer manually subtracts camX, camY for screen positioning while letting camScale drive on-screen-constant stroke widths and font sizes. This is consistent with the rest of the canvas debug overlays.
EXTRACT-CANDIDATE
- Arrowhead drawing (used here for velocity; could also be reused for contact normals, force vectors, drag/wind, or any directional-vector debug visualizer). Currently inline math with
±0.3 radandheadLenhardcoded. - Camera-transform-aware stroke/font scaling pattern (
size / camScalefor on-screen-constant rendering) is repeated 8 times in this file; a helper likescreenPx(n, camScale)could centralize it. - Color-array iteration for
debugRenderoutput (4 floats per segment, parallel colors array) is a Rapier-specific pattern that could move into a sharedrapierDebugBufferToSegments(buf)utility if other modules need raw shape data. - The
contactPairsWith/contactPairnested-callback iteration is a candidate for a flat generator/iterator wrapper if any other system needs to enumerate ship contacts.