feat(physics): decide per contact what it does this step - #584
Conversation
There was no `preSolve`/`enableContact` anywhere in the physics API, so a one-way platform - solid from above, passable from below - could not be expressed at all. Collision filters answer "may these two touch"; nothing answered "should this particular contact push right now". `PhysicsWorld.contactModifier` runs once per solid contact per fixed step, between contact generation and the sleep pass. It receives the two colliders, the two bodies, the A->B normal, the manifold point count and the deepest penetration, and may change `enabled`, `friction` and `restitution`. All three are re-derived from the two colliders before every step, so a change applies to that step only and nothing leaks into the next one. Disabling a contact skips it in the solver without touching detection: it stays geometrically touching and the collision events keep describing the real geometry. It also must not join its two bodies into one sleeping island, which is why the hook runs before `_updateSleeping` rather than after - the island pass now skips a disabled contact for both its union and its moving-boundary branch. A disabled contact's warm-start impulses are dropped, so re-enabling it starts from zero instead of releasing a stale impulse. Sensors never reach the modifier; they produce no contact to solve. One modifier per world, not a signal: it mutates simulation state, so with several listeners the outcome would depend on registration order. The context object is reused across contacts, so a step over thousands of them allocates nothing. Claude-Session: https://claude.ai/code/session_01R4RHfm7nhMPXAuxFrpgrqj
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Bundle ReportChanges will decrease total bundle size by 17.81MB (-44.87%) ⬇️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: exojs-physics-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
view changes for bundle: exo-full-iife-Exo-iifeAssets Changed:
Files in
view changes for bundle: site-server-esmAssets Changed:
App Routes Affected:
|
There was no
preSolve/enableContactanywhere in the physics API, so a one-way platform — solid from above, passable from below — could not be expressed at all. Collision filters answer "may these two touch"; nothing answered "should this particular contact push right now".The hook
PhysicsWorld.contactModifierruns once per solid contact per fixed step, between contact generation and the sleep pass:It receives the two colliders, the two bodies, the A→B normal, the manifold point count and the deepest penetration, and may change three things:
enabled,friction,restitution. All three are re-derived from the two colliders before every step (√(fA × fB),max(rA, rB),true), so a change applies to that step only and nothing leaks into the next one.Everything else on the context is read-only, and the object is reused across contacts — a step over thousands of them allocates nothing.
What disabling a contact means
onCollisionStart/onCollisionEndkeep describing the real geometry._updateSleepingrather than after; the island pass now skips a disabled contact for both its union branch and its moving-boundary branch.One modifier, not a signal
The hook mutates simulation state, so with several listeners the outcome would depend on registration order. A world has at most one;
nullremoves it.Validation
pnpm verify:quick16/16 ·pnpm test10 580 passed · API docs regenerated · guide section added to Joints, sleeping & CCD (typechecked).New
contact-modifier.test.tscovers: every solid contact seen once per step with the A→B normal; a no-op modifier changing nothing; a disabled contact applying no impulse while still firingcollisionStart; the controls being re-derived each step; both island branches (dynamic↔dynamic union, and a moving platform waking its passenger); the warm-start cache being dropped; sensors never reaching the modifier; and a full one-way platform, solid from above and passable from below.https://claude.ai/code/session_01R4RHfm7nhMPXAuxFrpgrqj