@flighthq/physics3d-abi 0.4.0-edge.1908.751021c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/contract.d.ts +6 -0
  2. package/dist/contract.d.ts.map +1 -0
  3. package/dist/contract.js +6 -0
  4. package/dist/contract.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/physics3DAbi.d.ts +11 -0
  10. package/dist/physics3DAbi.d.ts.map +1 -0
  11. package/dist/physics3DAbi.js +32 -0
  12. package/dist/physics3DAbi.js.map +1 -0
  13. package/dist/physics3DAbiBuffer.d.ts +10 -0
  14. package/dist/physics3DAbiBuffer.d.ts.map +1 -0
  15. package/dist/physics3DAbiBuffer.js +81 -0
  16. package/dist/physics3DAbiBuffer.js.map +1 -0
  17. package/dist/physics3DAbiCommand.d.ts +17 -0
  18. package/dist/physics3DAbiCommand.d.ts.map +1 -0
  19. package/dist/physics3DAbiCommand.js +554 -0
  20. package/dist/physics3DAbiCommand.js.map +1 -0
  21. package/dist/physics3DAbiLayout.d.ts +217 -0
  22. package/dist/physics3DAbiLayout.d.ts.map +1 -0
  23. package/dist/physics3DAbiLayout.js +224 -0
  24. package/dist/physics3DAbiLayout.js.map +1 -0
  25. package/dist/physics3DAbiQuery.d.ts +7 -0
  26. package/dist/physics3DAbiQuery.d.ts.map +1 -0
  27. package/dist/physics3DAbiQuery.js +16 -0
  28. package/dist/physics3DAbiQuery.js.map +1 -0
  29. package/dist/referencePhysics3DAbi.d.ts +3 -0
  30. package/dist/referencePhysics3DAbi.d.ts.map +1 -0
  31. package/dist/referencePhysics3DAbi.js +1165 -0
  32. package/dist/referencePhysics3DAbi.js.map +1 -0
  33. package/package.json +50 -0
  34. package/src/physics3DAbi.test.ts +666 -0
  35. package/src/physics3DAbiBuffer.test.ts +117 -0
  36. package/src/physics3DAbiCommand.test.ts +368 -0
  37. package/src/physics3DAbiLayout.test.ts +78 -0
  38. package/src/physics3DAbiQuery.test.ts +122 -0
  39. package/src/referencePhysics3DAbi.test.ts +19 -0
