Class Box2DAdapter

Owns the lifecycle of Box2D bodies derived from ChunkedBitmap data.

Two body categories are managed here:

  • Terrain bodies — one static b2Body per chunk, with one or more b2ChainShapes attached (one per contour). Created and destroyed via Box2DAdapter.rebuildChunk / destroyChunk. Tracked internally in a Map<Chunk, BodyId>.
  • Debris bodies — dynamic bodies, one shape each. Created via Box2DAdapter.createDebrisBody; the caller owns the returned handle and is responsible for destroyBody (typically when the debris settles or leaves the world).

Per CLAUDE.md hard rule #3 the adapter must NOT be invoked from inside a Box2D step. The intended caller is the DeferredRebuildQueue, drained at end-of-frame.

Constructors

Methods

  • Creates a dynamic body for a detached island contour.

    The contour is triangulated via earcut and each triangle becomes its own b2PolygonShape on a single dynamic body. This handles non-convex outlines (e.g. an L-shaped piece left over after a carve severs a neck) cleanly without falling back to chain shapes, which behave poorly on dynamic bodies (one-sided collision means the body doesn't act as a solid).

    Returns null if the contour is too small (< 3 vertices) or earcut could not produce any triangles (e.g. all vertices collinear). The body's COM is set to the centroid of the input contour and shape vertices are translated to body-local space; this is what makes the debris rotate naturally about its own center under gravity.

    Parameters

    Returns null | {
        __brand: "BodyId";
    }

  • Destroys a body the adapter previously created.

    Parameters

    • bodyId: {
          __brand: "BodyId";
      }
      • Readonly__brand: "BodyId"

    Returns void

  • Destroys the chunk's terrain body and removes the map entry. No-op if the chunk has no body.

    Parameters

    Returns void

  • Destroys every chunk body the adapter still holds and clears the internal map. Call this when the owning terrain GameObject is destroyed; unattached debris bodies (created via createDebrisBody) are not tracked here and must be cleaned up by the caller.

    Returns void

  • Returns the chunk's current terrain body, or null if none.

    Parameters

    Returns null | {
        __brand: "BodyId";
    }

  • Replaces the chunk's terrain body with a fresh static body whose polygon (triangle) shapes match the supplied contours.

    The previous body (if any) is destroyed first; pass an empty list to clear the chunk entirely. Contours with fewer than 3 vertices are silently skipped. If no contour produced any triangles, no body is created and the map entry is cleared.

    Why polygons (triangulated via earcut) rather than one-sided b2ChainShape: a dynamic body that drifts to the wrong side of a chain seam during a destroy/recreate cycle isn't seen as colliding with the chain (chain normals are one-sided) and falls through. Two-sided polygons resolve penetration regardless of which side the body ended up on, which fixes the tunneling under continuous carving.

    Note on lifecycle: persistent-body / chain-only-swap was tried and is not viable with phaser-box2d 1.1 — its b2DestroyChain doesn't unlink the chain from the body's chain list, so a subsequent b2DestroyBody double-frees the chain pool. We destroy and recreate the whole body. The DeferredRebuildQueue skips this rebuild when the contour set is unchanged across frames, which keeps churn down for terrain blobs unaffected by a given carve.

    Parameters

    Returns void

  • Restores a previously captured set of body snapshots. Writes the transform, linear/angular velocity, and awake flag back. Bodies whose handle has since gone invalid (e.g. the user destroyed them mid-frame) are silently skipped — the snapshot loop in DeferredRebuildQueue runs at end-of-frame after the user's logic.

    Critically, the awake flag is the last thing restored. SetTransform and SetLinearVelocity may wake a body internally; restoring awake = false after those calls puts settled bodies back to sleep so the next world step skips gravity integration on them.

    The awake restore is conditional in two ways:

    1. Sleeping body without support → wake. A body that was sleeping pre-rebuild is put back to sleep only if it still has at least one static shape overlapping its AABB after the rebuild. If the user carved away the body's support, the AABB query returns no static — we leave the body awake so the next world step's gravity integration drops it normally. Without this gate, snapshot/restore would force-sleep a body whose support no longer exists, leaving it suspended in midair (the "ghost float" bug).

    2. Awake low-velocity body with support → force-settle. During continuous-drag carving, every frame's b2DestroyShapeInternal wakes the body via its hardcoded wakeBodies = true. Box2D's natural sleep timer never completes the sleepTime window because the wake-up happens every frame. The body accumulates a small velocity from each step's gravity + restitution rebound and bounces forever at sub-pixel amplitude. This branch detects "the body is at rest for all practical purposes" — pre-rebuild speed² below 0.01 m²/s² (the FORCE_SETTLE_SPEED2_THRESHOLD constant) AND the AABB query confirms support — and force-zeroes the velocity

      • sleeps the body, breaking the cycle.

    Trade-off: a body that's transiently moving slowly (e.g. mid- settling-bounce after just landing) and overlaps a static AABB can be force-settled too eagerly during heavy carving. The threshold is tight enough (~0.1 m/s) that genuinely-rolling or -falling bodies are preserved; bodies in the narrow band [threshold, Box2D's sleep threshold] would have settled within sleepTime anyway. In practice the bouncing-ball-while- carving case is rare; in the common case the user is happy that settled debris stops shimmering.

    Parameters

    Returns void

  • Captures the kinematic state of every dynamic body whose AABB overlaps the supplied pixel-space AABB.

    Used by DeferredRebuildQueue to freeze bodies across a terrain-collider rebuild. Box2D's b2DestroyShapeInternal wakes bodies whose contacts touch destroyed shapes (PhaserBox2D.js:3173 hardcodes wakeBodies = true); without snapshot/restore, every carve frame would wake settled bodies, integrate one step of gravity on them, and let the resulting penetration ricochet into a continuous jitter on the body's resting surface.

    Filtering: only bodies whose b2Body_GetType is b2_dynamicBody are returned. Static and kinematic bodies are skipped because we never write their state. Bodies are deduped (multiple shapes on the same body produce one snapshot).

    Parameters

    • aabbPx: {
          maxX: number;
          maxY: number;
          minX: number;
          minY: number;
      }
      • maxX: number
      • maxY: number
      • minX: number
      • minY: number

    Returns BodySnapshot[]

  • Iterates the chunks that currently have a terrain body. Used by the deferred queue to find bodies that should be destroyed when their chunk no longer hosts any contours after a global rebuild.

    Returns Iterable<Chunk, any, any>