emeraldengine 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +1498 -1659
  2. package/dist/types/index.d.ts +4 -1
  3. package/dist/types/src/Animator.d.ts +1 -1
  4. package/dist/types/src/CollisionLayers.d.ts +2 -2
  5. package/dist/types/src/Color.d.ts +1 -0
  6. package/dist/types/src/Drawable.d.ts +1 -1
  7. package/dist/types/src/EmeraldDB.d.ts +2 -2
  8. package/dist/types/src/InstancedTexture.d.ts +17 -2
  9. package/dist/types/src/Material.d.ts +2 -2
  10. package/dist/types/src/MathUtils.d.ts +2 -1
  11. package/dist/types/src/ParticleEmitter.d.ts +1 -1
  12. package/dist/types/src/Physics.d.ts +148 -18
  13. package/dist/types/src/Scene.d.ts +1 -1
  14. package/dist/types/src/Shaders.d.ts +2 -2
  15. package/dist/types/src/Tilemap.d.ts +1 -1
  16. package/dist/types/src/UI.d.ts +1 -1
  17. package/dist/types/src/components/Behaviour.d.ts +2 -2
  18. package/dist/types/src/components/Collider.d.ts +7 -1
  19. package/dist/types/src/components/GameObject.d.ts +2 -2
  20. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  21. package/dist/types/src/components/RigidBody.d.ts +281 -8
  22. package/dist/types/src/importers/Aseprite.d.ts +2 -2
  23. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  24. package/dist/types/src/importers/TiledMap.d.ts +1 -1
  25. package/dist/types/src/managers/EventManager.d.ts +1 -1
  26. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  27. package/dist/types/src/managers/InputManager.d.ts +93 -2
  28. package/dist/types/src/managers/NetworkManager.d.ts +2 -2
  29. package/dist/types/src/managers/RenderStats.d.ts +1 -1
  30. package/dist/types/src/managers/TextureManager.d.ts +1 -1
  31. package/dist/types/src/physics/AABB.d.ts +92 -0
  32. package/dist/types/src/physics/Body.d.ts +435 -0
  33. package/dist/types/src/physics/BodyType.d.ts +6 -0
  34. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  35. package/dist/types/src/physics/Collision.d.ts +102 -0
  36. package/dist/types/src/physics/Contact.d.ts +206 -0
  37. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  38. package/dist/types/src/physics/Distance.d.ts +54 -0
  39. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  40. package/dist/types/src/physics/Fixture.d.ts +221 -0
  41. package/dist/types/src/physics/Island.d.ts +52 -0
  42. package/dist/types/src/physics/Joint.d.ts +59 -0
  43. package/dist/types/src/physics/Math2D.d.ts +371 -0
  44. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  45. package/dist/types/src/physics/Settings.d.ts +22 -0
  46. package/dist/types/src/physics/Shapes.d.ts +207 -0
  47. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  48. package/dist/types/src/physics/World.d.ts +274 -0
  49. package/dist/types/src/physics/index.d.ts +34 -0
  50. package/index.js +6 -0
  51. package/package.json +2 -3
  52. package/src/Animator.js +1 -1
  53. package/src/CollisionLayers.js +3 -3
  54. package/src/Color.js +8 -0
  55. package/src/Drawable.js +1 -1
  56. package/src/Emerald.js +1 -1
  57. package/src/EmeraldDB.js +2 -2
  58. package/src/InstancedTexture.js +57 -9
  59. package/src/Material.js +2 -2
  60. package/src/MathUtils.js +2 -1
  61. package/src/ParticleEmitter.js +1 -1
  62. package/src/Physics.js +270 -60
  63. package/src/Scene.js +1 -1
  64. package/src/Shaders.js +20 -20
  65. package/src/Tilemap.js +1 -1
  66. package/src/UI.js +1 -1
  67. package/src/components/Behaviour.js +2 -2
  68. package/src/components/BoxCollider.js +7 -9
  69. package/src/components/BoxColliderDebug.js +3 -4
  70. package/src/components/CircleCollider.js +7 -9
  71. package/src/components/CircleColliderDebug.js +3 -2
  72. package/src/components/Collider.js +13 -3
  73. package/src/components/GameObject.js +2 -2
  74. package/src/components/PolygonCollider.js +55 -0
  75. package/src/components/RigidBody.js +441 -14
  76. package/src/importers/Aseprite.js +2 -2
  77. package/src/importers/ForgeLevel.js +581 -0
  78. package/src/importers/TiledMap.js +1 -1
  79. package/src/managers/EventManager.js +1 -1
  80. package/src/managers/Gamepad.js +126 -0
  81. package/src/managers/InputManager.js +129 -3
  82. package/src/managers/NetworkManager.js +2 -2
  83. package/src/managers/RenderStats.js +1 -1
  84. package/src/managers/TextureManager.js +1 -1
  85. package/src/physics/AABB.js +207 -0
  86. package/src/physics/Body.js +862 -0
  87. package/src/physics/BodyType.js +16 -0
  88. package/src/physics/BroadPhase.js +641 -0
  89. package/src/physics/Collision.js +534 -0
  90. package/src/physics/Contact.js +500 -0
  91. package/src/physics/ContactSolver.js +526 -0
  92. package/src/physics/Distance.js +403 -0
  93. package/src/physics/DistanceJoint.js +227 -0
  94. package/src/physics/Fixture.js +346 -0
  95. package/src/physics/Island.js +203 -0
  96. package/src/physics/Joint.js +78 -0
  97. package/src/physics/Math2D.js +573 -0
  98. package/src/physics/RevoluteJoint.js +278 -0
  99. package/src/physics/Settings.js +78 -0
  100. package/src/physics/Shapes.js +549 -0
  101. package/src/physics/TimeOfImpact.js +87 -0
  102. package/src/physics/World.js +731 -0
  103. package/src/physics/index.js +79 -0
