From 7eca38c708f9813bb9ed24ae52ba207c91c169be Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Sat, 25 Jul 2026 19:17:47 -0500 Subject: [PATCH] fix some weird thigns in box2d? --- extensions/box2d.js | 120 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/extensions/box2d.js b/extensions/box2d.js index 2716acfe34..094759fff3 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12897,9 +12897,9 @@ }; const _placeBody = function (id, x, y, dir) { - if (bodies[id]) { - world.DestroyBody(bodies[id]); - } + // Replacing the body destroys its pin, so remember to make a new one. + const wasPinned = !!pinned[id]; + _removeBody(id); fixDef.filter.categoryBits = bodyCategoryBits; fixDef.filter.maskBits = bodyMaskBits; @@ -12912,13 +12912,21 @@ body.uid = id; body.CreateFixture(fixDef); bodies[id] = body; + + if (wasPinned) { + pinned[id] = _pinBodyToWorld(id); + } return body; }; const _removeBody = function (id) { - if (!bodies[id]) return; + const body = bodies[id]; + if (!body) return; - world.DestroyBody(bodies[id]); + // Box2D destroys every joint attached to a body along with it, so the pins + // have to be sorted out first or they'd be left dangling in `pinned`. + _releasePinsTo(body); + world.DestroyBody(body); delete bodies[id]; delete prevPos[id]; }; @@ -13039,6 +13047,66 @@ return joint; }; + /** + * Pin a sprite where it currently is, holding it against the world rather + * than against some other sprite. + * @param {*} id the target ID of an existing body. + * @returns {?Object} the new joint. + */ + const _pinBodyToWorld = function (id) { + const body = bodies[id]; + if (!body) return null; + + const pos = body.GetPosition(); + return _createJointOfType( + null, + "Rotating", + id, + 0, + 0, + null, + pos.x * zoom, + pos.y * zoom + ); + }; + + /** + * Deal with the pins that are about to be destroyed along with a body. + * Sprites pinned to it would otherwise silently come loose and start falling, + * so they get pinned to the world in the same place instead. + * @param {!Object} body the body that is about to be destroyed. + */ + const _releasePinsTo = function (body) { + for (const id in pinned) { + const joint = pinned[id]; + if (!joint) { + delete pinned[id]; + continue; + } + if (joint.GetBodyA() !== body && joint.GetBodyB() !== body) continue; + + world.DestroyJoint(joint); + delete pinned[id]; + // The body being destroyed loses its own pin for good; anything else + // stays pinned, just to the world from now on. + if (bodies[id] && bodies[id] !== body) { + pinned[id] = _pinBodyToWorld(id); + } + } + }; + + /** + * Destroy the pin holding a sprite in place, if it has one. + * @param {*} id the target ID. + */ + const _unpinBody = function (id) { + const joint = pinned[id]; + if (!joint) return; + + world.DestroyJoint(joint); + delete pinned[id]; + }; + /** * Set the X and Y coordinates (No Fencing) * @param {!RenderedTarget} rt the renderedTarget. @@ -13167,6 +13235,11 @@ // Clear target motion state values when the project starts. this.runtime.on("PROJECT_START", this.reset.bind(this)); + // Waiting until the next simulation step to notice that a sprite is gone + // would leave its body sitting in the world in the meantime, where new + // sprites can still collide with it or get pinned to it. + this.runtime.on("targetWasRemoved", (target) => _removeBody(target.id)); + world = new b2World( new b2Vec2(0, -10), // gravity (10) true // allow sleep @@ -13185,14 +13258,18 @@ reset() { for (const body in bodies) { - if (pinned[body]) { - world.DestroyJoint(pinned[body]); - delete pinned[body]; - } + // Destroying the body destroys its joints too, so the pins only need + // to be forgotten about, not destroyed a second time. + delete pinned[body]; world.DestroyBody(bodies[body]); delete bodies[body]; delete prevPos[body]; } + // Pins belonging to sprites that are already gone would otherwise be + // left behind pointing at destroyed joints. + for (const id in pinned) { + delete pinned[id]; + } tickRate = 30; // todo: delete joins? } @@ -13974,9 +14051,7 @@ const target = this.runtime.getTargetById(targetID); if (!target) { // Drop target from simulation - world.DestroyBody(body); - delete bodies[targetID]; - delete prevPos[targetID]; + _removeBody(targetID); continue; } @@ -14013,9 +14088,7 @@ const target = this.runtime.getTargetById(targetID); if (!target) { // Drop target from simulation - world.DestroyBody(body); - delete bodies[targetID]; - delete prevPos[targetID]; + _removeBody(targetID); continue; } @@ -14483,6 +14556,10 @@ fixedRotation ? 0 : (90 - target.direction) * toRad ); + // Whatever the sprite was pinned to before, that pin is replaced here. + // Leaving it in place would stack up joints that nothing destroys. + _unpinBody(target.id); + if (args.static === "pinned") { // Find what's behind the sprite (pin to that) const point = new b2AABB(); @@ -14492,7 +14569,11 @@ world.QueryAABB((fixture) => { const body2 = fixture.GetBody(); if (body2 !== body && fixture.TestPoint(pos.x, pos.y)) { - body2ID = body2.uid; + // Only sprites can be pinned to. The stage edges have no ID, and + // anything not in `bodies` no longer belongs to the simulation. + if (bodies[body2.uid] === body2) { + body2ID = body2.uid; + } return false; } return true; @@ -14508,13 +14589,6 @@ null, null ); - } else { - const pin = pinned[target.id]; - if (pin) { - world.DestroyJoint(pin); - // delete joints[pin.I]; - delete pinned[target.id]; - } } }