@@ -0,0 +1,666 @@
1
+ import {
2
+ addPhysics3DBody,
3
+ addPhysics3DCollider,
4
+ createPhysics3DBallAndSocketJoint,
5
+ createPhysics3DCollider,
6
+ createPhysics3DConeTwistJoint,
7
+ createPhysics3DDistanceJoint,
8
+ createPhysics3DFixedJoint,
9
+ createPhysics3DGeneric6DofJoint,
10
+ createPhysics3DHingeJoint,
11
+ createPhysics3DSolverConfig,
12
+ createPhysics3DSliderJoint,
13
+ createPhysics3DWorld,
14
+ createRigidBody3D,
15
+ stepPhysics3D,
16
+ } from '@flighthq/physics3d/contract';
17
+ import type {
18
+ CollisionColliderShape3D,
19
+ Physics3DAbi,
20
+ Physics3DAbiBodyBuffer,
21
+ Physics3DAbiCommandBuffer,
22
+ Physics3DJoint,
23
+ RigidBody3D,
24
+ } from '@flighthq/types/contract';
25
+ import { describe, expect, it } from 'vitest';
26
+
27
+ import {
28
+ createPhysics3DAbi,
29
+ createPhysics3DAbiWorld,
30
+ destroyPhysics3DAbiWorld,
31
+ executePhysics3DAbiCommands,
32
+ getPhysics3DAbiWorldStatus,
33
+ readPhysics3DAbiBodies,
34
+ readPhysics3DAbiContacts,
35
+ readPhysics3DAbiJoints,
36
+ stepPhysics3DAbiWorld,
37
+ } from './physics3DAbi';
38
+ import {
39
+ clearPhysics3DAbiCommandBuffer,
40
+ createPhysics3DAbiBodyBuffer,
41
+ createPhysics3DAbiCommandBuffer,
42
+ createPhysics3DAbiContactBuffer,
43
+ createPhysics3DAbiExecutionResult,
44
+ createPhysics3DAbiJointBuffer,
45
+ createPhysics3DAbiQueryBuffer,
46
+ } from './physics3DAbiBuffer';
47
+ import {
48
+ writePhysics3DAbiApplyForceAtPointCommand,
49
+ writePhysics3DAbiApplyForceCommand,
50
+ writePhysics3DAbiApplyLinearImpulseAtPointCommand,
51
+ writePhysics3DAbiApplyLinearImpulseCommand,
52
+ writePhysics3DAbiApplyTorqueCommand,
53
+ writePhysics3DAbiDestroyBodyCommand,
54
+ writePhysics3DAbiDestroyColliderCommand,
55
+ writePhysics3DAbiDestroyJointCommand,
56
+ writePhysics3DAbiSetBodyCommand,
57
+ writePhysics3DAbiSetColliderCommand,
58
+ writePhysics3DAbiSetGravityCommand,
59
+ writePhysics3DAbiSetJointCommand,
60
+ writePhysics3DAbiSetSolverConfigCommand,
61
+ writePhysics3DAbiWakeBodyCommand,
62
+ } from './physics3DAbiCommand';
63
+ import {
64
+ Physics3DAbiBodyValue,
65
+ Physics3DAbiBodyValueStride,
66
+ Physics3DAbiCapability,
67
+ Physics3DAbiCommandHeaderByteLength,
68
+ Physics3DAbiMaxContactPoints,
69
+ Physics3DAbiVersion,
70
+ } from './physics3DAbiLayout';
71
+ import { queryPhysics3DAbiPoint } from './physics3DAbiQuery';
72
+
73
+ describe('createPhysics3DAbi', () => {
74
+ it('publishes the reference version and required baseline capabilities', () => {
75
+ const abi = createPhysics3DAbi();
76
+ expect(abi.version).toBe(Physics3DAbiVersion);
77
+ expect(abi.capabilities).toBe(
78
+ Physics3DAbiCapability.ContactHooks |
79
+ Physics3DAbiCapability.PersistentWorlds |
80
+ Physics3DAbiCapability.Queries |
81
+ Physics3DAbiCapability.SelectiveReadback,
82
+ );
83
+ });
84
+
85
+ it('decodes all nine built-in collider shapes through the public command stream', () => {
86
+ const abi = createPhysics3DAbi();
87
+ const world = createPhysics3DAbiWorld(abi);
88
+ const commands = createPhysics3DAbiCommandBuffer(32768);
89
+ const body = createRigidBody3D('static');
90
+ expect(writePhysics3DAbiSetBodyCommand(commands, 1, body)).toBe(true);
91
+ const shapes = getColliderShapes();
92
+ for (let i = 0; i < shapes.length; i += 1) {
93
+ expect(writePhysics3DAbiSetColliderCommand(commands, i + 1, 1, createPhysics3DCollider(shapes[i]))).toBe(true);
94
+ }
95
+
96
+ const result = createPhysics3DAbiExecutionResult();
97
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(true);
98
+ expect(result.commandIndex).toBe(1 + shapes.length);
99
+ });
100
+
101
+ it('executes and reads through caller-owned shared-memory views', () => {
102
+ const abi = createPhysics3DAbi();
103
+ const world = createPhysics3DAbiWorld(abi);
104
+ const commands: Physics3DAbiCommandBuffer = {
105
+ data: new Uint8Array(new SharedArrayBuffer(512)),
106
+ byteLength: 0,
107
+ commandCount: 0,
108
+ };
109
+ clearPhysics3DAbiCommandBuffer(commands);
110
+ const body = createRigidBody3D();
111
+ body.x = 17;
112
+ expect(writePhysics3DAbiSetBodyCommand(commands, 3, body)).toBe(true);
113
+ executeOrThrow(abi, world, commands);
114
+
115
+ const out: Physics3DAbiBodyBuffer = {
116
+ ids: new Uint32Array(new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT)),
117
+ flags: new Uint32Array(new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT)),
118
+ values: new Float64Array(new SharedArrayBuffer(Physics3DAbiBodyValueStride * Float64Array.BYTES_PER_ELEMENT)),
119
+ count: 0,
120
+ requiredCount: 0,
121
+ };
122
+ expect(readPhysics3DAbiBodies(abi, world, null, out)).toBe(true);
123
+ expect(out.ids[0]).toBe(3);
124
+ expect(out.values[Physics3DAbiBodyValue.X]).toBe(17);
125
+ });
126
+ });
127
+
128
+ describe('createPhysics3DAbiWorld', () => {
129
+ it('allocates non-zero handles that are never reused by one ABI instance', () => {
130
+ const abi = createPhysics3DAbi();
131
+ const first = createPhysics3DAbiWorld(abi);
132
+ expect(first).toBeGreaterThan(0);
133
+ expect(destroyPhysics3DAbiWorld(abi, first)).toBe(true);
134
+ const second = createPhysics3DAbiWorld(abi);
135
+ expect(second).toBeGreaterThan(first);
136
+ });
137
+ });
138
+
139
+ describe('destroyPhysics3DAbiWorld', () => {
140
+ it('makes every later operation on that handle observably stale', () => {
141
+ const abi = createPhysics3DAbi();
142
+ const world = createPhysics3DAbiWorld(abi);
143
+ expect(destroyPhysics3DAbiWorld(abi, world)).toBe(true);
144
+ expect(destroyPhysics3DAbiWorld(abi, world)).toBe(false);
145
+ expect(stepPhysics3DAbiWorld(abi, world, 1 / 60)).toBe('StaleWorld');
146
+ expect(readPhysics3DAbiBodies(abi, world, null, createPhysics3DAbiBodyBuffer(1))).toBe(false);
147
+ });
148
+ });
149
+
150
+ describe('executePhysics3DAbiCommands', () => {
151
+ it('commits the valid prefix and identifies the first rejected command', () => {
152
+ const abi = createPhysics3DAbi();
153
+ const world = createPhysics3DAbiWorld(abi);
154
+ const commands = createPhysics3DAbiCommandBuffer();
155
+ const body = createRigidBody3D();
156
+ body.x = 6;
157
+ expect(writePhysics3DAbiSetBodyCommand(commands, 5, body)).toBe(true);
158
+ expect(writePhysics3DAbiDestroyBodyCommand(commands, 999)).toBe(true);
159
+
160
+ const result = createPhysics3DAbiExecutionResult();
161
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(false);
162
+ expect(result).toMatchObject({ status: 'MissingBody', commandIndex: 1 });
163
+
164
+ const bodies = createPhysics3DAbiBodyBuffer(1);
165
+ expect(readPhysics3DAbiBodies(abi, world, null, bodies)).toBe(true);
166
+ expect(bodies.ids[0]).toBe(5);
167
+ expect(bodies.values[Physics3DAbiBodyValue.X]).toBe(6);
168
+ });
169
+
170
+ it('rejects a corrupted header before applying anything', () => {
171
+ const abi = createPhysics3DAbi();
172
+ const world = createPhysics3DAbiWorld(abi);
173
+ const commands = createPhysics3DAbiCommandBuffer();
174
+ writePhysics3DAbiSetGravityCommand(commands, 1, 2, 3);
175
+ commands.data[0] ^= 0xff;
176
+ const result = createPhysics3DAbiExecutionResult();
177
+
178
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(false);
179
+ expect(result).toEqual({
180
+ status: 'InvalidBuffer',
181
+ commandIndex: 0,
182
+ byteOffset: Physics3DAbiCommandHeaderByteLength,
183
+ commandKind: 0,
184
+ });
185
+ });
186
+
187
+ it('rejects unknown flag bits rather than silently adopting future semantics', () => {
188
+ const abi = createPhysics3DAbi();
189
+ const world = createPhysics3DAbiWorld(abi);
190
+ const commands = createPhysics3DAbiCommandBuffer();
191
+ writePhysics3DAbiSetBodyCommand(commands, 1, createRigidBody3D());
192
+ new DataView(commands.data.buffer).setUint32(32, 1 << 31, true);
193
+ const result = createPhysics3DAbiExecutionResult();
194
+
195
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(false);
196
+ expect(result.status).toBe('InvalidCommand');
197
+ });
198
+
199
+ it('classifies a structurally minimal degenerate collider as a rejected mutation', () => {
200
+ const abi = createPhysics3DAbi();
201
+ const world = createPhysics3DAbiWorld(abi);
202
+ const commands = createPhysics3DAbiCommandBuffer();
203
+ expect(writePhysics3DAbiSetBodyCommand(commands, 1, createRigidBody3D('static'))).toBe(true);
204
+ expect(
205
+ writePhysics3DAbiSetColliderCommand(commands, 1, 1, createPhysics3DCollider({ kind: 'convex', points: [] })),
206
+ ).toBe(true);
207
+ const result = createPhysics3DAbiExecutionResult();
208
+
209
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(false);
210
+ expect(result).toMatchObject({ status: 'RejectedMutation', commandIndex: 1 });
211
+ });
212
+
213
+ it('applies configuration, force, impulse, torque, and wake commands to persistent state', () => {
214
+ const abi = createPhysics3DAbi();
215
+ const world = createPhysics3DAbiWorld(abi);
216
+ const body = createRigidBody3D();
217
+ body.mass = 2;
218
+ body.inertiaXX = 4;
219
+ body.inertiaYY = 4;
220
+ body.inertiaZZ = 4;
221
+ body.sleeping = true;
222
+ const config = createPhysics3DSolverConfig();
223
+ config.substeps = 2;
224
+
225
+ executeOrThrow(
226
+ abi,
227
+ world,
228
+ writeCommands((commands) => {
229
+ writePhysics3DAbiSetGravityCommand(commands, 0, 0, 0);
230
+ writePhysics3DAbiSetSolverConfigCommand(commands, config);
231
+ writePhysics3DAbiSetBodyCommand(commands, 7, body);
232
+ writePhysics3DAbiApplyForceCommand(commands, 7, 2, 0, 0);
233
+ writePhysics3DAbiApplyForceAtPointCommand(commands, 7, 2, 0, 0, 0, 1, 0);
234
+ writePhysics3DAbiApplyLinearImpulseCommand(commands, 7, 2, 0, 0);
235
+ writePhysics3DAbiApplyLinearImpulseAtPointCommand(commands, 7, 2, 0, 0, 0, 1, 0);
236
+ writePhysics3DAbiApplyTorqueCommand(commands, 7, 0, 0, 6);
237
+ writePhysics3DAbiWakeBodyCommand(commands, 7);
238
+ }),
239
+ );
240
+
241
+ const beforeStep = createPhysics3DAbiBodyBuffer(1);
242
+ expect(readPhysics3DAbiBodies(abi, world, null, beforeStep)).toBe(true);
243
+ expect(beforeStep.flags[0] & (1 << 4)).toBe(0);
244
+ expect(beforeStep.values[Physics3DAbiBodyValue.VelocityX]).toBeCloseTo(2, 12);
245
+ expect(beforeStep.values[Physics3DAbiBodyValue.AngularVelocityZ]).toBeCloseTo(-0.5, 12);
246
+ expect(beforeStep.values[Physics3DAbiBodyValue.ForceX]).toBe(4);
247
+ expect(beforeStep.values[Physics3DAbiBodyValue.TorqueZ]).toBe(4);
248
+
249
+ expect(stepPhysics3DAbiWorld(abi, world, 0.5)).toBe('Complete');
250
+ const afterStep = createPhysics3DAbiBodyBuffer(1);
251
+ expect(readPhysics3DAbiBodies(abi, world, null, afterStep)).toBe(true);
252
+ expect(afterStep.values[Physics3DAbiBodyValue.VelocityX]).toBeCloseTo(3, 12);
253
+ expect(afterStep.values[Physics3DAbiBodyValue.VelocityY]).toBe(0);
254
+ expect(afterStep.values[Physics3DAbiBodyValue.ForceX]).toBe(0);
255
+ expect(afterStep.values[Physics3DAbiBodyValue.TorqueZ]).toBe(0);
256
+ });
257
+
258
+ it('removes collider identity and broadphase query visibility atomically', () => {
259
+ const abi = createPhysics3DAbi();
260
+ const world = createPhysics3DAbiWorld(abi);
261
+ const body = createRigidBody3D('static');
262
+ executeOrThrow(
263
+ abi,
264
+ world,
265
+ writeCommands((commands) => {
266
+ writePhysics3DAbiSetBodyCommand(commands, 1, body);
267
+ writePhysics3DAbiSetColliderCommand(commands, 2, 1, createPhysics3DCollider(unitAabb()));
268
+ }),
269
+ );
270
+ const hits = createPhysics3DAbiQueryBuffer(1);
271
+ expect(queryPhysics3DAbiPoint(abi, world, 0, 0, 0, hits)).toBe(true);
272
+ expect([hits.count, hits.requiredCount]).toEqual([1, 1]);
273
+
274
+ executeOrThrow(
275
+ abi,
276
+ world,
277
+ writeCommands((commands) => {
278
+ writePhysics3DAbiDestroyColliderCommand(commands, 2);
279
+ }),
280
+ );
281
+ expect(queryPhysics3DAbiPoint(abi, world, 0, 0, 0, hits)).toBe(true);
282
+ expect([hits.count, hits.requiredCount]).toEqual([0, 0]);
283
+ });
284
+ });
285
+
286
+ describe('getPhysics3DAbiWorldStatus', () => {
287
+ it('distinguishes a usable handle from a destroyed one', () => {
288
+ const abi = createPhysics3DAbi();
289
+ const world = createPhysics3DAbiWorld(abi);
290
+ expect(getPhysics3DAbiWorldStatus(abi, world)).toBe('Ready');
291
+ destroyPhysics3DAbiWorld(abi, world);
292
+ expect(getPhysics3DAbiWorldStatus(abi, world)).toBe('Stale');
293
+ });
294
+ });
295
+
296
+ describe('readPhysics3DAbiBodies', () => {
297
+ it('reports a sorted full count and writes only the prefix that fits', () => {
298
+ const { abi, world } = createBodyReadWorld();
299
+ const out = createPhysics3DAbiBodyBuffer(1);
300
+
301
+ expect(readPhysics3DAbiBodies(abi, world, null, out)).toBe(true);
302
+ expect(out.requiredCount).toBe(2);
303
+ expect(out.count).toBe(1);
304
+ expect(out.ids[0]).toBe(10);
305
+ });
306
+
307
+ it('preserves selective caller order and silently omits missing ids', () => {
308
+ const { abi, world } = createBodyReadWorld();
309
+ const out = createPhysics3DAbiBodyBuffer(2);
310
+
311
+ expect(readPhysics3DAbiBodies(abi, world, new Uint32Array([20, 404, 10]), out)).toBe(true);
312
+ expect(out.requiredCount).toBe(2);
313
+ expect(out.count).toBe(2);
314
+ expect([...out.ids]).toEqual([20, 10]);
315
+ });
316
+ });
317
+
318
+ describe('readPhysics3DAbiContacts', () => {
319
+ it('publishes contact identities, events, and required capacity after a step', () => {
320
+ const { abi, world } = createContactWorld();
321
+ expect(stepPhysics3DAbiWorld(abi, world, 1 / 60)).toBe('Complete');
322
+
323
+ const out = createPhysics3DAbiContactBuffer(4, 16);
324
+ expect(readPhysics3DAbiContacts(abi, world, 'All', out)).toBe(true);
325
+ expect(out.count).toBeGreaterThan(0);
326
+ expect(new Set([out.ids[0], out.ids[1]])).toEqual(new Set([1, 2]));
327
+ expect(new Set([out.ids[2], out.ids[3]])).toEqual(new Set([11, 22]));
328
+
329
+ const began = createPhysics3DAbiContactBuffer(4, 16);
330
+ expect(readPhysics3DAbiContacts(abi, world, 'Began', began)).toBe(true);
331
+ expect(began.count).toBeGreaterThan(0);
332
+
333
+ const undersized = createPhysics3DAbiContactBuffer(0, 0);
334
+ expect(readPhysics3DAbiContacts(abi, world, 'All', undersized)).toBe(true);
335
+ expect(undersized.count).toBe(0);
336
+ expect(undersized.requiredCount).toBeGreaterThan(0);
337
+ expect(undersized.requiredPointCount).toBeGreaterThan(0);
338
+ });
339
+ });
340
+
341
+ describe('readPhysics3DAbiJoints', () => {
342
+ it('decodes every built-in joint and returns reactions in caller-id order', () => {
343
+ const abi = createPhysics3DAbi();
344
+ const world = createPhysics3DAbiWorld(abi);
345
+ const commands = createPhysics3DAbiCommandBuffer(16384);
346
+ writePhysics3DAbiSetBodyCommand(commands, 10, createRigidBody3D());
347
+ writePhysics3DAbiSetBodyCommand(commands, 20, createRigidBody3D());
348
+ const joints = getJoints();
349
+ const ids = [70, 10, 60, 20, 50, 30, 40];
350
+ for (let i = 0; i < joints.length; i += 1) {
351
+ expect(writePhysics3DAbiSetJointCommand(commands, ids[i], joints[i])).toBe(true);
352
+ }
353
+ const result = createPhysics3DAbiExecutionResult();
354
+ expect(executePhysics3DAbiCommands(abi, world, commands, result)).toBe(true);
355
+
356
+ const out = createPhysics3DAbiJointBuffer(3);
357
+ expect(readPhysics3DAbiJoints(abi, world, out)).toBe(true);
358
+ expect(out.requiredCount).toBe(7);
359
+ expect(out.count).toBe(3);
360
+ expect([...out.ids]).toEqual([10, 20, 30]);
361
+ expect([...out.values]).toEqual(new Array(18).fill(0));
362
+ });
363
+
364
+ it('removes a joint explicitly and cascades the others when a body is destroyed', () => {
365
+ const abi = createPhysics3DAbi();
366
+ const world = createPhysics3DAbiWorld(abi);
367
+ executeOrThrow(
368
+ abi,
369
+ world,
370
+ writeCommands((commands) => {
371
+ writePhysics3DAbiSetBodyCommand(commands, 1, createRigidBody3D());
372
+ writePhysics3DAbiSetBodyCommand(commands, 2, createRigidBody3D());
373
+ writePhysics3DAbiSetJointCommand(commands, 5, createPhysics3DDistanceJoint({ bodyA: 1, bodyB: 2 }));
374
+ writePhysics3DAbiSetJointCommand(commands, 6, createPhysics3DBallAndSocketJoint({ bodyA: 1, bodyB: 2 }));
375
+ }),
376
+ );
377
+ executeOrThrow(
378
+ abi,
379
+ world,
380
+ writeCommands((commands) => {
381
+ writePhysics3DAbiDestroyJointCommand(commands, 5);
382
+ writePhysics3DAbiDestroyBodyCommand(commands, 1);
383
+ }),
384
+ );
385
+
386
+ const out = createPhysics3DAbiJointBuffer(2);
387
+ expect(readPhysics3DAbiJoints(abi, world, out)).toBe(true);
388
+ expect([out.count, out.requiredCount]).toEqual([0, 0]);
389
+ });
390
+ });
391
+
392
+ describe('stepPhysics3DAbiWorld', () => {
393
+ it('matches standard free-flight integration exactly over a persistent multi-step world', () => {
394
+ const abi = createPhysics3DAbi();
395
+ const abiWorld = createPhysics3DAbiWorld(abi);
396
+ const source = createRigidBody3D();
397
+ source.x = -2;
398
+ source.y = 4;
399
+ source.velocityX = 1.25;
400
+ const collider = createPhysics3DCollider(unitAabb());
401
+ const commands = createPhysics3DAbiCommandBuffer();
402
+ writePhysics3DAbiSetBodyCommand(commands, 7, source);
403
+ writePhysics3DAbiSetColliderCommand(commands, 9, 7, collider);
404
+ executeOrThrow(abi, abiWorld, commands);
405
+
406
+ const directWorld = createPhysics3DWorld();
407
+ const directBody = createRigidBody3D();
408
+ directBody.x = source.x;
409
+ directBody.y = source.y;
410
+ directBody.velocityX = source.velocityX;
411
+ addPhysics3DBody(directWorld, directBody);
412
+ addPhysics3DCollider(directWorld, directBody, createPhysics3DCollider(unitAabb()));
413
+
414
+ for (let i = 0; i < 120; i += 1) {
415
+ expect(stepPhysics3DAbiWorld(abi, abiWorld, 1 / 120)).toBe('Complete');
416
+ stepPhysics3D(directWorld, 1 / 120);
417
+ }
418
+
419
+ const out = createPhysics3DAbiBodyBuffer(1);
420
+ readPhysics3DAbiBodies(abi, abiWorld, null, out);
421
+ expect(out.values[Physics3DAbiBodyValue.X]).toBe(directBody.x);
422
+ expect(out.values[Physics3DAbiBodyValue.Y]).toBe(directBody.y);
423
+ expect(out.values[Physics3DAbiBodyValue.VelocityY]).toBe(directBody.velocityY);
424
+ });
425
+
426
+ it('matches the standard contact solver exactly over a frictional settling stack', () => {
427
+ const abi = createPhysics3DAbi();
428
+ const abiWorld = createPhysics3DAbiWorld(abi);
429
+ const directWorld = createPhysics3DWorld();
430
+ const material = { density: 1, friction: 0.4, restitution: 0.2 };
431
+ const shapes: CollisionColliderShape3D[] = [
432
+ { kind: 'aabb', minX: -6, minY: -0.5, minZ: -6, maxX: 6, maxY: 0.5, maxZ: 6 },
433
+ unitAabb(),
434
+ unitAabb(),
435
+ unitAabb(),
436
+ ];
437
+ const poses: [RigidBody3D['type'], number, number, number][] = [
438
+ ['static', 0, -0.5, 0],
439
+ ['dynamic', 0, 0.5, 0],
440
+ ['dynamic', 0.2, 1.6, 0.1],
441
+ ['dynamic', -0.1, 2.7, 0],
442
+ ];
443
+ const bodies = poses.map(([type, x, y, z], index) => {
444
+ const body = createRigidBody3D(type);
445
+ body.x = x;
446
+ body.y = y;
447
+ body.z = z;
448
+ body.velocityX = type === 'dynamic' ? 0.3 : 0;
449
+ addPhysics3DBody(directWorld, body);
450
+ addPhysics3DCollider(directWorld, body, createPhysics3DCollider(shapes[index], material));
451
+ return body;
452
+ });
453
+ const commands = createPhysics3DAbiCommandBuffer(16384);
454
+ for (let i = 0; i < bodies.length; i += 1) {
455
+ expect(writePhysics3DAbiSetBodyCommand(commands, i + 1, bodies[i])).toBe(true);
456
+ expect(writePhysics3DAbiSetColliderCommand(commands, i + 1, i + 1, bodies[i].colliders[0])).toBe(true);
457
+ }
458
+ executeOrThrow(abi, abiWorld, commands);
459
+
460
+ for (let i = 0; i < 180; i += 1) {
461
+ stepPhysics3D(directWorld, 1 / 60);
462
+ expect(stepPhysics3DAbiWorld(abi, abiWorld, 1 / 60)).toBe('Complete');
463
+ }
464
+
465
+ const out = createPhysics3DAbiBodyBuffer(bodies.length);
466
+ expect(readPhysics3DAbiBodies(abi, abiWorld, null, out)).toBe(true);
467
+ expect(out.count).toBe(bodies.length);
468
+ for (let i = 0; i < bodies.length; i += 1) {
469
+ const at = i * Physics3DAbiBodyValueStride;
470
+ const body = bodies[i];
471
+ expect(out.values[at + Physics3DAbiBodyValue.X]).toBe(body.x);
472
+ expect(out.values[at + Physics3DAbiBodyValue.Y]).toBe(body.y);
473
+ expect(out.values[at + Physics3DAbiBodyValue.Z]).toBe(body.z);
474
+ expect(out.values[at + Physics3DAbiBodyValue.OrientationX]).toBe(body.orientationX);
475
+ expect(out.values[at + Physics3DAbiBodyValue.OrientationY]).toBe(body.orientationY);
476
+ expect(out.values[at + Physics3DAbiBodyValue.OrientationZ]).toBe(body.orientationZ);
477
+ expect(out.values[at + Physics3DAbiBodyValue.OrientationW]).toBe(body.orientationW);
478
+ expect(out.values[at + Physics3DAbiBodyValue.VelocityX]).toBe(body.velocityX);
479
+ expect(out.values[at + Physics3DAbiBodyValue.VelocityY]).toBe(body.velocityY);
480
+ expect(out.values[at + Physics3DAbiBodyValue.VelocityZ]).toBe(body.velocityZ);
481
+ expect(out.values[at + Physics3DAbiBodyValue.AngularVelocityX]).toBe(body.angularVelocityX);
482
+ expect(out.values[at + Physics3DAbiBodyValue.AngularVelocityY]).toBe(body.angularVelocityY);
483
+ expect(out.values[at + Physics3DAbiBodyValue.AngularVelocityZ]).toBe(body.angularVelocityZ);
484
+ }
485
+ expect(bodies[3].y).toBeLessThan(2.7);
486
+ expect(directWorld.contacts.length).toBeGreaterThan(0);
487
+ const contacts = createPhysics3DAbiContactBuffer(8, 8 * Physics3DAbiMaxContactPoints);
488
+ expect(readPhysics3DAbiContacts(abi, abiWorld, 'All', contacts)).toBe(true);
489
+ expect(contacts.count).toBe(directWorld.contacts.length);
490
+ expect(Math.max(...contacts.pointCounts.slice(0, contacts.count))).toBe(Physics3DAbiMaxContactPoints);
491
+ });
492
+
493
+ it('adapts synchronous contact hooks without exposing the owned world', () => {
494
+ const { abi, world } = createContactWorld();
495
+ const buffer = createPhysics3DAbiContactBuffer(1, Physics3DAbiMaxContactPoints);
496
+ const reentrantResult = createPhysics3DAbiExecutionResult();
497
+ let preSolveCount = 0;
498
+ let postSolveCount = 0;
499
+ let nestedStepStatus = '';
500
+ let statusDuringHook = '';
501
+ let readDuringHook = true;
502
+ let queryDuringHook = true;
503
+ let destroyDuringHook = true;
504
+
505
+ expect(
506
+ stepPhysics3DAbiWorld(abi, world, 1 / 60, {
507
+ buffer,
508
+ preSolve(contact): void {
509
+ preSolveCount += 1;
510
+ contact.values[3] = 0.75;
511
+ executePhysics3DAbiCommands(abi, world, createPhysics3DAbiCommandBuffer(), reentrantResult);
512
+ nestedStepStatus = stepPhysics3DAbiWorld(abi, world, 1 / 60);
513
+ statusDuringHook = getPhysics3DAbiWorldStatus(abi, world);
514
+ readDuringHook = readPhysics3DAbiBodies(abi, world, null, createPhysics3DAbiBodyBuffer(1));
515
+ queryDuringHook = queryPhysics3DAbiPoint(abi, world, 0, 0, 0, createPhysics3DAbiQueryBuffer(1));
516
+ destroyDuringHook = destroyPhysics3DAbiWorld(abi, world);
517
+ },
518
+ postSolve(): void {
519
+ postSolveCount += 1;
520
+ },
521
+ }),
522
+ ).toBe('Complete');
523
+ expect(preSolveCount).toBeGreaterThan(0);
524
+ expect(postSolveCount).toBeGreaterThan(0);
525
+ expect(reentrantResult.status).toBe('BusyWorld');
526
+ expect(nestedStepStatus).toBe('BusyWorld');
527
+ expect(statusDuringHook).toBe('Busy');
528
+ expect(readDuringHook).toBe(false);
529
+ expect(queryDuringHook).toBe(false);
530
+ expect(destroyDuringHook).toBe(false);
531
+
532
+ const contacts = createPhysics3DAbiContactBuffer(2, 8);
533
+ readPhysics3DAbiContacts(abi, world, 'All', contacts);
534
+ expect(contacts.values[3]).toBe(0.75);
535
+ });
536
+
537
+ it('declines invalid timesteps and insufficient live hook buffers explicitly', () => {
538
+ const { abi, world } = createContactWorld();
539
+ expect(stepPhysics3DAbiWorld(abi, world, 0)).toBe('Declined');
540
+ expect(
541
+ stepPhysics3DAbiWorld(abi, world, 1 / 60, {
542
+ buffer: createPhysics3DAbiContactBuffer(1, Physics3DAbiMaxContactPoints - 1),
543
+ preSolve(): void {},
544
+ postSolve: null,
545
+ }),
546
+ ).toBe('InsufficientHookBuffer');
547
+ expect(
548
+ stepPhysics3DAbiWorld(abi, world, 1 / 60, {
549
+ buffer: createPhysics3DAbiContactBuffer(0, 0),
550
+ preSolve: null,
551
+ postSolve: null,
552
+ }),
553
+ ).toBe('Complete');
554
+ });
555
+ });
556
+
557
+ function createBodyReadWorld(): { abi: Physics3DAbi; world: number } {
558
+ const abi = createPhysics3DAbi();
559
+ const world = createPhysics3DAbiWorld(abi);
560
+ const commands = createPhysics3DAbiCommandBuffer();
561
+ const body20 = createRigidBody3D();
562
+ body20.x = 20;
563
+ const body10 = createRigidBody3D();
564
+ body10.x = 10;
565
+ writePhysics3DAbiSetBodyCommand(commands, 20, body20);
566
+ writePhysics3DAbiSetBodyCommand(commands, 10, body10);
567
+ executeOrThrow(abi, world, commands);
568
+ return { abi, world };
569
+ }
570
+
571
+ function createContactWorld(): { abi: Physics3DAbi; world: number } {
572
+ const abi = createPhysics3DAbi();
573
+ const world = createPhysics3DAbiWorld(abi);
574
+ const commands = createPhysics3DAbiCommandBuffer();
575
+ const ground = createRigidBody3D('static');
576
+ const box = createRigidBody3D();
577
+ box.y = 0.75;
578
+ writePhysics3DAbiSetBodyCommand(commands, 2, ground);
579
+ writePhysics3DAbiSetColliderCommand(commands, 22, 2, createPhysics3DCollider(unitAabb()));
580
+ writePhysics3DAbiSetBodyCommand(commands, 1, box);
581
+ writePhysics3DAbiSetColliderCommand(commands, 11, 1, createPhysics3DCollider(unitAabb()));
582
+ executeOrThrow(abi, world, commands);
583
+ return { abi, world };
584
+ }
585
+
586
+ function executeOrThrow(abi: Readonly<Physics3DAbi>, world: number, commands: Physics3DAbiCommandBuffer): void {
587
+ const result = createPhysics3DAbiExecutionResult();
588
+ if (!executePhysics3DAbiCommands(abi, world, commands, result)) {
589
+ throw new Error(`Unexpected ABI command failure: ${result.status} at ${result.commandIndex}`);
590
+ }
591
+ }
592
+
593
+ function getColliderShapes(): CollisionColliderShape3D[] {
594
+ return [
595
+ { kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 },
596
+ unitAabb(),
597
+ {
598
+ kind: 'box',
599
+ x: 0,
600
+ y: 0,
601
+ z: 0,
602
+ halfX: 1,
603
+ halfY: 1,
604
+ halfZ: 1,
605
+ rotationX: 0,
606
+ rotationY: 0,
607
+ rotationZ: 0,
608
+ rotationW: 1,
609
+ },
610
+ { kind: 'capsule', x0: 0, y0: -1, z0: 0, x1: 0, y1: 1, z1: 0, radius: 0.5 },
611
+ { kind: 'cylinder', x0: 0, y0: -1, z0: 0, x1: 0, y1: 1, z1: 0, radius: 0.5 },
612
+ { kind: 'cone', apexX: 0, apexY: 1, apexZ: 0, baseX: 0, baseY: -1, baseZ: 0, radius: 1 },
613
+ { kind: 'convex', points: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] },
614
+ {
615
+ kind: 'triangle-mesh',
616
+ version: 0,
617
+ x: 0,
618
+ y: 0,
619
+ z: 0,
620
+ rotationX: 0,
621
+ rotationY: 0,
622
+ rotationZ: 0,
623
+ rotationW: 1,
624
+ points: [0, 0, 0, 1, 0, 0, 0, 0, 1],
625
+ indices: [0, 1, 2],
626
+ },
627
+ {
628
+ kind: 'heightfield',
629
+ columns: 2,
630
+ rows: 2,
631
+ version: 0,
632
+ cellSizeX: 1,
633
+ cellSizeZ: 1,
634
+ x: 0,
635
+ y: 0,
636
+ z: 0,
637
+ rotationX: 0,
638
+ rotationY: 0,
639
+ rotationZ: 0,
640
+ rotationW: 1,
641
+ heights: [0, 0, 0, 0],
642
+ },
643
+ ];
644
+ }
645
+
646
+ function getJoints(): Physics3DJoint[] {
647
+ return [
648
+ createPhysics3DBallAndSocketJoint({ bodyA: 10, bodyB: 20 }),
649
+ createPhysics3DDistanceJoint({ bodyA: 10, bodyB: 20, length: 1 }),
650
+ createPhysics3DFixedJoint({ bodyA: 10, bodyB: 20 }),
651
+ createPhysics3DHingeJoint({ bodyA: 10, bodyB: 20 }),
652
+ createPhysics3DSliderJoint({ bodyA: 10, bodyB: 20 }),
653
+ createPhysics3DConeTwistJoint({ bodyA: 10, bodyB: 20 }),
654
+ createPhysics3DGeneric6DofJoint({ bodyA: 10, bodyB: 20 }),
655
+ ];
656
+ }
657
+
658
+ function unitAabb(): CollisionColliderShape3D {
659
+ return { kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 };
660
+ }
661
+
662
+ function writeCommands(write: (commands: Physics3DAbiCommandBuffer) => void): Physics3DAbiCommandBuffer {
663
+ const commands = createPhysics3DAbiCommandBuffer();
664
+ write(commands);
665
+ return commands;
666
+ }