Skip to content

Multi-shape bodies report the geometry that actually collides - #1684

Merged
obiot merged 2 commits into
masterfrom
fix/multi-shape-rotation-adapters
Sep 18, 2026
Merged

obiot merged 2 commits into
masterfrom
fix/multi-shape-rotation-adapters

Conversation

@obiot

@obiot obiot commented Sep 18, 2026

Copy link
Copy Markdown
Member

A body built from several shapes came apart under rotation on matter, and swung away from the body on planck. One root cause, two faces, introduced by the getBodyShapes rotation fix in matter-adapter 1.2.1 / planck-adapter 1.3.1.

What was wrong

It rotated the authored shapes about the renderable's origin. Both halves of that were wrong:

  • Polygon#rotate moves a shape's points without its pos, so a shape carrying its offset in pos spun about its own origin while the origin stayed put. Several such shapes visibly separated.
  • The renderable's origin is not what either engine rotates about. Matter turns a body about its centre of mass; planck about its origin, which on a compound body is a third point again. (getLocalCenter() would have been a second wrong answer.)

The fix

Stop deriving the pose and read the engine's own geometry back. There is then no pivot to get wrong, and the report describes what actually collides rather than what was authored: an Ellipse is simulated as a circle of average radius, a concave polygon as convex pieces, a degenerate one as a box, and on planck a shape with isActive: false has no fixture at all. The overlay drew none of that truthfully before. Authored shapes are untouched on renderable.bodyDef.shapes.

Also fixed, all from review of the same code

  • updateShape at an unchanged angle reported the previous geometry, and a removed renderable stayed pinned: the cache was keyed by angle and cleared on no path but one.
  • The cache missed on every frame of a body that was actually turning (float equality on the angle), and each miss allocated a fresh shape set through the object pools without ever releasing it — on the debug overlay's per-body-per-frame path.
  • matter simulated shapes flagged isActive: false, which planck and the builtin both honour.
  • planck reported its fixtures newest-first, i.e. the reverse of def.shapes.

Engine, and the builtin

Polygon#rotate(angle, pivot) now turns the whole shape. Ellipse#rotate always rotated its pos; same signature, and Body#rotate calls both over one mixed list, so a body's ellipse parts orbited correctly and its polygon parts did not. Every Rect added to a body becomes a polygon offset by pos, so the documented feet-and-torso shape was among them. A polygon at the origin — every caller in the engine until now — rotates to exactly the numbers it always did.

The builtin's getBodyShapes is deliberately unchanged. Its SAT never reads body.angle, so an unrotated hitbox around a spinning sprite is the solver reporting what it genuinely collides as; making the overlay follow the visual angle would turn an honest inconsistency into a confident lie. Three tests pin that contract so it is not "fixed" later.

One claimed defect that did not survive measurement

planck's getBodyAABB reading an "enlarged and stale" broadphase proxy. Measured against the exact AABB across steps at 2000 px/s: a constant 0.001 m (0.03 px) that does not grow with speed. The larger gap is the polygon skin radius, which is real collision geometry. Left alone.

Verification

  • matter 196, planck 194, melonJS 7034, biome clean.
  • Every fix has a test, each verified to fail with its fix reverted, on each backend where it applies.
  • Repro reproduced and re-verified on all three backends; every physics example swept clean (platformer, platformer-matter, pool-matter, plinko-planck, isometric-rpg, whac-a-mole, space-invaders, jungle-rabbit) — worth doing because Polygon#rotate is core, so the blast radius is wider than the adapters.

Two of my own first test attempts were wrong and were replaced: a "the centroid does not move" invariant that only holds when the pivot is the centroid (false on planck), and a rotation check that encoded matter's pivot. Both are pivot-agnostic now.

Versions

matter-adapter 1.3.0, planck-adapter 1.4.0 — minor rather than patch, because getBodyShapes now returns different object types. Peer stays >=20.0.0: nothing here needs a newer engine, and the readback never calls rotate, so it does not depend on the core fix either.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t

