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.
- package/README.md +1498 -1659
- package/dist/types/index.d.ts +4 -1
- package/dist/types/src/Animator.d.ts +1 -1
- package/dist/types/src/CollisionLayers.d.ts +2 -2
- package/dist/types/src/Color.d.ts +1 -0
- package/dist/types/src/Drawable.d.ts +1 -1
- package/dist/types/src/EmeraldDB.d.ts +2 -2
- package/dist/types/src/InstancedTexture.d.ts +17 -2
- package/dist/types/src/Material.d.ts +2 -2
- package/dist/types/src/MathUtils.d.ts +2 -1
- package/dist/types/src/ParticleEmitter.d.ts +1 -1
- package/dist/types/src/Physics.d.ts +148 -18
- package/dist/types/src/Scene.d.ts +1 -1
- package/dist/types/src/Shaders.d.ts +2 -2
- package/dist/types/src/Tilemap.d.ts +1 -1
- package/dist/types/src/UI.d.ts +1 -1
- package/dist/types/src/components/Behaviour.d.ts +2 -2
- package/dist/types/src/components/Collider.d.ts +7 -1
- package/dist/types/src/components/GameObject.d.ts +2 -2
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +281 -8
- package/dist/types/src/importers/Aseprite.d.ts +2 -2
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +1 -1
- package/dist/types/src/managers/EventManager.d.ts +1 -1
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/InputManager.d.ts +93 -2
- package/dist/types/src/managers/NetworkManager.d.ts +2 -2
- package/dist/types/src/managers/RenderStats.d.ts +1 -1
- package/dist/types/src/managers/TextureManager.d.ts +1 -1
- package/dist/types/src/physics/AABB.d.ts +92 -0
- package/dist/types/src/physics/Body.d.ts +435 -0
- package/dist/types/src/physics/BodyType.d.ts +6 -0
- package/dist/types/src/physics/BroadPhase.d.ts +210 -0
- package/dist/types/src/physics/Collision.d.ts +102 -0
- package/dist/types/src/physics/Contact.d.ts +206 -0
- package/dist/types/src/physics/ContactSolver.d.ts +108 -0
- package/dist/types/src/physics/Distance.d.ts +54 -0
- package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
- package/dist/types/src/physics/Fixture.d.ts +221 -0
- package/dist/types/src/physics/Island.d.ts +52 -0
- package/dist/types/src/physics/Joint.d.ts +59 -0
- package/dist/types/src/physics/Math2D.d.ts +371 -0
- package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
- package/dist/types/src/physics/Settings.d.ts +22 -0
- package/dist/types/src/physics/Shapes.d.ts +207 -0
- package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
- package/dist/types/src/physics/World.d.ts +274 -0
- package/dist/types/src/physics/index.d.ts +34 -0
- package/index.js +6 -0
- package/package.json +2 -3
- package/src/Animator.js +1 -1
- package/src/CollisionLayers.js +3 -3
- package/src/Color.js +8 -0
- package/src/Drawable.js +1 -1
- package/src/Emerald.js +1 -1
- package/src/EmeraldDB.js +2 -2
- package/src/InstancedTexture.js +57 -9
- package/src/Material.js +2 -2
- package/src/MathUtils.js +2 -1
- package/src/ParticleEmitter.js +1 -1
- package/src/Physics.js +270 -60
- package/src/Scene.js +1 -1
- package/src/Shaders.js +20 -20
- package/src/Tilemap.js +1 -1
- package/src/UI.js +1 -1
- package/src/components/Behaviour.js +2 -2
- package/src/components/BoxCollider.js +7 -9
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +7 -9
- package/src/components/CircleColliderDebug.js +3 -2
- package/src/components/Collider.js +13 -3
- package/src/components/GameObject.js +2 -2
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +441 -14
- package/src/importers/Aseprite.js +2 -2
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +1 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/InputManager.js +129 -3
- package/src/managers/NetworkManager.js +2 -2
- package/src/managers/RenderStats.js +1 -1
- package/src/managers/TextureManager.js +1 -1
- package/src/physics/AABB.js +207 -0
- package/src/physics/Body.js +862 -0
- package/src/physics/BodyType.js +16 -0
- package/src/physics/BroadPhase.js +641 -0
- package/src/physics/Collision.js +534 -0
- package/src/physics/Contact.js +500 -0
- package/src/physics/ContactSolver.js +526 -0
- package/src/physics/Distance.js +403 -0
- package/src/physics/DistanceJoint.js +227 -0
- package/src/physics/Fixture.js +346 -0
- package/src/physics/Island.js +203 -0
- package/src/physics/Joint.js +78 -0
- package/src/physics/Math2D.js +573 -0
- package/src/physics/RevoluteJoint.js +278 -0
- package/src/physics/Settings.js +78 -0
- package/src/physics/Shapes.js +549 -0
- package/src/physics/TimeOfImpact.js +87 -0
- package/src/physics/World.js +731 -0
- package/src/physics/index.js +79 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @enum BodyType
|
|
3
|
+
* @description The three ways a body can take part in the simulation.
|
|
4
|
+
*
|
|
5
|
+
* - `static`: never moves, infinite mass. Ground, walls, platforms.
|
|
6
|
+
* - `kinematic`: moves only when you set its velocity; forces and collisions
|
|
7
|
+
* never push it back. Moving platforms, elevators.
|
|
8
|
+
* - `dynamic`: fully simulated, gravity, impulses and contacts all apply.
|
|
9
|
+
*/
|
|
10
|
+
const BodyType = {
|
|
11
|
+
STATIC: "static",
|
|
12
|
+
KINEMATIC: "kinematic",
|
|
13
|
+
DYNAMIC: "dynamic",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { BodyType };
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import AABB from "./AABB.js";
|
|
2
|
+
import Settings from "./Settings.js";
|
|
3
|
+
import { Vec2 } from "./Math2D.js";
|
|
4
|
+
|
|
5
|
+
const NULL_NODE = -1;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @class TreeNode
|
|
9
|
+
* @description One node of the dynamic AABB tree. Leaves hold a fixture proxy;
|
|
10
|
+
* internal nodes just bound their two children.
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
class TreeNode {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.aabb = new AABB();
|
|
16
|
+
this.userData = null;
|
|
17
|
+
this.parent = NULL_NODE;
|
|
18
|
+
this.child1 = NULL_NODE;
|
|
19
|
+
this.child2 = NULL_NODE;
|
|
20
|
+
/** -1 marks a free node, 0 a leaf. */
|
|
21
|
+
this.height = -1;
|
|
22
|
+
this.next = NULL_NODE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
isLeaf() {
|
|
26
|
+
return this.child1 === NULL_NODE;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @class DynamicTree
|
|
32
|
+
* @description A balanced tree of axis-aligned boxes over every fixture in the
|
|
33
|
+
* world. Leaves are stored with a "fat" AABB so a body can jiggle a little
|
|
34
|
+
* without forcing a re-insert, and the tree is rebalanced with rotations on the
|
|
35
|
+
* way back up after each change. Every broadphase query, world raycast and
|
|
36
|
+
* continuous-collision sweep goes through it.
|
|
37
|
+
*/
|
|
38
|
+
class DynamicTree {
|
|
39
|
+
constructor() {
|
|
40
|
+
/** @private */
|
|
41
|
+
this.root = NULL_NODE;
|
|
42
|
+
/** @private */
|
|
43
|
+
this.nodes = [];
|
|
44
|
+
/** @private */
|
|
45
|
+
this.freeList = NULL_NODE;
|
|
46
|
+
/** @private */
|
|
47
|
+
this.nodeCount = 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @method allocateNode
|
|
52
|
+
* @description Takes a node from the free list, growing the pool if needed.
|
|
53
|
+
* @returns {number} - The node id
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
allocateNode() {
|
|
57
|
+
if (this.freeList === NULL_NODE) {
|
|
58
|
+
const node = new TreeNode();
|
|
59
|
+
this.nodes.push(node);
|
|
60
|
+
this.freeList = this.nodes.length - 1;
|
|
61
|
+
node.next = NULL_NODE;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const id = this.freeList;
|
|
65
|
+
const node = this.nodes[id];
|
|
66
|
+
this.freeList = node.next;
|
|
67
|
+
node.parent = NULL_NODE;
|
|
68
|
+
node.child1 = NULL_NODE;
|
|
69
|
+
node.child2 = NULL_NODE;
|
|
70
|
+
node.height = 0;
|
|
71
|
+
node.userData = null;
|
|
72
|
+
this.nodeCount++;
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @method freeNode
|
|
78
|
+
* @description Returns a node to the free list.
|
|
79
|
+
* @param {number} id
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
freeNode(id) {
|
|
83
|
+
const node = this.nodes[id];
|
|
84
|
+
node.next = this.freeList;
|
|
85
|
+
node.height = -1;
|
|
86
|
+
node.userData = null;
|
|
87
|
+
this.freeList = id;
|
|
88
|
+
this.nodeCount--;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @method createProxy
|
|
93
|
+
* @description Adds a fixture proxy to the tree.
|
|
94
|
+
* @param {AABB} aabb - The tight world AABB
|
|
95
|
+
* @param {Object} userData - The FixtureProxy this leaf stands for
|
|
96
|
+
* @returns {number} - The proxy id
|
|
97
|
+
*/
|
|
98
|
+
createProxy(aabb, userData) {
|
|
99
|
+
const id = this.allocateNode();
|
|
100
|
+
const node = this.nodes[id];
|
|
101
|
+
node.aabb.copy(aabb).extend(Settings.aabbExtension);
|
|
102
|
+
node.userData = userData;
|
|
103
|
+
node.height = 0;
|
|
104
|
+
this.insertLeaf(id);
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @method destroyProxy
|
|
110
|
+
* @description Removes a proxy from the tree.
|
|
111
|
+
* @param {number} id
|
|
112
|
+
*/
|
|
113
|
+
destroyProxy(id) {
|
|
114
|
+
this.removeLeaf(id);
|
|
115
|
+
this.freeNode(id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @method moveProxy
|
|
120
|
+
* @description Re-fits a proxy whose fixture moved. Returns false (and does
|
|
121
|
+
* nothing) while the new box still fits inside the fat one.
|
|
122
|
+
* @param {number} id
|
|
123
|
+
* @param {AABB} aabb - The new tight AABB
|
|
124
|
+
* @param {Object} displacement - How far the body moved this step
|
|
125
|
+
* @returns {boolean} - Whether the proxy was actually re-inserted
|
|
126
|
+
*/
|
|
127
|
+
moveProxy(id, aabb, displacement) {
|
|
128
|
+
const node = this.nodes[id];
|
|
129
|
+
if (node.aabb.contains(aabb)) return false;
|
|
130
|
+
|
|
131
|
+
this.removeLeaf(id);
|
|
132
|
+
|
|
133
|
+
const fat = node.aabb.copy(aabb).extend(Settings.aabbExtension);
|
|
134
|
+
const dx = Settings.aabbMultiplier * displacement.x;
|
|
135
|
+
const dy = Settings.aabbMultiplier * displacement.y;
|
|
136
|
+
if (dx < 0) fat.lowerBound.x += dx;
|
|
137
|
+
else fat.upperBound.x += dx;
|
|
138
|
+
if (dy < 0) fat.lowerBound.y += dy;
|
|
139
|
+
else fat.upperBound.y += dy;
|
|
140
|
+
|
|
141
|
+
this.insertLeaf(id);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @method getFatAABB
|
|
147
|
+
* @description Returns the stored (fattened) AABB of a proxy.
|
|
148
|
+
* @param {number} id
|
|
149
|
+
* @returns {AABB}
|
|
150
|
+
*/
|
|
151
|
+
getFatAABB(id) {
|
|
152
|
+
return this.nodes[id].aabb;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @method getUserData
|
|
157
|
+
* @description Returns the proxy payload of a leaf.
|
|
158
|
+
* @param {number} id
|
|
159
|
+
* @returns {Object}
|
|
160
|
+
*/
|
|
161
|
+
getUserData(id) {
|
|
162
|
+
return this.nodes[id].userData;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @method insertLeaf
|
|
167
|
+
* @description Inserts a leaf, choosing the sibling that grows the tree's
|
|
168
|
+
* total surface area least, then rebalancing on the way back to the root.
|
|
169
|
+
* @param {number} leaf
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
insertLeaf(leaf) {
|
|
173
|
+
if (this.root === NULL_NODE) {
|
|
174
|
+
this.root = leaf;
|
|
175
|
+
this.nodes[leaf].parent = NULL_NODE;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const leafAABB = this.nodes[leaf].aabb;
|
|
180
|
+
let index = this.root;
|
|
181
|
+
const combined = new AABB();
|
|
182
|
+
const childCombined = new AABB();
|
|
183
|
+
|
|
184
|
+
while (!this.nodes[index].isLeaf()) {
|
|
185
|
+
const node = this.nodes[index];
|
|
186
|
+
const child1 = node.child1;
|
|
187
|
+
const child2 = node.child2;
|
|
188
|
+
|
|
189
|
+
const area = node.aabb.getPerimeter();
|
|
190
|
+
combined.combine(node.aabb, leafAABB);
|
|
191
|
+
const combinedArea = combined.getPerimeter();
|
|
192
|
+
|
|
193
|
+
const cost = 2 * combinedArea;
|
|
194
|
+
const inheritanceCost = 2 * (combinedArea - area);
|
|
195
|
+
|
|
196
|
+
childCombined.combine(leafAABB, this.nodes[child1].aabb);
|
|
197
|
+
let cost1;
|
|
198
|
+
if (this.nodes[child1].isLeaf()) {
|
|
199
|
+
cost1 = childCombined.getPerimeter() + inheritanceCost;
|
|
200
|
+
} else {
|
|
201
|
+
cost1 =
|
|
202
|
+
childCombined.getPerimeter() -
|
|
203
|
+
this.nodes[child1].aabb.getPerimeter() +
|
|
204
|
+
inheritanceCost;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
childCombined.combine(leafAABB, this.nodes[child2].aabb);
|
|
208
|
+
let cost2;
|
|
209
|
+
if (this.nodes[child2].isLeaf()) {
|
|
210
|
+
cost2 = childCombined.getPerimeter() + inheritanceCost;
|
|
211
|
+
} else {
|
|
212
|
+
cost2 =
|
|
213
|
+
childCombined.getPerimeter() -
|
|
214
|
+
this.nodes[child2].aabb.getPerimeter() +
|
|
215
|
+
inheritanceCost;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (cost < cost1 && cost < cost2) break;
|
|
219
|
+
index = cost1 < cost2 ? child1 : child2;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sibling = index;
|
|
223
|
+
const oldParent = this.nodes[sibling].parent;
|
|
224
|
+
const newParent = this.allocateNode();
|
|
225
|
+
const newParentNode = this.nodes[newParent];
|
|
226
|
+
newParentNode.parent = oldParent;
|
|
227
|
+
newParentNode.userData = null;
|
|
228
|
+
newParentNode.aabb.combine(leafAABB, this.nodes[sibling].aabb);
|
|
229
|
+
newParentNode.height = this.nodes[sibling].height + 1;
|
|
230
|
+
newParentNode.child1 = sibling;
|
|
231
|
+
newParentNode.child2 = leaf;
|
|
232
|
+
this.nodes[sibling].parent = newParent;
|
|
233
|
+
this.nodes[leaf].parent = newParent;
|
|
234
|
+
|
|
235
|
+
if (oldParent !== NULL_NODE) {
|
|
236
|
+
if (this.nodes[oldParent].child1 === sibling) {
|
|
237
|
+
this.nodes[oldParent].child1 = newParent;
|
|
238
|
+
} else {
|
|
239
|
+
this.nodes[oldParent].child2 = newParent;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
this.root = newParent;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let walk = this.nodes[leaf].parent;
|
|
246
|
+
while (walk !== NULL_NODE) {
|
|
247
|
+
walk = this.balance(walk);
|
|
248
|
+
const node = this.nodes[walk];
|
|
249
|
+
const c1 = this.nodes[node.child1];
|
|
250
|
+
const c2 = this.nodes[node.child2];
|
|
251
|
+
node.height = 1 + Math.max(c1.height, c2.height);
|
|
252
|
+
node.aabb.combine(c1.aabb, c2.aabb);
|
|
253
|
+
walk = node.parent;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @method removeLeaf
|
|
259
|
+
* @description Unlinks a leaf and collapses its now-single-child parent.
|
|
260
|
+
* @param {number} leaf
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
removeLeaf(leaf) {
|
|
264
|
+
if (leaf === this.root) {
|
|
265
|
+
this.root = NULL_NODE;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const parent = this.nodes[leaf].parent;
|
|
270
|
+
const grandParent = this.nodes[parent].parent;
|
|
271
|
+
const sibling =
|
|
272
|
+
this.nodes[parent].child1 === leaf
|
|
273
|
+
? this.nodes[parent].child2
|
|
274
|
+
: this.nodes[parent].child1;
|
|
275
|
+
|
|
276
|
+
if (grandParent !== NULL_NODE) {
|
|
277
|
+
if (this.nodes[grandParent].child1 === parent) {
|
|
278
|
+
this.nodes[grandParent].child1 = sibling;
|
|
279
|
+
} else {
|
|
280
|
+
this.nodes[grandParent].child2 = sibling;
|
|
281
|
+
}
|
|
282
|
+
this.nodes[sibling].parent = grandParent;
|
|
283
|
+
this.freeNode(parent);
|
|
284
|
+
|
|
285
|
+
let index = grandParent;
|
|
286
|
+
while (index !== NULL_NODE) {
|
|
287
|
+
index = this.balance(index);
|
|
288
|
+
const node = this.nodes[index];
|
|
289
|
+
const c1 = this.nodes[node.child1];
|
|
290
|
+
const c2 = this.nodes[node.child2];
|
|
291
|
+
node.aabb.combine(c1.aabb, c2.aabb);
|
|
292
|
+
node.height = 1 + Math.max(c1.height, c2.height);
|
|
293
|
+
index = node.parent;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
this.root = sibling;
|
|
297
|
+
this.nodes[sibling].parent = NULL_NODE;
|
|
298
|
+
this.freeNode(parent);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @method balance
|
|
304
|
+
* @description One AVL-style rotation at `iA` if its subtrees differ in
|
|
305
|
+
* height by more than one. Keeps queries logarithmic.
|
|
306
|
+
* @param {number} iA
|
|
307
|
+
* @returns {number} - The new root of that subtree
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
balance(iA) {
|
|
311
|
+
const A = this.nodes[iA];
|
|
312
|
+
if (A.isLeaf() || A.height < 2) return iA;
|
|
313
|
+
|
|
314
|
+
const iB = A.child1;
|
|
315
|
+
const iC = A.child2;
|
|
316
|
+
const B = this.nodes[iB];
|
|
317
|
+
const C = this.nodes[iC];
|
|
318
|
+
const balance = C.height - B.height;
|
|
319
|
+
|
|
320
|
+
if (balance > 1) {
|
|
321
|
+
const iF = C.child1;
|
|
322
|
+
const iG = C.child2;
|
|
323
|
+
const F = this.nodes[iF];
|
|
324
|
+
const G = this.nodes[iG];
|
|
325
|
+
|
|
326
|
+
C.child1 = iA;
|
|
327
|
+
C.parent = A.parent;
|
|
328
|
+
A.parent = iC;
|
|
329
|
+
|
|
330
|
+
if (C.parent !== NULL_NODE) {
|
|
331
|
+
if (this.nodes[C.parent].child1 === iA)
|
|
332
|
+
this.nodes[C.parent].child1 = iC;
|
|
333
|
+
else this.nodes[C.parent].child2 = iC;
|
|
334
|
+
} else {
|
|
335
|
+
this.root = iC;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (F.height > G.height) {
|
|
339
|
+
C.child2 = iF;
|
|
340
|
+
A.child2 = iG;
|
|
341
|
+
G.parent = iA;
|
|
342
|
+
A.aabb.combine(B.aabb, G.aabb);
|
|
343
|
+
C.aabb.combine(A.aabb, F.aabb);
|
|
344
|
+
A.height = 1 + Math.max(B.height, G.height);
|
|
345
|
+
C.height = 1 + Math.max(A.height, F.height);
|
|
346
|
+
} else {
|
|
347
|
+
C.child2 = iG;
|
|
348
|
+
A.child2 = iF;
|
|
349
|
+
F.parent = iA;
|
|
350
|
+
A.aabb.combine(B.aabb, F.aabb);
|
|
351
|
+
C.aabb.combine(A.aabb, G.aabb);
|
|
352
|
+
A.height = 1 + Math.max(B.height, F.height);
|
|
353
|
+
C.height = 1 + Math.max(A.height, G.height);
|
|
354
|
+
}
|
|
355
|
+
return iC;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (balance < -1) {
|
|
359
|
+
const iD = B.child1;
|
|
360
|
+
const iE = B.child2;
|
|
361
|
+
const D = this.nodes[iD];
|
|
362
|
+
const E = this.nodes[iE];
|
|
363
|
+
|
|
364
|
+
B.child1 = iA;
|
|
365
|
+
B.parent = A.parent;
|
|
366
|
+
A.parent = iB;
|
|
367
|
+
|
|
368
|
+
if (B.parent !== NULL_NODE) {
|
|
369
|
+
if (this.nodes[B.parent].child1 === iA)
|
|
370
|
+
this.nodes[B.parent].child1 = iB;
|
|
371
|
+
else this.nodes[B.parent].child2 = iB;
|
|
372
|
+
} else {
|
|
373
|
+
this.root = iB;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (D.height > E.height) {
|
|
377
|
+
B.child2 = iD;
|
|
378
|
+
A.child1 = iE;
|
|
379
|
+
E.parent = iA;
|
|
380
|
+
A.aabb.combine(C.aabb, E.aabb);
|
|
381
|
+
B.aabb.combine(A.aabb, D.aabb);
|
|
382
|
+
A.height = 1 + Math.max(C.height, E.height);
|
|
383
|
+
B.height = 1 + Math.max(A.height, D.height);
|
|
384
|
+
} else {
|
|
385
|
+
B.child2 = iE;
|
|
386
|
+
A.child1 = iD;
|
|
387
|
+
D.parent = iA;
|
|
388
|
+
A.aabb.combine(C.aabb, D.aabb);
|
|
389
|
+
B.aabb.combine(A.aabb, E.aabb);
|
|
390
|
+
A.height = 1 + Math.max(C.height, D.height);
|
|
391
|
+
B.height = 1 + Math.max(A.height, E.height);
|
|
392
|
+
}
|
|
393
|
+
return iB;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return iA;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @method query
|
|
401
|
+
* @description Calls `callback(proxyId)` for every leaf whose fat AABB
|
|
402
|
+
* overlaps `aabb`. Returning false from the callback stops the query.
|
|
403
|
+
* @param {AABB} aabb
|
|
404
|
+
* @param {Function} callback
|
|
405
|
+
*/
|
|
406
|
+
query(aabb, callback) {
|
|
407
|
+
if (this.root === NULL_NODE) return;
|
|
408
|
+
const stack = [this.root];
|
|
409
|
+
while (stack.length > 0) {
|
|
410
|
+
const id = stack.pop();
|
|
411
|
+
if (id === NULL_NODE) continue;
|
|
412
|
+
const node = this.nodes[id];
|
|
413
|
+
if (!AABB.testOverlap(node.aabb, aabb)) continue;
|
|
414
|
+
if (node.isLeaf()) {
|
|
415
|
+
if (callback(id) === false) return;
|
|
416
|
+
} else {
|
|
417
|
+
stack.push(node.child1, node.child2);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @method rayCast
|
|
424
|
+
* @description Walks the tree along a ray, calling
|
|
425
|
+
* `callback(subInput, proxyId)`. The callback returns the new max fraction
|
|
426
|
+
* (0 terminates the cast), which lets the traversal shrink as it finds hits.
|
|
427
|
+
* @param {Object} input - `{ p1, p2, maxFraction }`
|
|
428
|
+
* @param {Function} callback
|
|
429
|
+
*/
|
|
430
|
+
rayCast(input, callback) {
|
|
431
|
+
if (this.root === NULL_NODE) return;
|
|
432
|
+
|
|
433
|
+
let maxFraction = input.maxFraction;
|
|
434
|
+
const segment = {
|
|
435
|
+
p1: input.p1,
|
|
436
|
+
p2: new Vec2(),
|
|
437
|
+
maxFraction: 1,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const stack = [this.root];
|
|
441
|
+
while (stack.length > 0) {
|
|
442
|
+
const id = stack.pop();
|
|
443
|
+
if (id === NULL_NODE) continue;
|
|
444
|
+
const node = this.nodes[id];
|
|
445
|
+
|
|
446
|
+
segment.p2.set(
|
|
447
|
+
input.p1.x + maxFraction * (input.p2.x - input.p1.x),
|
|
448
|
+
input.p1.y + maxFraction * (input.p2.y - input.p1.y)
|
|
449
|
+
);
|
|
450
|
+
if (!node.aabb.rayCast(segment)) continue;
|
|
451
|
+
|
|
452
|
+
if (node.isLeaf()) {
|
|
453
|
+
const value = callback({ p1: input.p1, p2: input.p2, maxFraction }, id);
|
|
454
|
+
if (value === 0) return;
|
|
455
|
+
if (value > 0) maxFraction = value;
|
|
456
|
+
} else {
|
|
457
|
+
stack.push(node.child1, node.child2);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @method getHeight
|
|
464
|
+
* @description Height of the tree (0 when empty), useful when profiling.
|
|
465
|
+
* @returns {number}
|
|
466
|
+
*/
|
|
467
|
+
getHeight() {
|
|
468
|
+
return this.root === NULL_NODE ? 0 : this.nodes[this.root].height;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const PAIR_STRIDE = 2097152;
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @class BroadPhase
|
|
476
|
+
* @description Keeps the AABB tree and the list of proxies that moved since the
|
|
477
|
+
* last step, and turns those into the candidate pairs the narrowphase looks at.
|
|
478
|
+
*/
|
|
479
|
+
class BroadPhase {
|
|
480
|
+
constructor() {
|
|
481
|
+
/** @private */
|
|
482
|
+
this.tree = new DynamicTree();
|
|
483
|
+
/** @private */
|
|
484
|
+
this.moveBuffer = [];
|
|
485
|
+
/** @private */
|
|
486
|
+
this.proxyCount = 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @method createProxy
|
|
491
|
+
* @description Adds a fixture proxy and queues it for pairing.
|
|
492
|
+
* @param {AABB} aabb
|
|
493
|
+
* @param {Object} userData
|
|
494
|
+
* @returns {number} - Proxy id
|
|
495
|
+
*/
|
|
496
|
+
createProxy(aabb, userData) {
|
|
497
|
+
const id = this.tree.createProxy(aabb, userData);
|
|
498
|
+
this.proxyCount++;
|
|
499
|
+
this.bufferMove(id);
|
|
500
|
+
return id;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @method destroyProxy
|
|
505
|
+
* @description Removes a fixture proxy.
|
|
506
|
+
* @param {number} id
|
|
507
|
+
*/
|
|
508
|
+
destroyProxy(id) {
|
|
509
|
+
this.unbufferMove(id);
|
|
510
|
+
this.proxyCount--;
|
|
511
|
+
this.tree.destroyProxy(id);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @method moveProxy
|
|
516
|
+
* @description Updates a proxy's box, queueing it for re-pairing if the tree
|
|
517
|
+
* actually had to move it.
|
|
518
|
+
* @param {number} id
|
|
519
|
+
* @param {AABB} aabb
|
|
520
|
+
* @param {Object} displacement
|
|
521
|
+
*/
|
|
522
|
+
moveProxy(id, aabb, displacement) {
|
|
523
|
+
if (this.tree.moveProxy(id, aabb, displacement)) this.bufferMove(id);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @method touchProxy
|
|
528
|
+
* @description Forces a proxy to be re-paired next update even though it
|
|
529
|
+
* didn't move (used when a fixture's filter changes).
|
|
530
|
+
* @param {number} id
|
|
531
|
+
*/
|
|
532
|
+
touchProxy(id) {
|
|
533
|
+
this.bufferMove(id);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** @private */
|
|
537
|
+
bufferMove(id) {
|
|
538
|
+
this.moveBuffer.push(id);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** @private */
|
|
542
|
+
unbufferMove(id) {
|
|
543
|
+
const index = this.moveBuffer.indexOf(id);
|
|
544
|
+
if (index >= 0) this.moveBuffer[index] = NULL_NODE;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @method getFatAABB
|
|
549
|
+
* @description Returns a proxy's fattened AABB.
|
|
550
|
+
* @param {number} id
|
|
551
|
+
* @returns {AABB}
|
|
552
|
+
*/
|
|
553
|
+
getFatAABB(id) {
|
|
554
|
+
return this.tree.getFatAABB(id);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @method getUserData
|
|
559
|
+
* @description Returns a proxy's payload.
|
|
560
|
+
* @param {number} id
|
|
561
|
+
* @returns {Object}
|
|
562
|
+
*/
|
|
563
|
+
getUserData(id) {
|
|
564
|
+
return this.tree.getUserData(id);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @method testOverlap
|
|
569
|
+
* @description True when two proxies' fat AABBs overlap.
|
|
570
|
+
* @param {number} idA
|
|
571
|
+
* @param {number} idB
|
|
572
|
+
* @returns {boolean}
|
|
573
|
+
*/
|
|
574
|
+
testOverlap(idA, idB) {
|
|
575
|
+
return AABB.testOverlap(
|
|
576
|
+
this.tree.getFatAABB(idA),
|
|
577
|
+
this.tree.getFatAABB(idB)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @method query
|
|
583
|
+
* @description Forwards an AABB query to the tree.
|
|
584
|
+
* @param {AABB} aabb
|
|
585
|
+
* @param {Function} callback
|
|
586
|
+
*/
|
|
587
|
+
query(aabb, callback) {
|
|
588
|
+
this.tree.query(aabb, callback);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* @method rayCast
|
|
593
|
+
* @description Forwards a ray cast to the tree.
|
|
594
|
+
* @param {Object} input
|
|
595
|
+
* @param {Function} callback
|
|
596
|
+
*/
|
|
597
|
+
rayCast(input, callback) {
|
|
598
|
+
this.tree.rayCast(input, callback);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* @method updatePairs
|
|
603
|
+
* @description Emits `callback(userDataA, userDataB)` once for every new
|
|
604
|
+
* overlapping pair among the proxies that moved. Pairs are de-duplicated, so
|
|
605
|
+
* two proxies moving towards each other still report once.
|
|
606
|
+
* @param {Function} callback
|
|
607
|
+
*/
|
|
608
|
+
updatePairs(callback) {
|
|
609
|
+
if (this.moveBuffer.length === 0) return;
|
|
610
|
+
|
|
611
|
+
const seen = new Set();
|
|
612
|
+
const pairs = [];
|
|
613
|
+
|
|
614
|
+
for (const queryProxy of this.moveBuffer) {
|
|
615
|
+
if (queryProxy === NULL_NODE) continue;
|
|
616
|
+
const fatAABB = this.tree.getFatAABB(queryProxy);
|
|
617
|
+
|
|
618
|
+
this.tree.query(fatAABB, (proxyId) => {
|
|
619
|
+
if (proxyId === queryProxy) return true;
|
|
620
|
+
const a = Math.min(proxyId, queryProxy);
|
|
621
|
+
const b = Math.max(proxyId, queryProxy);
|
|
622
|
+
const key = a * PAIR_STRIDE + b;
|
|
623
|
+
if (seen.has(key)) return true;
|
|
624
|
+
seen.add(key);
|
|
625
|
+
pairs.push(a, b);
|
|
626
|
+
return true;
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.moveBuffer.length = 0;
|
|
631
|
+
|
|
632
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
633
|
+
callback(
|
|
634
|
+
this.tree.getUserData(pairs[i]),
|
|
635
|
+
this.tree.getUserData(pairs[i + 1])
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export { BroadPhase, DynamicTree, NULL_NODE };
|