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.
Destroys the chunk's terrain body and removes the map entry. No-op if the chunk has no body.
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 the chunk's current terrain body, or null if none.
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.
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:
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).
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
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.
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).
Owns the lifecycle of Box2D bodies derived from
ChunkedBitmapdata.Two body categories are managed here:
b2Bodyper chunk, with one or moreb2ChainShapes attached (one per contour). Created and destroyed via Box2DAdapter.rebuildChunk /destroyChunk. Tracked internally in aMap<Chunk, BodyId>.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.