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 stat
  • rapier-ship — ship rigid body and collider for velocity arrow + contact iteration
  • rapier-terrain — terrain/floater counts for stats string
  • rapier-enemies — enemy sensor count for stats string

Public API

Single exported object RapierDebug (singleton-style namespace).

MemberSignaturePurpose
setEnabled(value: boolean) => voidMaster on/off toggle
isEnabled() => booleanCurrent enabled state
setOptions(opts: Partial<RapierDebugOptions>) => voidPatch which overlays render
getOptions() => RapierDebugOptionsReturns a shallow copy of current options
render(ctx, camX, camY, camScale) => voidDraws active overlays
getStats() => stringOne-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, builds rgb(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, -camY to 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 to pos + 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/s in ${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, radius 3 / camScale.
  • Contact normal: uses manifold.localNormal1() plus the first solver contact point; draws a yellow (#ffff00) line of length 20 / camScale along the normal, width 1.5 / camScale.

The flipped manifold parameter is captured but not used.

Constants (extracted from code)

ConstantValueUsed for
Collider line width1.5 / camScaleShape outlines
Collider fallback color#00ffffWhen color array runs out
Velocity arrow scale0.3Multiplier on vel for arrow tip
Velocity min speed1Below this, no arrow drawn
Velocity line width2 / camScaleArrow body
Velocity color#ffffffArrow + label
Arrowhead length8 / camScaleEach splay leg
Arrowhead splay±0.3 radAngle off velocity vector
Speed label font${10/camScale}px monospaceLabel rendering
Contact dot color#ff0000Solver contact point
Contact dot radius3 / camScalePer-point circle
Normal color#ffff00Contact normal line
Normal length20 / camScaleNormal arrow
Normal line width1.5 / camScaleNormal line

getStats()

Returns one of:

  • 'Rapier: not initialized' if RapierWorld.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 rad and headLen hardcoded.
  • Camera-transform-aware stroke/font scaling pattern (size / camScale for on-screen-constant rendering) is repeated 8 times in this file; a helper like screenPx(n, camScale) could centralize it.
  • Color-array iteration for debugRender output (4 floats per segment, parallel colors array) is a Rapier-specific pattern that could move into a shared rapierDebugBufferToSegments(buf) utility if other modules need raw shape data.
  • The contactPairsWith / contactPair nested-callback iteration is a candidate for a flat generator/iterator wrapper if any other system needs to enumerate ship contacts.