Copilot AI lite review requested due to automatic review settings September 18, 2026 10:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

A body built from several shapes came apart under rotation on matter, and
swung away from the body on planck. One root cause, two faces, introduced
by the `getBodyShapes` rotation fix in matter-adapter 1.2.1 / planck-adapter
1.3.1: it rotated the AUTHORED shapes about the renderable's origin.

Both halves of that were wrong. `Polygon#rotate` moves a shape's points
without its `pos`, so a shape carrying its offset in `pos` spun about its
own origin while its origin stayed put — several such shapes visibly
separated. And the renderable's origin is not what either engine rotates
about: matter turns a body about its centre of mass, planck about its
ORIGIN, which on a compound body is a third point again.

So stop deriving the pose and read the engine's own geometry back. There
is then no pivot to get wrong, and the report describes what actually
collides rather than what was authored — an Ellipse is simulated as a
circle of average radius, a concave polygon as convex pieces, a degenerate
one as a box, and on planck a shape with `isActive: false` has no fixture
at all. The overlay drew none of that truthfully before.

Also fixed, all found by review of the same code:

- `updateShape` at an unchanged angle reported the previous geometry, and a
  removed renderable stayed pinned: the cache was keyed by angle and
  cleared on no path but one.
- The cache missed on every frame of a body that was actually turning —
  float equality on the angle — and each miss allocated a fresh shape set
  through the object pools without ever releasing it. On the debug
  overlay's per-body-per-frame path.
- matter simulated shapes flagged `isActive: false`, which planck and the
  builtin both honour, so one definition collided differently per backend.
- planck reported its fixtures newest-first, i.e. the reverse of
  `def.shapes`, so an index into one did not address the same shape.

Engine, separate from the adapters and hit by the builtin instead:
`Polygon#rotate(angle, pivot)` now turns the whole shape. `Ellipse#rotate`
always rotated its `pos`; the two have the same signature and `Body#rotate`
calls both over one mixed list, so a body's ellipse parts orbited correctly
and its polygon parts did not. Since every `Rect` added to a body becomes a
polygon offset by `pos`, the documented feet-and-torso shape was among them.
A polygon at the origin — every caller in the engine until now — rotates to
exactly the numbers it always did.

The builtin's `getBodyShapes` is deliberately unchanged. Its SAT never
reads `body.angle`, so an unrotated hitbox around a spinning sprite is the
solver reporting what it genuinely collides as; making the overlay follow
the visual angle would turn an honest inconsistency into a confident lie.
`body.rotate()` does move the shapes, and that case was already reported.

One claimed defect did not survive measurement: planck's `getBodyAABB`
reading an "enlarged and stale" broadphase proxy. Measured against the
exact AABB across steps at 2000 px/s, the difference is a constant 0.001 m
(0.03 px) that does not grow with speed, and the larger gap is the polygon
skin radius, which is real collision geometry. Left alone.

Tests: every fix above has one, each verified to fail with its fix
reverted, on all three backends where it applies. Two of my own first
attempts were wrong and are recorded as such — a "the centroid does not
move" invariant that only holds when the pivot IS the centroid (false on
planck), and a rotation check that encoded matter's pivot; both replaced
with pivot-agnostic ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
@obiot
obiot force-pushed the fix/multi-shape-rotation-adapters branch from 8d0f9e2 to a367a05 Compare September 18, 2026 10:46
Copilot AI review requested due to automatic review settings September 18, 2026 10:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

matter-adapter 1.3.0 and planck-adapter 1.4.0, dated for publication.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
Copilot AI review requested due to automatic review settings September 18, 2026 10:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit 791e589 into master Sep 18, 2026
6 checks passed
@obiot
obiot deleted the fix/multi-shape-rotation-adapters branch September 18, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants