* Called by the adapter at the end of a physics step. Diffs the * "seen this frame" set against the previous-frame active pairs: * - pairs in active but not seen → fire onCollisionEnd * - swap active ← seen for the next step's diff * @ignore
()
| 167 | * @ignore |
| 168 | */ |
| 169 | endFrame() { |
| 170 | for (const [key, pair] of this._activePairs) { |
| 171 | if (this._frameSeen.has(key)) { |
| 172 | continue; |
| 173 | } |
| 174 | const [a, b] = pair; |
| 175 | // Dispatch onCollisionEnd to whichever partner is still in the |
| 176 | // scene tree. If both have been detached (level teardown), skip |
| 177 | // entirely. Previously this short-circuited when *either* was |
| 178 | // null/undefined, which left the survivor with an unbalanced |
| 179 | // onCollisionStart / no End and they'd never learn the partner |
| 180 | // left. `ancestor` is set to `undefined` on removeChild, so use |
| 181 | // loose `!= null` to cover both null and undefined. |
| 182 | const aAttached = a.ancestor != null; |
| 183 | const bAttached = b.ancestor != null; |
| 184 | if (!aAttached && !bAttached) { |
| 185 | continue; |
| 186 | } |
| 187 | if (aAttached && typeof a.onCollisionEnd === "function") { |
| 188 | a.onCollisionEnd(undefined, b); |
| 189 | } |
| 190 | if (bAttached && typeof b.onCollisionEnd === "function") { |
| 191 | b.onCollisionEnd(undefined, a); |
| 192 | } |
| 193 | } |
| 194 | // rotate buffers: seen becomes the new active set |
| 195 | const prev = this._activePairs; |
| 196 | this._activePairs = this._frameSeen; |
| 197 | this._frameSeen = prev; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Build a stable order-independent key for a pair of renderables, |
no test coverage detected