@@ -0,0 +1,731 @@
1
+ import { Vec2 } from "./Math2D.js";
2
+ import Settings from "./Settings.js";
3
+ import AABB from "./AABB.js";
4
+ import { BroadPhase } from "./BroadPhase.js";
5
+ import { Body } from "./Body.js";
6
+ import { BodyType } from "./BodyType.js";
7
+ import { ContactManager, shouldCollide } from "./Contact.js";
8
+ import Island from "./Island.js";
9
+ import { DistanceProxy } from "./Distance.js";
10
+ import { timeOfImpact, TOIState } from "./TimeOfImpact.js";
11
+ import { JointType } from "./Joint.js";
12
+ import DistanceJoint from "./DistanceJoint.js";
13
+ import RevoluteJoint from "./RevoluteJoint.js";
14
+
15
+ /**
16
+ * @function removeFrom
17
+ * @description Splices the first occurrence of `item` out of `array`.
18
+ * @private
19
+ */
20
+ function removeFrom(array, item) {
21
+ const index = array.indexOf(item);
22
+ if (index >= 0) array.splice(index, 1);
23
+ }
24
+
25
+ const EVENTS = [
26
+ "begin-contact",
27
+ "end-contact",
28
+ "pre-solve",
29
+ "post-solve",
30
+ "remove-body",
31
+ "remove-fixture",
32
+ ];
33
+
34
+ /**
35
+ * @class World
36
+ * @description The physics world: it owns the bodies, finds which ones touch,
37
+ * and advances them all by one time step. This is Emerald's own rigid-body
38
+ * engine: broadphase, narrowphase, an impulse solver and continuous collision
39
+ * detection, with no external physics dependency.
40
+ *
41
+ * Games normally reach it through {@link Physics}, which adds the pixel/meter
42
+ * conversion, a fixed-timestep accumulator and collision routing to
43
+ * {@link Behaviour} components.
44
+ *
45
+ * @param {Object} [def] - `{ gravity, allowSleep, warmStarting,
46
+ * continuousPhysics, velocityThreshold, velocityIterations,
47
+ * positionIterations }`
48
+ */
49
+ class World {
50
+ constructor(def = {}) {
51
+ this.gravity = new Vec2(def.gravity || { x: 0, y: 0 });
52
+ this.broadPhase = new BroadPhase();
53
+ this.contactManager = new ContactManager(this);
54
+
55
+ /** Head of the body list; walk it with {@link Body#getNext}. @private */
56
+ this.bodyList = null;
57
+ this.bodyCount = 0;
58
+
59
+ /** Head of the joint list. @private */
60
+ this.jointList = null;
61
+ this.jointCount = 0;
62
+
63
+ this.allowSleep = def.allowSleep !== false;
64
+ this.warmStarting = def.warmStarting !== false;
65
+ this.continuousPhysics = def.continuousPhysics !== false;
66
+ this.autoClearForces = def.autoClearForces !== false;
67
+
68
+ this.velocityThreshold =
69
+ def.velocityThreshold ?? Settings.velocityThreshold;
70
+ this.velocityIterations =
71
+ def.velocityIterations ?? Settings.velocityIterations;
72
+ this.positionIterations =
73
+ def.positionIterations ?? Settings.positionIterations;
74
+
75
+ /** Optional `(fixtureA, fixtureB) => boolean` veto on new contacts. */
76
+ this.contactFilter = null;
77
+
78
+ /** @private */
79
+ this.newContacts = false;
80
+ /** @private */
81
+ this.locked = false;
82
+ /** @private */
83
+ this.island = new Island();
84
+ /** @private */
85
+ this.listeners = Object.create(null);
86
+ for (const name of EVENTS) this.listeners[name] = [];
87
+
88
+ /** @private */
89
+ this._toiProxyA = new DistanceProxy();
90
+ /** @private */
91
+ this._toiProxyB = new DistanceProxy();
92
+ /** What the last time-of-impact search hit. @private */
93
+ this._toiHitBody = null;
94
+ }
95
+
96
+ /**
97
+ * @method on
98
+ * @description Subscribes to a world event. Supported events:
99
+ * `begin-contact`, `end-contact`, `pre-solve`, `post-solve`, `remove-body`
100
+ * and `remove-fixture`.
101
+ * @param {string} name - Event name
102
+ * @param {Function} listener - Called with the contact (or body/fixture)
103
+ * @returns {World} - this
104
+ */
105
+ on(name, listener) {
106
+ if (!this.listeners[name]) this.listeners[name] = [];
107
+ if (typeof listener === "function") this.listeners[name].push(listener);
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * @method off
113
+ * @description Unsubscribes a listener.
114
+ * @param {string} name
115
+ * @param {Function} listener
116
+ * @returns {World} - this
117
+ */
118
+ off(name, listener) {
119
+ const list = this.listeners[name];
120
+ if (!list) return this;
121
+ const index = list.indexOf(listener);
122
+ if (index >= 0) list.splice(index, 1);
123
+ return this;
124
+ }
125
+
126
+ /**
127
+ * @method dispatch
128
+ * @description Fires a world event. Listener errors are contained so one bad
129
+ * callback can't halt the simulation mid-step.
130
+ * @param {string} name
131
+ * @param {...*} args
132
+ */
133
+ dispatch(name, ...args) {
134
+ const list = this.listeners[name];
135
+ if (!list || list.length === 0) return;
136
+ for (const listener of list) {
137
+ try {
138
+ listener(...args);
139
+ } catch (error) {
140
+ console.error(`[Physics] ${name} listener threw:`, error);
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @method createBody
147
+ * @description Creates a body in this world.
148
+ * @param {Object} [def] - See {@link Body}
149
+ * @returns {Body}
150
+ */
151
+ createBody(def = {}) {
152
+ if (this.locked) {
153
+ throw new Error(
154
+ "[Physics] Cannot create a body while the world is stepping."
155
+ );
156
+ }
157
+ const body = new Body(this, def);
158
+ body.prev = null;
159
+ body.next = this.bodyList;
160
+ if (this.bodyList) this.bodyList.prev = body;
161
+ this.bodyList = body;
162
+ this.bodyCount++;
163
+ return body;
164
+ }
165
+
166
+ /**
167
+ * @method destroyBody
168
+ * @description Removes a body, its fixtures and all its contacts.
169
+ * @param {Body} body
170
+ */
171
+ destroyBody(body) {
172
+ if (!body || body.world !== this) return;
173
+ if (this.locked) {
174
+ throw new Error(
175
+ "[Physics] Cannot destroy a body while the world is stepping."
176
+ );
177
+ }
178
+
179
+ for (let i = body.joints.length - 1; i >= 0; i--) {
180
+ this.destroyJoint(body.joints[i]);
181
+ }
182
+
183
+ for (let i = body.contacts.length - 1; i >= 0; i--) {
184
+ this.contactManager.destroy(body.contacts[i]);
185
+ }
186
+ body.contacts.length = 0;
187
+
188
+ let fixture = body.fixtureList;
189
+ while (fixture) {
190
+ const next = fixture.next;
191
+ this.dispatch("remove-fixture", fixture);
192
+ if (body.active) fixture.destroyProxies(this.broadPhase);
193
+ fixture.body = null;
194
+ fixture.next = null;
195
+ fixture = next;
196
+ }
197
+ body.fixtureList = null;
198
+ body.fixtureCount = 0;
199
+
200
+ this.dispatch("remove-body", body);
201
+
202
+ if (body.prev) body.prev.next = body.next;
203
+ if (body.next) body.next.prev = body.prev;
204
+ if (body === this.bodyList) this.bodyList = body.next;
205
+ body.prev = null;
206
+ body.next = null;
207
+ body.world = null;
208
+ this.bodyCount--;
209
+ }
210
+
211
+ /**
212
+ * @method getBodyList
213
+ * @description Returns the first body; walk the rest with
214
+ * {@link Body#getNext}.
215
+ * @returns {Body|null}
216
+ */
217
+ getBodyList() {
218
+ return this.bodyList;
219
+ }
220
+
221
+ /**
222
+ * @method getBodyCount
223
+ * @description Number of bodies in the world.
224
+ * @returns {number}
225
+ */
226
+ getBodyCount() {
227
+ return this.bodyCount;
228
+ }
229
+
230
+ /**
231
+ * @method createJoint
232
+ * @description Creates a constraint between two bodies, a
233
+ * {@link DistanceJoint} or a {@link RevoluteJoint}, chosen by `def.type`
234
+ * (see {@link JointType}). Wakes both bodies, since a joint pulling on a
235
+ * sleeping body needs it simulating again immediately.
236
+ * @param {Object} def - `{ type, bodyA, bodyB, ... }`, forwarded to the
237
+ * chosen joint's constructor
238
+ * @returns {Joint}
239
+ */
240
+ createJoint(def) {
241
+ if (this.locked) {
242
+ throw new Error(
243
+ "[Physics] Cannot create a joint while the world is stepping."
244
+ );
245
+ }
246
+
247
+ const joint =
248
+ def.type === JointType.REVOLUTE
249
+ ? new RevoluteJoint(def)
250
+ : new DistanceJoint(def);
251
+
252
+ joint.prev = null;
253
+ joint.next = this.jointList;
254
+ if (this.jointList) this.jointList.prev = joint;
255
+ this.jointList = joint;
256
+ this.jointCount++;
257
+
258
+ joint.bodyA.joints.push(joint);
259
+ joint.bodyB.joints.push(joint);
260
+ joint.bodyA.setAwake(true);
261
+ joint.bodyB.setAwake(true);
262
+
263
+ if (!joint.collideConnected) {
264
+ for (let i = joint.bodyA.contacts.length - 1; i >= 0; i--) {
265
+ const contact = joint.bodyA.contacts[i];
266
+ const other =
267
+ contact.fixtureA.body === joint.bodyA
268
+ ? contact.fixtureB.body
269
+ : contact.fixtureA.body;
270
+ if (other === joint.bodyB) this.contactManager.destroy(contact);
271
+ }
272
+ }
273
+
274
+ return joint;
275
+ }
276
+
277
+ /**
278
+ * @method destroyJoint
279
+ * @description Removes a joint. Safe to call as part of destroying one of
280
+ * its bodies (see {@link World#destroyBody}).
281
+ * @param {Joint} joint
282
+ */
283
+ destroyJoint(joint) {
284
+ if (!joint) return;
285
+ if (this.locked) {
286
+ throw new Error(
287
+ "[Physics] Cannot destroy a joint while the world is stepping."
288
+ );
289
+ }
290
+
291
+ joint.bodyA.setAwake(true);
292
+ joint.bodyB.setAwake(true);
293
+ removeFrom(joint.bodyA.joints, joint);
294
+ removeFrom(joint.bodyB.joints, joint);
295
+
296
+ if (joint.prev) joint.prev.next = joint.next;
297
+ if (joint.next) joint.next.prev = joint.prev;
298
+ if (joint === this.jointList) this.jointList = joint.next;
299
+ joint.prev = null;
300
+ joint.next = null;
301
+ this.jointCount--;
302
+ }
303
+
304
+ /**
305
+ * @method getJointList
306
+ * @description Returns the first joint; walk the rest with `joint.next`.
307
+ * @returns {Joint|null}
308
+ */
309
+ getJointList() {
310
+ return this.jointList;
311
+ }
312
+
313
+ /**
314
+ * @method getJointCount
315
+ * @returns {number}
316
+ */
317
+ getJointCount() {
318
+ return this.jointCount;
319
+ }
320
+
321
+ /**
322
+ * @method getContactList
323
+ * @description Returns every live contact (touching or merely close enough
324
+ * for their bounding boxes to overlap).
325
+ * @returns {Array<Contact>}
326
+ */
327
+ getContactList() {
328
+ return this.contactManager.contacts;
329
+ }
330
+
331
+ /**
332
+ * @method getContactCount
333
+ * @description Number of live contacts.
334
+ * @returns {number}
335
+ */
336
+ getContactCount() {
337
+ return this.contactManager.contacts.length;
338
+ }
339
+
340
+ /**
341
+ * @method setGravity
342
+ * @description Sets world gravity in physics units per second squared.
343
+ * @param {Object} gravity - `{ x, y }`
344
+ */
345
+ setGravity(gravity) {
346
+ this.gravity.set(gravity.x, gravity.y);
347
+ for (let body = this.bodyList; body; body = body.next) {
348
+ if (body.type === BodyType.DYNAMIC) body.setAwake(true);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @method getGravity
354
+ * @description Returns world gravity.
355
+ * @returns {Vec2}
356
+ */
357
+ getGravity() {
358
+ return this.gravity;
359
+ }
360
+
361
+ /**
362
+ * @method isLocked
363
+ * @description True while a step is in progress, when the body/fixture graph
364
+ * must not be modified.
365
+ * @returns {boolean}
366
+ */
367
+ isLocked() {
368
+ return this.locked;
369
+ }
370
+
371
+ /**
372
+ * @method setAllowSleeping
373
+ * @description Enables or disables sleeping world-wide.
374
+ * @param {boolean} flag
375
+ */
376
+ setAllowSleeping(flag) {
377
+ if (flag === this.allowSleep) return;
378
+ this.allowSleep = !!flag;
379
+ if (!this.allowSleep) {
380
+ for (let body = this.bodyList; body; body = body.next)
381
+ body.setAwake(true);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * @method step
387
+ * @description Advances the simulation by `dt` seconds: refresh contacts,
388
+ * solve the islands, then sweep any continuous (bullet) bodies.
389
+ * @param {number} dt - Time step in seconds
390
+ * @param {number} [velocityIterations] - Solver velocity iterations
391
+ * @param {number} [positionIterations] - Solver position iterations
392
+ */
393
+ step(
394
+ dt,
395
+ velocityIterations = this.velocityIterations,
396
+ positionIterations = this.positionIterations
397
+ ) {
398
+ if (!Number.isFinite(dt) || dt < 0) return;
399
+
400
+ if (this.newContacts) {
401
+ this.contactManager.findNewContacts();
402
+ this.newContacts = false;
403
+ }
404
+
405
+ this.locked = true;
406
+
407
+ const step = {
408
+ dt,
409
+ invDt: dt > 0 ? 1 / dt : 0,
410
+ velocityIterations,
411
+ positionIterations,
412
+ warmStarting: this.warmStarting,
413
+ velocityThreshold: this.velocityThreshold,
414
+ };
415
+
416
+ this.contactManager.collide();
417
+
418
+ if (dt > 0) {
419
+ this.solve(step);
420
+ if (this.continuousPhysics) this.solveContinuous();
421
+ }
422
+
423
+ this.locked = false;
424
+
425
+ if (this.autoClearForces) this.clearForces();
426
+ }
427
+
428
+ /**
429
+ * @method solve
430
+ * @description Groups the awake bodies into islands of things that touch and
431
+ * solves each island on its own.
432
+ * @param {Object} step
433
+ * @private
434
+ */
435
+ solve(step) {
436
+ for (let body = this.bodyList; body; body = body.next) {
437
+ body.islandFlag = false;
438
+ body.sweep.alpha0 = 0;
439
+ }
440
+ for (const contact of this.contactManager.contacts) {
441
+ contact.islandFlag = false;
442
+ }
443
+ for (let joint = this.jointList; joint; joint = joint.next) {
444
+ joint.islandFlag = false;
445
+ }
446
+
447
+ const island = this.island;
448
+ const stack = [];
449
+
450
+ for (let seed = this.bodyList; seed; seed = seed.next) {
451
+ if (seed.islandFlag) continue;
452
+ if (!seed.awake || !seed.active) continue;
453
+ if (seed.type === BodyType.STATIC) continue;
454
+
455
+ island.clear();
456
+ stack.length = 0;
457
+ stack.push(seed);
458
+ seed.islandFlag = true;
459
+
460
+ while (stack.length > 0) {
461
+ const body = stack.pop();
462
+ island.addBody(body);
463
+ body.awake = true;
464
+
465
+ if (body.type === BodyType.STATIC) continue;
466
+
467
+ for (const contact of body.contacts) {
468
+ if (contact.islandFlag) continue;
469
+ if (!contact.touching || !contact.enabled) continue;
470
+ if (contact.fixtureA.sensor || contact.fixtureB.sensor) continue;
471
+
472
+ island.addContact(contact);
473
+ contact.islandFlag = true;
474
+
475
+ const other =
476
+ contact.fixtureA.body === body
477
+ ? contact.fixtureB.body
478
+ : contact.fixtureA.body;
479
+ if (other.islandFlag) continue;
480
+ stack.push(other);
481
+ other.islandFlag = true;
482
+ }
483
+
484
+ for (const joint of body.joints) {
485
+ if (joint.islandFlag) continue;
486
+ const other = joint.bodyA === body ? joint.bodyB : joint.bodyA;
487
+ if (!other.active) continue;
488
+
489
+ island.addJoint(joint);
490
+ joint.islandFlag = true;
491
+
492
+ if (other.islandFlag) continue;
493
+ stack.push(other);
494
+ other.islandFlag = true;
495
+ }
496
+ }
497
+
498
+ island.solve(step, this.gravity, this.allowSleep);
499
+
500
+ for (const body of island.bodies) {
501
+ if (body.type === BodyType.STATIC) body.islandFlag = false;
502
+ }
503
+ }
504
+
505
+ for (let body = this.bodyList; body; body = body.next) {
506
+ if (!body.islandFlag) continue;
507
+ if (body.type === BodyType.STATIC) continue;
508
+ body.synchronizeFixtures();
509
+ }
510
+
511
+ this.contactManager.findNewContacts();
512
+ }
513
+
514
+ /**
515
+ * @method solveContinuous
516
+ * @description Sweeps every bullet body against the static and kinematic
517
+ * geometry it passed through this step, and rewinds it to the first impact.
518
+ * Without this, a body moving further than its own size in one step would
519
+ * simply appear on the far side of a thin wall.
520
+ * @private
521
+ */
522
+ solveContinuous() {
523
+ for (let body = this.bodyList; body; body = body.next) {
524
+ if (!body.bullet) continue;
525
+ if (body.type !== BodyType.DYNAMIC) continue;
526
+ if (!body.awake || !body.active) continue;
527
+
528
+ const dx = body.sweep.c.x - body.sweep.c0.x;
529
+ const dy = body.sweep.c.y - body.sweep.c0.y;
530
+ const travelSq = dx * dx + dy * dy;
531
+ if (travelSq === 0) continue;
532
+
533
+ const alpha = this._findTimeOfImpact(body);
534
+ if (alpha === null) continue;
535
+
536
+ body.advance(alpha);
537
+ this._absorbImpactVelocity(body, dx, dy);
538
+ body.synchronizeFixtures();
539
+ this.newContacts = true;
540
+ }
541
+
542
+ if (this.newContacts) {
543
+ this.contactManager.findNewContacts();
544
+ this.newContacts = false;
545
+ }
546
+ }
547
+
548
+ /**
549
+ * @method _absorbImpactVelocity
550
+ * @description Takes the excess approach speed out of a body that a rewind
551
+ * stopped short of a *moving* obstacle, leaving it travelling with whatever
552
+ * it landed on.
553
+ *
554
+ * The rewind puts the body where the two first touched, but the obstacle
555
+ * carries on to the end of the step, so the body is left hovering by up to
556
+ * one step of the obstacle's travel, too far apart for the discrete solver
557
+ * to see a contact next step. Keeping the body's original velocity as well
558
+ * means it arrives even faster the following step, is rewound again, and
559
+ * never lands: a rider on a descending platform would fall for ever without
560
+ * ever standing on it. Dropping the approach speed to the obstacle's own
561
+ * closes that gap within a step or two, which is where the discrete solver
562
+ * takes over and the two stay in contact.
563
+ *
564
+ * Static obstacles are left alone: they do not run away from the body, so
565
+ * the overlap the rewind aims for survives into the next step and the
566
+ * discrete solver already handles them.
567
+ *
568
+ * @param {Body} body - The body that was rewound
569
+ * @param {number} dx - How far it swept this step, before the rewind
570
+ * @param {number} dy
571
+ * @private
572
+ */
573
+ _absorbImpactVelocity(body, dx, dy) {
574
+ const other = this._toiHitBody;
575
+ if (!other || other.type === BodyType.STATIC) return;
576
+
577
+ const travel = Math.sqrt(dx * dx + dy * dy);
578
+ if (travel === 0) return;
579
+
580
+ const nx = dx / travel;
581
+ const ny = dy / travel;
582
+ const v = body.linearVelocity;
583
+ const w = other.linearVelocity;
584
+ const approach = (v.x - w.x) * nx + (v.y - w.y) * ny;
585
+ if (approach <= 0) return;
586
+
587
+ v.x -= approach * nx;
588
+ v.y -= approach * ny;
589
+ }
590
+
591
+ /**
592
+ * @method _findTimeOfImpact
593
+ * @description Earliest fraction of this step at which `body` first touches
594
+ * non-dynamic geometry, or null when it hits nothing. The body that was hit
595
+ * is left in `_toiHitBody`.
596
+ * @param {Body} body
597
+ * @returns {number|null}
598
+ * @private
599
+ */
600
+ _findTimeOfImpact(body) {
601
+ let minAlpha = 1;
602
+ let hit = false;
603
+ this._toiHitBody = null;
604
+ const proxyA = this._toiProxyA;
605
+ const proxyB = this._toiProxyB;
606
+
607
+ for (let fixture = body.fixtureList; fixture; fixture = fixture.next) {
608
+ if (fixture.sensor) continue;
609
+ proxyA.set(fixture.shape);
610
+
611
+ const sweptAABB = fixture.getAABB();
612
+
613
+ this.broadPhase.query(sweptAABB, (proxyId) => {
614
+ const other = this.broadPhase.getUserData(proxyId).fixture;
615
+ const otherBody = other.body;
616
+ if (otherBody === body) return true;
617
+ if (otherBody.type === BodyType.DYNAMIC) return true;
618
+ if (other.sensor) return true;
619
+ if (!otherBody.active) return true;
620
+ if (!shouldCollide(fixture, other)) return true;
621
+ if (this.contactFilter && !this.contactFilter(fixture, other)) {
622
+ return true;
623
+ }
624
+
625
+ proxyB.set(other.shape);
626
+ const result = timeOfImpact({
627
+ proxyA,
628
+ proxyB,
629
+ sweepA: body.sweep,
630
+ sweepB: otherBody.sweep,
631
+ tMax: minAlpha,
632
+ });
633
+
634
+ if (
635
+ (result.state === TOIState.TOUCHING ||
636
+ result.state === TOIState.OVERLAPPED) &&
637
+ result.t < minAlpha
638
+ ) {
639
+ minAlpha = result.t;
640
+ hit = true;
641
+ this._toiHitBody = otherBody;
642
+ }
643
+ return true;
644
+ });
645
+ }
646
+
647
+ if (!hit || minAlpha <= 0 || minAlpha >= 1) return null;
648
+ return minAlpha;
649
+ }
650
+
651
+ /**
652
+ * @method clearForces
653
+ * @description Zeroes the accumulated forces and torques on every body.
654
+ * Called automatically at the end of each step.
655
+ */
656
+ clearForces() {
657
+ for (let body = this.bodyList; body; body = body.next) {
658
+ body.force.setZero();
659
+ body.torque = 0;
660
+ }
661
+ }
662
+
663
+ /**
664
+ * @method rayCast
665
+ * @description Casts a ray from `p1` to `p2`, calling
666
+ * `callback(fixture, point, normal, fraction)` for every fixture hit.
667
+ *
668
+ * The callback controls what happens next, exactly like a filter: return the
669
+ * `fraction` to clip the ray there and keep looking for closer hits (the
670
+ * usual "closest hit" behaviour), `0` to stop immediately, `1` to keep the
671
+ * full ray and collect every hit, or `-1` to ignore this fixture entirely.
672
+ *
673
+ * @param {Object} p1 - Ray start `{ x, y }` in physics units
674
+ * @param {Object} p2 - Ray end `{ x, y }` in physics units
675
+ * @param {Function} callback
676
+ */
677
+ rayCast(p1, p2, callback) {
678
+ const input = {
679
+ p1: new Vec2(p1),
680
+ p2: new Vec2(p2),
681
+ maxFraction: 1,
682
+ };
683
+
684
+ this.broadPhase.rayCast(input, (subInput, proxyId) => {
685
+ const proxy = this.broadPhase.getUserData(proxyId);
686
+ const fixture = proxy.fixture;
687
+ const output = { fraction: 0, normal: new Vec2() };
688
+ const hit = fixture.rayCast(output, subInput);
689
+ if (!hit) return subInput.maxFraction;
690
+
691
+ const fraction = output.fraction;
692
+ const point = new Vec2(
693
+ (1 - fraction) * input.p1.x + fraction * input.p2.x,
694
+ (1 - fraction) * input.p1.y + fraction * input.p2.y
695
+ );
696
+ return callback(fixture, point, output.normal, fraction);
697
+ });
698
+ }
699
+
700
+ /**
701
+ * @method queryAABB
702
+ * @description Calls `callback(fixture)` for every fixture whose bounding box
703
+ * overlaps `aabb`. Returning false from the callback ends the query.
704
+ * @param {AABB} aabb
705
+ * @param {Function} callback
706
+ */
707
+ queryAABB(aabb, callback) {
708
+ this.broadPhase.query(aabb, (proxyId) => {
709
+ const fixture = this.broadPhase.getUserData(proxyId).fixture;
710
+ return callback(fixture);
711
+ });
712
+ }
713
+
714
+ /**
715
+ * @method queryPoint
716
+ * @description Calls `callback(fixture)` for every fixture containing a
717
+ * world-space point.
718
+ * @param {Object} point - `{ x, y }` in physics units
719
+ * @param {Function} callback
720
+ */
721
+ queryPoint(point, callback) {
722
+ const aabb = new AABB(point.x, point.y, point.x, point.y);
723
+ this.broadPhase.query(aabb, (proxyId) => {
724
+ const fixture = this.broadPhase.getUserData(proxyId).fixture;
725
+ if (!fixture.testPoint(point)) return true;
726
+ return callback(fixture);
727
+ });
728
+ }
729
+ }
730
+
731
+ export default World;