@codexo/exojs-physics 0.15.2 → 0.16.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 +51 -23
- package/dist/esm/Aabb.d.ts +6 -10
- package/dist/esm/Aabb.d.ts.map +1 -0
- package/dist/esm/Aabb.js +9 -5
- package/dist/esm/Aabb.js.map +1 -1
- package/dist/esm/Collider.d.ts +62 -10
- package/dist/esm/Collider.d.ts.map +1 -0
- package/dist/esm/Collider.js +263 -157
- package/dist/esm/Collider.js.map +1 -1
- package/dist/esm/ContactGraph.d.ts +51 -5
- package/dist/esm/ContactGraph.d.ts.map +1 -0
- package/dist/esm/ContactGraph.js +282 -223
- package/dist/esm/ContactGraph.js.map +1 -1
- package/dist/esm/ContactModifier.d.ts +85 -0
- package/dist/esm/ContactModifier.d.ts.map +1 -0
- package/dist/esm/ContactModifier.js +49 -0
- package/dist/esm/ContactModifier.js.map +1 -0
- package/dist/esm/PhysicsBody.d.ts +56 -22
- package/dist/esm/PhysicsBody.d.ts.map +1 -0
- package/dist/esm/PhysicsBody.js +461 -443
- package/dist/esm/PhysicsBody.js.map +1 -1
- package/dist/esm/PhysicsWorld.d.ts +276 -43
- package/dist/esm/PhysicsWorld.d.ts.map +1 -0
- package/dist/esm/PhysicsWorld.js +890 -626
- package/dist/esm/PhysicsWorld.js.map +1 -1
- package/dist/esm/TimeStepper.d.ts +3 -2
- package/dist/esm/TimeStepper.d.ts.map +1 -0
- package/dist/esm/TimeStepper.js +56 -67
- package/dist/esm/TimeStepper.js.map +1 -1
- package/dist/esm/backend/NativePhysicsBackend.d.ts +8 -4
- package/dist/esm/backend/NativePhysicsBackend.d.ts.map +1 -0
- package/dist/esm/backend/NativePhysicsBackend.js +48 -41
- package/dist/esm/backend/NativePhysicsBackend.js.map +1 -1
- package/dist/esm/backend/PhysicsBackend.d.ts +9 -2
- package/dist/esm/backend/PhysicsBackend.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.d.ts +25 -4
- package/dist/esm/binding/BindingRegistry.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.js +70 -36
- package/dist/esm/binding/BindingRegistry.js.map +1 -1
- package/dist/esm/binding/PhysicsBinding.d.ts +22 -5
- package/dist/esm/binding/PhysicsBinding.d.ts.map +1 -0
- package/dist/esm/binding/PhysicsBinding.js +46 -21
- package/dist/esm/binding/PhysicsBinding.js.map +1 -1
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts +68 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js +149 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js.map +1 -0
- package/dist/esm/broadphase/BroadPhase.d.ts +7 -4
- package/dist/esm/broadphase/BroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/index.d.ts +2 -1
- package/dist/esm/broadphase/index.d.ts.map +1 -0
- package/dist/esm/collision/CollisionProxy.d.ts +3 -2
- package/dist/esm/collision/CollisionProxy.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.d.ts +5 -4
- package/dist/esm/collision/Manifold.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.js +34 -25
- package/dist/esm/collision/Manifold.js.map +1 -1
- package/dist/esm/collision/chainAdjacency.d.ts +29 -0
- package/dist/esm/collision/chainAdjacency.d.ts.map +1 -0
- package/dist/esm/collision/chainAdjacency.js +44 -0
- package/dist/esm/collision/chainAdjacency.js.map +1 -0
- package/dist/esm/collision/dispatch.d.ts +35 -0
- package/dist/esm/collision/dispatch.d.ts.map +1 -0
- package/dist/esm/collision/dispatch.js +58 -0
- package/dist/esm/collision/dispatch.js.map +1 -0
- package/dist/esm/collision/index.d.ts +2 -0
- package/dist/esm/collision/index.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.d.ts +5 -2
- package/dist/esm/collision/narrowphase.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.js +524 -399
- package/dist/esm/collision/narrowphase.js.map +1 -1
- package/dist/esm/collision/segments.d.ts +19 -0
- package/dist/esm/collision/segments.d.ts.map +1 -0
- package/dist/esm/collision/segments.js +36 -0
- package/dist/esm/collision/segments.js.map +1 -0
- package/dist/esm/collision/sweep.d.ts +35 -0
- package/dist/esm/collision/sweep.d.ts.map +1 -0
- package/dist/esm/collision/sweep.js +437 -0
- package/dist/esm/collision/sweep.js.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.d.ts +23 -4
- package/dist/esm/debug/PhysicsDebugDraw.d.ts.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.js +230 -188
- package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
- package/dist/esm/debug/index.d.ts +1 -0
- package/dist/esm/debug/index.d.ts.map +1 -0
- package/dist/esm/debug/index.js +3 -2
- package/dist/esm/events.d.ts +1 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +24 -19
- package/dist/esm/joints/DistanceJoint.d.ts +4 -3
- package/dist/esm/joints/DistanceJoint.d.ts.map +1 -0
- package/dist/esm/joints/DistanceJoint.js +160 -172
- package/dist/esm/joints/DistanceJoint.js.map +1 -1
- package/dist/esm/joints/Joint.d.ts +5 -4
- package/dist/esm/joints/Joint.d.ts.map +1 -0
- package/dist/esm/joints/Joint.js +23 -21
- package/dist/esm/joints/Joint.js.map +1 -1
- package/dist/esm/joints/MouseJoint.d.ts +8 -7
- package/dist/esm/joints/MouseJoint.d.ts.map +1 -0
- package/dist/esm/joints/MouseJoint.js +132 -133
- package/dist/esm/joints/MouseJoint.js.map +1 -1
- package/dist/esm/joints/PrismaticJoint.d.ts +5 -4
- package/dist/esm/joints/PrismaticJoint.d.ts.map +1 -0
- package/dist/esm/joints/PrismaticJoint.js +211 -240
- package/dist/esm/joints/PrismaticJoint.js.map +1 -1
- package/dist/esm/joints/RevoluteJoint.d.ts +4 -3
- package/dist/esm/joints/RevoluteJoint.d.ts.map +1 -0
- package/dist/esm/joints/RevoluteJoint.js +191 -213
- package/dist/esm/joints/RevoluteJoint.js.map +1 -1
- package/dist/esm/joints/WeldJoint.d.ts +3 -2
- package/dist/esm/joints/WeldJoint.d.ts.map +1 -0
- package/dist/esm/joints/WeldJoint.js +156 -152
- package/dist/esm/joints/WeldJoint.js.map +1 -1
- package/dist/esm/joints/WheelJoint.d.ts +4 -3
- package/dist/esm/joints/WheelJoint.d.ts.map +1 -0
- package/dist/esm/joints/WheelJoint.js +228 -255
- package/dist/esm/joints/WheelJoint.js.map +1 -1
- package/dist/esm/math.d.ts +6 -9
- package/dist/esm/math.d.ts.map +1 -0
- package/dist/esm/math.js +37 -39
- package/dist/esm/math.js.map +1 -1
- package/dist/esm/physicsBuildInfo.d.ts +1 -0
- package/dist/esm/physicsBuildInfo.d.ts.map +1 -0
- package/dist/esm/physicsBuildInfo.js +6 -4
- package/dist/esm/physicsBuildInfo.js.map +1 -1
- package/dist/esm/public.d.ts +8 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.d.ts +56 -12
- package/dist/esm/query/QueryEngine.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.js +494 -237
- package/dist/esm/query/QueryEngine.js.map +1 -1
- package/dist/esm/query/SpatialIndex.d.ts +34 -0
- package/dist/esm/query/SpatialIndex.d.ts.map +1 -0
- package/dist/esm/shapes/AnyShape.d.ts +9 -4
- package/dist/esm/shapes/AnyShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.d.ts +2 -1
- package/dist/esm/shapes/BoxShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.js +37 -25
- package/dist/esm/shapes/BoxShape.js.map +1 -1
- package/dist/esm/shapes/CapsuleShape.d.ts +30 -0
- package/dist/esm/shapes/CapsuleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CapsuleShape.js +88 -0
- package/dist/esm/shapes/CapsuleShape.js.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts +30 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.js +66 -0
- package/dist/esm/shapes/ChainEdgeShape.js.map +1 -0
- package/dist/esm/shapes/ChainShape.d.ts +52 -0
- package/dist/esm/shapes/ChainShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainShape.js +85 -0
- package/dist/esm/shapes/ChainShape.js.map +1 -0
- package/dist/esm/shapes/CircleShape.d.ts +3 -4
- package/dist/esm/shapes/CircleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CircleShape.js +27 -26
- package/dist/esm/shapes/CircleShape.js.map +1 -1
- package/dist/esm/shapes/PolygonShape.d.ts +7 -8
- package/dist/esm/shapes/PolygonShape.d.ts.map +1 -0
- package/dist/esm/shapes/PolygonShape.js +131 -162
- package/dist/esm/shapes/PolygonShape.js.map +1 -1
- package/dist/esm/shapes/SegmentShape.d.ts +31 -0
- package/dist/esm/shapes/SegmentShape.d.ts.map +1 -0
- package/dist/esm/shapes/SegmentShape.js +62 -0
- package/dist/esm/shapes/SegmentShape.js.map +1 -0
- package/dist/esm/shapes/Shape.d.ts +28 -17
- package/dist/esm/shapes/Shape.d.ts.map +1 -0
- package/dist/esm/shapes/Shape.js +12 -13
- package/dist/esm/shapes/Shape.js.map +1 -1
- package/dist/esm/shapes/convexParts.d.ts +21 -0
- package/dist/esm/shapes/convexParts.d.ts.map +1 -0
- package/dist/esm/shapes/convexParts.js +26 -0
- package/dist/esm/shapes/convexParts.js.map +1 -0
- package/dist/esm/shapes/decompose.d.ts +24 -0
- package/dist/esm/shapes/decompose.d.ts.map +1 -0
- package/dist/esm/shapes/decompose.js +272 -0
- package/dist/esm/shapes/decompose.js.map +1 -0
- package/dist/esm/shapes/index.d.ts +4 -0
- package/dist/esm/shapes/index.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.d.ts +12 -5
- package/dist/esm/solver/ContactSolver.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.js +425 -471
- package/dist/esm/solver/ContactSolver.js.map +1 -1
- package/dist/esm/solver/tolerances.d.ts +34 -0
- package/dist/esm/solver/tolerances.d.ts.map +1 -0
- package/dist/esm/solver/tolerances.js +38 -0
- package/dist/esm/solver/tolerances.js.map +1 -0
- package/dist/esm/sort.d.ts +5 -4
- package/dist/esm/sort.d.ts.map +1 -0
- package/dist/esm/sort.js +41 -51
- package/dist/esm/sort.js.map +1 -1
- package/dist/esm/types.d.ts +4 -8
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +15 -17
- package/dist/esm/types.js.map +1 -1
- package/package.json +6 -6
- package/dist/esm/broadphase/SweepAndPrune.d.ts +0 -15
- package/dist/esm/broadphase/SweepAndPrune.js +0 -77
- package/dist/esm/broadphase/SweepAndPrune.js.map +0 -1
- package/dist/esm/debug/index.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
package/dist/esm/PhysicsWorld.js
CHANGED
|
@@ -1,630 +1,894 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { aabbOverlap, createAabb } from "./Aabb.js";
|
|
2
|
+
import { shouldCollide } from "./types.js";
|
|
3
|
+
import { Collider, authoredCollider } from "./Collider.js";
|
|
4
|
+
import { PhysicsBody } from "./PhysicsBody.js";
|
|
5
|
+
import { sleepPenetrationTolerance } from "./solver/tolerances.js";
|
|
6
|
+
import { NativePhysicsBackend } from "./backend/NativePhysicsBackend.js";
|
|
7
|
+
import { BindingRegistry } from "./binding/BindingRegistry.js";
|
|
8
|
+
import { sweepProxies } from "./collision/sweep.js";
|
|
9
|
+
import { QueryEngine } from "./query/QueryEngine.js";
|
|
10
|
+
import { TimeStepper } from "./TimeStepper.js";
|
|
11
|
+
import { Signal, Vector } from "@codexo/exojs";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
const ccdSkin = 0.05;
|
|
13
|
+
//#region src/PhysicsWorld.ts
|
|
11
14
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
15
|
+
* Overlap left after clamping a bullet at its time of impact (the clamp
|
|
16
|
+
* overshoots the contact by this much along the motion), so the next step's
|
|
17
|
+
* discrete detection forms a real contact and the solver owns resting,
|
|
18
|
+
* friction and events from then on.
|
|
19
|
+
*/
|
|
20
|
+
const ccdEmbed = .05;
|
|
21
|
+
/**
|
|
22
|
+
* Keep a host-supplied blend factor inside `[0, 1]`. A caller's own accumulator
|
|
23
|
+
* can hand over a value outside it (a stale read, a custom scheduler), and an
|
|
24
|
+
* unclamped one extrapolates the node past the simulated state instead of
|
|
25
|
+
* blending between two known ones.
|
|
26
|
+
*/
|
|
27
|
+
const clampAlpha = (alpha) => {
|
|
28
|
+
if (!(alpha > 0)) return 0;
|
|
29
|
+
return alpha < 1 ? alpha : 1;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Reject removing the last collider a dynamic body's mass rests on while it still
|
|
33
|
+
* holds others. Such a body keeps colliding but has `invMass === 0`, which the
|
|
34
|
+
* solver cannot tell apart from a static body - the same state `PhysicsBody`
|
|
35
|
+
* refuses to be constructed in.
|
|
36
|
+
*
|
|
37
|
+
* Removing the body's *only* collider is fine: a body with no geometry at all is
|
|
38
|
+
* a body still being assembled, not a silently broken one.
|
|
39
|
+
*/
|
|
40
|
+
const assertBodyKeepsItsMass = (collider) => {
|
|
41
|
+
const body = collider.body;
|
|
42
|
+
if (body.type !== "dynamic" || collider.shape.massProperties === null || collider.density <= 0) return;
|
|
43
|
+
let remaining = 0;
|
|
44
|
+
for (const other of body.colliders) {
|
|
45
|
+
if (other === collider) continue;
|
|
46
|
+
if (other.shape.massProperties !== null && other.density > 0) return;
|
|
47
|
+
remaining++;
|
|
48
|
+
}
|
|
49
|
+
if (remaining === 0) return;
|
|
50
|
+
throw new Error("PhysicsWorld.destroyCollider: this is the only collider carrying mass for a dynamic body — removing it would leave the body colliding but massless. Destroy the body instead, or give it another solid collider first.");
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* `true` when a static/kinematic body - an island boundary, never an island
|
|
54
|
+
* member - is being driven this step, either through its velocity or by a
|
|
55
|
+
* {@link PhysicsBody.setTransform} teleport. Speed is deliberately not compared
|
|
56
|
+
* against the sleep thresholds: a platform creeping along at 1 px/s still has to
|
|
57
|
+
* push its passengers, and it is exactly that sub-threshold case where nothing
|
|
58
|
+
* else would keep them awake.
|
|
59
|
+
*/
|
|
60
|
+
const isMovingBoundary = (body) => body._teleported || body.linearVelocityX !== 0 || body.linearVelocityY !== 0 || body.angularVelocity !== 0;
|
|
61
|
+
/**
|
|
62
|
+
* `true` while a contact still carries more overlap than the solver leaves
|
|
63
|
+
* behind at rest. Read from the manifold detection just produced, which is the
|
|
64
|
+
* same separation the solver's push-out bias is about to act on - the sleep gate
|
|
65
|
+
* and the constraint it gates therefore agree by construction.
|
|
66
|
+
*/
|
|
67
|
+
const isUnresolved = (manifold) => {
|
|
68
|
+
for (let i = 0; i < manifold.pointCount; i++) if ((i === 0 ? manifold.points[0] : manifold.points[1]).penetration > .75) return true;
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* {@link PhysicsWorld.attach}'s default `position`: `node`'s current WORLD
|
|
73
|
+
* translation, duck-typed the same way `AudioListener` reads a follow target -
|
|
74
|
+
* real {@link SceneNode}s expose `getWorldTransform()`, test doubles that omit
|
|
75
|
+
* it fall back to `(0, 0)` (the previous, surprising default).
|
|
76
|
+
*/
|
|
77
|
+
const worldPositionOf = (node) => {
|
|
78
|
+
const asNode = node;
|
|
79
|
+
if (typeof asNode.getWorldTransform === "function") {
|
|
80
|
+
const world = asNode.getWorldTransform();
|
|
81
|
+
return {
|
|
82
|
+
x: world.x,
|
|
83
|
+
y: world.y
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
x: 0,
|
|
88
|
+
y: 0
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* {@link PhysicsWorld.attach}'s default `angle` (radians): `node`'s current
|
|
93
|
+
* WORLD rotation, decomposed from `getWorldTransform()`'s linear part
|
|
94
|
+
* (`atan2(-c, a)` - `SceneNode.updateTransform` builds its rotation block as
|
|
95
|
+
* `[[cosθ, sinθ], [-sinθ, cosθ]]`, i.e. `b = sinθ`/`c = -sinθ`, matching the
|
|
96
|
+
* radians ⇄ degrees round-trip `PhysicsBinding.sync` already relies on).
|
|
97
|
+
* Falls back to `0` for a duck-typed node without `getWorldTransform`.
|
|
98
|
+
*/
|
|
99
|
+
const worldAngleOf = (node) => {
|
|
100
|
+
const asNode = node;
|
|
101
|
+
if (typeof asNode.getWorldTransform === "function") {
|
|
102
|
+
const world = asNode.getWorldTransform();
|
|
103
|
+
return Math.atan2(-world.c, world.a);
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The collision/query world: owns bodies, colliders, the detection backend,
|
|
109
|
+
* bindings, the query engine and the fixed-step accumulator. Stepped by the
|
|
110
|
+
* caller (commonly from a `Scene.update`), each fixed sub-step it integrates
|
|
111
|
+
* body velocities, runs broad- and narrow-phase detection, solves contacts and
|
|
112
|
+
* integrates positions, then fires immutable contact/sensor events and writes
|
|
113
|
+
* bound node transforms. It holds **no module-level state**, so any number of
|
|
114
|
+
* worlds run in isolation.
|
|
115
|
+
*
|
|
116
|
+
* The dynamics are a native, warm-started **TGS-Soft** solver (Box2D-v3 "soft
|
|
117
|
+
* step"): each fixed step runs detection once, then several sub-steps, each
|
|
118
|
+
* integrating gravity over the sub-step and solving contacts with a soft
|
|
119
|
+
* position bias plus a bias-free relax pass; a 2-point block normal solve
|
|
120
|
+
* propagates stack loads, and restitution is a separate final pass. Decoupling
|
|
121
|
+
* stiffness from the iteration count keeps tall towers stable. The detection
|
|
122
|
+
* backend sits behind an internal seam, so the solver is swappable without
|
|
123
|
+
* touching this public surface.
|
|
124
|
+
*
|
|
125
|
+
* **Operating envelope.** The soft solver trades a little accuracy for
|
|
126
|
+
* robustness, so it has a few documented limits - each stays finite and stable,
|
|
127
|
+
* and each is pinned by a gate in `dynamics.test.ts` or
|
|
128
|
+
* `contact-push-out.test.ts`:
|
|
129
|
+
* - **Resting contacts** settle within a small, fixed tolerance. A face contact
|
|
130
|
+
* settles at exactly that tolerance; a single-point contact settles slightly
|
|
131
|
+
* deeper, and that gap grows with acceleration. Very high accelerations
|
|
132
|
+
* eventually exceed what the push-out can resolve within a step, at which
|
|
133
|
+
* point the contact holds a slightly deeper resting depth instead of the
|
|
134
|
+
* tolerance - keep gravity in the range ordinary 2D games use.
|
|
135
|
+
* - **Mass ratio** - contacts between bodies of very different mass degrade
|
|
136
|
+
* gradually rather than at a fixed ratio. What sets the practical limit is how
|
|
137
|
+
* thick the supporting geometry is relative to the lighter body, not the ratio
|
|
138
|
+
* alone: a light body squeezed against a boundary thinner than itself is the
|
|
139
|
+
* case that fails first, and it fails by sinking through.
|
|
140
|
+
* - **Scale-relative behaviour** - the solver's tolerances are absolute lengths,
|
|
141
|
+
* so a very small shape sees them as a large fraction of itself, and a very
|
|
142
|
+
* large one as a negligible one. Keep the shapes of one world within a couple
|
|
143
|
+
* of orders of magnitude of each other.
|
|
144
|
+
* - **CCD is opt-in and translation-only** - detection runs once per fixed
|
|
145
|
+
* step, so an ordinary body that crosses more than roughly half a barrier's
|
|
146
|
+
* total thickness within one step may end up on the wrong side of it, either
|
|
147
|
+
* passing through or being resolved out of the far face. Flag fast
|
|
148
|
+
* projectiles with {@link PhysicsBody.isBullet}: each of the body's colliders
|
|
149
|
+
* is then shape-cast along the step's motion (an exact translation-only sweep
|
|
150
|
+
* of the full shape, not just the centre) and clamped at the first impact.
|
|
151
|
+
* Rotation over the step is not swept - a long body spinning fast enough to
|
|
152
|
+
* sweep past an obstacle within one step can still miss it.
|
|
153
|
+
* - **{@link PhysicsWorldOptions.subStepCount}** - the default `4` is
|
|
154
|
+
* load-bearing for tall-stack stability; lowering it below `2` visibly
|
|
155
|
+
* degrades stacking, so do not reduce it for performance.
|
|
156
|
+
* - **Broad phase is a dynamic AABB tree** (`AabbTreeBroadPhase`), stateful
|
|
157
|
+
* across fixed steps: a collider whose tight AABB stays inside its stored
|
|
158
|
+
* fat AABB costs nothing to re-sync, and only colliders that actually move
|
|
159
|
+
* outside their margin trigger a tree update and a local re-query for new
|
|
160
|
+
* neighbours. Detection still walks every live collider once per step (a
|
|
161
|
+
* cheap containment check for each), so there is a small linear floor, but
|
|
162
|
+
* the dominant cost - reinsertion and neighbour discovery - is driven by
|
|
163
|
+
* how much actually moved, not by the total live collider count; sleeping
|
|
164
|
+
* bodies skip that dominant cost entirely. Scales to tens of thousands of
|
|
165
|
+
* simultaneously-live colliders, including dense clusters that would
|
|
166
|
+
* degrade a sort-and-sweep broad phase; very large or highly dynamic
|
|
167
|
+
* worlds may still benefit from splitting into several smaller
|
|
168
|
+
* `PhysicsWorld` instances (e.g. per room/chunk).
|
|
169
|
+
*/
|
|
170
|
+
var PhysicsWorld = class {
|
|
171
|
+
/** Fires when two solid colliders begin touching. Argument is an immutable snapshot. */
|
|
172
|
+
onCollisionStart = new Signal();
|
|
173
|
+
/** Fires when two solid colliders stop touching (or one is destroyed). */
|
|
174
|
+
onCollisionEnd = new Signal();
|
|
175
|
+
/** Fires when a collider enters a sensor. */
|
|
176
|
+
onSensorEnter = new Signal();
|
|
177
|
+
/** Fires when a collider leaves a sensor. */
|
|
178
|
+
onSensorExit = new Signal();
|
|
179
|
+
/** World gravity (px/s², +Y down). Integrated each sub-step. */
|
|
180
|
+
gravity;
|
|
181
|
+
/** The fixed-step accumulator. */
|
|
182
|
+
timeStepper;
|
|
183
|
+
/** TGS-Soft sub-steps per fixed step. */
|
|
184
|
+
subStepCount;
|
|
185
|
+
/** Soft-contact stiffness in Hz. */
|
|
186
|
+
contactHertz;
|
|
187
|
+
/** Soft-contact damping ratio. */
|
|
188
|
+
dampingRatio;
|
|
189
|
+
/** Whether resting bodies are put to sleep. */
|
|
190
|
+
enableSleeping;
|
|
191
|
+
/** Linear sleep threshold (px/s). */
|
|
192
|
+
sleepLinearVelocity;
|
|
193
|
+
/** Angular sleep threshold (rad/s). */
|
|
194
|
+
sleepAngularVelocity;
|
|
195
|
+
/** Seconds below the thresholds before a body sleeps. */
|
|
196
|
+
timeToSleep;
|
|
197
|
+
/**
|
|
198
|
+
* Runs once per solid contact per fixed step, after contact generation and
|
|
199
|
+
* before island building and the solver, so a contact it disables applies no
|
|
200
|
+
* impulse and does not couple its two bodies into one sleeping island.
|
|
201
|
+
*
|
|
202
|
+
* At most one modifier per world - it mutates simulation state, so a
|
|
203
|
+
* multi-listener signal would make the outcome depend on registration order.
|
|
204
|
+
* Set to `null` to remove it; the per-contact values then stay at the
|
|
205
|
+
* defaults derived from the two colliders.
|
|
206
|
+
*/
|
|
207
|
+
contactModifier;
|
|
208
|
+
/**
|
|
209
|
+
* Whether bound nodes are placed between the last two fixed states rather than
|
|
210
|
+
* snapped to the latest one. Toggleable at runtime; switching it off snaps
|
|
211
|
+
* every bound node back to the current fixed state on the next sync.
|
|
212
|
+
*/
|
|
213
|
+
interpolation;
|
|
214
|
+
/**
|
|
215
|
+
* Supplies the `[0, 1)` blend factor for interpolated bindings. Defaults to
|
|
216
|
+
* this world's own accumulator; point it at the host's own fraction
|
|
217
|
+
* (`() => app.frameAlpha`) when the world runs as a `System`, because
|
|
218
|
+
* {@link fixedUpdate} never advances the local accumulator.
|
|
219
|
+
*/
|
|
220
|
+
frameAlphaSource;
|
|
221
|
+
_backend = new NativePhysicsBackend();
|
|
222
|
+
_bodies = [];
|
|
223
|
+
_colliders = [];
|
|
224
|
+
/**
|
|
225
|
+
* What detection actually runs over: the authored colliders, with a chain
|
|
226
|
+
* replaced by the edge proxies it fans out into. The broad phase, the narrow
|
|
227
|
+
* phase and the queries all read this list; `_colliders` stays the authored
|
|
228
|
+
* set every public surface reports.
|
|
229
|
+
*/
|
|
230
|
+
_detectionColliders = [];
|
|
231
|
+
/** Pooled scratch for re-syncing a single body's broad-phase leaves. */
|
|
232
|
+
_leafScratch = [];
|
|
233
|
+
_joints = [];
|
|
234
|
+
_bindings = new BindingRegistry();
|
|
235
|
+
_query;
|
|
236
|
+
_commands = [];
|
|
237
|
+
/** Pooled union-find parent array for the per-step island pass (reused; sized to the body count). */
|
|
238
|
+
_islandParent = [];
|
|
239
|
+
/** Pooled per-island minimum sleep time, indexed by union-find root. */
|
|
240
|
+
_islandMinSleep = [];
|
|
241
|
+
/** Pooled scratch for the CCD pass (clamp target, per-pair sweep hit, best hit, swept AABB) - keeps the sweep allocation-free. */
|
|
242
|
+
_ccdClampPosition = {
|
|
243
|
+
x: 0,
|
|
244
|
+
y: 0
|
|
245
|
+
};
|
|
246
|
+
_ccdSweepHit = {
|
|
247
|
+
t: 0,
|
|
248
|
+
normalX: 0,
|
|
249
|
+
normalY: 0
|
|
250
|
+
};
|
|
251
|
+
_ccdBestHit = {
|
|
252
|
+
t: 0,
|
|
253
|
+
normalX: 0,
|
|
254
|
+
normalY: 0
|
|
255
|
+
};
|
|
256
|
+
_ccdSweptAabb = createAabb();
|
|
257
|
+
/** Pooled result buffer for the CCD pass's broad-phase query (refilled per bullet collider). */
|
|
258
|
+
_ccdCandidates = [];
|
|
259
|
+
/**
|
|
260
|
+
* Narrow-phase sweep tests run by the CCD pass since world creation. Pairs
|
|
261
|
+
* rejected by the swept-AABB prune never count, so slow bullets far from any
|
|
262
|
+
* geometry keep this at zero (the cheap path).
|
|
263
|
+
*
|
|
264
|
+
* @internal - test/diagnostic hook.
|
|
265
|
+
*/
|
|
266
|
+
_ccdSweepTests = 0;
|
|
267
|
+
/**
|
|
268
|
+
* Colliders the CCD pass pulled out of the broad phase since world creation -
|
|
269
|
+
* the size of the candidate set the swept-AABB prune and the narrow-phase
|
|
270
|
+
* sweep then work on. Counted separately from {@link _ccdSweepTests} because
|
|
271
|
+
* the two measure different halves: this is what the pass looks at, that is
|
|
272
|
+
* what it actually sweeps. Stays proportional to the geometry near the
|
|
273
|
+
* bullets, not to the world's collider count.
|
|
274
|
+
*
|
|
275
|
+
* @internal - test/diagnostic hook.
|
|
276
|
+
*/
|
|
277
|
+
_ccdBroadPhaseCandidates = 0;
|
|
278
|
+
_nextBodyId = 1;
|
|
279
|
+
_nextColliderId = 1;
|
|
280
|
+
_dispatching = false;
|
|
281
|
+
_destroyed = false;
|
|
282
|
+
constructor(options = {}) {
|
|
283
|
+
this.gravity = new Vector(options.gravity?.x ?? 0, options.gravity?.y ?? 0);
|
|
284
|
+
this.timeStepper = new TimeStepper({
|
|
285
|
+
...options.fixedDelta !== void 0 && { fixedDelta: options.fixedDelta },
|
|
286
|
+
...options.maxSubSteps !== void 0 && { maxSubSteps: options.maxSubSteps }
|
|
287
|
+
});
|
|
288
|
+
const subStepCount = options.subStepCount ?? 4;
|
|
289
|
+
if (!Number.isInteger(subStepCount) || subStepCount < 1) throw new RangeError(`PhysicsWorld: subStepCount must be an integer ≥ 1, received ${subStepCount}.`);
|
|
290
|
+
this.subStepCount = subStepCount;
|
|
291
|
+
this.contactHertz = options.contactHertz ?? 30;
|
|
292
|
+
this.dampingRatio = options.dampingRatio ?? 10;
|
|
293
|
+
this.enableSleeping = options.enableSleeping ?? true;
|
|
294
|
+
this.sleepLinearVelocity = options.sleepLinearVelocity ?? 5;
|
|
295
|
+
this.sleepAngularVelocity = options.sleepAngularVelocity ?? .06;
|
|
296
|
+
this.timeToSleep = options.timeToSleep ?? .5;
|
|
297
|
+
this.contactModifier = options.contactModifier ?? null;
|
|
298
|
+
this.interpolation = options.interpolation ?? false;
|
|
299
|
+
this.frameAlphaSource = options.frameAlphaSource ?? (() => this.timeStepper.alpha);
|
|
300
|
+
this._query = new QueryEngine(this._detectionColliders, this._backend.spatialIndex);
|
|
301
|
+
}
|
|
302
|
+
/** Live bodies (read-only view). */
|
|
303
|
+
get bodies() {
|
|
304
|
+
return this._bodies;
|
|
305
|
+
}
|
|
306
|
+
/** Live colliders (read-only view). */
|
|
307
|
+
get colliders() {
|
|
308
|
+
return this._colliders;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* @internal - the geometry the broad phase and the narrow phase actually hold:
|
|
312
|
+
* every authored collider, with a chain replaced by its per-edge proxies.
|
|
313
|
+
* Consumed by the debug draw layer, which visualises what the solver sees;
|
|
314
|
+
* public identity stays {@link colliders}.
|
|
315
|
+
*/
|
|
316
|
+
get detectionGeometry() {
|
|
317
|
+
return this._detectionColliders;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Add a body to the world: allocates the body and its collider ids, registers
|
|
321
|
+
* the colliders, computes the mass model and tracks the body for stepping.
|
|
322
|
+
* Construct the body freely first (`new PhysicsBody({ ... })`), then add it.
|
|
323
|
+
* Safe to call inside an event callback - the body push is deferred to the end
|
|
324
|
+
* of the step, exactly like collider registration. Returns the body.
|
|
325
|
+
*
|
|
326
|
+
* @throws if the body has already been added to a world.
|
|
327
|
+
*/
|
|
328
|
+
add(body) {
|
|
329
|
+
this._assertAlive();
|
|
330
|
+
if (body.attached) throw new Error("PhysicsWorld.add: this body has already been added to a world.");
|
|
331
|
+
body._attachToWorld(this, this._nextBodyId++);
|
|
332
|
+
this._defer(() => {
|
|
333
|
+
if (!body.destroyed) this._bodies.push(body);
|
|
334
|
+
});
|
|
335
|
+
return body;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Convenience: create a body carrying a single collider, add it to the world
|
|
339
|
+
* and bind it to `node` in one call. The node tracks `body.position` after each
|
|
340
|
+
* step. Returns the body. Equivalent to `new PhysicsBody(...)` + `add` + `bind`.
|
|
341
|
+
*
|
|
342
|
+
* When `options.position`/`options.angle` are omitted, the body starts at
|
|
343
|
+
* `node`'s current WORLD position/rotation (via `getWorldTransform()`,
|
|
344
|
+
* composed through any transform-group boundary) rather than `(0, 0)` -
|
|
345
|
+
* otherwise a body attached to an already-placed node would visibly "teleport"
|
|
346
|
+
* to the origin on the next step. Pass `position`/`angle` explicitly to override.
|
|
347
|
+
*/
|
|
348
|
+
attach(node, options) {
|
|
349
|
+
const body = new PhysicsBody({
|
|
350
|
+
...options.type !== void 0 && { type: options.type },
|
|
351
|
+
position: options.position ?? worldPositionOf(node),
|
|
352
|
+
angle: options.angle ?? worldAngleOf(node),
|
|
353
|
+
...options.gravityScale !== void 0 && { gravityScale: options.gravityScale },
|
|
354
|
+
...options.fixedRotation !== void 0 && { fixedRotation: options.fixedRotation },
|
|
355
|
+
colliders: [new Collider({
|
|
356
|
+
shape: options.shape,
|
|
357
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
358
|
+
...options.rotation !== void 0 && { rotation: options.rotation },
|
|
359
|
+
...options.density !== void 0 && { density: options.density },
|
|
360
|
+
...options.friction !== void 0 && { friction: options.friction },
|
|
361
|
+
...options.restitution !== void 0 && { restitution: options.restitution },
|
|
362
|
+
...options.isSensor !== void 0 && { isSensor: options.isSensor },
|
|
363
|
+
...options.filter !== void 0 && { filter: options.filter }
|
|
364
|
+
})]
|
|
365
|
+
});
|
|
366
|
+
this.add(body);
|
|
367
|
+
this.bind(body, node);
|
|
368
|
+
return body;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Destroy a body and its colliders. Every dynamic body touching it is woken,
|
|
372
|
+
* so anything that was resting on the destroyed body falls once its support is
|
|
373
|
+
* gone. Deferred when called inside a callback.
|
|
374
|
+
*/
|
|
375
|
+
destroyBody(body) {
|
|
376
|
+
this._defer(() => this._removeBody(body));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Destroy a single collider, recomputing its body's mass. Wakes every dynamic
|
|
380
|
+
* body touching it, the same way {@link destroyBody} does. Deferred when
|
|
381
|
+
* called inside a callback.
|
|
382
|
+
*/
|
|
383
|
+
destroyCollider(collider) {
|
|
384
|
+
assertBodyKeepsItsMass(collider);
|
|
385
|
+
this._defer(() => this._removeCollider(collider));
|
|
386
|
+
}
|
|
387
|
+
/** Live joints (read-only view). */
|
|
388
|
+
get joints() {
|
|
389
|
+
return this._joints;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Add a constraint joint. Construct it first (`new DistanceJoint({ ... })`),
|
|
393
|
+
* then add it. Wakes both bodies; safe inside a callback (registration is
|
|
394
|
+
* deferred). Returns the joint.
|
|
395
|
+
*/
|
|
396
|
+
addJoint(joint) {
|
|
397
|
+
this._assertAlive();
|
|
398
|
+
joint.bodyA.wake();
|
|
399
|
+
joint.bodyB.wake();
|
|
400
|
+
this._defer(() => {
|
|
401
|
+
if (!this._joints.includes(joint)) this._joints.push(joint);
|
|
402
|
+
});
|
|
403
|
+
return joint;
|
|
404
|
+
}
|
|
405
|
+
/** Remove a joint, waking both bodies so they respond to the lost constraint. Deferred when called inside a callback. */
|
|
406
|
+
removeJoint(joint) {
|
|
407
|
+
joint.bodyA.wake();
|
|
408
|
+
joint.bodyB.wake();
|
|
409
|
+
this._defer(() => {
|
|
410
|
+
const index = this._joints.indexOf(joint);
|
|
411
|
+
if (index !== -1) this._joints.splice(index, 1);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Advance the world by `frameDeltaSeconds`. Accumulates into fixed steps; each
|
|
416
|
+
* fixed step runs detection once, then a TGS-Soft sub-step loop (integrate
|
|
417
|
+
* gravity, solve contacts with a soft bias, integrate positions, relax) and a
|
|
418
|
+
* restitution pass, then writes the accumulated motion into each body. Finally
|
|
419
|
+
* dispatches events and writes bound node transforms.
|
|
420
|
+
*
|
|
421
|
+
* **Prefer registering the world as a system instead**
|
|
422
|
+
* (`app.systems.add(world, { order: SystemOrder.Physics })` or the scene
|
|
423
|
+
* equivalent) so {@link fixedUpdate} drives stepping directly from the
|
|
424
|
+
* engine's own fixed-timestep scheduler - one call per fixed step, no
|
|
425
|
+
* accumulator duplication. `step` remains available here for manual/advanced
|
|
426
|
+
* driving (e.g. a fixed-but-non-standard rate, or stepping outside the
|
|
427
|
+
* normal frame loop entirely): pass any delta and this accumulator converts
|
|
428
|
+
* it into the right number of fixed sub-steps (0, 1, or several, clamped by
|
|
429
|
+
* {@link PhysicsWorldOptions.maxSubSteps}).
|
|
430
|
+
*
|
|
431
|
+
* Either way the simulation is frame-rate independent and deterministic -
|
|
432
|
+
* the same sequence of deltas replays identically. `timeStepper.alpha`
|
|
433
|
+
* (`[0, 1)`) is the leftover sub-step fraction after this call, for callers
|
|
434
|
+
* that want to interpolate a bound node's rendered position between the last
|
|
435
|
+
* two fixed states instead of snapping to the latest one (bindings do not do
|
|
436
|
+
* this automatically - {@link bind} always writes the latest fixed-step
|
|
437
|
+
* transform verbatim).
|
|
438
|
+
*/
|
|
439
|
+
step(frameDeltaSeconds) {
|
|
440
|
+
this._assertAlive();
|
|
441
|
+
const steps = this.timeStepper.advance(frameDeltaSeconds);
|
|
442
|
+
if (steps > 0) {
|
|
443
|
+
const subStepCount = this.subStepCount;
|
|
444
|
+
const h = this.timeStepper.fixedDelta / subStepCount;
|
|
445
|
+
const gravityX = this.gravity.x;
|
|
446
|
+
const gravityY = this.gravity.y;
|
|
447
|
+
const contactHertz = this.contactHertz;
|
|
448
|
+
const dampingRatio = this.dampingRatio;
|
|
449
|
+
const hasJoints = this._joints.length > 0;
|
|
450
|
+
const hasBullets = this._hasBullets();
|
|
451
|
+
for (let step = 0; step < steps; step++) this._stepOnce(h, subStepCount, gravityX, gravityY, contactHertz, dampingRatio, hasJoints, hasBullets);
|
|
452
|
+
this._dispatchEvents();
|
|
453
|
+
}
|
|
454
|
+
this._syncBindings();
|
|
455
|
+
this._drainCommands();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Advance by exactly one fixed step, driven directly by the engine's own
|
|
459
|
+
* fixed-timestep scheduler when this world is registered as a `System`
|
|
460
|
+
* (`app.systems.add(world, { order: SystemOrder.Physics })` or the scene
|
|
461
|
+
* equivalent) - bypasses {@link timeStepper}'s variable-delta accumulator
|
|
462
|
+
* entirely, since the caller has already decided exactly when a fixed step
|
|
463
|
+
* occurs. Prefer this over manual {@link step} once the world is
|
|
464
|
+
* system-registered; `step` remains available for advanced manual driving.
|
|
465
|
+
* Always advances by exactly {@link timeStepper}'s configured `fixedDelta`,
|
|
466
|
+
* regardless of the caller's actual fixed-step interval - if the engine's
|
|
467
|
+
* `Application.fixedTimeStep` doesn't match this world's `fixedDelta`, the
|
|
468
|
+
* simulation stays deterministic but runs at the wrong wall-clock speed
|
|
469
|
+
* relative to real time.
|
|
470
|
+
*/
|
|
471
|
+
fixedUpdate(_step) {
|
|
472
|
+
this._assertAlive();
|
|
473
|
+
const subStepCount = this.subStepCount;
|
|
474
|
+
const h = this.timeStepper.fixedDelta / subStepCount;
|
|
475
|
+
this._stepOnce(h, subStepCount, this.gravity.x, this.gravity.y, this.contactHertz, this.dampingRatio, this._joints.length > 0, this._hasBullets());
|
|
476
|
+
this._dispatchEvents();
|
|
477
|
+
if (!this.interpolation) this._bindings.sync();
|
|
478
|
+
this._drainCommands();
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Variable-rate `System` phase: places bound nodes for the frame that is about
|
|
482
|
+
* to be drawn. Only does work while {@link interpolation} is on - otherwise
|
|
483
|
+
* {@link fixedUpdate} has already snapped them to the latest fixed state.
|
|
484
|
+
*/
|
|
485
|
+
update(_delta) {
|
|
486
|
+
this._assertAlive();
|
|
487
|
+
if (this.interpolation) this._bindings.syncInterpolated(clampAlpha(this.frameAlphaSource()));
|
|
488
|
+
}
|
|
489
|
+
/** Present bound nodes, snapping or interpolating according to {@link interpolation}. */
|
|
490
|
+
_syncBindings() {
|
|
491
|
+
if (this.interpolation) this._bindings.syncInterpolated(clampAlpha(this.frameAlphaSource()));
|
|
492
|
+
else this._bindings.sync();
|
|
493
|
+
}
|
|
494
|
+
_stepOnce(h, subStepCount, gravityX, gravityY, contactHertz, dampingRatio, hasJoints, hasBullets) {
|
|
495
|
+
this._backend.detect(this._detectionColliders);
|
|
496
|
+
if (this.contactModifier !== null) this._backend.applyContactModifier(this.contactModifier);
|
|
497
|
+
if (this.enableSleeping) this._updateSleeping(this.timeStepper.fixedDelta);
|
|
498
|
+
this._backend.prepareSolve(h, contactHertz, dampingRatio);
|
|
499
|
+
if (hasJoints) this._prepareJoints(h);
|
|
500
|
+
if (hasBullets) this._recordBulletPositions();
|
|
501
|
+
for (let subStep = 0; subStep < subStepCount; subStep++) {
|
|
502
|
+
for (const body of this._bodies) body._integrateVelocity(h, gravityX, gravityY);
|
|
503
|
+
this._backend.warmStart();
|
|
504
|
+
if (hasJoints) this._warmStartJoints();
|
|
505
|
+
this._backend.solveVelocities(true);
|
|
506
|
+
if (hasJoints) this._solveJoints(true);
|
|
507
|
+
for (const body of this._bodies) body._integratePosition(h);
|
|
508
|
+
this._backend.solveVelocities(false);
|
|
509
|
+
if (hasJoints) this._solveJoints(false);
|
|
510
|
+
}
|
|
511
|
+
this._backend.applyRestitution();
|
|
512
|
+
for (const body of this._bodies) body._finalizePosition();
|
|
513
|
+
if (hasBullets) this._advanceBullets();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Link a body to a scene node; the node tracks the body after each step.
|
|
517
|
+
* Destroying the node is enough to end the link - the next step drops it
|
|
518
|
+
* rather than writing into the node's released transform, so an explicit
|
|
519
|
+
* {@link unbind} is only needed to stop tracking a node that stays alive.
|
|
520
|
+
*
|
|
521
|
+
* @throws if `node` is already destroyed.
|
|
522
|
+
*/
|
|
523
|
+
bind(body, node) {
|
|
524
|
+
return this._bindings.bind(body, node);
|
|
525
|
+
}
|
|
526
|
+
/** Remove a body↔node link. */
|
|
527
|
+
unbind(body) {
|
|
528
|
+
this._bindings.unbind(body);
|
|
529
|
+
}
|
|
530
|
+
/** Colliders containing `point`. Fresh array. */
|
|
531
|
+
queryPoint(point, filter) {
|
|
532
|
+
return this._query.queryPoint(point, filter);
|
|
533
|
+
}
|
|
534
|
+
/** Colliders whose AABB overlaps `bounds`. Writes into `out` (cleared) if given. */
|
|
535
|
+
queryAabb(bounds, filter, out) {
|
|
536
|
+
return this._query.queryAabb(bounds, filter, out);
|
|
537
|
+
}
|
|
538
|
+
/** Invoke `callback` for each collider whose AABB overlaps `bounds`. Allocation-free. */
|
|
539
|
+
forEachAabbHit(bounds, filter, callback) {
|
|
540
|
+
this._query.forEachAabbHit(bounds, filter, callback);
|
|
541
|
+
}
|
|
542
|
+
/** Nearest collider hit by the ray, or `null`. */
|
|
543
|
+
rayCast(origin, direction, filter, maxDistance) {
|
|
544
|
+
return this._query.rayCast(origin, direction, filter, maxDistance);
|
|
545
|
+
}
|
|
546
|
+
/** All collider hits along the ray, sorted by distance. Writes into `out` (cleared) if given. */
|
|
547
|
+
rayCastAll(origin, direction, filter, out, maxDistance) {
|
|
548
|
+
return this._query.rayCastAll(origin, direction, filter, out, maxDistance);
|
|
549
|
+
}
|
|
550
|
+
/** Colliders overlapping `shape` placed at `position`/`angle`. Fresh array. */
|
|
551
|
+
overlapShape(shape, position, filter, angle) {
|
|
552
|
+
return this._query.overlapShape(shape, position, filter, angle);
|
|
553
|
+
}
|
|
554
|
+
/** Release every body, collider, binding and backend resource. */
|
|
555
|
+
destroy() {
|
|
556
|
+
if (this._destroyed) return;
|
|
557
|
+
this._destroyed = true;
|
|
558
|
+
for (const body of this._bodies) {
|
|
559
|
+
for (const collider of body.colliders) collider._markDestroyed();
|
|
560
|
+
body._markDestroyed();
|
|
561
|
+
}
|
|
562
|
+
this._bodies.length = 0;
|
|
563
|
+
this._colliders.length = 0;
|
|
564
|
+
this._detectionColliders.length = 0;
|
|
565
|
+
this._joints.length = 0;
|
|
566
|
+
this._commands.length = 0;
|
|
567
|
+
this._bindings.clear();
|
|
568
|
+
this._backend.destroy();
|
|
569
|
+
this.onCollisionStart.destroy();
|
|
570
|
+
this.onCollisionEnd.destroy();
|
|
571
|
+
this.onSensorEnter.destroy();
|
|
572
|
+
this.onSensorExit.destroy();
|
|
573
|
+
}
|
|
574
|
+
_allocateColliderId() {
|
|
575
|
+
return this._nextColliderId++;
|
|
576
|
+
}
|
|
577
|
+
_registerCollider(collider) {
|
|
578
|
+
this._defer(() => {
|
|
579
|
+
if (collider.destroyed) return;
|
|
580
|
+
this._colliders.push(collider);
|
|
581
|
+
const edges = collider.chainEdges;
|
|
582
|
+
if (edges === null) {
|
|
583
|
+
this._detectionColliders.push(collider);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
for (const edge of edges) this._detectionColliders.push(edge);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/** The detection backend (internal; consumed by the debug draw layer). */
|
|
590
|
+
get backend() {
|
|
591
|
+
return this._backend;
|
|
592
|
+
}
|
|
593
|
+
_dispatchEvents() {
|
|
594
|
+
const graph = this._backend.contactGraph;
|
|
595
|
+
this._dispatching = true;
|
|
596
|
+
for (const event of graph.collisionEnd) this.onCollisionEnd.dispatch(event);
|
|
597
|
+
for (const event of graph.sensorExit) this.onSensorExit.dispatch(event);
|
|
598
|
+
for (const event of graph.collisionStart) this.onCollisionStart.dispatch(event);
|
|
599
|
+
for (const event of graph.sensorEnter) this.onSensorEnter.dispatch(event);
|
|
600
|
+
this._dispatching = false;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Accumulate per-body sleep timers and put/keep islands of resting bodies
|
|
604
|
+
* asleep so a stack sleeps and wakes as one unit. An island is a connected
|
|
605
|
+
* component of dynamic bodies joined by touching solid contacts (static and
|
|
606
|
+
* kinematic bodies are boundaries, not nodes); it sleeps once every member has
|
|
607
|
+
* stayed below the sleep thresholds for `timeToSleep`, and wakes the instant
|
|
608
|
+
* any member does (e.g. an awake body merges into it via a new contact).
|
|
609
|
+
* A boundary that is itself moving resets the sleep timer of every dynamic
|
|
610
|
+
* body it touches, so a platform keeps its passengers awake however slowly it
|
|
611
|
+
* travels; so does a contact still carrying more penetration than the solver
|
|
612
|
+
* leaves at rest, because the push-out that resolves it is slower than the
|
|
613
|
+
* sleep velocity threshold and a body would otherwise be frozen embedded.
|
|
614
|
+
* Deterministic: union-find roots break ties by lower index and the contact
|
|
615
|
+
* set is id-sorted.
|
|
616
|
+
*/
|
|
617
|
+
_updateSleeping(dt) {
|
|
618
|
+
const bodies = this._bodies;
|
|
619
|
+
const count = bodies.length;
|
|
620
|
+
const parent = this._islandParent;
|
|
621
|
+
const minSleep = this._islandMinSleep;
|
|
622
|
+
for (let i = 0; i < count; i++) {
|
|
623
|
+
const body = bodies[i];
|
|
624
|
+
body._islandIndex = i;
|
|
625
|
+
parent[i] = i;
|
|
626
|
+
minSleep[i] = Infinity;
|
|
627
|
+
if (body.type === "dynamic" && !body.isSleeping) body._accumulateSleepTime(dt, this.sleepLinearVelocity, this.sleepAngularVelocity);
|
|
628
|
+
}
|
|
629
|
+
parent.length = count;
|
|
630
|
+
minSleep.length = count;
|
|
631
|
+
this._unionContactIslands();
|
|
632
|
+
for (const joint of this._joints) {
|
|
633
|
+
const bodyA = joint.bodyA;
|
|
634
|
+
const bodyB = joint.bodyB;
|
|
635
|
+
if (joint.enabled && bodyA.type === "dynamic" && bodyB.type === "dynamic") this._union(bodyA._islandIndex, bodyB._islandIndex);
|
|
636
|
+
}
|
|
637
|
+
for (let i = 0; i < count; i++) {
|
|
638
|
+
const body = bodies[i];
|
|
639
|
+
if (body.type === "dynamic") {
|
|
640
|
+
const root = this._find(i);
|
|
641
|
+
if (body._sleepTime < minSleep[root]) minSleep[root] = body._sleepTime;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const timeToSleep = this.timeToSleep;
|
|
645
|
+
for (let i = 0; i < count; i++) {
|
|
646
|
+
const body = bodies[i];
|
|
647
|
+
if (body.type === "dynamic") body._setSleeping(minSleep[this._find(i)] >= timeToSleep);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Union dynamic↔dynamic solid contacts into islands. A dynamic body touching a
|
|
652
|
+
* MOVING static/kinematic body has its sleep timer reset instead: those types
|
|
653
|
+
* are island boundaries, not members, so nothing else would keep the passenger
|
|
654
|
+
* of a slow-moving platform awake - and the solver skips a contact whose
|
|
655
|
+
* dynamic side is asleep, letting the platform drive straight through it.
|
|
656
|
+
*
|
|
657
|
+
* The same traversal resets the sleep timer of both sides of a contact whose
|
|
658
|
+
* penetration the solver has not worked off yet, so the island it belongs to
|
|
659
|
+
* stays awake until the overlap is within the tolerance the solver itself
|
|
660
|
+
* converges to.
|
|
661
|
+
*/
|
|
662
|
+
_unionContactIslands() {
|
|
663
|
+
for (const contact of this._backend.contactGraph.solidContacts) {
|
|
664
|
+
if (!contact.enabled) continue;
|
|
665
|
+
const bodyA = contact.a.body;
|
|
666
|
+
const bodyB = contact.b.body;
|
|
667
|
+
const dynamicA = bodyA.type === "dynamic";
|
|
668
|
+
const dynamicB = bodyB.type === "dynamic";
|
|
669
|
+
if (dynamicA && dynamicB) this._union(bodyA._islandIndex, bodyB._islandIndex);
|
|
670
|
+
else if (dynamicA && isMovingBoundary(bodyB)) bodyA._sleepTime = 0;
|
|
671
|
+
else if (dynamicB && isMovingBoundary(bodyA)) bodyB._sleepTime = 0;
|
|
672
|
+
if ((dynamicA || dynamicB) && isUnresolved(contact.manifold)) {
|
|
673
|
+
if (dynamicA) bodyA._sleepTime = 0;
|
|
674
|
+
if (dynamicB) bodyB._sleepTime = 0;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** Union-find union by lower index (deterministic roots). */
|
|
679
|
+
_union(a, b) {
|
|
680
|
+
const rootA = this._find(a);
|
|
681
|
+
const rootB = this._find(b);
|
|
682
|
+
if (rootA < rootB) this._islandParent[rootB] = rootA;
|
|
683
|
+
else if (rootB < rootA) this._islandParent[rootA] = rootB;
|
|
684
|
+
}
|
|
685
|
+
/** Union-find find with path halving. */
|
|
686
|
+
_find(index) {
|
|
687
|
+
const parent = this._islandParent;
|
|
688
|
+
while (parent[index] !== index) {
|
|
689
|
+
const grandparent = parent[parent[index]];
|
|
690
|
+
parent[index] = grandparent;
|
|
691
|
+
index = grandparent;
|
|
692
|
+
}
|
|
693
|
+
return index;
|
|
694
|
+
}
|
|
695
|
+
/** Build each joint's per-frame constraint data (once per fixed step). */
|
|
696
|
+
_prepareJoints(h) {
|
|
697
|
+
for (const joint of this._joints) joint._prepare(h);
|
|
698
|
+
}
|
|
699
|
+
/** Re-apply each joint's accumulated impulse (each sub-step). */
|
|
700
|
+
_warmStartJoints() {
|
|
701
|
+
for (const joint of this._joints) joint._warmStart();
|
|
702
|
+
}
|
|
703
|
+
/** One joint velocity pass (each sub-step, after the contacts). */
|
|
704
|
+
_solveJoints(useBias) {
|
|
705
|
+
for (const joint of this._joints) joint._solve(useBias);
|
|
706
|
+
}
|
|
707
|
+
/** Whether any dynamic body is flagged for continuous collision (bullet mode). */
|
|
708
|
+
_hasBullets() {
|
|
709
|
+
for (const body of this._bodies) if (body.isBullet && body.type === "dynamic") return true;
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
/** Snapshot each bullet's centre of mass at the start of the fixed step (the swept-test origin). */
|
|
713
|
+
_recordBulletPositions() {
|
|
714
|
+
for (const body of this._bodies) if (body.isBullet && body.type === "dynamic") {
|
|
715
|
+
body._ccdPrevX = body.worldCenterOfMassX;
|
|
716
|
+
body._ccdPrevY = body.worldCenterOfMassY;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Shape-cast each bullet's colliders along this fixed step's motion against
|
|
721
|
+
* every other body's colliders; if any would cross one, clamp the body at the
|
|
722
|
+
* earliest impact and resolve it about the surface normal (a slide for a
|
|
723
|
+
* non-bouncy body, an elastic reflection as restitution → 1) so it cannot
|
|
724
|
+
* tunnel. The sweep is an exact translation-only cast of the full shapes
|
|
725
|
+
* (Minkowski ray for circles, swept SAT for polygon pairs) - the step's
|
|
726
|
+
* rotation is applied before the sweep, not swept through. Pairs already
|
|
727
|
+
* overlapping at the start of the step are skipped: the discrete solver owns
|
|
728
|
+
* them (that hand-off is what lets a landed bullet rest on real contacts).
|
|
729
|
+
* Filters and sensors behave exactly as in the discrete narrow phase.
|
|
730
|
+
*/
|
|
731
|
+
_advanceBullets() {
|
|
732
|
+
this._backend.spatialIndex?.sync(this._colliders);
|
|
733
|
+
for (const body of this._bodies) {
|
|
734
|
+
if (!body.isBullet || body.type !== "dynamic" || body.isSleeping) continue;
|
|
735
|
+
const newX = body.worldCenterOfMassX;
|
|
736
|
+
const newY = body.worldCenterOfMassY;
|
|
737
|
+
const dx = newX - body._ccdPrevX;
|
|
738
|
+
const dy = newY - body._ccdPrevY;
|
|
739
|
+
const distance = Math.hypot(dx, dy);
|
|
740
|
+
if (distance < 1e-6) continue;
|
|
741
|
+
const blocked = this._sweepBulletColliders(body, dx, dy);
|
|
742
|
+
if (blocked === null) continue;
|
|
743
|
+
const best = this._ccdBestHit;
|
|
744
|
+
const clampDistance = Math.min(distance, best.t * distance + ccdEmbed);
|
|
745
|
+
if (clampDistance < distance) {
|
|
746
|
+
const pullBack = (clampDistance - distance) / distance;
|
|
747
|
+
this._ccdClampPosition.x = body.x + dx * pullBack;
|
|
748
|
+
this._ccdClampPosition.y = body.y + dy * pullBack;
|
|
749
|
+
body.setTransform(this._ccdClampPosition, body.angle);
|
|
750
|
+
this._backend.spatialIndex?.sync(this._detectionLeavesOf(body));
|
|
751
|
+
}
|
|
752
|
+
const vn = body.linearVelocityX * best.normalX + body.linearVelocityY * best.normalY;
|
|
753
|
+
if (vn < 0) {
|
|
754
|
+
const impulse = -(1 + (vn < -1 ? Math.max(this._bulletRestitution(body), blocked.restitution) : 0)) * vn;
|
|
755
|
+
body.linearVelocityX += impulse * best.normalX;
|
|
756
|
+
body.linearVelocityY += impulse * best.normalY;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Find the earliest impact for a bullet whose colliders translated by
|
|
762
|
+
* `(dx, dy)` this step. Each of the bullet's colliders queries the broad
|
|
763
|
+
* phase with its swept AABB, so the pass costs what the geometry around the
|
|
764
|
+
* bullet's path costs - not one iteration per collider in the world. Returns
|
|
765
|
+
* the blocking collider (with the impact written into {@link _ccdBestHit}),
|
|
766
|
+
* or `null` when nothing blocks the motion.
|
|
767
|
+
*/
|
|
768
|
+
_sweepBulletColliders(body, dx, dy) {
|
|
769
|
+
const hit = this._ccdSweepHit;
|
|
770
|
+
const best = this._ccdBestHit;
|
|
771
|
+
const swept = this._ccdSweptAabb;
|
|
772
|
+
const spatialIndex = this._backend.spatialIndex;
|
|
773
|
+
let blocked = null;
|
|
774
|
+
best.t = Infinity;
|
|
775
|
+
for (const collider of body.colliders) {
|
|
776
|
+
if (collider.isSensor) continue;
|
|
777
|
+
const aabb = collider.aabb;
|
|
778
|
+
swept.minX = dx > 0 ? aabb.minX - dx : aabb.minX;
|
|
779
|
+
swept.maxX = dx < 0 ? aabb.maxX - dx : aabb.maxX;
|
|
780
|
+
swept.minY = dy > 0 ? aabb.minY - dy : aabb.minY;
|
|
781
|
+
swept.maxY = dy < 0 ? aabb.maxY - dy : aabb.maxY;
|
|
782
|
+
const candidates = spatialIndex === void 0 ? this._detectionColliders : spatialIndex.queryAabb(swept, this._ccdCandidates);
|
|
783
|
+
this._ccdBroadPhaseCandidates += candidates.length;
|
|
784
|
+
for (const other of candidates) {
|
|
785
|
+
const target = authoredCollider(other);
|
|
786
|
+
if (target.isSensor || other.body === body || !shouldCollide(collider.filter, target.filter)) continue;
|
|
787
|
+
if (!aabbOverlap(swept, other.aabb)) continue;
|
|
788
|
+
this._ccdSweepTests++;
|
|
789
|
+
if (sweepProxies(collider, dx, dy, other, hit) && hit.t < best.t) {
|
|
790
|
+
best.t = hit.t;
|
|
791
|
+
best.normalX = hit.normalX;
|
|
792
|
+
best.normalY = hit.normalY;
|
|
793
|
+
blocked = target;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return blocked;
|
|
798
|
+
}
|
|
799
|
+
/** The highest restitution among a body's colliders (its CCD bounce factor). */
|
|
800
|
+
_bulletRestitution(body) {
|
|
801
|
+
let restitution = 0;
|
|
802
|
+
for (const collider of body.colliders) if (collider.restitution > restitution) restitution = collider.restitution;
|
|
803
|
+
return restitution;
|
|
804
|
+
}
|
|
805
|
+
/** Run `command` now, or queue it when inside an event dispatch (deferred to end of step). */
|
|
806
|
+
_defer(command) {
|
|
807
|
+
if (this._dispatching) this._commands.push(command);
|
|
808
|
+
else command();
|
|
809
|
+
}
|
|
810
|
+
_drainCommands() {
|
|
811
|
+
if (this._commands.length === 0) return;
|
|
812
|
+
const commands = this._commands.splice(0);
|
|
813
|
+
for (const command of commands) command();
|
|
814
|
+
}
|
|
815
|
+
_removeBody(body) {
|
|
816
|
+
const index = this._bodies.indexOf(body);
|
|
817
|
+
if (index === -1) {
|
|
818
|
+
this._teardownBody(body);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this._bodies.splice(index, 1);
|
|
822
|
+
this._teardownBody(body);
|
|
823
|
+
}
|
|
824
|
+
_teardownBody(body) {
|
|
825
|
+
for (const collider of body.colliders) this._detachCollider(collider);
|
|
826
|
+
this._bindings.unbind(body);
|
|
827
|
+
body._markDestroyed();
|
|
828
|
+
}
|
|
829
|
+
_removeCollider(collider) {
|
|
830
|
+
this._detachCollider(collider);
|
|
831
|
+
collider.body._removeCollider(collider);
|
|
832
|
+
}
|
|
833
|
+
_detachCollider(collider) {
|
|
834
|
+
const index = this._colliders.indexOf(collider);
|
|
835
|
+
if (index !== -1) this._colliders.splice(index, 1);
|
|
836
|
+
this._wakeTouchingBodies(collider);
|
|
837
|
+
const edges = collider.chainEdges;
|
|
838
|
+
if (edges === null) this._removeDetectionCollider(collider);
|
|
839
|
+
else for (const edge of edges) this._removeDetectionCollider(edge);
|
|
840
|
+
collider._markDestroyed();
|
|
841
|
+
}
|
|
842
|
+
/** Drop one broad-phase leaf: an authored collider, or one chain edge proxy. */
|
|
843
|
+
_removeDetectionCollider(collider) {
|
|
844
|
+
const index = this._detectionColliders.indexOf(collider);
|
|
845
|
+
if (index !== -1) this._detectionColliders.splice(index, 1);
|
|
846
|
+
this._backend.removeCollider(collider);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* The broad-phase leaves of one body - its colliders, with a chain replaced by
|
|
850
|
+
* its edge proxies. Returns pooled scratch, valid until the next call.
|
|
851
|
+
*/
|
|
852
|
+
_detectionLeavesOf(body) {
|
|
853
|
+
const leaves = this._leafScratch;
|
|
854
|
+
leaves.length = 0;
|
|
855
|
+
for (const collider of body.colliders) {
|
|
856
|
+
const edges = collider.chainEdges;
|
|
857
|
+
if (edges === null) {
|
|
858
|
+
leaves.push(collider);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
for (const edge of edges) leaves.push(edge);
|
|
862
|
+
}
|
|
863
|
+
return leaves;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Wake every dynamic body still touching `collider` at the moment it leaves
|
|
867
|
+
* the world. A sleeping body's sleep timer is frozen, so the island pass alone
|
|
868
|
+
* can never re-open its sleep decision: losing the support it rested on just
|
|
869
|
+
* removes a contact, and the island (now smaller, or a lone body) still reports
|
|
870
|
+
* a long-expired timer and is put straight back to sleep - a stack whose floor,
|
|
871
|
+
* platform or bottom box is destroyed would hang in mid-air forever. Kinematic
|
|
872
|
+
* and static supports are the sharp edge, since they are island boundaries
|
|
873
|
+
* rather than nodes and so are invisible to wake propagation, but the same hole
|
|
874
|
+
* exists for a dynamic support. Waking the far side of each contact is enough:
|
|
875
|
+
* the woken body's reset timer propagates through the rest of its sleeping
|
|
876
|
+
* island on the next step, exactly like any other wake event.
|
|
877
|
+
*/
|
|
878
|
+
_wakeTouchingBodies(collider) {
|
|
879
|
+
for (const contact of this._backend.contactGraph.solidContacts) {
|
|
880
|
+
let other;
|
|
881
|
+
if (contact.ownerA === collider) other = contact.ownerB;
|
|
882
|
+
else if (contact.ownerB === collider) other = contact.ownerA;
|
|
883
|
+
else continue;
|
|
884
|
+
if (other.body.type === "dynamic") other.body.wake();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
_assertAlive() {
|
|
888
|
+
if (this._destroyed) throw new Error("PhysicsWorld: the world has been destroyed.");
|
|
889
|
+
}
|
|
890
|
+
};
|
|
628
891
|
|
|
892
|
+
//#endregion
|
|
629
893
|
export { PhysicsWorld };
|
|
630
|
-
//# sourceMappingURL=PhysicsWorld.js.map
|
|
894
|
+
//# sourceMappingURL=PhysicsWorld.js.map
|