babylon-box3d 0.1.0 → 0.2.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.
@@ -0,0 +1,2432 @@
1
+ import { Matrix, Quaternion, TmpVectors, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
2
+ import { PhysicsShapeType, PhysicsConstraintType, PhysicsMotionType, PhysicsConstraintAxis, PhysicsConstraintAxisLimitMode, PhysicsConstraintMotorType, PhysicsEventType, PhysicsPrestepType, PhysicsActivationControl, } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin.js";
3
+ import { PhysicsRaycastResult } from "@babylonjs/core/Physics/physicsRaycastResult.js";
4
+ import { Logger } from "@babylonjs/core/Misc/logger.js";
5
+ import { BoundingBox } from "@babylonjs/core/Culling/boundingBox.js";
6
+ import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
7
+ import { InstancedMesh } from "@babylonjs/core/Meshes/instancedMesh.js";
8
+ import { VertexBuffer } from "@babylonjs/core/Buffers/buffer.js";
9
+ import { Observable } from "@babylonjs/core/Misc/observable.js";
10
+ /** The cell material that cuts a hole in a height field: nothing collides with the cell and rays pass through it. */
11
+ export const BOX3D_HEIGHT_FIELD_HOLE = 255;
12
+ /**
13
+ * Per body plugin data. One instance per Box3D body (thin instances get one each).
14
+ */
15
+ class Box3DBodyData {
16
+ constructor(
17
+ /** slot handle inside the WASM shim */
18
+ slot) {
19
+ this.slot = slot;
20
+ this.userMassProps = {};
21
+ /** Box3D angular motion locks currently set on the body, bits x, y, z */
22
+ this.angularLocks = 0;
23
+ /** Havok's EventType bits: 1 collision started, 2 collision continued, 4 collision finished */
24
+ this.eventMask = 0;
25
+ this.motionType = PhysicsMotionType.STATIC;
26
+ /** Babylon activation control; ALWAYS_INACTIVE parks the body as a static Box3D body, see setActivationControl */
27
+ this.activation = PhysicsActivationControl.SIMULATION_CONTROLLED;
28
+ }
29
+ }
30
+ /**
31
+ * Per shape plugin data. A Babylon shape is a description that gets instantiated on each body it is set on.
32
+ */
33
+ class Box3DShapeData {
34
+ constructor(slot, type) {
35
+ this.slot = slot;
36
+ this.type = type;
37
+ this.material = {};
38
+ /** bodies currently instantiating this shape, refreshed when the description changes */
39
+ this.users = new Set();
40
+ /** for containers: child shapes, so changes propagate to users */
41
+ this.children = [];
42
+ this.parents = new Set();
43
+ }
44
+ }
45
+ class Box3DConstraintData {
46
+ constructor() {
47
+ this.joints = [];
48
+ this.pairs = [];
49
+ this.type = PhysicsConstraintType.LOCK;
50
+ this.jointType = 0;
51
+ this.enabled = true;
52
+ this.collisions = false;
53
+ this.frames = new Float32Array(14);
54
+ this.length = 0;
55
+ this.axes = new Map();
56
+ /** SIX_DOF only */
57
+ this.plan = null;
58
+ }
59
+ }
60
+ // shim joint types
61
+ var Box3DJointType;
62
+ (function (Box3DJointType) {
63
+ Box3DJointType[Box3DJointType["WELD"] = 0] = "WELD";
64
+ Box3DJointType[Box3DJointType["SPHERICAL"] = 1] = "SPHERICAL";
65
+ Box3DJointType[Box3DJointType["REVOLUTE"] = 2] = "REVOLUTE";
66
+ Box3DJointType[Box3DJointType["PRISMATIC"] = 3] = "PRISMATIC";
67
+ Box3DJointType[Box3DJointType["DISTANCE"] = 4] = "DISTANCE";
68
+ Box3DJointType[Box3DJointType["FILTER"] = 5] = "FILTER";
69
+ Box3DJointType[Box3DJointType["WHEEL"] = 6] = "WHEEL";
70
+ Box3DJointType[Box3DJointType["PARALLEL"] = 7] = "PARALLEL";
71
+ })(Box3DJointType || (Box3DJointType = {}));
72
+ /**
73
+ * Handle to a Box3D wheel joint created with Box3DPlugin.createWheelJoint.
74
+ */
75
+ export class Box3DWheelJoint {
76
+ /**
77
+ * @internal
78
+ */
79
+ constructor(_plugin,
80
+ /** @internal */
81
+ _slot) {
82
+ this._plugin = _plugin;
83
+ this._slot = _slot;
84
+ }
85
+ /**
86
+ * Sets the target angular speed of the spin motor.
87
+ * @param speed radians per second
88
+ */
89
+ setSpinSpeed(speed) {
90
+ this._plugin._wheelCall("_bx_WheelJoint_SetSpinMotorSpeed", this._slot, speed);
91
+ }
92
+ /**
93
+ * Sets the maximum torque the spin motor can apply.
94
+ * @param torque newton meters
95
+ */
96
+ setMaxSpinTorque(torque) {
97
+ this._plugin._wheelCall("_bx_WheelJoint_SetMaxSpinTorque", this._slot, torque);
98
+ }
99
+ /**
100
+ * Enables or disables the spin motor (disabled wheels roll freely).
101
+ * @param enabled true to enable
102
+ */
103
+ enableSpinMotor(enabled) {
104
+ this._plugin._wheelCall("_bx_WheelJoint_EnableSpinMotor", this._slot, enabled ? 1 : 0);
105
+ }
106
+ /**
107
+ * Sets the target steering angle.
108
+ * @param radians steering angle
109
+ */
110
+ setSteeringAngle(radians) {
111
+ this._plugin._wheelCall("_bx_WheelJoint_SetTargetSteeringAngle", this._slot, radians);
112
+ }
113
+ /**
114
+ * Tunes the suspension spring.
115
+ * @param hertz stiffness
116
+ * @param dampingRatio damping ratio
117
+ */
118
+ setSuspension(hertz, dampingRatio) {
119
+ this._plugin._wheelCall("_bx_WheelJoint_SetSuspension", this._slot, hertz, dampingRatio);
120
+ }
121
+ /**
122
+ * Removes the joint.
123
+ */
124
+ dispose() {
125
+ this._plugin._destroyExtraJoint(this._slot);
126
+ this._slot = 0;
127
+ }
128
+ }
129
+ /**
130
+ * Body event mask bits, the same values Havok's plugin uses so masks can be copied over from Havok code.
131
+ * Box3D reports begin and end touch events together (they are one shape flag) and hit events separately.
132
+ */
133
+ var Box3DEventBits;
134
+ (function (Box3DEventBits) {
135
+ Box3DEventBits[Box3DEventBits["COLLISION_STARTED"] = 1] = "COLLISION_STARTED";
136
+ Box3DEventBits[Box3DEventBits["COLLISION_CONTINUED"] = 2] = "COLLISION_CONTINUED";
137
+ Box3DEventBits[Box3DEventBits["COLLISION_FINISHED"] = 4] = "COLLISION_FINISHED";
138
+ Box3DEventBits[Box3DEventBits["ALL"] = 7] = "ALL";
139
+ })(Box3DEventBits || (Box3DEventBits = {}));
140
+ /**
141
+ * How much heavier a locked rotation axis is made than the heaviest free one. Babylon locks an axis with a zero inertia
142
+ * component, Box3D needs an invertible tensor; 1e5 leaves a body a hundred thousand times harder to turn about that axis
143
+ * while the tensor stays well conditioned in float32.
144
+ */
145
+ const LockedInertiaRatio = 1e5;
146
+ /**
147
+ * Diagonalizes a symmetric inertia tensor [ixx, iyy, izz, ixy, ixz, iyz] into its principal moments and the rotation
148
+ * that turns principal axes into body axes (Babylon's inertiaOrientation). Jacobi rotations, a few sweeps are plenty for
149
+ * a 3x3, and the common diagonal case exits immediately.
150
+ */
151
+ function DiagonalizeInertia(tensor, moments, orientation) {
152
+ const a = [
153
+ [tensor[0], tensor[3], tensor[4]],
154
+ [tensor[3], tensor[1], tensor[5]],
155
+ [tensor[4], tensor[5], tensor[2]],
156
+ ];
157
+ orientation.set(0, 0, 0, 1);
158
+ moments.set(tensor[0], tensor[1], tensor[2]);
159
+ const q = [0, 0, 0, 1];
160
+ for (let sweep = 0; sweep < 24; sweep++) {
161
+ // rotation matrix of q, columns are the current principal axes
162
+ const [x, y, z, w] = q;
163
+ const r = [
164
+ [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
165
+ [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
166
+ [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
167
+ ];
168
+ // d = transpose(r) * a * r
169
+ const d = [0, 1, 2].map((i) => [0, 1, 2].map((j) => [0, 1, 2].reduce((sum, k) => sum + r[k][i] * [0, 1, 2].reduce((s2, l) => s2 + a[k][l] * r[l][j], 0), 0)));
170
+ moments.set(d[0][0], d[1][1], d[2][2]);
171
+ orientation.set(q[0], q[1], q[2], q[3]);
172
+ const offDiagonal = [Math.abs(d[1][2]), Math.abs(d[0][2]), Math.abs(d[0][1])];
173
+ const k = offDiagonal.indexOf(Math.max(...offDiagonal));
174
+ const k1 = (k + 1) % 3;
175
+ const k2 = (k + 2) % 3;
176
+ const off = d[k1][k2];
177
+ const scale = Math.abs(d[0][0]) + Math.abs(d[1][1]) + Math.abs(d[2][2]) + 1e-30;
178
+ if (Math.abs(off) <= 1e-9 * scale) {
179
+ return;
180
+ }
181
+ let theta = (d[k2][k2] - d[k1][k1]) / (2 * off);
182
+ const sign = theta > 0 ? 1 : -1;
183
+ theta *= sign;
184
+ const t = sign / (theta + (theta < 1e6 ? Math.sqrt(theta * theta + 1) : theta));
185
+ const c = 1 / Math.sqrt(t * t + 1);
186
+ if (c === 1) {
187
+ return;
188
+ }
189
+ const jacobi = [0, 0, 0, 0];
190
+ // d is transpose(r) * a * r and the step right multiplies q, so d becomes transpose(jacobi) * d * jacobi: the
191
+ // quaternion has to turn by -theta where the matrix form of the Jacobi rotation turns by +theta. With the other
192
+ // sign every sweep puts the off diagonal term straight back and the loop never converges.
193
+ jacobi[k] = -sign * Math.sqrt((1 - c) / 2);
194
+ jacobi[3] = Math.sqrt(1 - jacobi[k] * jacobi[k]);
195
+ // q = q * jacobi
196
+ const [qx, qy, qz, qw] = q;
197
+ const [jx, jy, jz, jw] = jacobi;
198
+ q[0] = qw * jx + qx * jw + qy * jz - qz * jy;
199
+ q[1] = qw * jy - qx * jz + qy * jw + qz * jx;
200
+ q[2] = qw * jz + qx * jy - qy * jx + qz * jw;
201
+ q[3] = qw * jw - qx * jx - qy * jy - qz * jz;
202
+ const length = Math.hypot(q[0], q[1], q[2], q[3]) || 1;
203
+ for (let i = 0; i < 4; i++) {
204
+ q[i] /= length;
205
+ }
206
+ }
207
+ }
208
+ /** Builds the symmetric tensor [ixx, iyy, izz, ixy, ixz, iyz] of principal moments rotated into body space. */
209
+ function ComposeInertia(moments, orientation) {
210
+ const { x, y, z, w } = orientation;
211
+ const r = [
212
+ [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
213
+ [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
214
+ [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
215
+ ];
216
+ const at = (i, j) => r[i][0] * moments[0] * r[j][0] + r[i][1] * moments[1] * r[j][1] + r[i][2] * moments[2] * r[j][2];
217
+ return [at(0, 0), at(1, 1), at(2, 2), at(0, 1), at(0, 2), at(1, 2)];
218
+ }
219
+ /** Shape description properties that Box3D can change on live shapes, see bx_Body_SyncShapeDesc. */
220
+ var Box3DShapeSync;
221
+ (function (Box3DShapeSync) {
222
+ Box3DShapeSync[Box3DShapeSync["FILTER"] = 1] = "FILTER";
223
+ Box3DShapeSync[Box3DShapeSync["MATERIAL"] = 2] = "MATERIAL";
224
+ Box3DShapeSync[Box3DShapeSync["DENSITY"] = 4] = "DENSITY";
225
+ })(Box3DShapeSync || (Box3DShapeSync = {}));
226
+ const MOVE_STRIDE = 9;
227
+ const CONTACT_STRIDE = 12;
228
+ const SENSOR_STRIDE = 5;
229
+ const RAY_STRIDE = 11;
230
+ /**
231
+ * Collects mesh geometry in the physics body's local space (scaling baked in).
232
+ */
233
+ class MeshGeometryAccumulator {
234
+ constructor(_collectIndices, _flipWinding) {
235
+ this._collectIndices = _collectIndices;
236
+ this._flipWinding = _flipWinding;
237
+ this._vertices = [];
238
+ this._indices = [];
239
+ }
240
+ addNodeMeshes(mesh, includeChildren) {
241
+ mesh.computeWorldMatrix(true);
242
+ const rootScaled = TmpVectors.Matrix[0];
243
+ Matrix.ScalingToRef(mesh.absoluteScaling.x, mesh.absoluteScaling.y, mesh.absoluteScaling.z, rootScaled);
244
+ if (mesh instanceof Mesh) {
245
+ this._addMesh(mesh, rootScaled);
246
+ }
247
+ else if (mesh instanceof InstancedMesh) {
248
+ this._addMesh(mesh.sourceMesh, rootScaled);
249
+ }
250
+ if (includeChildren) {
251
+ const worldToRoot = TmpVectors.Matrix[1];
252
+ mesh.computeWorldMatrix().invertToRef(worldToRoot);
253
+ const worldToRootScaled = TmpVectors.Matrix[2];
254
+ worldToRoot.multiplyToRef(rootScaled, worldToRootScaled);
255
+ const children = mesh.getChildMeshes(false).filter((m) => !m.physicsBody);
256
+ for (const m of children) {
257
+ const childToWorld = m.computeWorldMatrix();
258
+ const childToRootScaled = TmpVectors.Matrix[3];
259
+ childToWorld.multiplyToRef(worldToRootScaled, childToRootScaled);
260
+ if (m instanceof Mesh) {
261
+ this._addMesh(m, childToRootScaled);
262
+ }
263
+ else if (m instanceof InstancedMesh) {
264
+ this._addMesh(m.sourceMesh, childToRootScaled);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ _addMesh(mesh, meshToRoot) {
270
+ const vertexData = mesh.getVerticesData(VertexBuffer.PositionKind) || [];
271
+ const numVerts = vertexData.length / 3;
272
+ const indexOffset = this._vertices.length / 3;
273
+ const p = TmpVectors.Vector3[0];
274
+ for (let v = 0; v < numVerts; v++) {
275
+ p.set(vertexData[v * 3 + 0], vertexData[v * 3 + 1], vertexData[v * 3 + 2]);
276
+ Vector3.TransformCoordinatesToRef(p, meshToRoot, p);
277
+ this._vertices.push(p.x, p.y, p.z);
278
+ }
279
+ if (this._collectIndices) {
280
+ const meshIndices = mesh.getIndices();
281
+ if (meshIndices) {
282
+ for (let i = 0; i < meshIndices.length; i += 3) {
283
+ if (this._flipWinding) {
284
+ this._indices.push(meshIndices[i + 2] + indexOffset, meshIndices[i + 1] + indexOffset, meshIndices[i + 0] + indexOffset);
285
+ }
286
+ else {
287
+ this._indices.push(meshIndices[i + 0] + indexOffset, meshIndices[i + 1] + indexOffset, meshIndices[i + 2] + indexOffset);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ get vertexCount() {
294
+ return this._vertices.length / 3;
295
+ }
296
+ get triangleCount() {
297
+ return this._indices.length / 3;
298
+ }
299
+ get vertices() {
300
+ return this._vertices;
301
+ }
302
+ get indices() {
303
+ return this._indices;
304
+ }
305
+ }
306
+ /**
307
+ * Every native entry point the plugin calls. The constructor checks that the module has all of them, because the two ways this goes wrong
308
+ * in practice - a box3d.js loader left behind by an older install, or a box3d.wasm served from a stale copy - otherwise turn into a physics
309
+ * world where nothing ever happens. A test keeps the list in step with the code.
310
+ */
311
+ const RequiredNativeExports = [
312
+ "_bx_Body_AllowFastRotation", "_bx_Body_ApplyAngularImpulse", "_bx_Body_ApplyForce", "_bx_Body_ApplyLinearImpulse",
313
+ "_bx_Body_ApplyMassFromShapes", "_bx_Body_ApplyTorque", "_bx_Body_ComputeShapeMassData", "_bx_Body_EnableSleep", "_bx_Body_GetAABB",
314
+ "_bx_Body_GetAngularDamping", "_bx_Body_GetAngularVelocity", "_bx_Body_GetGravityScale", "_bx_Body_GetLinearDamping",
315
+ "_bx_Body_GetLinearVelocity", "_bx_Body_GetMassData", "_bx_Body_GetTransform", "_bx_Body_SetAngularDamping", "_bx_Body_SetAngularVelocity",
316
+ "_bx_Body_SetAwake", "_bx_Body_SetEventFlags", "_bx_Body_SetGravityScale", "_bx_Body_SetLinearDamping", "_bx_Body_SetLinearVelocity",
317
+ "_bx_Body_SetMassDataFull", "_bx_Body_SetMotionLocks", "_bx_Body_SetShape", "_bx_Body_SetTargetTransform", "_bx_Body_SetTransform",
318
+ "_bx_Body_SetType", "_bx_Body_SyncShapeDesc", "_bx_ComputeAlignedFrameB", "_bx_ContactEventsPtr", "_bx_CreateBody", "_bx_CreateJoint",
319
+ "_bx_CreateWorld", "_bx_DebugIndexCount", "_bx_DebugIndicesPtr", "_bx_DebugPositionsPtr", "_bx_DestroyBody", "_bx_DestroyJoint",
320
+ "_bx_DestroyWorld", "_bx_GetRejectedSlotCount", "_bx_Joint_EnableLimit", "_bx_Joint_EnableMotor", "_bx_Joint_EnableSpring", "_bx_Joint_IsValid",
321
+ "_bx_Joint_SetCollideConnected", "_bx_Joint_SetConstraintTuning", "_bx_Joint_SetLimits", "_bx_Joint_SetMaxMotorForce",
322
+ "_bx_Joint_SetMotorSpeed", "_bx_Joint_SetSphericalMotor", "_bx_Joint_SetSphericalTarget", "_bx_Joint_SetSpring", "_bx_Joint_SetTarget",
323
+ "_bx_Joint_SetTwistLimits", "_bx_Joint_UpdateMotorFrame", "_bx_Joint_WakeBodies", "_bx_MoveEventsPtr", "_bx_RayHitsPtr", "_bx_Scratch",
324
+ "_bx_SensorEventsPtr", "_bx_ShapeDesc_AddChild", "_bx_ShapeDesc_BuildDebugGeometry", "_bx_ShapeDesc_CreateBox", "_bx_ShapeDesc_CreateCapsule",
325
+ "_bx_ShapeDesc_CreateContainer", "_bx_ShapeDesc_CreateCylinder", "_bx_ShapeDesc_CreateHeightField", "_bx_ShapeDesc_CreateHull",
326
+ "_bx_ShapeDesc_CreateMesh", "_bx_ShapeDesc_CreateSphere", "_bx_ShapeDesc_Destroy", "_bx_ShapeDesc_GetAABB", "_bx_ShapeDesc_GetCategoryBits",
327
+ "_bx_ShapeDesc_GetChildCount", "_bx_ShapeDesc_GetDensity", "_bx_ShapeDesc_GetFriction", "_bx_ShapeDesc_GetMaskBits",
328
+ "_bx_ShapeDesc_GetRestitution", "_bx_ShapeDesc_RemoveChild", "_bx_ShapeDesc_SetDensity", "_bx_ShapeDesc_SetFilter", "_bx_ShapeDesc_SetGroup",
329
+ "_bx_ShapeDesc_SetMaterial", "_bx_ShapeDesc_SetRollingResistance", "_bx_ShapeDesc_SetSensor", "_bx_World_CastRay", "_bx_World_EnableContinuous",
330
+ "_bx_World_EnableSleeping", "_bx_World_Explode", "_bx_World_GetContactEvents", "_bx_World_GetMaximumLinearSpeed", "_bx_World_GetMoveEvents",
331
+ "_bx_World_GetSensorEvents", "_bx_World_GetStats", "_bx_World_SetGravity", "_bx_World_SetMaximumLinearSpeed", "_bx_World_Step"
332
+ ];
333
+ /**
334
+ * Box3D physics plugin for Babylon.js physics v2.
335
+ * Box3D is Erin Catto's 3D rigid body engine (https://github.com/erincatto/box3d). This plugin drives the
336
+ * WebAssembly build that ships in this package (a flat, handle based C shim over box3d).
337
+ * Usage mirrors the Havok plugin:
338
+ * ```ts
339
+ * import { Box3D, Box3DPlugin } from "babylon-box3d";
340
+ * const box3d = await Box3D();
341
+ * scene.enablePhysics(new Vector3(0, -9.81, 0), new Box3DPlugin(true, box3d));
342
+ * ```
343
+ */
344
+ export class Box3DPlugin {
345
+ /**
346
+ * Creates a Box3D plugin.
347
+ * @param _useDeltaForWorldStep step the world with the frame delta (true) or a fixed time step (false)
348
+ * @param box3dModule the resolved Box3D WASM module (await Box3DModule())
349
+ */
350
+ constructor(_useDeltaForWorldStep = true, box3dModule) {
351
+ this._useDeltaForWorldStep = _useDeltaForWorldStep;
352
+ /** Name of the plugin */
353
+ this.name = "Box3DPlugin";
354
+ /** Number of solver sub steps per world step. Box3D recommends 4. */
355
+ this.subStepCount = 4;
356
+ /** Wall clock time of the last world step in milliseconds (JavaScript side, includes the event sync). */
357
+ this.lastStepTimeMs = 0;
358
+ this._worldSlot = 0;
359
+ this._fixedTimeStep = 1 / 60;
360
+ this._timeStep = 1 / 60;
361
+ this._maxAngularVelocity = 4 * Math.PI * 10;
362
+ this._bodies = [];
363
+ this._shapes = [];
364
+ this._bodyCollisionObservable = new Map();
365
+ this._bodyCollisionEndedObservable = new Map();
366
+ this._touchedInstanceMeshes = new Set();
367
+ /** spherical joints whose velocity motor target has to be re-expressed in world space every step */
368
+ this._motorJoints = new Set();
369
+ this._tmpVec3 = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
370
+ this._tmpQuat = [new Quaternion(), new Quaternion(), new Quaternion()];
371
+ this._warned = new Set();
372
+ /** the shim's rejected slot counter as of the last step, see executeStep */
373
+ this._rejectedSlots = 0;
374
+ /** Observable for collision started and continued events */
375
+ this.onCollisionObservable = new Observable();
376
+ /** Observable for collision ended events */
377
+ this.onCollisionEndedObservable = new Observable();
378
+ /** Observable for trigger entered and exited events */
379
+ this.onTriggerCollisionObservable = new Observable();
380
+ if (!box3dModule) {
381
+ throw new Error("Box3D module is required: pass the awaited result of the Box3D module factory.");
382
+ }
383
+ const missing = RequiredNativeExports.filter((name) => typeof box3dModule[name] !== "function");
384
+ if (missing.length > 0) {
385
+ throw new Error(`Box3DPlugin: the Box3D module is missing ${missing.length} entry point${missing.length === 1 ? "" : "s"} this build needs` +
386
+ ` (${missing.slice(0, 3).join(", ")}${missing.length > 3 ? ", ..." : ""}). box3d.js and box3d.wasm have to come from the` +
387
+ " same build of this package: check what locateFile points at and clear any copied or cached box3d.wasm.");
388
+ }
389
+ this._b3 = box3dModule;
390
+ this.world = box3dModule;
391
+ this._worldSlot = this._b3._bx_CreateWorld(0, -9.81, 0);
392
+ if (!this._worldSlot) {
393
+ throw new Error("Box3D: could not create a world (max worlds reached?)");
394
+ }
395
+ }
396
+ // ----------------------------------------------------------------------------------------
397
+ // helpers
398
+ // ----------------------------------------------------------------------------------------
399
+ _scratch() {
400
+ return new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_Scratch(), 64);
401
+ }
402
+ _warnOnce(key, message) {
403
+ if (!this._warned.has(key)) {
404
+ this._warned.add(key);
405
+ Logger.Warn(`Box3DPlugin: ${message}`);
406
+ }
407
+ }
408
+ _getPluginReference(body, instanceIndex) {
409
+ return body._pluginDataInstances?.length ? body._pluginDataInstances[instanceIndex ?? 0] : body._pluginData;
410
+ }
411
+ _applyToBodyOrInstances(body, fn, instanceIndex) {
412
+ if (body._pluginDataInstances?.length > 0 && instanceIndex === undefined) {
413
+ for (const data of body._pluginDataInstances) {
414
+ fn(data);
415
+ }
416
+ }
417
+ else {
418
+ fn(this._getPluginReference(body, instanceIndex));
419
+ }
420
+ }
421
+ _motionTypeToNative(motionType) {
422
+ switch (motionType) {
423
+ case PhysicsMotionType.STATIC:
424
+ return 0;
425
+ case PhysicsMotionType.ANIMATED:
426
+ return 1;
427
+ default:
428
+ return 2;
429
+ }
430
+ }
431
+ _nodeTransformToRef(node, position, orientation) {
432
+ if (node.parent) {
433
+ node.computeWorldMatrix(true);
434
+ position.copyFrom(node.absolutePosition);
435
+ orientation.copyFrom(node.absoluteRotationQuaternion);
436
+ return;
437
+ }
438
+ position.copyFrom(node.position);
439
+ if (node.rotationQuaternion) {
440
+ orientation.copyFrom(node.rotationQuaternion);
441
+ }
442
+ else {
443
+ Quaternion.FromEulerAnglesToRef(node.rotation.x, node.rotation.y, node.rotation.z, orientation);
444
+ }
445
+ }
446
+ // ----------------------------------------------------------------------------------------
447
+ // world
448
+ // ----------------------------------------------------------------------------------------
449
+ setGravity(gravity) {
450
+ this._b3._bx_World_SetGravity(this._worldSlot, gravity.x, gravity.y, gravity.z);
451
+ }
452
+ setTimeStep(timeStep) {
453
+ this._fixedTimeStep = timeStep;
454
+ }
455
+ getTimeStep() {
456
+ return this._fixedTimeStep;
457
+ }
458
+ executeStep(delta, physicsBodies) {
459
+ for (const physicsBody of physicsBodies) {
460
+ if (physicsBody.disablePreStep) {
461
+ continue;
462
+ }
463
+ this.setPhysicsBodyTransformation(physicsBody, physicsBody.transformNode);
464
+ }
465
+ for (const joint of this._motorJoints) {
466
+ // a joint whose bodies were destroyed is gone with them, drop it instead of holding the slot forever
467
+ if (this._b3._bx_Joint_IsValid(joint)) {
468
+ this._b3._bx_Joint_UpdateMotorFrame(joint);
469
+ }
470
+ else {
471
+ this._motorJoints.delete(joint);
472
+ }
473
+ }
474
+ const deltaTime = this._useDeltaForWorldStep ? delta : this._fixedTimeStep;
475
+ this._timeStep = deltaTime;
476
+ const start = performance.now();
477
+ this._b3._bx_World_Step(this._worldSlot, deltaTime, this.subStepCount);
478
+ // Only bodies that moved report an event, so sync those instead of iterating every body.
479
+ const moveCount = this._b3._bx_World_GetMoveEvents(this._worldSlot);
480
+ if (moveCount > 0) {
481
+ const movePointer = this._b3._bx_MoveEventsPtr();
482
+ let buffer = new Float32Array(this._b3.HEAPF32.buffer, movePointer, moveCount * MOVE_STRIDE);
483
+ for (let i = 0; i < moveCount; i++) {
484
+ if (buffer.buffer !== this._b3.HEAPF32.buffer) {
485
+ // a world matrix observer grew the wasm memory; the records are still in place
486
+ buffer = new Float32Array(this._b3.HEAPF32.buffer, movePointer, moveCount * MOVE_STRIDE);
487
+ }
488
+ const offset = i * MOVE_STRIDE;
489
+ const ref = this._bodies[buffer[offset]];
490
+ if (!ref || ref.body.disableSync) {
491
+ continue;
492
+ }
493
+ this._syncBodyFromBuffer(ref.body, ref.index, buffer, offset + 1);
494
+ }
495
+ for (const mesh of this._touchedInstanceMeshes) {
496
+ mesh.thinInstanceBufferUpdated("matrix");
497
+ }
498
+ this._touchedInstanceMeshes.clear();
499
+ }
500
+ this._notifyCollisions();
501
+ this._notifyTriggers();
502
+ this.lastStepTimeMs = performance.now() - start;
503
+ // Every native entry point returns quietly when it does not recognize a handle. That is what teardown needs and
504
+ // it hides everything else, so say it once instead of simulating a world where half the calls do nothing.
505
+ const rejected = this._b3._bx_GetRejectedSlotCount();
506
+ if (rejected > this._rejectedSlots) {
507
+ this._rejectedSlots = rejected;
508
+ this._warnOnce("rejected-slots", `the native module did not recognize ${rejected} of the handles it was given, and those calls did nothing. Something is using a ` +
509
+ "body, shape or joint after it was destroyed, or box3d.js and box3d.wasm are from different builds.");
510
+ }
511
+ }
512
+ getPluginVersion() {
513
+ return 2;
514
+ }
515
+ setVelocityLimits(maxLinearVelocity, maxAngularVelocity) {
516
+ this._b3._bx_World_SetMaximumLinearSpeed(this._worldSlot, maxLinearVelocity);
517
+ this._maxAngularVelocity = maxAngularVelocity;
518
+ }
519
+ getMaxLinearVelocity() {
520
+ return this._b3._bx_World_GetMaximumLinearSpeed(this._worldSlot);
521
+ }
522
+ getMaxAngularVelocity() {
523
+ return this._maxAngularVelocity;
524
+ }
525
+ /**
526
+ * Box3D counters and timings for the current world.
527
+ * @returns body, shape, contact, joint and island counts plus the last step time in milliseconds
528
+ */
529
+ getStats() {
530
+ this._b3._bx_World_GetStats(this._worldSlot);
531
+ const s = this._scratch();
532
+ return { bodies: s[0], shapes: s[1], contacts: s[2], joints: s[3], islands: s[4], stepMs: s[5], collideMs: s[6], solveMs: s[7] };
533
+ }
534
+ /**
535
+ * Enables or disables island sleeping.
536
+ * @param enabled true to let resting bodies sleep
537
+ */
538
+ setSleepingEnabled(enabled) {
539
+ this._b3._bx_World_EnableSleeping(this._worldSlot, enabled ? 1 : 0);
540
+ }
541
+ /**
542
+ * Enables or disables continuous collision against static geometry.
543
+ * @param enabled true to enable continuous collision
544
+ */
545
+ setContinuousEnabled(enabled) {
546
+ this._b3._bx_World_EnableContinuous(this._worldSlot, enabled ? 1 : 0);
547
+ }
548
+ // ----------------------------------------------------------------------------------------
549
+ // bodies
550
+ // ----------------------------------------------------------------------------------------
551
+ _createNativeBody(motionType, position, orientation, startAsleep) {
552
+ const slot = this._b3._bx_CreateBody(this._worldSlot, this._motionTypeToNative(motionType), position.x, position.y, position.z, orientation.x, orientation.y, orientation.z, orientation.w, startAsleep ? 0 : 1);
553
+ const data = new Box3DBodyData(slot);
554
+ data.motionType = motionType;
555
+ return data;
556
+ }
557
+ initBody(body, motionType, position, orientation) {
558
+ const data = this._createNativeBody(motionType, position, orientation, body.startAsleep);
559
+ body._pluginData = data;
560
+ this._bodies[data.slot] = { body, index: 0, data };
561
+ }
562
+ initBodyInstances(body, motionType, mesh) {
563
+ const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
564
+ const matrixData = mesh._thinInstanceDataStorage?.matrixData;
565
+ if (!matrixData) {
566
+ return;
567
+ }
568
+ const shape = body.shape?._pluginData;
569
+ if (instancesCount > 8 && shape && this._bakesMeshCopies(shape)) {
570
+ this._warnOnce("instanced-mesh-child", "a mesh inside a container has no local transform in Box3D, so every instance gets its own baked copy of the mesh data; " +
571
+ "put the mesh on the body directly, or bake the offset into the mesh, to share it.");
572
+ }
573
+ this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, false);
574
+ }
575
+ _createOrUpdateBodyInstances(body, motionType, matrixData, startIndex, endIndex, update) {
576
+ const rotation = TmpVectors.Quaternion[0];
577
+ const rotationMatrix = TmpVectors.Matrix[0];
578
+ const position = TmpVectors.Vector3[0];
579
+ for (let i = startIndex; i < endIndex; i++) {
580
+ position.set(matrixData[i * 16 + 12], matrixData[i * 16 + 13], matrixData[i * 16 + 14]);
581
+ Matrix.FromArrayToRef(matrixData, i * 16, rotationMatrix);
582
+ rotationMatrix.decompose(undefined, rotation, undefined);
583
+ if (update) {
584
+ const data = body._pluginDataInstances[i];
585
+ this._b3._bx_Body_SetTransform(data.slot, position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w);
586
+ }
587
+ else {
588
+ const data = this._createNativeBody(motionType, position, rotation, body.startAsleep);
589
+ const sibling = body._pluginDataInstances[0];
590
+ if (sibling) {
591
+ // a new instance behaves like the ones already there: same mass properties, events and activation
592
+ data.userMassProps = sibling.userMassProps;
593
+ data.eventMask = sibling.eventMask;
594
+ this._applyEventFlags(data);
595
+ if (sibling.activation !== PhysicsActivationControl.SIMULATION_CONTROLLED) {
596
+ data.activation = sibling.activation;
597
+ this._applyNativeMotionType(data);
598
+ this._b3._bx_Body_EnableSleep(data.slot, sibling.activation === PhysicsActivationControl.ALWAYS_ACTIVE ? 0 : 1);
599
+ }
600
+ }
601
+ body._pluginDataInstances.push(data);
602
+ this._bodies[data.slot] = { body, index: i, data };
603
+ }
604
+ }
605
+ }
606
+ updateBodyInstances(body, mesh) {
607
+ const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
608
+ const matrixData = mesh._thinInstanceDataStorage?.matrixData;
609
+ if (!matrixData) {
610
+ return;
611
+ }
612
+ const pluginInstancesCount = body._pluginDataInstances.length;
613
+ const motionType = this.getMotionType(body);
614
+ if (instancesCount > pluginInstancesCount) {
615
+ this._createOrUpdateBodyInstances(body, motionType, matrixData, pluginInstancesCount, instancesCount, false);
616
+ const shape = body.shape;
617
+ if (shape && shape._pluginData) {
618
+ for (let i = pluginInstancesCount; i < instancesCount; i++) {
619
+ this._setBodyShape(body._pluginDataInstances[i], shape._pluginData);
620
+ }
621
+ }
622
+ }
623
+ else if (instancesCount < pluginInstancesCount) {
624
+ const instancesToRemove = pluginInstancesCount - instancesCount;
625
+ for (let i = 0; i < instancesToRemove; i++) {
626
+ const data = body._pluginDataInstances.pop();
627
+ this._destroyBodyData(data);
628
+ }
629
+ this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, true);
630
+ }
631
+ }
632
+ _destroyBodyData(data) {
633
+ if (!data) {
634
+ return;
635
+ }
636
+ for (const shape of this._shapes) {
637
+ shape?.users.delete(data);
638
+ }
639
+ this._bodyCollisionObservable.delete(data.slot);
640
+ this._bodyCollisionEndedObservable.delete(data.slot);
641
+ this._bodies[data.slot] = undefined;
642
+ this._b3._bx_DestroyBody(data.slot);
643
+ data.slot = 0;
644
+ }
645
+ removeBody(body) {
646
+ // Box3D has no add/remove without destroying. The body is destroyed and re-created by initBody if needed.
647
+ if (body._pluginDataInstances?.length) {
648
+ for (const data of body._pluginDataInstances) {
649
+ this._destroyBodyData(data);
650
+ }
651
+ }
652
+ if (body._pluginData) {
653
+ this._destroyBodyData(body._pluginData);
654
+ }
655
+ }
656
+ sync(body) {
657
+ this.syncTransform(body, body.transformNode);
658
+ }
659
+ _syncBodyFromBuffer(body, index, buffer, offset) {
660
+ const node = body.transformNode;
661
+ if (body._pluginDataInstances.length) {
662
+ const m = node;
663
+ const matrixData = m._thinInstanceDataStorage?.matrixData;
664
+ if (!matrixData) {
665
+ return;
666
+ }
667
+ const scale = this._tmpVec3[0];
668
+ const base = index * 16;
669
+ scale.set(Math.hypot(matrixData[base], matrixData[base + 1], matrixData[base + 2]), Math.hypot(matrixData[base + 4], matrixData[base + 5], matrixData[base + 6]), Math.hypot(matrixData[base + 8], matrixData[base + 9], matrixData[base + 10]));
670
+ const quat = this._tmpQuat[0];
671
+ quat.set(buffer[offset + 3], buffer[offset + 4], buffer[offset + 5], buffer[offset + 6]);
672
+ const position = this._tmpVec3[1];
673
+ position.set(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
674
+ const matrix = TmpVectors.Matrix[0];
675
+ Matrix.ComposeToRef(scale, quat, position, matrix);
676
+ matrix.copyToArray(matrixData, base);
677
+ this._touchedInstanceMeshes.add(m);
678
+ return;
679
+ }
680
+ this._applyTransformToNode(node, buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3], buffer[offset + 4], buffer[offset + 5], buffer[offset + 6]);
681
+ }
682
+ _applyTransformToNode(node, px, py, pz, qx, qy, qz, qw) {
683
+ const quat = this._tmpQuat[0];
684
+ quat.set(qx, qy, qz, qw);
685
+ const parent = node.parent;
686
+ if (parent && !parent.getWorldMatrix().isIdentity()) {
687
+ parent.computeWorldMatrix(true);
688
+ const savedScaling = this._tmpVec3[2];
689
+ savedScaling.copyFrom(node.scaling);
690
+ const finalTransform = TmpVectors.Matrix[0];
691
+ const finalTranslation = this._tmpVec3[3];
692
+ finalTranslation.set(px, py, pz);
693
+ Matrix.ComposeToRef(node.absoluteScaling, quat, finalTranslation, finalTransform);
694
+ const parentInverse = TmpVectors.Matrix[1];
695
+ parent.getWorldMatrix().invertToRef(parentInverse);
696
+ const localTransform = TmpVectors.Matrix[2];
697
+ finalTransform.multiplyToRef(parentInverse, localTransform);
698
+ localTransform.decomposeToTransformNode(node);
699
+ node.rotationQuaternion?.normalize();
700
+ node.scaling.copyFrom(savedScaling);
701
+ }
702
+ else {
703
+ node.position.set(px, py, pz);
704
+ if (node.rotationQuaternion) {
705
+ node.rotationQuaternion.copyFrom(quat);
706
+ }
707
+ else {
708
+ quat.toEulerAnglesToRef(node.rotation);
709
+ }
710
+ }
711
+ }
712
+ syncTransform(body, transformNode) {
713
+ if (body._pluginDataInstances.length) {
714
+ const m = transformNode;
715
+ const matrixData = m._thinInstanceDataStorage?.matrixData;
716
+ if (!matrixData) {
717
+ return;
718
+ }
719
+ for (let i = 0; i < body._pluginDataInstances.length; i++) {
720
+ const data = body._pluginDataInstances[i];
721
+ this._b3._bx_Body_GetTransform(data.slot);
722
+ this._syncBodyFromBuffer(body, i, this._scratch(), 0);
723
+ }
724
+ m.thinInstanceBufferUpdated("matrix");
725
+ this._touchedInstanceMeshes.delete(m);
726
+ return;
727
+ }
728
+ const data = body._pluginData;
729
+ if (!data || !data.slot) {
730
+ return;
731
+ }
732
+ this._b3._bx_Body_GetTransform(data.slot);
733
+ const s = this._scratch();
734
+ this._applyTransformToNode(transformNode, s[0], s[1], s[2], s[3], s[4], s[5], s[6]);
735
+ }
736
+ /**
737
+ * Pushes the transform node's pose into the physics body (called during the pre step).
738
+ * @param body the physics body
739
+ * @param node the transform node to read from
740
+ */
741
+ setPhysicsBodyTransformation(body, node) {
742
+ const prestep = body.getPrestepType();
743
+ if (prestep == PhysicsPrestepType.TELEPORT) {
744
+ if (body._pluginDataInstances.length > 0) {
745
+ const m = node;
746
+ const matrixData = m._thinInstanceDataStorage?.matrixData;
747
+ if (!matrixData) {
748
+ return;
749
+ }
750
+ this._createOrUpdateBodyInstances(body, body.getMotionType(), matrixData, 0, body._pluginDataInstances.length, true);
751
+ return;
752
+ }
753
+ const data = body._pluginData;
754
+ const position = this._tmpVec3[0];
755
+ const orientation = this._tmpQuat[0];
756
+ this._nodeTransformToRef(node, position, orientation);
757
+ this._b3._bx_Body_SetTransform(data.slot, position.x, position.y, position.z, orientation.x, orientation.y, orientation.z, orientation.w);
758
+ }
759
+ else if (prestep == PhysicsPrestepType.ACTION) {
760
+ this.setTargetTransform(body, node.absolutePosition, node.absoluteRotationQuaternion);
761
+ }
762
+ else if (prestep == PhysicsPrestepType.DISABLED) {
763
+ Logger.Warn("Prestep type is set to DISABLED. Unable to set physics body transformation.");
764
+ }
765
+ }
766
+ setTargetTransform(body, position, rotation, instanceIndex) {
767
+ this._applyToBodyOrInstances(body, (data) => {
768
+ this._b3._bx_Body_SetTargetTransform(data.slot, position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, this._timeStep);
769
+ }, instanceIndex);
770
+ }
771
+ /** True when the shape puts a mesh inside a container, which Box3D can only do by baking a copy per body. */
772
+ _bakesMeshCopies(data, visited = new Set()) {
773
+ if (visited.has(data)) {
774
+ return false;
775
+ }
776
+ visited.add(data);
777
+ return data.children.some((child) => child.type === PhysicsShapeType.MESH || this._bakesMeshCopies(child, visited));
778
+ }
779
+ _setBodyShape(data, shape) {
780
+ for (const existing of this._shapes) {
781
+ existing?.users.delete(data);
782
+ }
783
+ this._b3._bx_Body_SetShape(data.slot, shape ? shape.slot : 0);
784
+ if (shape) {
785
+ shape.users.add(data);
786
+ }
787
+ this._applyEventFlags(data);
788
+ this._internalUpdateMassProperties(data);
789
+ }
790
+ setShape(body, shape) {
791
+ const shapeData = shape && shape._pluginData ? shape._pluginData : null;
792
+ this._applyToBodyOrInstances(body, (data) => this._setBodyShape(data, shapeData));
793
+ }
794
+ getShape(body) {
795
+ return body.shape ?? null;
796
+ }
797
+ getShapeType(shape) {
798
+ return shape.type ?? shape._pluginData?.type ?? PhysicsShapeType.CONTAINER;
799
+ }
800
+ /** Turns the body's event mask into Box3D's two shape flags, so a body only pays for the events it asked for. */
801
+ _applyEventFlags(data) {
802
+ const touch = data.eventMask & (Box3DEventBits.COLLISION_STARTED | Box3DEventBits.COLLISION_FINISHED) ? 1 : 0;
803
+ const hit = data.eventMask & Box3DEventBits.COLLISION_CONTINUED ? 1 : 0;
804
+ this._b3._bx_Body_SetEventFlags(data.slot, touch, hit);
805
+ }
806
+ setEventMask(body, eventMask, instanceIndex) {
807
+ this._applyToBodyOrInstances(body, (data) => {
808
+ data.eventMask = eventMask;
809
+ this._applyEventFlags(data);
810
+ }, instanceIndex);
811
+ }
812
+ getEventMask(body, instanceIndex) {
813
+ return this._getPluginReference(body, instanceIndex).eventMask;
814
+ }
815
+ setMotionType(body, motionType, instanceIndex) {
816
+ this._applyToBodyOrInstances(body, (data) => {
817
+ data.motionType = motionType;
818
+ this._applyNativeMotionType(data);
819
+ this._internalUpdateMassProperties(data);
820
+ }, instanceIndex);
821
+ }
822
+ /** The Box3D body type: the Babylon motion type, unless the body is parked by ALWAYS_INACTIVE. */
823
+ _applyNativeMotionType(data) {
824
+ const native = data.activation === PhysicsActivationControl.ALWAYS_INACTIVE ? 0 : this._motionTypeToNative(data.motionType);
825
+ this._b3._bx_Body_SetType(data.slot, native);
826
+ }
827
+ /**
828
+ * Babylon's activation control, matched to Havok's behaviour with Box3D sleeping.
829
+ * ALWAYS_ACTIVE turns sleeping off for the body, SIMULATION_CONTROLLED turns it back on. ALWAYS_INACTIVE parks the
830
+ * body: in Havok such a body ignores impulses and velocity changes, is not woken by anything that touches it and
831
+ * still blocks other bodies, which Box3D sleeping alone cannot do, so it becomes a static Box3D body until the
832
+ * control changes. Coming back it is left asleep, like Havok, until something wakes it.
833
+ * @param body the physics body
834
+ * @param controlMode how the body may be activated and deactivated
835
+ * @param instanceIndex optional thin instance index
836
+ */
837
+ setActivationControl(body, controlMode, instanceIndex) {
838
+ this._applyToBodyOrInstances(body, (data) => {
839
+ if (data.activation === controlMode) {
840
+ return;
841
+ }
842
+ const wasInactive = data.activation === PhysicsActivationControl.ALWAYS_INACTIVE;
843
+ data.activation = controlMode;
844
+ if (controlMode === PhysicsActivationControl.ALWAYS_INACTIVE) {
845
+ this._b3._bx_Body_SetLinearVelocity(data.slot, 0, 0, 0);
846
+ this._b3._bx_Body_SetAngularVelocity(data.slot, 0, 0, 0);
847
+ this._applyNativeMotionType(data);
848
+ return;
849
+ }
850
+ this._applyNativeMotionType(data);
851
+ this._internalUpdateMassProperties(data);
852
+ this._b3._bx_Body_EnableSleep(data.slot, controlMode === PhysicsActivationControl.ALWAYS_ACTIVE ? 0 : 1);
853
+ if (wasInactive && controlMode === PhysicsActivationControl.SIMULATION_CONTROLLED) {
854
+ this._b3._bx_Body_SetAwake(data.slot, 0);
855
+ }
856
+ }, instanceIndex);
857
+ }
858
+ getMotionType(body, instanceIndex) {
859
+ return this._getPluginReference(body, instanceIndex).motionType;
860
+ }
861
+ /**
862
+ * Reads Box3D's mass data and converts it to Babylon's form: the inertia is the principal moments of the tensor
863
+ * divided by the mass (Babylon's inertia is per unit mass) and the orientation rotates the principal axes into
864
+ * body space.
865
+ */
866
+ _readMassData(data) {
867
+ // Box3D keeps mass 0 on static and kinematic bodies; Havok reports what the shapes weigh whatever the type
868
+ if (data.motionType === PhysicsMotionType.DYNAMIC && data.activation !== PhysicsActivationControl.ALWAYS_INACTIVE) {
869
+ this._b3._bx_Body_GetMassData(data.slot);
870
+ }
871
+ else {
872
+ this._b3._bx_Body_ComputeShapeMassData(data.slot);
873
+ }
874
+ const s = this._scratch();
875
+ const mass = s[0];
876
+ const principal = new Vector3();
877
+ const orientation = new Quaternion();
878
+ DiagonalizeInertia([s[4], s[5], s[6], s[7], s[8], s[9]], principal, orientation);
879
+ if (mass > 0) {
880
+ principal.scaleInPlace(1 / mass);
881
+ }
882
+ return {
883
+ mass,
884
+ centerOfMass: new Vector3(s[1], s[2], s[3]),
885
+ inertia: principal,
886
+ inertiaOrientation: orientation,
887
+ };
888
+ }
889
+ /** The mass properties of the body: what the user asked for, filled in from the shapes where they said nothing. */
890
+ _resolveMassProperties(data) {
891
+ const computed = this._readMassData(data);
892
+ const user = data.userMassProps ?? {};
893
+ // no invented mass here: a body with no shapes really does weigh nothing, only the tensor written to Box3D
894
+ // needs a positive mass (see _internalUpdateMassProperties)
895
+ const mass = user.mass !== undefined && user.mass > 0 ? user.mass : computed.mass;
896
+ return {
897
+ mass,
898
+ centerOfMass: user.centerOfMass ?? computed.centerOfMass,
899
+ inertia: user.inertia ?? computed.inertia,
900
+ inertiaOrientation: user.inertiaOrientation ?? (user.inertia ? Quaternion.Identity() : computed.inertiaOrientation),
901
+ };
902
+ }
903
+ _internalUpdateMassProperties(data) {
904
+ if (data.motionType !== PhysicsMotionType.DYNAMIC || data.activation === PhysicsActivationControl.ALWAYS_INACTIVE) {
905
+ // motion locks are world space velocity locks, they would fight a kinematic body's target transform
906
+ this._applyMotionLocks(data, false, false, false);
907
+ return;
908
+ }
909
+ this._b3._bx_Body_ApplyMassFromShapes(data.slot);
910
+ const user = data.userMassProps;
911
+ const hasUser = !!user && (user.mass !== undefined || !!user.centerOfMass || !!user.inertia || !!user.inertiaOrientation);
912
+ if (!hasUser) {
913
+ this._applyMotionLocks(data, false, false, false);
914
+ return;
915
+ }
916
+ const props = this._resolveMassProperties(data);
917
+ // a dynamic body with no mass at all would ignore gravity and every impulse
918
+ const mass = props.mass > 0 ? props.mass : 1;
919
+ const inertia = props.inertia;
920
+ const orientation = props.inertiaOrientation;
921
+ // Babylon's inertia is per unit mass and a zero component means infinite inertia about that principal axis.
922
+ // Box3D needs an invertible tensor, so a locked axis gets a very large moment; where the free axes stay aligned
923
+ // with the world, Box3D's motion locks are added on top and make it exact.
924
+ const locked = [inertia.x <= 0, inertia.y <= 0, inertia.z <= 0];
925
+ const largest = Math.max(inertia.x, inertia.y, inertia.z, 1e-4) * LockedInertiaRatio;
926
+ const moments = [locked[0] ? largest : inertia.x, locked[1] ? largest : inertia.y, locked[2] ? largest : inertia.z].map((value) => value * mass);
927
+ const tensor = ComposeInertia(moments, orientation);
928
+ this._b3._bx_Body_SetMassDataFull(data.slot, mass, props.centerOfMass.x, props.centerOfMass.y, props.centerOfMass.z, tensor[0], tensor[1], tensor[2], tensor[3], tensor[4], tensor[5]);
929
+ this._applyLockedAxes(data, locked, orientation);
930
+ }
931
+ /**
932
+ * Box3D locks are world space, Babylon's zero inertia components are body local. Locking two of them leaves a single
933
+ * free axis, and if that axis is world aligned the body can only ever spin about it, so the world locks are exact
934
+ * and stay exact. Everything else relies on the large moment above.
935
+ */
936
+ _applyLockedAxes(data, locked, orientation) {
937
+ const count = locked.filter(Boolean).length;
938
+ if (count === 3) {
939
+ this._applyMotionLocks(data, true, true, true);
940
+ return;
941
+ }
942
+ if (count !== 2) {
943
+ this._applyMotionLocks(data, false, false, false);
944
+ return;
945
+ }
946
+ this._b3._bx_Body_GetTransform(data.slot);
947
+ const s = this._scratch();
948
+ const bodyRotation = this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
949
+ const free = locked.indexOf(false);
950
+ const axis = this._tmpVec3[0].set(free === 0 ? 1 : 0, free === 1 ? 1 : 0, free === 2 ? 1 : 0);
951
+ axis.applyRotationQuaternionInPlace(orientation).applyRotationQuaternionInPlace(bodyRotation);
952
+ const components = [Math.abs(axis.x), Math.abs(axis.y), Math.abs(axis.z)];
953
+ const dominant = components.indexOf(Math.max(...components));
954
+ if (components[dominant] > 0.9999) {
955
+ this._applyMotionLocks(data, dominant !== 0, dominant !== 1, dominant !== 2);
956
+ }
957
+ else {
958
+ this._applyMotionLocks(data, false, false, false);
959
+ }
960
+ }
961
+ _applyMotionLocks(data, x, y, z) {
962
+ const locks = (x ? 1 : 0) | (y ? 2 : 0) | (z ? 4 : 0);
963
+ if (locks === data.angularLocks) {
964
+ return;
965
+ }
966
+ data.angularLocks = locks;
967
+ this._b3._bx_Body_SetMotionLocks(data.slot, 0, 0, 0, x ? 1 : 0, y ? 1 : 0, z ? 1 : 0);
968
+ }
969
+ computeMassProperties(body, instanceIndex) {
970
+ const data = this._getPluginReference(body, instanceIndex);
971
+ this._b3._bx_Body_ApplyMassFromShapes(data.slot);
972
+ const computed = this._readMassData(data);
973
+ this._internalUpdateMassProperties(data);
974
+ return computed;
975
+ }
976
+ setMassProperties(body, massProps, instanceIndex) {
977
+ this._applyToBodyOrInstances(body, (data) => {
978
+ data.userMassProps = massProps;
979
+ this._internalUpdateMassProperties(data);
980
+ }, instanceIndex);
981
+ }
982
+ getMassProperties(body, instanceIndex) {
983
+ return this._resolveMassProperties(this._getPluginReference(body, instanceIndex));
984
+ }
985
+ setLinearDamping(body, damping, instanceIndex) {
986
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetLinearDamping(data.slot, damping), instanceIndex);
987
+ }
988
+ getLinearDamping(body, instanceIndex) {
989
+ return this._b3._bx_Body_GetLinearDamping(this._getPluginReference(body, instanceIndex).slot);
990
+ }
991
+ setAngularDamping(body, damping, instanceIndex) {
992
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetAngularDamping(data.slot, damping), instanceIndex);
993
+ }
994
+ getAngularDamping(body, instanceIndex) {
995
+ return this._b3._bx_Body_GetAngularDamping(this._getPluginReference(body, instanceIndex).slot);
996
+ }
997
+ setLinearVelocity(body, linVel, instanceIndex) {
998
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetLinearVelocity(data.slot, linVel.x, linVel.y, linVel.z), instanceIndex);
999
+ }
1000
+ getLinearVelocityToRef(body, linVel, instanceIndex) {
1001
+ this._b3._bx_Body_GetLinearVelocity(this._getPluginReference(body, instanceIndex).slot);
1002
+ const s = this._scratch();
1003
+ linVel.set(s[0], s[1], s[2]);
1004
+ }
1005
+ applyImpulse(body, impulse, location, instanceIndex) {
1006
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyLinearImpulse(data.slot, impulse.x, impulse.y, impulse.z, location.x, location.y, location.z), instanceIndex);
1007
+ }
1008
+ applyAngularImpulse(body, angularImpulse, instanceIndex) {
1009
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyAngularImpulse(data.slot, angularImpulse.x, angularImpulse.y, angularImpulse.z), instanceIndex);
1010
+ }
1011
+ applyForce(body, force, location, instanceIndex) {
1012
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyForce(data.slot, force.x, force.y, force.z, location.x, location.y, location.z), instanceIndex);
1013
+ }
1014
+ applyTorque(body, torque, instanceIndex) {
1015
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_ApplyTorque(data.slot, torque.x, torque.y, torque.z), instanceIndex);
1016
+ }
1017
+ setAngularVelocity(body, angVel, instanceIndex) {
1018
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetAngularVelocity(data.slot, angVel.x, angVel.y, angVel.z), instanceIndex);
1019
+ }
1020
+ getAngularVelocityToRef(body, angVel, instanceIndex) {
1021
+ this._b3._bx_Body_GetAngularVelocity(this._getPluginReference(body, instanceIndex).slot);
1022
+ const s = this._scratch();
1023
+ angVel.set(s[0], s[1], s[2]);
1024
+ }
1025
+ getBodyGeometry(body) {
1026
+ const shape = body.shape?._pluginData;
1027
+ if (!shape) {
1028
+ return { positions: [], indices: [] };
1029
+ }
1030
+ const vertexCount = this._b3._bx_ShapeDesc_BuildDebugGeometry(shape.slot);
1031
+ const indexCount = this._b3._bx_DebugIndexCount();
1032
+ if (!vertexCount || !indexCount) {
1033
+ return { positions: [], indices: [] };
1034
+ }
1035
+ const positions = new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_DebugPositionsPtr(), vertexCount * 3).slice();
1036
+ const indices = new Uint32Array(this._b3.HEAPU32.buffer, this._b3._bx_DebugIndicesPtr(), indexCount).slice();
1037
+ return { positions, indices };
1038
+ }
1039
+ disposeBody(body) {
1040
+ this.removeBody(body);
1041
+ body._pluginData = undefined;
1042
+ body._pluginDataInstances.length = 0;
1043
+ }
1044
+ setCollisionCallbackEnabled(body, enabled, instanceIndex) {
1045
+ // Havok replaces the mask with all three collision events, or clears it
1046
+ this.setEventMask(body, enabled ? Box3DEventBits.ALL : 0, instanceIndex);
1047
+ }
1048
+ setCollisionEndedCallbackEnabled(body, enabled, instanceIndex) {
1049
+ this._applyToBodyOrInstances(body, (data) => {
1050
+ data.eventMask = enabled ? data.eventMask | Box3DEventBits.COLLISION_FINISHED : data.eventMask & ~Box3DEventBits.COLLISION_FINISHED;
1051
+ this._applyEventFlags(data);
1052
+ }, instanceIndex);
1053
+ }
1054
+ getCollisionObservable(body, instanceIndex) {
1055
+ const slot = this._getPluginReference(body, instanceIndex).slot;
1056
+ let observable = this._bodyCollisionObservable.get(slot);
1057
+ if (!observable) {
1058
+ observable = new Observable();
1059
+ this._bodyCollisionObservable.set(slot, observable);
1060
+ }
1061
+ return observable;
1062
+ }
1063
+ getCollisionEndedObservable(body, instanceIndex) {
1064
+ const slot = this._getPluginReference(body, instanceIndex).slot;
1065
+ let observable = this._bodyCollisionEndedObservable.get(slot);
1066
+ if (!observable) {
1067
+ observable = new Observable();
1068
+ this._bodyCollisionEndedObservable.set(slot, observable);
1069
+ }
1070
+ return observable;
1071
+ }
1072
+ setGravityFactor(body, factor, instanceIndex) {
1073
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_SetGravityScale(data.slot, factor), instanceIndex);
1074
+ }
1075
+ getGravityFactor(body, instanceIndex) {
1076
+ return this._b3._bx_Body_GetGravityScale(this._getPluginReference(body, instanceIndex).slot);
1077
+ }
1078
+ // ----------------------------------------------------------------------------------------
1079
+ // shapes
1080
+ // ----------------------------------------------------------------------------------------
1081
+ _registerShape(shape, slot, type) {
1082
+ if (!slot) {
1083
+ throw new Error("Box3DPlugin: shape creation failed (degenerate geometry?)");
1084
+ }
1085
+ const data = new Box3DShapeData(slot, type);
1086
+ shape._pluginData = data;
1087
+ this._shapes[slot] = data;
1088
+ return data;
1089
+ }
1090
+ _withFloatBuffer(values, fn) {
1091
+ const ptr = this._b3._malloc(values.length * 4);
1092
+ new Float32Array(this._b3.HEAPF32.buffer, ptr, values.length).set(values);
1093
+ try {
1094
+ return fn(ptr);
1095
+ }
1096
+ finally {
1097
+ this._b3._free(ptr);
1098
+ }
1099
+ }
1100
+ /** Copies bytes into the module for the length of `fn`; with no bytes, `fn` gets a null pointer. */
1101
+ _withByteBuffer(values, fn) {
1102
+ if (!values || values.length === 0) {
1103
+ return fn(0);
1104
+ }
1105
+ const ptr = this._b3._malloc(values.length);
1106
+ new Uint8Array(this._b3.HEAPU8.buffer, ptr, values.length).set(values);
1107
+ try {
1108
+ return fn(ptr);
1109
+ }
1110
+ finally {
1111
+ this._b3._free(ptr);
1112
+ }
1113
+ }
1114
+ _withIntBuffer(values, fn) {
1115
+ const ptr = this._b3._malloc(values.length * 4);
1116
+ new Int32Array(this._b3.HEAP32.buffer, ptr, values.length).set(values);
1117
+ try {
1118
+ return fn(ptr);
1119
+ }
1120
+ finally {
1121
+ this._b3._free(ptr);
1122
+ }
1123
+ }
1124
+ _createOptionsFromGroundMesh(options) {
1125
+ const mesh = options.groundMesh;
1126
+ if (!mesh) {
1127
+ return;
1128
+ }
1129
+ const pos = mesh.getVerticesData(VertexBuffer.PositionKind);
1130
+ const transform = mesh.computeWorldMatrix(true);
1131
+ const transformed = [];
1132
+ const p = TmpVectors.Vector3[0];
1133
+ for (let i = 0; i < pos.length; i += 3) {
1134
+ Vector3.FromArrayToRef(pos, i, p);
1135
+ Vector3.TransformCoordinatesToRef(p, transform, p);
1136
+ p.toArray(transformed, i);
1137
+ }
1138
+ const arraySize = ~~(Math.sqrt(transformed.length / 3) - 1);
1139
+ const boundingInfo = mesh.getBoundingInfo();
1140
+ const dim = Math.min(boundingInfo.boundingBox.extendSizeWorld.x, boundingInfo.boundingBox.extendSizeWorld.z);
1141
+ const minX = boundingInfo.boundingBox.minimumWorld.x;
1142
+ const minY = boundingInfo.boundingBox.minimumWorld.y;
1143
+ const minZ = boundingInfo.boundingBox.minimumWorld.z;
1144
+ const matrix = new Float32Array((arraySize + 1) * (arraySize + 1));
1145
+ const elementSize = (dim * 2) / arraySize;
1146
+ matrix.fill(minY);
1147
+ for (let i = 0; i < transformed.length; i += 3) {
1148
+ const x = Math.round((transformed[i + 0] - minX) / elementSize);
1149
+ const z = arraySize - Math.round((transformed[i + 2] - minZ) / elementSize);
1150
+ matrix[z * (arraySize + 1) + x] = transformed[i + 1] - minY;
1151
+ }
1152
+ options.numHeightFieldSamplesX = arraySize + 1;
1153
+ options.numHeightFieldSamplesZ = arraySize + 1;
1154
+ options.heightFieldSizeX = boundingInfo.boundingBox.extendSizeWorld.x * 2;
1155
+ options.heightFieldSizeZ = boundingInfo.boundingBox.extendSizeWorld.z * 2;
1156
+ options.heightFieldData = matrix;
1157
+ }
1158
+ initShape(shape, type, options) {
1159
+ const b3 = this._b3;
1160
+ const center = options.center ?? Vector3.ZeroReadOnly;
1161
+ const rotation = options.rotation ?? Quaternion.Identity();
1162
+ switch (type) {
1163
+ case PhysicsShapeType.SPHERE: {
1164
+ const radius = options.radius ?? 1;
1165
+ this._registerShape(shape, b3._bx_ShapeDesc_CreateSphere(center.x, center.y, center.z, radius), type);
1166
+ break;
1167
+ }
1168
+ case PhysicsShapeType.CAPSULE: {
1169
+ const a = options.pointA ?? Vector3.ZeroReadOnly;
1170
+ const b = options.pointB ?? Vector3.UpReadOnly;
1171
+ this._registerShape(shape, b3._bx_ShapeDesc_CreateCapsule(a.x, a.y, a.z, b.x, b.y, b.z, options.radius ?? 0), type);
1172
+ break;
1173
+ }
1174
+ case PhysicsShapeType.BOX: {
1175
+ const extents = options.extents ?? Vector3.OneReadOnly;
1176
+ this._registerShape(shape, b3._bx_ShapeDesc_CreateBox(extents.x * 0.5, extents.y * 0.5, extents.z * 0.5, center.x, center.y, center.z, rotation.x, rotation.y, rotation.z, rotation.w), type);
1177
+ break;
1178
+ }
1179
+ case PhysicsShapeType.CYLINDER: {
1180
+ const a = options.pointA ?? Vector3.ZeroReadOnly;
1181
+ const b = options.pointB ?? Vector3.UpReadOnly;
1182
+ const axis = b.subtract(a);
1183
+ const height = axis.length();
1184
+ const mid = a.add(b).scaleInPlace(0.5);
1185
+ const q = this._tmpQuat[0];
1186
+ if (height > 1e-6) {
1187
+ axis.scaleInPlace(1 / height);
1188
+ Quaternion.FromUnitVectorsToRef(Vector3.UpReadOnly, axis, q);
1189
+ }
1190
+ else {
1191
+ q.set(0, 0, 0, 1);
1192
+ }
1193
+ this._registerShape(shape, b3._bx_ShapeDesc_CreateCylinder(height, options.radius ?? 0, 16, mid.x, mid.y, mid.z, q.x, q.y, q.z, q.w), type);
1194
+ break;
1195
+ }
1196
+ case PhysicsShapeType.CONVEX_HULL:
1197
+ case PhysicsShapeType.MESH: {
1198
+ const mesh = options.mesh;
1199
+ if (!mesh) {
1200
+ throw new Error("No mesh provided to create physics shape.");
1201
+ }
1202
+ const needIndices = type === PhysicsShapeType.MESH;
1203
+ const flipWinding = !mesh.getScene().useRightHandedSystem;
1204
+ const accum = new MeshGeometryAccumulator(needIndices, flipWinding);
1205
+ accum.addNodeMeshes(mesh, !!options.includeChildMeshes);
1206
+ if (type === PhysicsShapeType.CONVEX_HULL) {
1207
+ const slot = this._withFloatBuffer(accum.vertices, (ptr) => b3._bx_ShapeDesc_CreateHull(ptr, accum.vertexCount));
1208
+ this._registerShape(shape, slot, type);
1209
+ }
1210
+ else {
1211
+ const slot = this._withFloatBuffer(accum.vertices, (vptr) => this._withIntBuffer(accum.indices, (iptr) => b3._bx_ShapeDesc_CreateMesh(vptr, accum.vertexCount, iptr, accum.triangleCount, 1, 1, 1, 0, 0)));
1212
+ this._registerShape(shape, slot, type);
1213
+ }
1214
+ break;
1215
+ }
1216
+ case PhysicsShapeType.HEIGHTFIELD: {
1217
+ if (options.groundMesh) {
1218
+ this._createOptionsFromGroundMesh(options);
1219
+ }
1220
+ const nx = options.numHeightFieldSamplesX;
1221
+ const nz = options.numHeightFieldSamplesZ;
1222
+ if (!nx || !nz || !options.heightFieldSizeX || !options.heightFieldSizeZ || !options.heightFieldData) {
1223
+ throw new Error("Missing required heightfield parameters");
1224
+ }
1225
+ const heights = new Float32Array(nx * nz);
1226
+ for (let x = 0; x < nx; x++) {
1227
+ for (let z = 0; z < nz; z++) {
1228
+ // box3d rows run along z (index z * countX + x); Babylon data rows are flipped along z
1229
+ heights[z * nx + x] = options.heightFieldData[(nz - 1 - z) * nx + x];
1230
+ }
1231
+ }
1232
+ const scaleX = options.heightFieldSizeX / (nx - 1);
1233
+ const scaleZ = options.heightFieldSizeZ / (nz - 1);
1234
+ const cellsX = nx - 1;
1235
+ const cellsZ = nz - 1;
1236
+ const source = options.heightFieldMaterials;
1237
+ if (source && source.length !== cellsX * cellsZ) {
1238
+ throw new Error(`heightFieldMaterials needs ${cellsX * cellsZ} cells (${cellsX} × ${cellsZ}), got ${source.length}`);
1239
+ }
1240
+ // Cells flip along z the same way the heights do.
1241
+ const materials = source ? new Uint8Array(cellsX * cellsZ) : null;
1242
+ if (source && materials) {
1243
+ for (let x = 0; x < cellsX; x++) {
1244
+ for (let z = 0; z < cellsZ; z++) {
1245
+ materials[z * cellsX + x] = source[(cellsZ - 1 - z) * cellsX + x];
1246
+ }
1247
+ }
1248
+ }
1249
+ const slot = this._withFloatBuffer(heights, (heightsPtr) => this._withByteBuffer(materials, (materialsPtr) => b3._bx_ShapeDesc_CreateHeightField(heightsPtr, materialsPtr, nx, nz, scaleX, 1, scaleZ, 0, -options.heightFieldSizeX * 0.5, 0, -options.heightFieldSizeZ * 0.5)));
1250
+ this._registerShape(shape, slot, type);
1251
+ break;
1252
+ }
1253
+ case PhysicsShapeType.CONTAINER: {
1254
+ this._registerShape(shape, b3._bx_ShapeDesc_CreateContainer(), type);
1255
+ break;
1256
+ }
1257
+ default:
1258
+ throw new Error("Unsupported Shape Type.");
1259
+ }
1260
+ }
1261
+ // Re-instantiates the shape on every body that uses it (or uses a container holding it). Only needed for geometry
1262
+ // and child changes; filters, materials and density are applied in place, see _syncShapeUsers.
1263
+ _refreshShapeUsers(data, visited = new Set()) {
1264
+ if (visited.has(data)) {
1265
+ return;
1266
+ }
1267
+ visited.add(data);
1268
+ for (const body of data.users) {
1269
+ this._b3._bx_Body_SetShape(body.slot, data.slot);
1270
+ this._applyEventFlags(body);
1271
+ this._internalUpdateMassProperties(body);
1272
+ }
1273
+ for (const parent of data.parents) {
1274
+ this._refreshShapeUsers(parent, visited);
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Pushes changed description properties into the Box3D shapes that are already on the bodies using this shape,
1279
+ * directly or through a container. Rebuilding them instead would drop their contacts and re-apply the body mass.
1280
+ */
1281
+ _syncShapeUsers(data, what, visited = new Set(), changed = data) {
1282
+ if (visited.has(data)) {
1283
+ return;
1284
+ }
1285
+ visited.add(data);
1286
+ for (const body of data.users) {
1287
+ this._b3._bx_Body_SyncShapeDesc(body.slot, changed.slot, what);
1288
+ if (what & Box3DShapeSync.DENSITY) {
1289
+ this._internalUpdateMassProperties(body);
1290
+ }
1291
+ }
1292
+ for (const parent of data.parents) {
1293
+ this._syncShapeUsers(parent, what, visited, changed);
1294
+ }
1295
+ }
1296
+ setShapeFilterMembershipMask(shape, membershipMask) {
1297
+ const data = shape._pluginData;
1298
+ const collide = this._b3._bx_ShapeDesc_GetMaskBits(data.slot);
1299
+ this._b3._bx_ShapeDesc_SetFilter(data.slot, membershipMask >>> 0, collide >>> 0);
1300
+ this._syncShapeUsers(data, Box3DShapeSync.FILTER);
1301
+ }
1302
+ getShapeFilterMembershipMask(shape) {
1303
+ return this._b3._bx_ShapeDesc_GetCategoryBits(shape._pluginData.slot);
1304
+ }
1305
+ setShapeFilterCollideMask(shape, collideMask) {
1306
+ const data = shape._pluginData;
1307
+ const membership = this._b3._bx_ShapeDesc_GetCategoryBits(data.slot);
1308
+ this._b3._bx_ShapeDesc_SetFilter(data.slot, membership >>> 0, collideMask >>> 0);
1309
+ this._syncShapeUsers(data, Box3DShapeSync.FILTER);
1310
+ }
1311
+ getShapeFilterCollideMask(shape) {
1312
+ return this._b3._bx_ShapeDesc_GetMaskBits(shape._pluginData.slot);
1313
+ }
1314
+ setMaterial(shape, material) {
1315
+ const data = shape._pluginData;
1316
+ data.material = material;
1317
+ this._b3._bx_ShapeDesc_SetMaterial(data.slot, material.friction ?? 0.5, material.restitution ?? 0);
1318
+ this._syncShapeUsers(data, Box3DShapeSync.MATERIAL);
1319
+ }
1320
+ getMaterial(shape) {
1321
+ const data = shape._pluginData;
1322
+ return {
1323
+ friction: this._b3._bx_ShapeDesc_GetFriction(data.slot),
1324
+ staticFriction: data.material.staticFriction,
1325
+ restitution: this._b3._bx_ShapeDesc_GetRestitution(data.slot),
1326
+ frictionCombine: data.material.frictionCombine,
1327
+ restitutionCombine: data.material.restitutionCombine,
1328
+ };
1329
+ }
1330
+ setDensity(shape, density) {
1331
+ const data = shape._pluginData;
1332
+ this._b3._bx_ShapeDesc_SetDensity(data.slot, density);
1333
+ this._syncShapeUsers(data, Box3DShapeSync.DENSITY);
1334
+ }
1335
+ getDensity(shape) {
1336
+ return this._b3._bx_ShapeDesc_GetDensity(shape._pluginData.slot);
1337
+ }
1338
+ addChild(shape, newChild, translation, rotation, scale) {
1339
+ const parent = shape._pluginData;
1340
+ const child = newChild._pluginData;
1341
+ const t = translation ?? Vector3.ZeroReadOnly;
1342
+ const r = rotation ?? Quaternion.Identity();
1343
+ const s = scale ?? Vector3.OneReadOnly;
1344
+ this._b3._bx_ShapeDesc_AddChild(parent.slot, child.slot, t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z);
1345
+ parent.children.push(child);
1346
+ child.parents.add(parent);
1347
+ this._refreshShapeUsers(parent);
1348
+ }
1349
+ removeChild(shape, childIndex) {
1350
+ const parent = shape._pluginData;
1351
+ this._b3._bx_ShapeDesc_RemoveChild(parent.slot, childIndex);
1352
+ const [child] = parent.children.splice(childIndex, 1);
1353
+ if (child && !parent.children.includes(child)) {
1354
+ child.parents.delete(parent);
1355
+ }
1356
+ this._refreshShapeUsers(parent);
1357
+ }
1358
+ getNumChildren(shape) {
1359
+ return this._b3._bx_ShapeDesc_GetChildCount(shape._pluginData.slot);
1360
+ }
1361
+ getBoundingBox(shape) {
1362
+ this._b3._bx_ShapeDesc_GetAABB(shape._pluginData.slot);
1363
+ const s = this._scratch();
1364
+ TmpVectors.Vector3[0].set(s[0], s[1], s[2]);
1365
+ TmpVectors.Vector3[1].set(s[3], s[4], s[5]);
1366
+ return new BoundingBox(TmpVectors.Vector3[0], TmpVectors.Vector3[1], Matrix.IdentityReadOnly);
1367
+ }
1368
+ getBodyBoundingBox(body) {
1369
+ const data = this._getPluginReference(body);
1370
+ this._b3._bx_Body_GetAABB(data.slot);
1371
+ const s = this._scratch();
1372
+ TmpVectors.Vector3[0].set(s[0], s[1], s[2]);
1373
+ TmpVectors.Vector3[1].set(s[3], s[4], s[5]);
1374
+ return new BoundingBox(TmpVectors.Vector3[0], TmpVectors.Vector3[1], Matrix.IdentityReadOnly);
1375
+ }
1376
+ disposeShape(shape) {
1377
+ const data = shape._pluginData;
1378
+ if (!data) {
1379
+ return;
1380
+ }
1381
+ for (const parent of data.parents) {
1382
+ const index = parent.children.indexOf(data);
1383
+ if (index >= 0) {
1384
+ parent.children.splice(index, 1);
1385
+ this._b3._bx_ShapeDesc_RemoveChild(parent.slot, index);
1386
+ }
1387
+ }
1388
+ for (const child of data.children) {
1389
+ child.parents.delete(data);
1390
+ }
1391
+ this._shapes[data.slot] = undefined;
1392
+ this._b3._bx_ShapeDesc_Destroy(data.slot);
1393
+ shape._pluginData = undefined;
1394
+ }
1395
+ setTrigger(shape, isTrigger) {
1396
+ const data = shape._pluginData;
1397
+ this._b3._bx_ShapeDesc_SetSensor(data.slot, isTrigger ? 1 : 0);
1398
+ // Box3D decides at creation whether a shape is a sensor, so this one does need a rebuild
1399
+ this._refreshShapeUsers(data);
1400
+ }
1401
+ // ----------------------------------------------------------------------------------------
1402
+ // constraints
1403
+ // ----------------------------------------------------------------------------------------
1404
+ _frameToBuffer(buffer, offset, pivot, xAxis, yAxis, zAxis) {
1405
+ const m = TmpVectors.Matrix[0];
1406
+ Matrix.FromValuesToRef(xAxis.x, xAxis.y, xAxis.z, 0, yAxis.x, yAxis.y, yAxis.z, 0, zAxis.x, zAxis.y, zAxis.z, 0, 0, 0, 0, 1, m);
1407
+ const q = this._tmpQuat[2];
1408
+ Quaternion.FromRotationMatrixToRef(m, q);
1409
+ q.normalize();
1410
+ buffer[offset] = pivot.x;
1411
+ buffer[offset + 1] = pivot.y;
1412
+ buffer[offset + 2] = pivot.z;
1413
+ buffer[offset + 3] = q.x;
1414
+ buffer[offset + 4] = q.y;
1415
+ buffer[offset + 5] = q.z;
1416
+ buffer[offset + 6] = q.w;
1417
+ }
1418
+ _buildFrames(cdata, constraint, bodyA, bodyB) {
1419
+ const options = constraint.options;
1420
+ const pivotA = options.pivotA ?? Vector3.ZeroReadOnly;
1421
+ const pivotB = options.pivotB ?? Vector3.ZeroReadOnly;
1422
+ const axisA = (options.axisA ?? Vector3.RightReadOnly).normalizeToNew();
1423
+ const axisB = (options.axisB ?? Vector3.RightReadOnly).normalizeToNew();
1424
+ const perpA = options.perpAxisA ? options.perpAxisA.normalizeToNew() : axisA.getNormalToRef(new Vector3());
1425
+ const perpB = options.perpAxisB ? options.perpAxisB.normalizeToNew() : axisB.getNormalToRef(new Vector3());
1426
+ // Gram-Schmidt in case the user's perp axis is not exactly perpendicular
1427
+ perpA.subtractInPlace(axisA.scale(Vector3.Dot(axisA, perpA))).normalize();
1428
+ perpB.subtractInPlace(axisB.scale(Vector3.Dot(axisB, perpB))).normalize();
1429
+ const thirdA = Vector3.Cross(axisA, perpA);
1430
+ const thirdB = Vector3.Cross(axisB, perpB);
1431
+ if (!constraint._initOptions) {
1432
+ constraint._initOptions = {
1433
+ axisA: axisA.clone(),
1434
+ axisB: axisB.clone(),
1435
+ perpAxisA: perpA.clone(),
1436
+ perpAxisB: perpB.clone(),
1437
+ pivotA: pivotA.clone(),
1438
+ pivotB: pivotB.clone(),
1439
+ };
1440
+ }
1441
+ const frames = cdata.frames;
1442
+ if (cdata.plan) {
1443
+ // SIX_DOF: the constraint basis is Havok's [axis, perpAxis, axis x perpAxis]; the plan picks which of them
1444
+ // becomes Box3D's frame x, y and z (cyclic permutations, so the frames stay right handed)
1445
+ const basis = cdata.plan.basis;
1446
+ if (basis) {
1447
+ const basisA = [axisA, perpA, thirdA];
1448
+ const basisB = [axisB, perpB, thirdB];
1449
+ this._frameToBuffer(frames, 0, pivotA, basisA[basis[0]], basisA[basis[1]], basisA[basis[2]]);
1450
+ this._frameToBuffer(frames, 7, pivotB, basisB[basis[0]], basisB[basis[1]], basisB[basis[2]]);
1451
+ }
1452
+ else {
1453
+ frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, 0, 0, 0, 1]);
1454
+ }
1455
+ return;
1456
+ }
1457
+ switch (cdata.type) {
1458
+ case PhysicsConstraintType.HINGE:
1459
+ case PhysicsConstraintType.BALL_AND_SOCKET:
1460
+ // box3d: revolute rotates about the frame z axis, spherical cone is centered on frame A z
1461
+ this._frameToBuffer(frames, 0, pivotA, perpA, Vector3.Cross(axisA, perpA), axisA);
1462
+ this._frameToBuffer(frames, 7, pivotB, perpB, Vector3.Cross(axisB, perpB), axisB);
1463
+ break;
1464
+ case PhysicsConstraintType.PRISMATIC:
1465
+ case PhysicsConstraintType.SLIDER:
1466
+ // box3d: prismatic slides along the frame A x axis
1467
+ this._frameToBuffer(frames, 0, pivotA, axisA, perpA, thirdA);
1468
+ this._frameToBuffer(frames, 7, pivotB, axisB, perpB, thirdB);
1469
+ break;
1470
+ case PhysicsConstraintType.LOCK: {
1471
+ // weld the bodies in their current relative pose
1472
+ this._b3._bx_Body_GetTransform(bodyA.slot);
1473
+ let s = this._scratch();
1474
+ const qA = this._tmpQuat[0];
1475
+ qA.set(s[3], s[4], s[5], s[6]);
1476
+ this._b3._bx_Body_GetTransform(bodyB.slot);
1477
+ s = this._scratch();
1478
+ const qB = this._tmpQuat[1];
1479
+ qB.set(s[3], s[4], s[5], s[6]);
1480
+ // frameB rotation = inv(qB) * qA so both frames coincide in world space now
1481
+ const rel = qB.invert().multiply(qA).normalize();
1482
+ frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, rel.x, rel.y, rel.z, rel.w]);
1483
+ break;
1484
+ }
1485
+ default:
1486
+ frames.set([pivotA.x, pivotA.y, pivotA.z, 0, 0, 0, 1, pivotB.x, pivotB.y, pivotB.z, 0, 0, 0, 1]);
1487
+ break;
1488
+ }
1489
+ }
1490
+ _createJoint(cdata, bodyA, bodyB) {
1491
+ this._scratch().set(cdata.frames);
1492
+ const joint = this._b3._bx_CreateJoint(this._worldSlot, cdata.jointType, bodyA.slot, bodyB.slot, cdata.collisions ? 1 : 0, cdata.length);
1493
+ if (!joint) {
1494
+ Logger.Warn("Box3DPlugin: joint creation failed.");
1495
+ return 0;
1496
+ }
1497
+ // A slider allows rotation about the axis in Havok. Box3D's prismatic locks it.
1498
+ this._applyAxisState(cdata, joint, bodyA, bodyB);
1499
+ return joint;
1500
+ }
1501
+ /** Reads Physics6DoFConstraint.limits into the axis table with Havok's rules. */
1502
+ _readSixDofLimits(cdata, limits) {
1503
+ for (const limit of limits ?? []) {
1504
+ const state = this._axisState(cdata, limit.axis);
1505
+ if (limit.minLimit === 0 && limit.maxLimit === 0) {
1506
+ state.mode = PhysicsConstraintAxisLimitMode.LOCKED;
1507
+ state.min = 0;
1508
+ state.max = 0;
1509
+ }
1510
+ else if (limit.minLimit !== undefined || limit.maxLimit !== undefined) {
1511
+ // a missing side is unbounded
1512
+ state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
1513
+ state.min = limit.minLimit ?? -Infinity;
1514
+ state.max = limit.maxLimit ?? Infinity;
1515
+ }
1516
+ state.stiffness = limit.stiffness ?? 0;
1517
+ state.damping = limit.damping ?? 0;
1518
+ }
1519
+ }
1520
+ /**
1521
+ * Maps the six axes of a SIX_DOF constraint onto one Box3D joint, following Havok's semantics: an axis that is not
1522
+ * listed is free, 0/0 is locked, anything else is limited. The constraint frame is [axis, perpAxis, axis x perpAxis];
1523
+ * ANGULAR_X rotates about axis, ANGULAR_Y about perpAxis and ANGULAR_Z about axis x perpAxis, all measured as the
1524
+ * rotation of the child frame relative to the parent frame (right handed, like Havok).
1525
+ * - all linear locked, all angular locked: weld
1526
+ * - all linear locked, one angular axis open: revolute about it, with its limits (one sided ranges work)
1527
+ * - all linear locked, two or three angular axes open: spherical, twist limits from ANGULAR_X and one symmetric
1528
+ * cone from ANGULAR_Y/Z (Box3D has no elliptical cone, the larger swing range is used)
1529
+ * - two linear locked, angular locked: prismatic along the open linear axis
1530
+ * - only LINEAR_DISTANCE constrained: distance joint (a spring when min == max and a stiffness is set)
1531
+ * - nothing constrained: filter joint (collision filtering only)
1532
+ * Anything else warns once and uses the closest of these.
1533
+ */
1534
+ _planSixDof(cdata) {
1535
+ const Axis = PhysicsConstraintAxis;
1536
+ const Mode = PhysicsConstraintAxisLimitMode;
1537
+ const mode = (axis) => cdata.axes.get(axis)?.mode ?? Mode.FREE;
1538
+ const range = (axis) => {
1539
+ const state = cdata.axes.get(axis);
1540
+ return state.mode === Mode.LOCKED ? [0, 0] : [Math.min(state.min, state.max), Math.max(state.min, state.max)];
1541
+ };
1542
+ const clamp = (value, limit) => Math.max(-limit, Math.min(limit, value));
1543
+ const maxAngle = 0.99 * Math.PI;
1544
+ // Box3D's B3_HUGE, keeps an unbounded side of a linear range out of the solver as a real number
1545
+ const maxLength = 100000;
1546
+ const plan = {
1547
+ jointType: Box3DJointType.WELD,
1548
+ basis: [0, 1, 2],
1549
+ primaryAxis: null,
1550
+ coneAngle: -1,
1551
+ twist: null,
1552
+ limit: null,
1553
+ spring: false,
1554
+ stiffness: 0,
1555
+ damping: 0,
1556
+ angularStiffness: false,
1557
+ };
1558
+ for (const [axis, state] of cdata.axes) {
1559
+ if ((state.stiffness > 0 || state.damping > 0) && state.stiffness >= plan.stiffness) {
1560
+ plan.stiffness = state.stiffness;
1561
+ plan.damping = state.damping;
1562
+ plan.angularStiffness = axis === Axis.ANGULAR_X || axis === Axis.ANGULAR_Y || axis === Axis.ANGULAR_Z;
1563
+ }
1564
+ }
1565
+ const linear = [Axis.LINEAR_X, Axis.LINEAR_Y, Axis.LINEAR_Z];
1566
+ const angular = [Axis.ANGULAR_X, Axis.ANGULAR_Y, Axis.ANGULAR_Z];
1567
+ const linearLocked = linear.filter((a) => mode(a) === Mode.LOCKED).length;
1568
+ const linearFree = linear.filter((a) => mode(a) === Mode.FREE).length;
1569
+ const angularOpen = angular.filter((a) => mode(a) !== Mode.LOCKED);
1570
+ const angularFree = angular.filter((a) => mode(a) === Mode.FREE).length;
1571
+ const distanceMode = mode(Axis.LINEAR_DISTANCE);
1572
+ if (distanceMode !== Mode.FREE) {
1573
+ if (linearFree === 3 && angularFree === 3) {
1574
+ const [lower, upper] = range(Axis.LINEAR_DISTANCE);
1575
+ plan.jointType = Box3DJointType.DISTANCE;
1576
+ plan.basis = null;
1577
+ plan.primaryAxis = Axis.LINEAR_DISTANCE;
1578
+ plan.limit = [Math.min(Math.max(0, lower), maxLength), Math.min(Math.max(0, upper), maxLength)];
1579
+ plan.spring = plan.stiffness > 0 && Math.abs(upper - lower) < 1e-6 && isFinite(lower);
1580
+ return plan;
1581
+ }
1582
+ this._warnOnce("sixdof-distance", "SIX_DOF: LINEAR_DISTANCE combined with other constrained axes is not supported with Box3D, the distance limit is ignored.");
1583
+ }
1584
+ // one open linear axis (free or limited) with the other two locked slides; see the prismatic case below
1585
+ let pointJoint = linearLocked === 3;
1586
+ if (!pointJoint && linearLocked !== 2 && linearFree === 0) {
1587
+ this._warnOnce("sixdof-linear-limited", "SIX_DOF: two or more limited linear axes are treated as locked with Box3D.");
1588
+ pointJoint = true;
1589
+ }
1590
+ if (pointJoint) {
1591
+ if (angularOpen.length === 0) {
1592
+ return plan;
1593
+ }
1594
+ if (angularOpen.length === 1) {
1595
+ // Box3D revolutes rotate about frame z
1596
+ const axis = angularOpen[0];
1597
+ plan.jointType = Box3DJointType.REVOLUTE;
1598
+ plan.basis = [
1599
+ [1, 2, 0],
1600
+ [2, 0, 1],
1601
+ [0, 1, 2],
1602
+ ][angular.indexOf(axis)];
1603
+ plan.primaryAxis = axis;
1604
+ if (mode(axis) === Mode.LIMITED) {
1605
+ const [lower, upper] = range(axis);
1606
+ plan.limit = [clamp(lower, maxAngle), clamp(upper, maxAngle)];
1607
+ }
1608
+ return plan;
1609
+ }
1610
+ // spherical: cone centered on frame z (axis), twist about frame z
1611
+ plan.jointType = Box3DJointType.SPHERICAL;
1612
+ plan.basis = [1, 2, 0];
1613
+ if (mode(Axis.ANGULAR_X) !== Mode.FREE) {
1614
+ const [lower, upper] = range(Axis.ANGULAR_X);
1615
+ plan.twist = [clamp(lower, maxAngle), clamp(upper, maxAngle)];
1616
+ }
1617
+ const swingModes = [mode(Axis.ANGULAR_Y), mode(Axis.ANGULAR_Z)];
1618
+ if (swingModes.some((m) => m !== Mode.FREE)) {
1619
+ const extents = [];
1620
+ let asymmetric = false;
1621
+ for (const axis of [Axis.ANGULAR_Y, Axis.ANGULAR_Z]) {
1622
+ if (mode(axis) === Mode.FREE) {
1623
+ continue;
1624
+ }
1625
+ const [lower, upper] = range(axis);
1626
+ extents.push(Math.max(Math.abs(lower), Math.abs(upper)));
1627
+ asymmetric || (asymmetric = Math.abs(lower + upper) > 1e-4);
1628
+ }
1629
+ const cone = Math.max(...extents);
1630
+ if (isFinite(cone)) {
1631
+ if (swingModes.includes(Mode.FREE) || Math.abs(extents[0] - extents[1]) > 1e-4 || asymmetric) {
1632
+ this._warnOnce("sixdof-cone", "SIX_DOF: Box3D has one symmetric swing cone, ANGULAR_Y and ANGULAR_Z use the larger of their ranges.");
1633
+ }
1634
+ if (cone > Math.PI / 2) {
1635
+ this._warnOnce("sixdof-cone-90", "SIX_DOF: Box3D cone limits are at most 90 degrees, larger swing ranges are clamped.");
1636
+ }
1637
+ plan.coneAngle = Math.min(cone, Math.PI / 2);
1638
+ }
1639
+ }
1640
+ return plan;
1641
+ }
1642
+ if (linearLocked === 2) {
1643
+ // Box3D prismatics slide along frame x
1644
+ const slide = linear.find((a) => mode(a) !== Mode.LOCKED);
1645
+ plan.jointType = Box3DJointType.PRISMATIC;
1646
+ plan.basis = [
1647
+ [0, 1, 2],
1648
+ [1, 2, 0],
1649
+ [2, 0, 1],
1650
+ ][linear.indexOf(slide)];
1651
+ plan.primaryAxis = slide;
1652
+ if (mode(slide) === Mode.LIMITED) {
1653
+ const [lower, upper] = range(slide);
1654
+ plan.limit = [clamp(lower, maxLength), clamp(upper, maxLength)];
1655
+ }
1656
+ if (angularOpen.length) {
1657
+ this._warnOnce("sixdof-prismatic", "SIX_DOF: a sliding axis with rotation is not supported with Box3D, rotation is locked.");
1658
+ }
1659
+ return plan;
1660
+ }
1661
+ plan.jointType = Box3DJointType.FILTER;
1662
+ plan.basis = null;
1663
+ if (linearFree !== 3 || angularFree !== 3) {
1664
+ this._warnOnce("sixdof-unsupported", "SIX_DOF: this combination of free and limited axes is not supported with Box3D, the bodies are left unconstrained.");
1665
+ }
1666
+ return plan;
1667
+ }
1668
+ /** Box3D spring settings for a Havok spring constant and damping on a body pair. */
1669
+ _springToHertz(stiffness, damping, angular, bodyA, bodyB) {
1670
+ // effective mass (or rotational inertia) of the pair, a static or animated body counts as infinitely heavy
1671
+ const inverse = (data) => {
1672
+ if (data.motionType !== PhysicsMotionType.DYNAMIC) {
1673
+ return 0;
1674
+ }
1675
+ this._b3._bx_Body_GetMassData(data.slot);
1676
+ const s = this._scratch();
1677
+ const value = angular ? (s[4] + s[5] + s[6]) / 3 : s[0];
1678
+ return value > 0 ? 1 / value : 0;
1679
+ };
1680
+ const inverseSum = inverse(bodyA) + inverse(bodyB);
1681
+ const mass = inverseSum > 0 ? 1 / inverseSum : 1;
1682
+ const k = Math.max(stiffness, 0);
1683
+ return {
1684
+ hertz: Math.sqrt(k / mass) / (2 * Math.PI),
1685
+ dampingRatio: k > 0 ? damping / (2 * Math.sqrt(k * mass)) : 1,
1686
+ };
1687
+ }
1688
+ initConstraint(constraint, body, childBody, instanceIndex, childInstanceIndex) {
1689
+ const type = constraint.type;
1690
+ const options = constraint.options;
1691
+ if (!type || !options) {
1692
+ Logger.Warn("No constraint type or options. Constraint is invalid.");
1693
+ return;
1694
+ }
1695
+ if ((body._pluginDataInstances.length > 0 && instanceIndex === undefined) || (childBody._pluginDataInstances.length > 0 && childInstanceIndex === undefined)) {
1696
+ Logger.Warn("Body is instanced but no instance index was specified. Constraint will not be applied.");
1697
+ return;
1698
+ }
1699
+ const firstPair = !constraint._pluginData;
1700
+ const cdata = constraint._pluginData ?? new Box3DConstraintData();
1701
+ constraint._pluginData = cdata;
1702
+ cdata.type = type;
1703
+ if (firstPair) {
1704
+ cdata.collisions = !!options.collision;
1705
+ }
1706
+ switch (type) {
1707
+ case PhysicsConstraintType.BALL_AND_SOCKET:
1708
+ cdata.jointType = Box3DJointType.SPHERICAL;
1709
+ break;
1710
+ case PhysicsConstraintType.HINGE:
1711
+ cdata.jointType = Box3DJointType.REVOLUTE;
1712
+ break;
1713
+ case PhysicsConstraintType.PRISMATIC:
1714
+ case PhysicsConstraintType.SLIDER:
1715
+ cdata.jointType = Box3DJointType.PRISMATIC;
1716
+ break;
1717
+ case PhysicsConstraintType.LOCK:
1718
+ cdata.jointType = Box3DJointType.WELD;
1719
+ break;
1720
+ case PhysicsConstraintType.DISTANCE:
1721
+ cdata.jointType = Box3DJointType.DISTANCE;
1722
+ break;
1723
+ case PhysicsConstraintType.SIX_DOF:
1724
+ // later pairs of the same constraint share the axis table, including changes made since the first pair
1725
+ if (firstPair) {
1726
+ this._readSixDofLimits(cdata, constraint.limits);
1727
+ }
1728
+ cdata.plan = this._planSixDof(cdata);
1729
+ cdata.jointType = cdata.plan.jointType;
1730
+ break;
1731
+ default:
1732
+ Logger.Warn("Box3DPlugin: unknown constraint type " + type);
1733
+ return;
1734
+ }
1735
+ const bodyA = this._getPluginReference(body, instanceIndex);
1736
+ const bodyB = this._getPluginReference(childBody, childInstanceIndex);
1737
+ this._buildFrames(cdata, constraint, bodyA, bodyB);
1738
+ this._updateJointLength(cdata, options.maxDistance, bodyA, bodyB);
1739
+ // joints[i] belongs to pairs[i], 0 while the constraint is disabled or when creation failed
1740
+ const joint = cdata.enabled ? this._createJoint(cdata, bodyA, bodyB) : 0;
1741
+ cdata.joints.push(joint);
1742
+ cdata.pairs.push({ parent: body, parentIndex: instanceIndex ?? 0, child: childBody, childIndex: childInstanceIndex ?? 0, parentData: bodyA, childData: bodyB });
1743
+ }
1744
+ /** Length parameter passed to Box3D's distance joint at creation (the rest length of its spring). */
1745
+ _updateJointLength(cdata, maxDistance, bodyA, bodyB) {
1746
+ if (cdata.type === PhysicsConstraintType.DISTANCE) {
1747
+ cdata.length = maxDistance ?? 0;
1748
+ }
1749
+ else if (cdata.plan?.jointType === Box3DJointType.DISTANCE && cdata.plan.limit) {
1750
+ // a spring rests at its (single) limit; for a rope the rest length is unused, its limits do the work
1751
+ cdata.length = cdata.plan.limit[0];
1752
+ }
1753
+ else {
1754
+ return;
1755
+ }
1756
+ if (cdata.length <= 0) {
1757
+ // use the current distance between the world space pivots
1758
+ cdata.length = Math.max(0.01, this._currentPivotDistance(cdata, bodyA, bodyB));
1759
+ }
1760
+ }
1761
+ _currentPivotDistance(cdata, bodyA, bodyB) {
1762
+ const f = cdata.frames;
1763
+ const worldA = this._tmpVec3[0];
1764
+ const worldB = this._tmpVec3[1];
1765
+ this._b3._bx_Body_GetTransform(bodyA.slot);
1766
+ let s = this._scratch();
1767
+ this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
1768
+ worldA.set(f[0], f[1], f[2]).applyRotationQuaternionInPlace(this._tmpQuat[0]).addInPlaceFromFloats(s[0], s[1], s[2]);
1769
+ this._b3._bx_Body_GetTransform(bodyB.slot);
1770
+ s = this._scratch();
1771
+ this._tmpQuat[0].set(s[3], s[4], s[5], s[6]);
1772
+ worldB.set(f[7], f[8], f[9]).applyRotationQuaternionInPlace(this._tmpQuat[0]).addInPlaceFromFloats(s[0], s[1], s[2]);
1773
+ return Vector3.Distance(worldA, worldB);
1774
+ }
1775
+ setEnabled(constraint, isEnabled) {
1776
+ const cdata = constraint._pluginData;
1777
+ if (!cdata || cdata.enabled === isEnabled) {
1778
+ return;
1779
+ }
1780
+ cdata.enabled = isEnabled;
1781
+ // Box3D has no enable flag on joints: destroy them, or re-create them from the saved frames, plan and axis state.
1782
+ this._recreateJoints(cdata);
1783
+ }
1784
+ _destroyJoint(joint) {
1785
+ this._motorJoints.delete(joint);
1786
+ this._b3._bx_DestroyJoint(joint);
1787
+ }
1788
+ _recreateJoints(cdata) {
1789
+ for (let i = 0; i < cdata.pairs.length; i++) {
1790
+ if (cdata.joints[i]) {
1791
+ this._destroyJoint(cdata.joints[i]);
1792
+ }
1793
+ const pair = cdata.pairs[i];
1794
+ cdata.joints[i] = cdata.enabled && pair.parentData.slot && pair.childData.slot ? this._createJoint(cdata, pair.parentData, pair.childData) : 0;
1795
+ }
1796
+ }
1797
+ getEnabled(constraint) {
1798
+ return constraint._pluginData?.enabled ?? false;
1799
+ }
1800
+ setCollisionsEnabled(constraint, isEnabled) {
1801
+ const cdata = constraint._pluginData;
1802
+ cdata.collisions = isEnabled;
1803
+ for (const joint of cdata.joints) {
1804
+ if (joint) {
1805
+ this._b3._bx_Joint_SetCollideConnected(joint, isEnabled ? 1 : 0);
1806
+ }
1807
+ }
1808
+ }
1809
+ getCollisionsEnabled(constraint) {
1810
+ return constraint._pluginData?.collisions ?? false;
1811
+ }
1812
+ // The one axis box3d exposes for this constraint type.
1813
+ _primaryAxis(cdata) {
1814
+ switch (cdata.type) {
1815
+ case PhysicsConstraintType.HINGE:
1816
+ return PhysicsConstraintAxis.ANGULAR_X;
1817
+ case PhysicsConstraintType.PRISMATIC:
1818
+ case PhysicsConstraintType.SLIDER:
1819
+ return PhysicsConstraintAxis.LINEAR_X;
1820
+ case PhysicsConstraintType.DISTANCE:
1821
+ return PhysicsConstraintAxis.LINEAR_DISTANCE;
1822
+ case PhysicsConstraintType.BALL_AND_SOCKET:
1823
+ return PhysicsConstraintAxis.ANGULAR_Y;
1824
+ case PhysicsConstraintType.SIX_DOF:
1825
+ return cdata.plan?.primaryAxis ?? null;
1826
+ default:
1827
+ return null;
1828
+ }
1829
+ }
1830
+ _isAngularAxis(axis) {
1831
+ return axis === PhysicsConstraintAxis.ANGULAR_X || axis === PhysicsConstraintAxis.ANGULAR_Y || axis === PhysicsConstraintAxis.ANGULAR_Z;
1832
+ }
1833
+ _isPrimaryAxis(cdata, axis) {
1834
+ if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET || cdata.plan?.jointType === Box3DJointType.SPHERICAL) {
1835
+ return this._isAngularAxis(axis);
1836
+ }
1837
+ return axis === this._primaryAxis(cdata);
1838
+ }
1839
+ _axisState(cdata, axis) {
1840
+ let state = cdata.axes.get(axis);
1841
+ if (!state) {
1842
+ state = {
1843
+ mode: PhysicsConstraintAxisLimitMode.FREE,
1844
+ min: 0,
1845
+ max: 0,
1846
+ motor: PhysicsConstraintMotorType.NONE,
1847
+ target: 0,
1848
+ maxForce: 0,
1849
+ friction: 0,
1850
+ stiffness: 0,
1851
+ damping: 0,
1852
+ };
1853
+ cdata.axes.set(axis, state);
1854
+ }
1855
+ return state;
1856
+ }
1857
+ _applyAxisState(cdata, joint, bodyA, bodyB) {
1858
+ const b3 = this._b3;
1859
+ if (cdata.plan) {
1860
+ this._applySixDofPlan(cdata, joint, bodyA, bodyB);
1861
+ b3._bx_Joint_WakeBodies(joint);
1862
+ return;
1863
+ }
1864
+ const primary = this._primaryAxis(cdata);
1865
+ if (primary === null) {
1866
+ return;
1867
+ }
1868
+ for (const [axis, state] of cdata.axes) {
1869
+ if (!this._isPrimaryAxis(cdata, axis)) {
1870
+ continue;
1871
+ }
1872
+ if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET) {
1873
+ if (axis === PhysicsConstraintAxis.ANGULAR_X) {
1874
+ // twist about the primary axis (frame z in box3d)
1875
+ b3._bx_Joint_SetTwistLimits(joint, state.mode === PhysicsConstraintAxisLimitMode.FREE ? 0 : 1, state.min, state.max);
1876
+ continue;
1877
+ }
1878
+ }
1879
+ const limited = state.mode !== PhysicsConstraintAxisLimitMode.FREE;
1880
+ if (limited) {
1881
+ const min = state.mode === PhysicsConstraintAxisLimitMode.LOCKED ? 0 : state.min;
1882
+ const max = state.mode === PhysicsConstraintAxisLimitMode.LOCKED ? 0 : state.max;
1883
+ b3._bx_Joint_SetLimits(joint, min, max);
1884
+ }
1885
+ b3._bx_Joint_EnableLimit(joint, limited ? 1 : 0);
1886
+ if (cdata.type !== PhysicsConstraintType.BALL_AND_SOCKET) {
1887
+ this._applyMotor(joint, state);
1888
+ }
1889
+ }
1890
+ if (cdata.type === PhysicsConstraintType.BALL_AND_SOCKET) {
1891
+ this._applySphericalMotor(cdata, joint);
1892
+ }
1893
+ b3._bx_Joint_WakeBodies(joint);
1894
+ }
1895
+ /**
1896
+ * Box3D's spherical joint has one motor for the whole joint: a relative angular velocity, and one spring towards a
1897
+ * target rotation. Babylon has a motor per axis, so the three angular axes are combined into one target in the
1898
+ * joint frame (ANGULAR_X about frame z, ANGULAR_Y about frame x, ANGULAR_Z about frame y).
1899
+ */
1900
+ _applySphericalMotor(cdata, joint) {
1901
+ const b3 = this._b3;
1902
+ const twist = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_X);
1903
+ const swing1 = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_Y);
1904
+ const swing2 = cdata.axes.get(PhysicsConstraintAxis.ANGULAR_Z);
1905
+ const states = [twist, swing1, swing2];
1906
+ const velocity = (state) => (state?.motor === PhysicsConstraintMotorType.VELOCITY ? state.target : 0);
1907
+ const hasVelocity = states.some((state) => state?.motor === PhysicsConstraintMotorType.VELOCITY);
1908
+ const hasPosition = states.some((state) => state?.motor === PhysicsConstraintMotorType.POSITION);
1909
+ const maxTorque = Math.max(...states.map((state) => (state?.motor === PhysicsConstraintMotorType.VELOCITY ? state.maxForce : 0)));
1910
+ b3._bx_Joint_SetSphericalMotor(joint, hasVelocity ? 1 : 0, velocity(swing1), velocity(swing2), velocity(twist), maxTorque);
1911
+ const target = velocity(swing1) !== 0 || velocity(swing2) !== 0 || velocity(twist) !== 0;
1912
+ if (hasVelocity && target) {
1913
+ // the world space target has to follow body A as it turns
1914
+ this._motorJoints.add(joint);
1915
+ }
1916
+ else {
1917
+ this._motorJoints.delete(joint);
1918
+ }
1919
+ b3._bx_Joint_EnableSpring(joint, hasPosition ? 1 : 0);
1920
+ if (hasPosition) {
1921
+ const position = (state) => (state?.motor === PhysicsConstraintMotorType.POSITION ? state.target : 0);
1922
+ const rotation = this._tmpQuat[0];
1923
+ Quaternion.RotationYawPitchRollToRef(0, 0, position(twist), rotation);
1924
+ const swing = this._tmpQuat[1];
1925
+ Quaternion.RotationYawPitchRollToRef(position(swing2), position(swing1), 0, swing);
1926
+ rotation.multiplyInPlace(swing);
1927
+ b3._bx_Joint_SetSpring(joint, 5, 1);
1928
+ b3._bx_Joint_SetSphericalTarget(joint, rotation.x, rotation.y, rotation.z, rotation.w);
1929
+ }
1930
+ }
1931
+ _applyMotor(joint, state) {
1932
+ const b3 = this._b3;
1933
+ switch (state.motor) {
1934
+ case PhysicsConstraintMotorType.VELOCITY:
1935
+ b3._bx_Joint_EnableSpring(joint, 0);
1936
+ b3._bx_Joint_EnableMotor(joint, 1);
1937
+ b3._bx_Joint_SetMotorSpeed(joint, state.target);
1938
+ b3._bx_Joint_SetMaxMotorForce(joint, state.maxForce);
1939
+ break;
1940
+ case PhysicsConstraintMotorType.POSITION:
1941
+ b3._bx_Joint_EnableMotor(joint, 0);
1942
+ b3._bx_Joint_EnableSpring(joint, 1);
1943
+ b3._bx_Joint_SetSpring(joint, 5, 1);
1944
+ b3._bx_Joint_SetTarget(joint, state.target);
1945
+ break;
1946
+ default:
1947
+ b3._bx_Joint_EnableMotor(joint, 0);
1948
+ b3._bx_Joint_EnableSpring(joint, 0);
1949
+ break;
1950
+ }
1951
+ }
1952
+ /** Applies a SIX_DOF plan's limits, springs and motors to one of its Box3D joints. */
1953
+ _applySixDofPlan(cdata, joint, bodyA, bodyB) {
1954
+ const b3 = this._b3;
1955
+ const plan = cdata.plan;
1956
+ switch (plan.jointType) {
1957
+ case Box3DJointType.SPHERICAL:
1958
+ if (plan.coneAngle >= 0) {
1959
+ b3._bx_Joint_SetLimits(joint, -plan.coneAngle, plan.coneAngle);
1960
+ }
1961
+ b3._bx_Joint_EnableLimit(joint, plan.coneAngle >= 0 ? 1 : 0);
1962
+ b3._bx_Joint_SetTwistLimits(joint, plan.twist ? 1 : 0, plan.twist ? plan.twist[0] : 0, plan.twist ? plan.twist[1] : 0);
1963
+ break;
1964
+ case Box3DJointType.REVOLUTE:
1965
+ case Box3DJointType.PRISMATIC:
1966
+ if (plan.limit) {
1967
+ b3._bx_Joint_SetLimits(joint, plan.limit[0], plan.limit[1]);
1968
+ }
1969
+ b3._bx_Joint_EnableLimit(joint, plan.limit ? 1 : 0);
1970
+ break;
1971
+ case Box3DJointType.DISTANCE:
1972
+ if (plan.spring) {
1973
+ const spring = this._springToHertz(plan.stiffness, plan.damping, false, bodyA, bodyB);
1974
+ b3._bx_Joint_EnableLimit(joint, 0);
1975
+ b3._bx_Joint_EnableSpring(joint, 1);
1976
+ b3._bx_Joint_SetSpring(joint, spring.hertz, spring.dampingRatio);
1977
+ b3._bx_Joint_SetTarget(joint, cdata.length);
1978
+ }
1979
+ else {
1980
+ // an enabled spring with 0 hertz leaves the length free between the limits (a rope); Box3D keeps
1981
+ // equal limits rigid
1982
+ b3._bx_Joint_SetLimits(joint, plan.limit[0], plan.limit[1]);
1983
+ b3._bx_Joint_EnableLimit(joint, 1);
1984
+ b3._bx_Joint_EnableSpring(joint, 1);
1985
+ b3._bx_Joint_SetSpring(joint, 0, 0);
1986
+ }
1987
+ break;
1988
+ default:
1989
+ break;
1990
+ }
1991
+ if (plan.stiffness > 0 && !plan.spring) {
1992
+ this._warnOnce("sixdof-softness", "SIX_DOF: limit stiffness and damping are approximated with Box3D's per joint constraint softness, which applies to every axis of the joint.");
1993
+ const soft = this._springToHertz(plan.stiffness, plan.damping, plan.angularStiffness, bodyA, bodyB);
1994
+ b3._bx_Joint_SetConstraintTuning(joint, soft.hertz, soft.dampingRatio);
1995
+ }
1996
+ if (plan.jointType === Box3DJointType.SPHERICAL) {
1997
+ this._applySphericalMotor(cdata, joint);
1998
+ }
1999
+ else if (plan.primaryAxis !== null && plan.jointType !== Box3DJointType.DISTANCE) {
2000
+ const state = cdata.axes.get(plan.primaryAxis);
2001
+ if (state) {
2002
+ this._applyMotor(joint, state);
2003
+ }
2004
+ }
2005
+ }
2006
+ _setAxis(constraint, axis, apply) {
2007
+ const cdata = constraint._pluginData;
2008
+ if (!cdata) {
2009
+ return;
2010
+ }
2011
+ const state = this._axisState(cdata, axis);
2012
+ const previousMotor = { motor: state.motor, target: state.target, maxForce: state.maxForce };
2013
+ apply(state);
2014
+ if (cdata.plan) {
2015
+ // SIX_DOF: every axis shapes the plan. A different Box3D joint type or frame needs new joints.
2016
+ const plan = this._planSixDof(cdata);
2017
+ const rebuild = plan.jointType !== cdata.plan.jointType || String(plan.basis) !== String(cdata.plan.basis);
2018
+ cdata.plan = plan;
2019
+ cdata.jointType = plan.jointType;
2020
+ const motorChanged = previousMotor.motor !== state.motor || previousMotor.target !== state.target || previousMotor.maxForce !== state.maxForce;
2021
+ if (motorChanged && state.motor !== PhysicsConstraintMotorType.NONE && !this._isPrimaryAxis(cdata, axis)) {
2022
+ this._warnOnce(`axis-motor-${axis}`, `SIX_DOF: a motor on axis ${axis} has no effect with the Box3D joint this constraint maps to.`);
2023
+ }
2024
+ const first = cdata.pairs[0];
2025
+ if (first) {
2026
+ // the rest length of a distance joint follows its limits, whether or not the joint is rebuilt
2027
+ this._updateJointLength(cdata, constraint.options.maxDistance, first.parentData, first.childData);
2028
+ }
2029
+ if (rebuild) {
2030
+ if (first) {
2031
+ this._buildFrames(cdata, constraint, first.parentData, first.childData);
2032
+ }
2033
+ this._recreateJoints(cdata);
2034
+ return;
2035
+ }
2036
+ }
2037
+ else if (!this._isPrimaryAxis(cdata, axis)) {
2038
+ this._warnOnce(`axis-${cdata.type}-${axis}`, `axis ${axis} is not configurable on constraint type ${cdata.type} with Box3D; the value is stored but has no effect.`);
2039
+ return;
2040
+ }
2041
+ for (let i = 0; i < cdata.joints.length; i++) {
2042
+ if (cdata.joints[i]) {
2043
+ this._applyAxisState(cdata, cdata.joints[i], cdata.pairs[i].parentData, cdata.pairs[i].childData);
2044
+ }
2045
+ }
2046
+ }
2047
+ setAxisFriction(constraint, axis, friction) {
2048
+ const cdata = constraint._pluginData;
2049
+ if (cdata) {
2050
+ this._axisState(cdata, axis).friction = friction;
2051
+ }
2052
+ }
2053
+ getAxisFriction(constraint, axis) {
2054
+ return constraint._pluginData?.axes.get(axis)?.friction ?? null;
2055
+ }
2056
+ setAxisMode(constraint, axis, limitMode) {
2057
+ this._setAxis(constraint, axis, (state) => (state.mode = limitMode));
2058
+ }
2059
+ getAxisMode(constraint, axis) {
2060
+ return constraint._pluginData?.axes.get(axis)?.mode ?? null;
2061
+ }
2062
+ setAxisMinLimit(constraint, axis, minLimit) {
2063
+ this._setAxis(constraint, axis, (state) => {
2064
+ state.min = minLimit;
2065
+ if (state.mode === PhysicsConstraintAxisLimitMode.FREE) {
2066
+ state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
2067
+ }
2068
+ });
2069
+ }
2070
+ getAxisMinLimit(constraint, axis) {
2071
+ return constraint._pluginData?.axes.get(axis)?.min ?? null;
2072
+ }
2073
+ setAxisMaxLimit(constraint, axis, limit) {
2074
+ this._setAxis(constraint, axis, (state) => {
2075
+ state.max = limit;
2076
+ if (state.mode === PhysicsConstraintAxisLimitMode.FREE) {
2077
+ state.mode = PhysicsConstraintAxisLimitMode.LIMITED;
2078
+ }
2079
+ });
2080
+ }
2081
+ getAxisMaxLimit(constraint, axis) {
2082
+ return constraint._pluginData?.axes.get(axis)?.max ?? null;
2083
+ }
2084
+ setAxisMotorType(constraint, axis, motorType) {
2085
+ this._setAxis(constraint, axis, (state) => (state.motor = motorType));
2086
+ }
2087
+ getAxisMotorType(constraint, axis) {
2088
+ return constraint._pluginData?.axes.get(axis)?.motor ?? null;
2089
+ }
2090
+ setAxisMotorTarget(constraint, axis, target) {
2091
+ this._setAxis(constraint, axis, (state) => (state.target = target));
2092
+ }
2093
+ getAxisMotorTarget(constraint, axis) {
2094
+ return constraint._pluginData?.axes.get(axis)?.target ?? null;
2095
+ }
2096
+ setAxisMotorMaxForce(constraint, axis, maxForce) {
2097
+ this._setAxis(constraint, axis, (state) => (state.maxForce = maxForce));
2098
+ }
2099
+ getAxisMotorMaxForce(constraint, axis) {
2100
+ return constraint._pluginData?.axes.get(axis)?.maxForce ?? null;
2101
+ }
2102
+ disposeConstraint(constraint) {
2103
+ const cdata = constraint._pluginData;
2104
+ if (!cdata) {
2105
+ return;
2106
+ }
2107
+ for (const joint of cdata.joints) {
2108
+ if (joint) {
2109
+ this._destroyJoint(joint);
2110
+ }
2111
+ }
2112
+ cdata.joints.length = 0;
2113
+ cdata.pairs.length = 0;
2114
+ }
2115
+ getBodiesUsingConstraint(constraint) {
2116
+ const cdata = constraint._pluginData;
2117
+ if (!cdata) {
2118
+ return [];
2119
+ }
2120
+ return cdata.pairs.map((p) => ({ parentBody: p.parent, parentBodyIndex: p.parentIndex, childBody: p.child, childBodyIndex: p.childIndex }));
2121
+ }
2122
+ addConstraint(body, childBody, constraint, instanceIndex, childInstanceIndex) {
2123
+ this.initConstraint(constraint, body, childBody, instanceIndex, childInstanceIndex);
2124
+ }
2125
+ // ----------------------------------------------------------------------------------------
2126
+ // Box3D specific extras
2127
+ // ----------------------------------------------------------------------------------------
2128
+ /**
2129
+ * Applies a radial impulse to every shape within the radius, like a grenade.
2130
+ * @param position world position of the explosion
2131
+ * @param radius radius of the explosion
2132
+ * @param impulsePerArea impulse per surface area of the affected shapes
2133
+ * @param falloff distance beyond the radius over which the impulse fades to zero
2134
+ */
2135
+ explode(position, radius, impulsePerArea, falloff = 0) {
2136
+ this._b3._bx_World_Explode(this._worldSlot, position.x, position.y, position.z, radius, falloff, impulsePerArea);
2137
+ }
2138
+ /**
2139
+ * Box2D style collision group. Shapes sharing a negative group never collide with each other,
2140
+ * shapes sharing a positive group always collide. Zero means no group.
2141
+ * @param shape the shape
2142
+ * @param groupIndex the group index
2143
+ */
2144
+ setShapeFilterGroup(shape, groupIndex) {
2145
+ const data = shape._pluginData;
2146
+ this._b3._bx_ShapeDesc_SetGroup(data.slot, groupIndex | 0);
2147
+ this._syncShapeUsers(data, Box3DShapeSync.FILTER);
2148
+ }
2149
+ /**
2150
+ * Rolling resistance for spheres and capsules (Babylon materials have no equivalent).
2151
+ * @param shape the shape
2152
+ * @param value resistance, typically in [0, 1]
2153
+ */
2154
+ setShapeRollingResistance(shape, value) {
2155
+ const data = shape._pluginData;
2156
+ this._b3._bx_ShapeDesc_SetRollingResistance(data.slot, value);
2157
+ this._syncShapeUsers(data, Box3DShapeSync.MATERIAL);
2158
+ }
2159
+ /**
2160
+ * Lets a body spin faster than the default angular speed cap (wheels).
2161
+ * @param body the body
2162
+ * @param allow true to allow fast rotation
2163
+ * @param instanceIndex optional thin instance index
2164
+ */
2165
+ setAllowFastRotation(body, allow, instanceIndex) {
2166
+ this._applyToBodyOrInstances(body, (data) => this._b3._bx_Body_AllowFastRotation(data.slot, allow ? 1 : 0), instanceIndex);
2167
+ }
2168
+ /**
2169
+ * Creates a Box3D wheel joint between a chassis and a wheel body.
2170
+ * @param chassis the chassis body
2171
+ * @param wheel the wheel body
2172
+ * @param options wheel joint options
2173
+ * @returns a handle used to drive and steer the wheel
2174
+ */
2175
+ createWheelJoint(chassis, wheel, options) {
2176
+ const s = this._scratch();
2177
+ const axisA = (options.axisA ?? Vector3.UpReadOnly).normalizeToNew();
2178
+ const axleA = (options.axleA ?? Vector3.Forward()).normalizeToNew();
2179
+ // suspension travels along frame A x, the wheel spins about frame z
2180
+ axleA.subtractInPlace(axisA.scale(Vector3.Dot(axisA, axleA))).normalize();
2181
+ this._frameToBuffer(s, 0, options.pivotA, axisA, Vector3.Cross(axleA, axisA), axleA);
2182
+ // frame B must coincide with frame A in world space at creation, compute it from the current poses
2183
+ const pivotB = options.pivotB ?? Vector3.ZeroReadOnly;
2184
+ s[7] = pivotB.x;
2185
+ s[8] = pivotB.y;
2186
+ s[9] = pivotB.z;
2187
+ const chassisData = this._getPluginReference(chassis);
2188
+ const wheelData = this._getPluginReference(wheel);
2189
+ this._b3._bx_ComputeAlignedFrameB(chassisData.slot, wheelData.slot);
2190
+ const susp = options.suspensionLimits ?? [-0.2, 0.2];
2191
+ const steer = options.steeringLimits ?? [-Math.PI / 4, Math.PI / 4];
2192
+ s.set([
2193
+ 1,
2194
+ options.suspensionHertz ?? 4,
2195
+ options.suspensionDampingRatio ?? 0.7,
2196
+ 1,
2197
+ susp[0],
2198
+ susp[1],
2199
+ options.enableSpinMotor ? 1 : 0,
2200
+ options.maxSpinTorque ?? 5,
2201
+ 0,
2202
+ options.enableSteering ? 1 : 0,
2203
+ options.steeringHertz ?? 10,
2204
+ options.steeringDampingRatio ?? 0.7,
2205
+ 0,
2206
+ options.maxSteeringTorque ?? 5,
2207
+ 1,
2208
+ steer[0],
2209
+ steer[1],
2210
+ ], 14);
2211
+ const slot = this._b3._bx_CreateJoint(this._worldSlot, Box3DJointType.WHEEL, chassisData.slot, wheelData.slot, 0, 0);
2212
+ if (!slot) {
2213
+ throw new Error("Box3DPlugin: wheel joint creation failed");
2214
+ }
2215
+ return new Box3DWheelJoint(this, slot);
2216
+ }
2217
+ /**
2218
+ * Creates a Box3D parallel joint: a spring that keeps a local axis of bodyB parallel to an axis
2219
+ * of bodyA, for example to keep a vehicle upright without locking its motion.
2220
+ * @param bodyA first body (often a static body)
2221
+ * @param bodyB body to align
2222
+ * @param axisA axis in bodyA local space
2223
+ * @param axisB axis in bodyB local space
2224
+ * @param hertz spring stiffness
2225
+ * @param dampingRatio spring damping
2226
+ * @param maxTorque maximum spring torque
2227
+ * @returns a joint slot that can be passed to disposeExtraJoint
2228
+ */
2229
+ createParallelJoint(bodyA, bodyB, axisA, axisB, hertz = 1, dampingRatio = 1, maxTorque = 3.4e38) {
2230
+ const s = this._scratch();
2231
+ const a = axisA.normalizeToNew();
2232
+ const pa = a.getNormalToRef(new Vector3());
2233
+ this._frameToBuffer(s, 0, Vector3.ZeroReadOnly, pa, Vector3.Cross(a, pa), a);
2234
+ const b = axisB.normalizeToNew();
2235
+ const pb = b.getNormalToRef(new Vector3());
2236
+ this._frameToBuffer(s, 7, Vector3.ZeroReadOnly, pb, Vector3.Cross(b, pb), b);
2237
+ s.set([hertz, dampingRatio, maxTorque], 14);
2238
+ return this._b3._bx_CreateJoint(this._worldSlot, Box3DJointType.PARALLEL, this._getPluginReference(bodyA).slot, this._getPluginReference(bodyB).slot, 1, 0);
2239
+ }
2240
+ /**
2241
+ * Destroys a joint created by createParallelJoint.
2242
+ * @param slot the joint slot
2243
+ */
2244
+ disposeExtraJoint(slot) {
2245
+ this._destroyExtraJoint(slot);
2246
+ }
2247
+ /**
2248
+ * @internal
2249
+ */
2250
+ _destroyExtraJoint(slot) {
2251
+ if (slot) {
2252
+ this._destroyJoint(slot);
2253
+ }
2254
+ }
2255
+ /**
2256
+ * @internal
2257
+ */
2258
+ _wheelCall(fn, slot, ...args) {
2259
+ if (slot) {
2260
+ this._b3[fn](slot, ...args);
2261
+ }
2262
+ }
2263
+ // ----------------------------------------------------------------------------------------
2264
+ // queries
2265
+ // ----------------------------------------------------------------------------------------
2266
+ raycast(from, to, result, query) {
2267
+ const results = Array.isArray(result) ? result : [result];
2268
+ for (const r of results) {
2269
+ r.reset(from, to);
2270
+ }
2271
+ const membership = (query?.membership ?? ~0) >>> 0;
2272
+ const collideWith = (query?.collideWith ?? ~0) >>> 0;
2273
+ const ignore = query?.ignoreBody ? (query.ignoreBody._pluginData?.slot ?? 0) : 0;
2274
+ const closestOnly = results.length === 1;
2275
+ const count = this._b3._bx_World_CastRay(this._worldSlot, from.x, from.y, from.z, to.x - from.x, to.y - from.y, to.z - from.z, membership, collideWith, ignore, query?.shouldHitTriggers ? 1 : 0, closestOnly ? 1 : 0);
2276
+ if (count <= 0) {
2277
+ return;
2278
+ }
2279
+ const buffer = new Float32Array(this._b3.HEAPF32.buffer, this._b3._bx_RayHitsPtr(), count * RAY_STRIDE);
2280
+ const hits = [];
2281
+ for (let i = 0; i < count; i++) {
2282
+ hits.push(i);
2283
+ }
2284
+ hits.sort((a, b) => buffer[a * RAY_STRIDE + 6] - buffer[b * RAY_STRIDE + 6]);
2285
+ if (!results.length) {
2286
+ for (let i = 0; i < count; i++) {
2287
+ const r = new PhysicsRaycastResult();
2288
+ r.reset(from, to);
2289
+ results.push(r);
2290
+ }
2291
+ }
2292
+ const n = Math.min(count, results.length);
2293
+ for (let i = 0; i < n; i++) {
2294
+ const o = hits[i] * RAY_STRIDE;
2295
+ const r = results[i];
2296
+ const bodyRef = this._bodies[buffer[o + 7]];
2297
+ r.body = bodyRef?.body;
2298
+ r.bodyIndex = bodyRef?.index;
2299
+ r.shape = bodyRef?.body.shape ?? undefined;
2300
+ r.setHitData({ x: buffer[o + 3], y: buffer[o + 4], z: buffer[o + 5] }, { x: buffer[o], y: buffer[o + 1], z: buffer[o + 2] }, buffer[o + 9]);
2301
+ r.calculateHitDistance();
2302
+ }
2303
+ }
2304
+ // ----------------------------------------------------------------------------------------
2305
+ // events
2306
+ // ----------------------------------------------------------------------------------------
2307
+ /**
2308
+ * Copies `count` event records out of wasm memory and resolves their two body slots (at `slotOffset`) to the plugin's
2309
+ * body references. Observers run user code that can grow the wasm memory, which detaches every HEAPF32 view (creating
2310
+ * bodies or shapes, casting rays), or dispose bodies and hand their slots to new ones, so nothing is read from wasm
2311
+ * memory or the slot table once the first observer has been called.
2312
+ */
2313
+ _readEvents(pointer, count, stride, slotOffset) {
2314
+ const records = new Float32Array(this._b3.HEAPF32.buffer, pointer, count * stride).slice();
2315
+ const refs = new Array(count * 2);
2316
+ for (let i = 0; i < count; i++) {
2317
+ refs[i * 2] = this._bodies[records[i * stride + slotOffset]];
2318
+ refs[i * 2 + 1] = this._bodies[records[i * stride + slotOffset + 1]];
2319
+ }
2320
+ return { records, refs };
2321
+ }
2322
+ _notifyCollisions() {
2323
+ const count = this._b3._bx_World_GetContactEvents(this._worldSlot);
2324
+ if (!count) {
2325
+ return;
2326
+ }
2327
+ const { records: buffer, refs } = this._readEvents(this._b3._bx_ContactEventsPtr(), count, CONTACT_STRIDE, 3);
2328
+ for (let i = 0; i < count; i++) {
2329
+ const o = i * CONTACT_STRIDE;
2330
+ const kind = buffer[o];
2331
+ const refA = refs[i * 2];
2332
+ const refB = refs[i * 2 + 1];
2333
+ // a body disposed by an earlier observer has slot 0, its remaining events are dropped
2334
+ if (!refA || !refB || !refA.data.slot || !refB.data.slot) {
2335
+ continue;
2336
+ }
2337
+ // like Havok, an event is reported when either body asked for that event type
2338
+ const bit = kind === 0 ? Box3DEventBits.COLLISION_STARTED : kind === 1 ? Box3DEventBits.COLLISION_FINISHED : Box3DEventBits.COLLISION_CONTINUED;
2339
+ if (((refA.data.eventMask | refB.data.eventMask) & bit) === 0) {
2340
+ continue;
2341
+ }
2342
+ if (kind === 1) {
2343
+ const ended = {
2344
+ collider: refA.body,
2345
+ colliderIndex: refA.index,
2346
+ collidedAgainst: refB.body,
2347
+ collidedAgainstIndex: refB.index,
2348
+ type: PhysicsEventType.COLLISION_FINISHED,
2349
+ };
2350
+ this.onCollisionEndedObservable.notifyObservers(ended);
2351
+ this._bodyCollisionEndedObservable.get(refA.data.slot)?.notifyObservers(ended);
2352
+ const endedB = this._bodyCollisionEndedObservable.get(refB.data.slot);
2353
+ if (endedB) {
2354
+ endedB.notifyObservers({ ...ended, collider: refB.body, colliderIndex: refB.index, collidedAgainst: refA.body, collidedAgainstIndex: refA.index });
2355
+ }
2356
+ continue;
2357
+ }
2358
+ // begin events carry the manifold point, normal and normal impulse, hit events the approach speed
2359
+ const type = kind === 0 ? PhysicsEventType.COLLISION_STARTED : PhysicsEventType.COLLISION_CONTINUED;
2360
+ const point = new Vector3(buffer[o + 5], buffer[o + 6], buffer[o + 7]);
2361
+ const normal = new Vector3(buffer[o + 8], buffer[o + 9], buffer[o + 10]);
2362
+ const info = {
2363
+ collider: refA.body,
2364
+ colliderIndex: refA.index,
2365
+ collidedAgainst: refB.body,
2366
+ collidedAgainstIndex: refB.index,
2367
+ type,
2368
+ point,
2369
+ normal,
2370
+ distance: 0,
2371
+ impulse: buffer[o + 11],
2372
+ };
2373
+ this.onCollisionObservable.notifyObservers(info);
2374
+ if (this._bodyCollisionObservable.size) {
2375
+ this._bodyCollisionObservable.get(refA.data.slot)?.notifyObservers(info);
2376
+ const obsB = this._bodyCollisionObservable.get(refB.data.slot);
2377
+ if (obsB) {
2378
+ obsB.notifyObservers({
2379
+ ...info,
2380
+ collider: refB.body,
2381
+ colliderIndex: refB.index,
2382
+ collidedAgainst: refA.body,
2383
+ collidedAgainstIndex: refA.index,
2384
+ normal: normal ? normal.negate() : null,
2385
+ });
2386
+ }
2387
+ }
2388
+ }
2389
+ }
2390
+ _notifyTriggers() {
2391
+ const count = this._b3._bx_World_GetSensorEvents(this._worldSlot);
2392
+ if (!count || !this.onTriggerCollisionObservable.hasObservers()) {
2393
+ return;
2394
+ }
2395
+ const { records: buffer, refs } = this._readEvents(this._b3._bx_SensorEventsPtr(), count, SENSOR_STRIDE, 3);
2396
+ for (let i = 0; i < count; i++) {
2397
+ const o = i * SENSOR_STRIDE;
2398
+ const refA = refs[i * 2];
2399
+ const refB = refs[i * 2 + 1];
2400
+ if (!refA || !refB || !refA.data.slot || !refB.data.slot) {
2401
+ continue;
2402
+ }
2403
+ this.onTriggerCollisionObservable.notifyObservers({
2404
+ collider: refA.body,
2405
+ colliderIndex: refA.index,
2406
+ collidedAgainst: refB.body,
2407
+ collidedAgainstIndex: refB.index,
2408
+ type: buffer[o] === 0 ? PhysicsEventType.TRIGGER_ENTERED : PhysicsEventType.TRIGGER_EXITED,
2409
+ });
2410
+ }
2411
+ }
2412
+ dispose() {
2413
+ this._motorJoints.clear();
2414
+ this.onCollisionObservable.clear();
2415
+ this.onCollisionEndedObservable.clear();
2416
+ this.onTriggerCollisionObservable.clear();
2417
+ this._bodyCollisionObservable.clear();
2418
+ this._bodyCollisionEndedObservable.clear();
2419
+ for (const shape of this._shapes) {
2420
+ if (shape) {
2421
+ this._b3._bx_ShapeDesc_Destroy(shape.slot);
2422
+ }
2423
+ }
2424
+ this._shapes.length = 0;
2425
+ this._bodies.length = 0;
2426
+ if (this._worldSlot) {
2427
+ this._b3._bx_DestroyWorld(this._worldSlot);
2428
+ this._worldSlot = 0;
2429
+ }
2430
+ }
2431
+ }
2432
+ //# sourceMappingURL=box3dPlugin.js.map