@flighthq/physics3d-abi 0.4.0-next.1827.9db114b

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 +216 -0
  22. package/dist/physics3DAbiLayout.d.ts.map +1 -0
  23. package/dist/physics3DAbiLayout.js +221 -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 +584 -0
  35. package/src/physics3DAbiBuffer.test.ts +117 -0
  36. package/src/physics3DAbiCommand.test.ts +368 -0
  37. package/src/physics3DAbiLayout.test.ts +64 -0
  38. package/src/physics3DAbiQuery.test.ts +122 -0
  39. package/src/referencePhysics3DAbi.test.ts +19 -0
@@ -0,0 +1,368 @@
1
+ import {
2
+ createPhysics3DBallAndSocketJoint,
3
+ createPhysics3DCollider,
4
+ createPhysics3DConeTwistJoint,
5
+ createPhysics3DDistanceJoint,
6
+ createPhysics3DFixedJoint,
7
+ createPhysics3DGeneric6DofJoint,
8
+ createPhysics3DHingeJoint,
9
+ createPhysics3DSliderJoint,
10
+ createPhysics3DSolverConfig,
11
+ createRigidBody3D,
12
+ } from '@flighthq/physics3d/contract';
13
+ import type { CollisionColliderShape3D, Physics3DAbiCommandBuffer, Physics3DJoint } from '@flighthq/types/contract';
14
+ import { describe, expect, it } from 'vitest';
15
+
16
+ import { clearPhysics3DAbiCommandBuffer, createPhysics3DAbiCommandBuffer } from './physics3DAbiBuffer';
17
+ import {
18
+ getPhysics3DAbiSetColliderCommandByteLength,
19
+ writePhysics3DAbiApplyForceAtPointCommand,
20
+ writePhysics3DAbiApplyForceCommand,
21
+ writePhysics3DAbiApplyLinearImpulseAtPointCommand,
22
+ writePhysics3DAbiApplyLinearImpulseCommand,
23
+ writePhysics3DAbiApplyTorqueCommand,
24
+ writePhysics3DAbiDestroyBodyCommand,
25
+ writePhysics3DAbiDestroyColliderCommand,
26
+ writePhysics3DAbiDestroyJointCommand,
27
+ writePhysics3DAbiSetBodyCommand,
28
+ writePhysics3DAbiSetColliderCommand,
29
+ writePhysics3DAbiSetGravityCommand,
30
+ writePhysics3DAbiSetJointCommand,
31
+ writePhysics3DAbiSetSolverConfigCommand,
32
+ writePhysics3DAbiWakeBodyCommand,
33
+ } from './physics3DAbiCommand';
34
+ import {
35
+ Physics3DAbiBodyType,
36
+ Physics3DAbiCommandByteLength,
37
+ Physics3DAbiCommandHeaderByteLength,
38
+ Physics3DAbiCommandKind,
39
+ Physics3DAbiCommandRecordHeaderByteLength,
40
+ Physics3DAbiJointKind,
41
+ Physics3DAbiSetColliderPayloadOffset,
42
+ Physics3DAbiShapeKind,
43
+ } from './physics3DAbiLayout';
44
+
45
+ describe('getPhysics3DAbiSetColliderCommandByteLength', () => {
46
+ it('matches exactly the number of bytes the collider writer publishes', () => {
47
+ const collider = createPhysics3DCollider({ kind: 'sphere', x: 0, y: 0, z: 0, radius: 2 });
48
+ const out = createPhysics3DAbiCommandBuffer(1024);
49
+ const before = out.byteLength;
50
+
51
+ expect(writePhysics3DAbiSetColliderCommand(out, 4, 3, collider)).toBe(true);
52
+ expect(out.byteLength - before).toBe(getPhysics3DAbiSetColliderCommandByteLength(collider));
53
+ });
54
+ });
55
+
56
+ describe('writePhysics3DAbiApplyForceAtPointCommand', () => {
57
+ it('writes all six vector components into one body-action record', () => {
58
+ expectBodyAction(writePhysics3DAbiApplyForceAtPointCommand, Physics3DAbiCommandKind.ApplyForceAtPoint, true);
59
+ });
60
+ });
61
+
62
+ describe('writePhysics3DAbiApplyForceCommand', () => {
63
+ it('writes a force and zeroes the unused point slots', () => {
64
+ expectBodyAction(writePhysics3DAbiApplyForceCommand, Physics3DAbiCommandKind.ApplyForce, false);
65
+ });
66
+ });
67
+
68
+ describe('writePhysics3DAbiApplyLinearImpulseAtPointCommand', () => {
69
+ it('writes all six impulse and point components', () => {
70
+ expectBodyAction(
71
+ writePhysics3DAbiApplyLinearImpulseAtPointCommand,
72
+ Physics3DAbiCommandKind.ApplyLinearImpulseAtPoint,
73
+ true,
74
+ );
75
+ });
76
+ });
77
+
78
+ describe('writePhysics3DAbiApplyLinearImpulseCommand', () => {
79
+ it('writes an impulse and zeroes the unused point slots', () => {
80
+ expectBodyAction(writePhysics3DAbiApplyLinearImpulseCommand, Physics3DAbiCommandKind.ApplyLinearImpulse, false);
81
+ });
82
+ });
83
+
84
+ describe('writePhysics3DAbiApplyTorqueCommand', () => {
85
+ it('writes a torque and zeroes the unused point slots', () => {
86
+ expectBodyAction(writePhysics3DAbiApplyTorqueCommand, Physics3DAbiCommandKind.ApplyTorque, false);
87
+ });
88
+ });
89
+
90
+ describe('writePhysics3DAbiDestroyBodyCommand', () => {
91
+ it('writes an empty body record with a stable caller id', () => {
92
+ expectEmptyCommand(writePhysics3DAbiDestroyBodyCommand, Physics3DAbiCommandKind.DestroyBody);
93
+ });
94
+ });
95
+
96
+ describe('writePhysics3DAbiDestroyColliderCommand', () => {
97
+ it('writes an empty collider record with a stable caller id', () => {
98
+ expectEmptyCommand(writePhysics3DAbiDestroyColliderCommand, Physics3DAbiCommandKind.DestroyCollider);
99
+ });
100
+ });
101
+
102
+ describe('writePhysics3DAbiDestroyJointCommand', () => {
103
+ it('writes an empty joint record with a stable caller id', () => {
104
+ expectEmptyCommand(writePhysics3DAbiDestroyJointCommand, Physics3DAbiCommandKind.DestroyJoint);
105
+ });
106
+ });
107
+
108
+ describe('writePhysics3DAbiSetBodyCommand', () => {
109
+ it('encodes the complete authored and dynamic body record', () => {
110
+ const body = createRigidBody3D('kinematic');
111
+ body.x = 3;
112
+ body.velocityZ = -4;
113
+ body.gravityScale = 0.25;
114
+ body.fixedRotation = true;
115
+ const out = createPhysics3DAbiCommandBuffer();
116
+
117
+ expect(writePhysics3DAbiSetBodyCommand(out, 9, body)).toBe(true);
118
+
119
+ const view = commandView(out);
120
+ expect(view.getUint32(0, true)).toBe(Physics3DAbiCommandKind.SetBody);
121
+ expect(view.getUint32(4, true)).toBe(Physics3DAbiCommandByteLength.SetBody);
122
+ expect(view.getUint32(8, true)).toBe(9);
123
+ expect(view.getUint32(16, true) & 0b11).toBe(Physics3DAbiBodyType.Kinematic);
124
+ expect(view.getFloat64(24, true)).toBe(3);
125
+ expect(view.getFloat64(24 + 9 * 8, true)).toBe(-4);
126
+ expect(view.getFloat64(24 + 31 * 8, true)).toBe(0.25);
127
+ });
128
+
129
+ it('does not publish or overwrite bytes when the record does not fit', () => {
130
+ const out = createPhysics3DAbiCommandBuffer(Physics3DAbiCommandHeaderByteLength);
131
+ const before = [...out.data];
132
+
133
+ expect(writePhysics3DAbiSetBodyCommand(out, 1, createRigidBody3D())).toBe(false);
134
+ expect([...out.data]).toEqual(before);
135
+ expect(out.commandCount).toBe(0);
136
+ });
137
+ });
138
+
139
+ describe('writePhysics3DAbiSetColliderCommand', () => {
140
+ it.each(getColliderShapeCases())('encodes and aligns the $name shape', ({ shape, code }) => {
141
+ const collider = createPhysics3DCollider(shape);
142
+ const out = createPhysics3DAbiCommandBuffer(16384);
143
+ out.data.fill(0xff);
144
+ clearPhysics3DAbiCommandBuffer(out);
145
+
146
+ expect(writePhysics3DAbiSetColliderCommand(out, 12, 8, collider)).toBe(true);
147
+
148
+ const view = commandView(out);
149
+ expect(view.getUint32(0, true)).toBe(Physics3DAbiCommandKind.SetCollider);
150
+ expect(view.getUint32(8, true)).toBe(12);
151
+ expect(view.getUint32(12, true)).toBe(8);
152
+ expect(
153
+ view.getUint32(Physics3DAbiCommandRecordHeaderByteLength + Physics3DAbiSetColliderPayloadOffset.Shape, true),
154
+ ).toBe(code);
155
+ expect(view.getUint32(4, true) % 8).toBe(0);
156
+ expect(out.data[out.byteLength - 1]).not.toBe(0xff);
157
+ });
158
+ });
159
+
160
+ describe('writePhysics3DAbiSetGravityCommand', () => {
161
+ it('writes three little-endian Float64 components', () => {
162
+ const out = createPhysics3DAbiCommandBuffer();
163
+ expect(writePhysics3DAbiSetGravityCommand(out, 1.5, -2.5, 3.5)).toBe(true);
164
+ const view = commandView(out);
165
+ expect(view.getUint32(0, true)).toBe(Physics3DAbiCommandKind.SetGravity);
166
+ expect([view.getFloat64(16, true), view.getFloat64(24, true), view.getFloat64(32, true)]).toEqual([1.5, -2.5, 3.5]);
167
+ });
168
+
169
+ it('leaves a corrupted stream untouched instead of appending behind an invalid header', () => {
170
+ const out = createPhysics3DAbiCommandBuffer();
171
+ out.data[0] = 0;
172
+ const before = [...out.data];
173
+ expect(writePhysics3DAbiSetGravityCommand(out, 1, 2, 3)).toBe(false);
174
+ expect([...out.data]).toEqual(before);
175
+ });
176
+ });
177
+
178
+ describe('writePhysics3DAbiSetJointCommand', () => {
179
+ it.each(getJointCases())('encodes the $name joint under its fixed wire kind', ({ joint, code }) => {
180
+ const out = createPhysics3DAbiCommandBuffer();
181
+ expect(writePhysics3DAbiSetJointCommand(out, 77, joint)).toBe(true);
182
+ const view = commandView(out);
183
+ expect(view.getUint32(0, true)).toBe(Physics3DAbiCommandKind.SetJoint);
184
+ expect(view.getUint32(16, true)).toBe(code);
185
+ expect(view.getUint32(20, true)).toBe(10);
186
+ expect(view.getUint32(24, true)).toBe(20);
187
+ });
188
+ });
189
+
190
+ describe('writePhysics3DAbiSetSolverConfigCommand', () => {
191
+ it('encodes every iteration, CCD, sleep, and warm-start setting', () => {
192
+ const config = createPhysics3DSolverConfig();
193
+ config.substeps = 3;
194
+ config.maxCcdSubsteps = 7;
195
+ config.maxCcdRotationSubsteps = 9;
196
+ config.sequentialImpulse.velocityIterations = 11;
197
+ config.sequentialImpulse.positionIterations = 13;
198
+ config.sequentialImpulse.warmStarting = false;
199
+ const out = createPhysics3DAbiCommandBuffer();
200
+
201
+ expect(writePhysics3DAbiSetSolverConfigCommand(out, config)).toBe(true);
202
+
203
+ const view = commandView(out);
204
+ expect(view.getUint32(20, true)).toBe(3);
205
+ expect(view.getUint32(24, true)).toBe(7);
206
+ expect(view.getUint32(28, true)).toBe(9);
207
+ expect(view.getUint32(32, true)).toBe(11);
208
+ expect(view.getUint32(36, true)).toBe(13);
209
+ expect(view.getUint32(16, true) & (1 << 2)).toBe(0);
210
+ });
211
+ });
212
+
213
+ describe('writePhysics3DAbiWakeBodyCommand', () => {
214
+ it('writes an empty wake record', () => {
215
+ expectEmptyCommand(writePhysics3DAbiWakeBodyCommand, Physics3DAbiCommandKind.WakeBody);
216
+ });
217
+ });
218
+
219
+ function commandView(out: Readonly<Physics3DAbiCommandBuffer>): DataView {
220
+ return new DataView(
221
+ out.data.buffer,
222
+ out.data.byteOffset + Physics3DAbiCommandHeaderByteLength,
223
+ out.byteLength - Physics3DAbiCommandHeaderByteLength,
224
+ );
225
+ }
226
+
227
+ function expectBodyAction(
228
+ write: (out: Physics3DAbiCommandBuffer, id: number, ...values: number[]) => boolean,
229
+ kind: number,
230
+ hasPoint: boolean,
231
+ ): void {
232
+ const out = createPhysics3DAbiCommandBuffer();
233
+ expect(write(out, 42, 1, 2, 3, 4, 5, 6)).toBe(true);
234
+ const view = commandView(out);
235
+ expect(view.getUint32(0, true)).toBe(kind);
236
+ expect(view.getUint32(4, true)).toBe(Physics3DAbiCommandByteLength.BodyAction);
237
+ expect(view.getUint32(8, true)).toBe(42);
238
+ expect(Array.from({ length: 6 }, (_, i) => view.getFloat64(16 + i * 8, true))).toEqual(
239
+ hasPoint ? [1, 2, 3, 4, 5, 6] : [1, 2, 3, 0, 0, 0],
240
+ );
241
+ }
242
+
243
+ function expectEmptyCommand(write: (out: Physics3DAbiCommandBuffer, id: number) => boolean, kind: number): void {
244
+ const out = createPhysics3DAbiCommandBuffer();
245
+ expect(write(out, 0)).toBe(false);
246
+ expect(write(out, 55)).toBe(true);
247
+ const view = commandView(out);
248
+ expect(view.getUint32(0, true)).toBe(kind);
249
+ expect(view.getUint32(4, true)).toBe(Physics3DAbiCommandRecordHeaderByteLength);
250
+ expect(view.getUint32(8, true)).toBe(55);
251
+ }
252
+
253
+ function getColliderShapeCases(): ReadonlyArray<{
254
+ name: string;
255
+ shape: CollisionColliderShape3D;
256
+ code: number;
257
+ }> {
258
+ return [
259
+ { name: 'sphere', shape: { kind: 'sphere', x: 0, y: 0, z: 0, radius: 1 }, code: Physics3DAbiShapeKind.Sphere },
260
+ { name: 'aabb', shape: unitAabb(), code: Physics3DAbiShapeKind.Aabb },
261
+ {
262
+ name: 'box',
263
+ shape: {
264
+ kind: 'box',
265
+ x: 0,
266
+ y: 0,
267
+ z: 0,
268
+ halfX: 1,
269
+ halfY: 2,
270
+ halfZ: 3,
271
+ rotationX: 0,
272
+ rotationY: 0,
273
+ rotationZ: 0,
274
+ rotationW: 1,
275
+ },
276
+ code: Physics3DAbiShapeKind.Box,
277
+ },
278
+ {
279
+ name: 'capsule',
280
+ shape: { kind: 'capsule', x0: 0, y0: -1, z0: 0, x1: 0, y1: 1, z1: 0, radius: 0.5 },
281
+ code: Physics3DAbiShapeKind.Capsule,
282
+ },
283
+ {
284
+ name: 'cylinder',
285
+ shape: { kind: 'cylinder', x0: 0, y0: -1, z0: 0, x1: 0, y1: 1, z1: 0, radius: 0.5 },
286
+ code: Physics3DAbiShapeKind.Cylinder,
287
+ },
288
+ {
289
+ name: 'cone',
290
+ shape: { kind: 'cone', apexX: 0, apexY: 1, apexZ: 0, baseX: 0, baseY: -1, baseZ: 0, radius: 1 },
291
+ code: Physics3DAbiShapeKind.Cone,
292
+ },
293
+ {
294
+ name: 'convex',
295
+ shape: { kind: 'convex', points: [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] },
296
+ code: Physics3DAbiShapeKind.Convex,
297
+ },
298
+ {
299
+ name: 'triangle mesh',
300
+ shape: {
301
+ kind: 'triangle-mesh',
302
+ version: 2,
303
+ x: 0,
304
+ y: 0,
305
+ z: 0,
306
+ rotationX: 0,
307
+ rotationY: 0,
308
+ rotationZ: 0,
309
+ rotationW: 1,
310
+ points: [0, 0, 0, 1, 0, 0, 0, 0, 1],
311
+ indices: [0, 1, 2],
312
+ },
313
+ code: Physics3DAbiShapeKind.TriangleMesh,
314
+ },
315
+ {
316
+ name: 'heightfield',
317
+ shape: {
318
+ kind: 'heightfield',
319
+ columns: 2,
320
+ rows: 2,
321
+ version: 3,
322
+ cellSizeX: 1,
323
+ cellSizeZ: 1,
324
+ x: 0,
325
+ y: 0,
326
+ z: 0,
327
+ rotationX: 0,
328
+ rotationY: 0,
329
+ rotationZ: 0,
330
+ rotationW: 1,
331
+ heights: [0, 0, 0, 0],
332
+ },
333
+ code: Physics3DAbiShapeKind.Heightfield,
334
+ },
335
+ ];
336
+ }
337
+
338
+ function getJointCases(): ReadonlyArray<{ name: string; joint: Physics3DJoint; code: number }> {
339
+ return [
340
+ {
341
+ name: 'ball-and-socket',
342
+ joint: createPhysics3DBallAndSocketJoint({ bodyA: 10, bodyB: 20 }),
343
+ code: Physics3DAbiJointKind.BallAndSocket,
344
+ },
345
+ {
346
+ name: 'distance',
347
+ joint: createPhysics3DDistanceJoint({ bodyA: 10, bodyB: 20, length: 2 }),
348
+ code: Physics3DAbiJointKind.Distance,
349
+ },
350
+ { name: 'fixed', joint: createPhysics3DFixedJoint({ bodyA: 10, bodyB: 20 }), code: Physics3DAbiJointKind.Fixed },
351
+ { name: 'hinge', joint: createPhysics3DHingeJoint({ bodyA: 10, bodyB: 20 }), code: Physics3DAbiJointKind.Hinge },
352
+ { name: 'slider', joint: createPhysics3DSliderJoint({ bodyA: 10, bodyB: 20 }), code: Physics3DAbiJointKind.Slider },
353
+ {
354
+ name: 'cone-twist',
355
+ joint: createPhysics3DConeTwistJoint({ bodyA: 10, bodyB: 20 }),
356
+ code: Physics3DAbiJointKind.ConeTwist,
357
+ },
358
+ {
359
+ name: 'generic-6dof',
360
+ joint: createPhysics3DGeneric6DofJoint({ bodyA: 10, bodyB: 20 }),
361
+ code: Physics3DAbiJointKind.Generic6Dof,
362
+ },
363
+ ];
364
+ }
365
+
366
+ function unitAabb(): CollisionColliderShape3D {
367
+ return { kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 };
368
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ Physics3DAbiBodyValue,
5
+ Physics3DAbiBodyValueStride,
6
+ Physics3DAbiCommandHeaderByteLength,
7
+ Physics3DAbiCommandKind,
8
+ Physics3DAbiCommandMagic,
9
+ Physics3DAbiCommandRecordHeaderByteLength,
10
+ Physics3DAbiContactId,
11
+ Physics3DAbiContactIdStride,
12
+ Physics3DAbiContactPointValue,
13
+ Physics3DAbiContactPointValueStride,
14
+ Physics3DAbiContactValue,
15
+ Physics3DAbiContactValueStride,
16
+ Physics3DAbiJointKind,
17
+ Physics3DAbiJointValue,
18
+ Physics3DAbiJointValueStride,
19
+ Physics3DAbiQueryValue,
20
+ Physics3DAbiQueryValueStride,
21
+ Physics3DAbiShapeKind,
22
+ Physics3DAbiVersion,
23
+ } from './physics3DAbiLayout';
24
+
25
+ describe('Physics3D ABI wire layout', () => {
26
+ it('locks the version, magic bytes, and record framing', () => {
27
+ const bytes = new Uint8Array(4);
28
+ new DataView(bytes.buffer).setUint32(0, Physics3DAbiCommandMagic, true);
29
+ expect(new TextDecoder().decode(bytes)).toBe('P3DA');
30
+ expect(Physics3DAbiVersion).toBe(1);
31
+ expect(Physics3DAbiCommandHeaderByteLength).toBe(16);
32
+ expect(Physics3DAbiCommandRecordHeaderByteLength).toBe(16);
33
+ });
34
+
35
+ it('locks command, shape, and joint discriminants independently', () => {
36
+ expect(Physics3DAbiCommandKind).toEqual({
37
+ SetGravity: 1,
38
+ SetSolverConfig: 2,
39
+ SetBody: 3,
40
+ DestroyBody: 4,
41
+ SetCollider: 5,
42
+ DestroyCollider: 6,
43
+ SetJoint: 7,
44
+ DestroyJoint: 8,
45
+ ApplyForce: 9,
46
+ ApplyForceAtPoint: 10,
47
+ ApplyLinearImpulse: 11,
48
+ ApplyLinearImpulseAtPoint: 12,
49
+ ApplyTorque: 13,
50
+ WakeBody: 14,
51
+ });
52
+ expect(Object.values(Physics3DAbiShapeKind)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
53
+ expect(Object.values(Physics3DAbiJointKind)).toEqual([1, 2, 3, 4, 5, 6, 7]);
54
+ });
55
+
56
+ it('keeps every structure-of-arrays index map dense and stride-terminated', () => {
57
+ expect(Math.max(...Object.values(Physics3DAbiBodyValue)) + 1).toBe(Physics3DAbiBodyValueStride);
58
+ expect(Math.max(...Object.values(Physics3DAbiContactId)) + 1).toBe(Physics3DAbiContactIdStride);
59
+ expect(Math.max(...Object.values(Physics3DAbiContactValue)) + 1).toBe(Physics3DAbiContactValueStride);
60
+ expect(Math.max(...Object.values(Physics3DAbiContactPointValue)) + 1).toBe(Physics3DAbiContactPointValueStride);
61
+ expect(Math.max(...Object.values(Physics3DAbiJointValue)) + 1).toBe(Physics3DAbiJointValueStride);
62
+ expect(Math.max(...Object.values(Physics3DAbiQueryValue)) + 1).toBe(Physics3DAbiQueryValueStride);
63
+ });
64
+ });
@@ -0,0 +1,122 @@
1
+ import { createPhysics3DCollider, createPhysics3DQueryFilter, createRigidBody3D } from '@flighthq/physics3d/contract';
2
+ import type { Physics3DAbi, Physics3DAbiCommandBuffer, SpatialAabb3D } from '@flighthq/types/contract';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createPhysics3DAbi, createPhysics3DAbiWorld, executePhysics3DAbiCommands } from './physics3DAbi';
6
+ import {
7
+ createPhysics3DAbiCommandBuffer,
8
+ createPhysics3DAbiExecutionResult,
9
+ createPhysics3DAbiQueryBuffer,
10
+ } from './physics3DAbiBuffer';
11
+ import { writePhysics3DAbiSetBodyCommand, writePhysics3DAbiSetColliderCommand } from './physics3DAbiCommand';
12
+ import { Physics3DAbiQueryValue } from './physics3DAbiLayout';
13
+ import {
14
+ queryPhysics3DAbiPoint,
15
+ queryPhysics3DAbiRay,
16
+ queryPhysics3DAbiRayClosest,
17
+ queryPhysics3DAbiRegion,
18
+ queryPhysics3DAbiShapeCast,
19
+ } from './physics3DAbiQuery';
20
+
21
+ describe('queryPhysics3DAbiPoint', () => {
22
+ it('reports all hits and the complete count when output capacity truncates them', () => {
23
+ const { abi, world } = createQueryWorld();
24
+ const out = createPhysics3DAbiQueryBuffer(1);
25
+
26
+ expect(queryPhysics3DAbiPoint(abi, world, 5, 0, 0, out)).toBe(true);
27
+ expect(out.count).toBe(1);
28
+ expect(out.requiredCount).toBe(2);
29
+ expect([101, 102]).toContain(out.bodyIds[0]);
30
+ expect([201, 202]).toContain(out.colliderIds[0]);
31
+ expect([...out.values]).toEqual(new Array(7).fill(0));
32
+ });
33
+
34
+ it('applies the standard Physics3D query filter', () => {
35
+ const { abi, world } = createQueryWorld();
36
+ const filter = createPhysics3DQueryFilter();
37
+ filter.includeStatic = false;
38
+ const out = createPhysics3DAbiQueryBuffer(2);
39
+
40
+ expect(queryPhysics3DAbiPoint(abi, world, 5, 0, 0, out, filter)).toBe(true);
41
+ expect([out.count, out.requiredCount]).toEqual([0, 0]);
42
+ });
43
+ });
44
+
45
+ describe('queryPhysics3DAbiRay', () => {
46
+ it('writes fraction, hit point, and normal for every ray hit', () => {
47
+ const { abi, world } = createQueryWorld();
48
+ const out = createPhysics3DAbiQueryBuffer(2);
49
+
50
+ expect(queryPhysics3DAbiRay(abi, world, 0, 0, 0, 1, 0, 0, out, 10)).toBe(true);
51
+ expect(out.count).toBe(2);
52
+ expect(out.requiredCount).toBe(2);
53
+ expect(out.values[Physics3DAbiQueryValue.Fraction]).toBeCloseTo(4.5, 12);
54
+ expect(out.values[Physics3DAbiQueryValue.X]).toBeCloseTo(4.5, 12);
55
+ expect(out.values[Physics3DAbiQueryValue.NormalX]).toBe(-1);
56
+ });
57
+ });
58
+
59
+ describe('queryPhysics3DAbiRayClosest', () => {
60
+ it('publishes exactly one deterministic closest hit', () => {
61
+ const { abi, world } = createQueryWorld();
62
+ const out = createPhysics3DAbiQueryBuffer(2);
63
+
64
+ expect(queryPhysics3DAbiRayClosest(abi, world, 0, 0, 0, 1, 0, 0, out, 10)).toBe(true);
65
+ expect([out.count, out.requiredCount]).toEqual([1, 1]);
66
+ expect(out.bodyIds[0]).toBe(101);
67
+ });
68
+ });
69
+
70
+ describe('queryPhysics3DAbiRegion', () => {
71
+ it('maps broadphase region results back to caller-owned ids', () => {
72
+ const { abi, world } = createQueryWorld();
73
+ const out = createPhysics3DAbiQueryBuffer(2);
74
+ const region: SpatialAabb3D = { minX: 4, minY: -1, minZ: -1, maxX: 6, maxY: 1, maxZ: 1 };
75
+
76
+ expect(queryPhysics3DAbiRegion(abi, world, region, out)).toBe(true);
77
+ expect(out.count).toBe(2);
78
+ expect(new Set(out.bodyIds)).toEqual(new Set([101, 102]));
79
+ });
80
+ });
81
+
82
+ describe('queryPhysics3DAbiShapeCast', () => {
83
+ it('writes the first swept-shape impact into the shared query layout', () => {
84
+ const { abi, world } = createQueryWorld();
85
+ const out = createPhysics3DAbiQueryBuffer(1);
86
+
87
+ expect(queryPhysics3DAbiShapeCast(abi, world, unitAabb(), 10, 0, 0, out)).toBe(true);
88
+ expect([out.count, out.requiredCount]).toEqual([1, 1]);
89
+ expect(out.bodyIds[0]).toBe(101);
90
+ expect(out.values[Physics3DAbiQueryValue.Fraction]).toBeCloseTo(0.4, 4);
91
+ });
92
+ });
93
+
94
+ function createQueryWorld(): { abi: Physics3DAbi; world: number } {
95
+ const abi = createPhysics3DAbi();
96
+ const world = createPhysics3DAbiWorld(abi);
97
+ const commands = createPhysics3DAbiCommandBuffer();
98
+ addQueryBody(commands, 101, 201);
99
+ addQueryBody(commands, 102, 202);
100
+ const result = createPhysics3DAbiExecutionResult();
101
+ if (!executePhysics3DAbiCommands(abi, world, commands, result)) throw new Error(result.status);
102
+ return { abi, world };
103
+ }
104
+
105
+ function addQueryBody(commands: Physics3DAbiCommandBuffer, bodyId: number, colliderId: number): void {
106
+ const body = createRigidBody3D('static');
107
+ body.x = 5;
108
+ writePhysics3DAbiSetBodyCommand(commands, bodyId, body);
109
+ writePhysics3DAbiSetColliderCommand(commands, colliderId, bodyId, createPhysics3DCollider(unitAabb()));
110
+ }
111
+
112
+ function unitAabb(): {
113
+ kind: 'aabb';
114
+ minX: number;
115
+ minY: number;
116
+ minZ: number;
117
+ maxX: number;
118
+ maxY: number;
119
+ maxZ: number;
120
+ } {
121
+ return { kind: 'aabb', minX: -0.5, minY: -0.5, minZ: -0.5, maxX: 0.5, maxY: 0.5, maxZ: 0.5 };
122
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Physics3DAbiVersion } from './physics3DAbiLayout';
4
+ import { createReferencePhysics3DAbi } from './referencePhysics3DAbi';
5
+
6
+ describe('createReferencePhysics3DAbi', () => {
7
+ it('creates isolated persistent-world storage under the public ABI version', () => {
8
+ const first = createReferencePhysics3DAbi();
9
+ const second = createReferencePhysics3DAbi();
10
+ const firstWorld = first.createWorld();
11
+ const secondWorld = second.createWorld();
12
+
13
+ expect(first.version).toBe(Physics3DAbiVersion);
14
+ expect(firstWorld).toBe(1);
15
+ expect(secondWorld).toBe(1);
16
+ expect(first.destroyWorld(firstWorld)).toBe(true);
17
+ expect(second.destroyWorld(secondWorld)).toBe(true);
18
+ });
19
+ });