@flighthq/physics2d-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/physics2DAbi.d.ts +11 -0
  10. package/dist/physics2DAbi.d.ts.map +1 -0
  11. package/dist/physics2DAbi.js +32 -0
  12. package/dist/physics2DAbi.js.map +1 -0
  13. package/dist/physics2DAbiBuffer.d.ts +10 -0
  14. package/dist/physics2DAbiBuffer.d.ts.map +1 -0
  15. package/dist/physics2DAbiBuffer.js +81 -0
  16. package/dist/physics2DAbiBuffer.js.map +1 -0
  17. package/dist/physics2DAbiCommand.d.ts +17 -0
  18. package/dist/physics2DAbiCommand.d.ts.map +1 -0
  19. package/dist/physics2DAbiCommand.js +459 -0
  20. package/dist/physics2DAbiCommand.js.map +1 -0
  21. package/dist/physics2DAbiLayout.d.ts +220 -0
  22. package/dist/physics2DAbiLayout.d.ts.map +1 -0
  23. package/dist/physics2DAbiLayout.js +250 -0
  24. package/dist/physics2DAbiLayout.js.map +1 -0
  25. package/dist/physics2DAbiQuery.d.ts +7 -0
  26. package/dist/physics2DAbiQuery.d.ts.map +1 -0
  27. package/dist/physics2DAbiQuery.js +16 -0
  28. package/dist/physics2DAbiQuery.js.map +1 -0
  29. package/dist/referencePhysics2DAbi.d.ts +3 -0
  30. package/dist/referencePhysics2DAbi.d.ts.map +1 -0
  31. package/dist/referencePhysics2DAbi.js +1089 -0
  32. package/dist/referencePhysics2DAbi.js.map +1 -0
  33. package/package.json +50 -0
  34. package/src/physics2DAbi.test.ts +348 -0
  35. package/src/physics2DAbiBuffer.test.ts +121 -0
  36. package/src/physics2DAbiCommand.test.ts +337 -0
  37. package/src/physics2DAbiLayout.test.ts +293 -0
  38. package/src/physics2DAbiQuery.test.ts +145 -0
  39. package/src/referencePhysics2DAbi.test.ts +271 -0
@@ -0,0 +1,145 @@
1
+ import { createPhysics2DCollider, createRigidBody2D } from '@flighthq/physics2d/contract';
2
+ import type { Physics2DAbi } from '@flighthq/types/contract';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createPhysics2DAbi, createPhysics2DAbiWorld, executePhysics2DAbiCommands } from './physics2DAbi';
6
+ import {
7
+ createPhysics2DAbiCommandBuffer,
8
+ createPhysics2DAbiExecutionResult,
9
+ createPhysics2DAbiQueryBuffer,
10
+ } from './physics2DAbiBuffer';
11
+ import { writePhysics2DAbiSetBodyCommand, writePhysics2DAbiSetColliderCommand } from './physics2DAbiCommand';
12
+ import { Physics2DAbiQueryValue } from './physics2DAbiLayout';
13
+ import {
14
+ queryPhysics2DAbiPoint,
15
+ queryPhysics2DAbiRay,
16
+ queryPhysics2DAbiRayClosest,
17
+ queryPhysics2DAbiRegion,
18
+ queryPhysics2DAbiShapeCast,
19
+ } from './physics2DAbiQuery';
20
+
21
+ const MATERIAL = { density: 1, friction: 0.3, restitution: 0 };
22
+
23
+ // Two unit boxes on the x axis at 0 and 10, so a ray along +x meets a known order and a point query
24
+ // has an unambiguous answer.
25
+ function world(): { abi: Physics2DAbi; handle: number } {
26
+ const abi = createPhysics2DAbi();
27
+ const handle = createPhysics2DAbiWorld(abi);
28
+ const commands = createPhysics2DAbiCommandBuffer(4096);
29
+ for (const [id, x] of [
30
+ [1, 0],
31
+ [2, 10],
32
+ ] as const) {
33
+ const body = createRigidBody2D('static', x, 0);
34
+ body.colliders.push(
35
+ createPhysics2DCollider({ kind: 'aabb', minX: -0.5, minY: -0.5, maxX: 0.5, maxY: 0.5 }, MATERIAL),
36
+ );
37
+ writePhysics2DAbiSetBodyCommand(commands, id, body);
38
+ writePhysics2DAbiSetColliderCommand(commands, id, id, body.colliders[0]);
39
+ }
40
+ executePhysics2DAbiCommands(abi, handle, commands, createPhysics2DAbiExecutionResult());
41
+ return { abi, handle };
42
+ }
43
+
44
+ describe('queryPhysics2DAbiPoint', () => {
45
+ it('names the body containing the point and zeroes the geometric row', () => {
46
+ const { abi, handle } = world();
47
+ const out = createPhysics2DAbiQueryBuffer(4);
48
+ expect(queryPhysics2DAbiPoint(abi, handle, 0, 0, out)).toBe(true);
49
+ expect(out.count).toBe(1);
50
+ expect(out.bodyIds[0]).toBe(1);
51
+ expect(out.colliderIds[0]).toBe(1);
52
+ // A point hit has no fraction or normal, and reporting a stale one would be worse than none.
53
+ expect([...out.values.slice(0, 5)]).toEqual([0, 0, 0, 0, 0]);
54
+ });
55
+
56
+ it('returns an empty answer for empty space', () => {
57
+ const { abi, handle } = world();
58
+ const out = createPhysics2DAbiQueryBuffer(4);
59
+ expect(queryPhysics2DAbiPoint(abi, handle, 5, 5, out)).toBe(true);
60
+ expect(out.count).toBe(0);
61
+ expect(out.requiredCount).toBe(0);
62
+ });
63
+
64
+ it('does not republish a previous hit when a reused query result misses', () => {
65
+ const { abi, handle } = world();
66
+ const out = createPhysics2DAbiQueryBuffer(4);
67
+ expect(queryPhysics2DAbiPoint(abi, handle, 0, 0, out)).toBe(true);
68
+ expect(out.count).toBe(1);
69
+
70
+ expect(queryPhysics2DAbiPoint(abi, handle, 5, 5, out)).toBe(true);
71
+ expect(out.count).toBe(0);
72
+ expect(out.requiredCount).toBe(0);
73
+ });
74
+ });
75
+
76
+ describe('queryPhysics2DAbiRay', () => {
77
+ it('reports every crossing with its fraction, point, and normal', () => {
78
+ const { abi, handle } = world();
79
+ const out = createPhysics2DAbiQueryBuffer(8);
80
+ expect(queryPhysics2DAbiRay(abi, handle, -5, 0, 1, 0, out, 100)).toBe(true);
81
+ expect(out.count).toBe(2);
82
+ for (let i = 0; i < out.count; i += 1) {
83
+ const base = i * 5;
84
+ expect(out.values[base + Physics2DAbiQueryValue.Fraction]).toBeGreaterThan(0);
85
+ expect(out.values[base + Physics2DAbiQueryValue.NormalX]).toBeCloseTo(-1, 9);
86
+ expect(out.values[base + Physics2DAbiQueryValue.Y]).toBeCloseTo(0, 9);
87
+ }
88
+ });
89
+ });
90
+
91
+ describe('queryPhysics2DAbiRayClosest', () => {
92
+ it('keeps only the nearest crossing', () => {
93
+ const { abi, handle } = world();
94
+ const out = createPhysics2DAbiQueryBuffer(8);
95
+ expect(queryPhysics2DAbiRayClosest(abi, handle, -5, 0, 1, 0, out, 100)).toBe(true);
96
+ expect(out.count).toBe(1);
97
+ expect(out.bodyIds[0]).toBe(1);
98
+ expect(out.values[Physics2DAbiQueryValue.X]).toBeCloseTo(-0.5, 9);
99
+ });
100
+
101
+ it('publishes only the current closest hit after an all-hits query reused the scratch rows', () => {
102
+ const { abi, handle } = world();
103
+ const out = createPhysics2DAbiQueryBuffer(8);
104
+ expect(queryPhysics2DAbiRay(abi, handle, -5, 0, 1, 0, out, 100)).toBe(true);
105
+ expect(out.count).toBe(2);
106
+
107
+ expect(queryPhysics2DAbiRayClosest(abi, handle, -5, 0, 1, 0, out, 100)).toBe(true);
108
+ expect(out.count).toBe(1);
109
+ expect(out.requiredCount).toBe(1);
110
+ expect(out.bodyIds[0]).toBe(1);
111
+ });
112
+ });
113
+
114
+ describe('queryPhysics2DAbiRegion', () => {
115
+ it('reports capacity exhaustion through requiredCount rather than by truncating silently', () => {
116
+ const { abi, handle } = world();
117
+ const out = createPhysics2DAbiQueryBuffer(1);
118
+ expect(queryPhysics2DAbiRegion(abi, handle, { minX: -20, minY: -20, maxX: 20, maxY: 20 }, out)).toBe(true);
119
+ expect(out.count).toBe(1);
120
+ expect(out.requiredCount).toBe(2);
121
+ });
122
+ });
123
+
124
+ describe('queryPhysics2DAbiShapeCast', () => {
125
+ it('stops at the first body along the sweep', () => {
126
+ const { abi, handle } = world();
127
+ const out = createPhysics2DAbiQueryBuffer(4);
128
+ const cast = queryPhysics2DAbiShapeCast(abi, handle, { kind: 'circle', x: -5, y: 0, radius: 0.25 }, 20, 0, out, 1);
129
+ expect(cast).toBe(true);
130
+ expect(out.count).toBe(1);
131
+ expect(out.bodyIds[0]).toBe(1);
132
+ expect(out.values[Physics2DAbiQueryValue.Fraction]).toBeGreaterThan(0);
133
+ expect(out.values[Physics2DAbiQueryValue.Fraction]).toBeLessThan(1);
134
+ });
135
+
136
+ it('reports a clean miss as an empty answer', () => {
137
+ const { abi, handle } = world();
138
+ const out = createPhysics2DAbiQueryBuffer(4);
139
+ expect(queryPhysics2DAbiShapeCast(abi, handle, { kind: 'circle', x: -5, y: 40, radius: 0.25 }, 20, 0, out, 1)).toBe(
140
+ true,
141
+ );
142
+ expect(out.count).toBe(0);
143
+ expect(out.requiredCount).toBe(0);
144
+ });
145
+ });
@@ -0,0 +1,271 @@
1
+ import {
2
+ addPhysics2DBody,
3
+ createPhysics2DCollider,
4
+ createPhysics2DRevoluteJoint,
5
+ createPhysics2DWorld,
6
+ createRigidBody2D,
7
+ registerBuiltInPhysics2DJointSolvers,
8
+ addPhysics2DJoint,
9
+ stepPhysics2D,
10
+ } from '@flighthq/physics2d/contract';
11
+ import type { CollisionBuiltInShape2D, Physics2DAbiCommandBuffer, RigidBody2D } from '@flighthq/types/contract';
12
+ import { describe, expect, it } from 'vitest';
13
+
14
+ import { createPhysics2DAbi, readPhysics2DAbiBodies, stepPhysics2DAbiWorld } from './physics2DAbi';
15
+ import {
16
+ createPhysics2DAbiBodyBuffer,
17
+ createPhysics2DAbiCommandBuffer,
18
+ createPhysics2DAbiExecutionResult,
19
+ createPhysics2DAbiJointBuffer,
20
+ } from './physics2DAbiBuffer';
21
+ import {
22
+ writePhysics2DAbiApplyLinearImpulseCommand,
23
+ writePhysics2DAbiSetBodyCommand,
24
+ writePhysics2DAbiSetColliderCommand,
25
+ writePhysics2DAbiSetGravityCommand,
26
+ writePhysics2DAbiSetJointCommand,
27
+ } from './physics2DAbiCommand';
28
+ import { Physics2DAbiBodyValue, Physics2DAbiJointFlag } from './physics2DAbiLayout';
29
+
30
+ const MATERIAL = { density: 1, friction: 0.3, restitution: 0 };
31
+
32
+ function box(halfW: number, halfH: number): CollisionBuiltInShape2D {
33
+ return { kind: 'aabb', minX: -halfW, minY: -halfH, maxX: halfW, maxY: halfH };
34
+ }
35
+
36
+ function publish(commands: Physics2DAbiCommandBuffer, bodies: readonly Readonly<RigidBody2D>[]): void {
37
+ let colliderId = 1;
38
+ for (let i = 0; i < bodies.length; i += 1) {
39
+ expect(writePhysics2DAbiSetBodyCommand(commands, i + 1, bodies[i])).toBe(true);
40
+ for (const collider of bodies[i].colliders) {
41
+ expect(writePhysics2DAbiSetColliderCommand(commands, colliderId++, i + 1, collider)).toBe(true);
42
+ }
43
+ }
44
+ }
45
+
46
+ describe('createReferencePhysics2DAbi', () => {
47
+ // The load-bearing test of the whole package. Every other test here checks that the ABI is
48
+ // self-consistent; this one checks it means what Physics2D means, by running the SAME scene through
49
+ // the standard object API and asserting the trajectories agree to the bit. A wire contract that
50
+ // round-trips perfectly and simulates something else is worthless.
51
+ it('reproduces the standard solver exactly, over a settling stack', () => {
52
+ const shapes = [box(6, 0.5), box(0.5, 0.5), box(0.5, 0.5), box(0.5, 0.5)];
53
+ const poses: [RigidBody2D['type'], number, number][] = [
54
+ ['static', 0, -0.5],
55
+ ['dynamic', 0, 0.5],
56
+ ['dynamic', 0.2, 1.6],
57
+ ['dynamic', -0.1, 2.7],
58
+ ];
59
+
60
+ const world = createPhysics2DWorld();
61
+ registerBuiltInPhysics2DJointSolvers(world);
62
+ const bodies = poses.map(([type, x, y], i) => {
63
+ const body = createRigidBody2D(type, x, y);
64
+ body.colliders.push(createPhysics2DCollider(shapes[i], MATERIAL));
65
+ addPhysics2DBody(world, body);
66
+ return body;
67
+ });
68
+
69
+ const abi = createPhysics2DAbi();
70
+ const handle = abi.createWorld();
71
+ const commands = createPhysics2DAbiCommandBuffer(8192);
72
+ publish(commands, bodies);
73
+ const result = createPhysics2DAbiExecutionResult();
74
+ expect(abi.execute(handle, commands, result)).toBe(true);
75
+
76
+ const out = createPhysics2DAbiBodyBuffer(8);
77
+ for (let step = 0; step < 120; step += 1) {
78
+ stepPhysics2D(world, 1 / 60);
79
+ expect(stepPhysics2DAbiWorld(abi, handle, 1 / 60)).toBe('Complete');
80
+ }
81
+ expect(readPhysics2DAbiBodies(abi, handle, null, out)).toBe(true);
82
+ expect(out.count).toBe(4);
83
+
84
+ for (let i = 0; i < bodies.length; i += 1) {
85
+ const base = i * 17;
86
+ expect(out.values[base + Physics2DAbiBodyValue.X], `body ${String(i)} x`).toBe(bodies[i].x);
87
+ expect(out.values[base + Physics2DAbiBodyValue.Y], `body ${String(i)} y`).toBe(bodies[i].y);
88
+ expect(out.values[base + Physics2DAbiBodyValue.Angle], `body ${String(i)} angle`).toBe(bodies[i].angle);
89
+ expect(out.values[base + Physics2DAbiBodyValue.VelocityX]).toBe(bodies[i].velocityX);
90
+ expect(out.values[base + Physics2DAbiBodyValue.VelocityY]).toBe(bodies[i].velocityY);
91
+ expect(out.values[base + Physics2DAbiBodyValue.AngularVelocity]).toBe(bodies[i].angularVelocity);
92
+ }
93
+ // The stack must actually have done something, or bit-equality is a statement about two worlds
94
+ // that both did nothing.
95
+ expect(bodies[3].y).toBeLessThan(2.7);
96
+ });
97
+
98
+ it('derives mass from colliders rather than from the wire', () => {
99
+ // Mass is the one body block the mutation path deliberately ignores, so a command carrying a
100
+ // fabricated mass must not produce a body that weighs it. Physics2D derives mass from collider
101
+ // geometry and density, and the ABI keeps that the single source.
102
+ const source = createRigidBody2D('dynamic', 0, 0);
103
+ source.colliders.push(createPhysics2DCollider(box(1, 1), MATERIAL));
104
+ const world = createPhysics2DWorld();
105
+ addPhysics2DBody(world, source);
106
+ const derivedMass = source.mass;
107
+ expect(derivedMass).toBeGreaterThan(0);
108
+
109
+ const abi = createPhysics2DAbi();
110
+ const handle = abi.createWorld();
111
+ const commands = createPhysics2DAbiCommandBuffer(4096);
112
+ source.mass = 12345;
113
+ expect(writePhysics2DAbiSetBodyCommand(commands, 1, source)).toBe(true);
114
+ expect(writePhysics2DAbiSetColliderCommand(commands, 1, 1, source.colliders[0])).toBe(true);
115
+ expect(abi.execute(handle, commands, createPhysics2DAbiExecutionResult())).toBe(true);
116
+
117
+ const out = createPhysics2DAbiBodyBuffer(1);
118
+ expect(readPhysics2DAbiBodies(abi, handle, null, out)).toBe(true);
119
+ expect(out.values[Physics2DAbiBodyValue.Mass]).toBe(derivedMass);
120
+ expect(out.values[Physics2DAbiBodyValue.Mass]).not.toBe(12345);
121
+ });
122
+
123
+ it('reports a joint that broke, which the world no longer holds', () => {
124
+ // A 2D joint does not carry a `broken` field: breaking REMOVES it. So the readback flag is the
125
+ // only channel by which a caller holding an id can learn its joint is gone, and it must arrive
126
+ // with the load that did it.
127
+ const anchor = createRigidBody2D('static', 0, 0);
128
+ const hanging = createRigidBody2D('dynamic', 0, -1);
129
+ hanging.colliders.push(createPhysics2DCollider(box(0.5, 0.5), MATERIAL));
130
+ const joint = createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1, breakForce: 0.001 });
131
+
132
+ const abi = createPhysics2DAbi();
133
+ const handle = abi.createWorld();
134
+ const commands = createPhysics2DAbiCommandBuffer(4096);
135
+ publish(commands, [anchor, hanging]);
136
+ expect(writePhysics2DAbiSetJointCommand(commands, 1, 1, 2, joint)).toBe(true);
137
+ expect(abi.execute(handle, commands, createPhysics2DAbiExecutionResult())).toBe(true);
138
+
139
+ expect(stepPhysics2DAbiWorld(abi, handle, 1 / 60)).toBe('Complete');
140
+ const joints = createPhysics2DAbiJointBuffer(4);
141
+ expect(abi.readJoints(handle, joints)).toBe(true);
142
+ expect(joints.count).toBe(1);
143
+ expect(joints.ids[0]).toBe(1);
144
+ expect(joints.flags[0] & Physics2DAbiJointFlag.Broken).toBe(Physics2DAbiJointFlag.Broken);
145
+ expect(Math.hypot(joints.values[0], joints.values[1])).toBeGreaterThan(0);
146
+
147
+ expect(abi.readJoints(handle, joints)).toBe(true);
148
+ expect(joints.count).toBe(0);
149
+ expect(joints.requiredCount).toBe(0);
150
+ });
151
+
152
+ it('retires an unread broken event when its id is reused by a live joint', () => {
153
+ const anchor = createRigidBody2D('static', 0, 0);
154
+ const hanging = createRigidBody2D('dynamic', 0, -1);
155
+ hanging.colliders.push(createPhysics2DCollider(box(0.5, 0.5), MATERIAL));
156
+
157
+ const abi = createPhysics2DAbi();
158
+ const handle = abi.createWorld();
159
+ const setup = createPhysics2DAbiCommandBuffer(4096);
160
+ publish(setup, [anchor, hanging]);
161
+ expect(
162
+ writePhysics2DAbiSetJointCommand(
163
+ setup,
164
+ 1,
165
+ 1,
166
+ 2,
167
+ createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1, breakForce: 0.001 }),
168
+ ),
169
+ ).toBe(true);
170
+ expect(abi.execute(handle, setup, createPhysics2DAbiExecutionResult())).toBe(true);
171
+ expect(stepPhysics2DAbiWorld(abi, handle, 1 / 60)).toBe('Complete');
172
+
173
+ const replacement = createPhysics2DAbiCommandBuffer(512);
174
+ expect(
175
+ writePhysics2DAbiSetJointCommand(replacement, 1, 1, 2, createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1 })),
176
+ ).toBe(true);
177
+ expect(abi.execute(handle, replacement, createPhysics2DAbiExecutionResult())).toBe(true);
178
+
179
+ const joints = createPhysics2DAbiJointBuffer(4);
180
+ expect(abi.readJoints(handle, joints)).toBe(true);
181
+ expect(joints.count).toBe(1);
182
+ expect(joints.requiredCount).toBe(1);
183
+ expect(joints.ids[0]).toBe(1);
184
+ expect(joints.flags[0] & Physics2DAbiJointFlag.Broken).toBe(0);
185
+ });
186
+
187
+ it('keeps a joint reported and unbroken while it holds', () => {
188
+ const anchor = createRigidBody2D('static', 0, 0);
189
+ const hanging = createRigidBody2D('dynamic', 0, -1);
190
+ hanging.colliders.push(createPhysics2DCollider(box(0.5, 0.5), MATERIAL));
191
+ const joint = createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1 });
192
+
193
+ const abi = createPhysics2DAbi();
194
+ const handle = abi.createWorld();
195
+ const commands = createPhysics2DAbiCommandBuffer(4096);
196
+ publish(commands, [anchor, hanging]);
197
+ expect(writePhysics2DAbiSetJointCommand(commands, 7, 1, 2, joint)).toBe(true);
198
+ expect(abi.execute(handle, commands, createPhysics2DAbiExecutionResult())).toBe(true);
199
+ expect(stepPhysics2DAbiWorld(abi, handle, 1 / 60)).toBe('Complete');
200
+
201
+ const joints = createPhysics2DAbiJointBuffer(4);
202
+ expect(abi.readJoints(handle, joints)).toBe(true);
203
+ expect(joints.count).toBe(1);
204
+ expect(joints.ids[0]).toBe(7);
205
+ expect(joints.flags[0] & Physics2DAbiJointFlag.Broken).toBe(0);
206
+ });
207
+
208
+ it('applies an impulse through the wire exactly as the standard helper does', () => {
209
+ const direct = createRigidBody2D('dynamic', 0, 0);
210
+ direct.colliders.push(createPhysics2DCollider(box(0.5, 0.5), MATERIAL));
211
+ const world = createPhysics2DWorld();
212
+ world.gravityY = 0;
213
+ addPhysics2DBody(world, direct);
214
+
215
+ const abi = createPhysics2DAbi();
216
+ const handle = abi.createWorld();
217
+ const commands = createPhysics2DAbiCommandBuffer(4096);
218
+ expect(writePhysics2DAbiSetGravityCommand(commands, 0, 0)).toBe(true);
219
+ publish(commands, [direct]);
220
+ expect(writePhysics2DAbiApplyLinearImpulseCommand(commands, 1, 3, -2)).toBe(true);
221
+ expect(abi.execute(handle, commands, createPhysics2DAbiExecutionResult())).toBe(true);
222
+
223
+ direct.velocityX += 3 / direct.mass;
224
+ direct.velocityY += -2 / direct.mass;
225
+
226
+ const out = createPhysics2DAbiBodyBuffer(1);
227
+ expect(readPhysics2DAbiBodies(abi, handle, null, out)).toBe(true);
228
+ expect(out.values[Physics2DAbiBodyValue.VelocityX]).toBeCloseTo(direct.velocityX, 12);
229
+ expect(out.values[Physics2DAbiBodyValue.VelocityY]).toBeCloseTo(direct.velocityY, 12);
230
+ });
231
+
232
+ it('declines a step the standard world would also decline', () => {
233
+ const abi = createPhysics2DAbi();
234
+ const handle = abi.createWorld();
235
+ expect(stepPhysics2DAbiWorld(abi, handle, Number.NaN)).toBe('Declined');
236
+ expect(stepPhysics2DAbiWorld(abi, handle, -1)).toBe('Declined');
237
+ });
238
+
239
+ it('reports a destroyed world as stale rather than failing silently', () => {
240
+ const abi = createPhysics2DAbi();
241
+ const handle = abi.createWorld();
242
+ expect(abi.destroyWorld(handle)).toBe(true);
243
+ expect(abi.getWorldStatus(handle)).toBe('Stale');
244
+ expect(stepPhysics2DAbiWorld(abi, handle, 1 / 60)).toBe('StaleWorld');
245
+ expect(readPhysics2DAbiBodies(abi, handle, null, createPhysics2DAbiBodyBuffer(1))).toBe(false);
246
+ });
247
+
248
+ it('adds a joint the standard world also accepts', () => {
249
+ const world = createPhysics2DWorld();
250
+ registerBuiltInPhysics2DJointSolvers(world);
251
+ const a = createRigidBody2D('static', 0, 0);
252
+ const b = createRigidBody2D('dynamic', 0, -1);
253
+ b.colliders.push(createPhysics2DCollider(box(0.5, 0.5), MATERIAL));
254
+ addPhysics2DBody(world, a);
255
+ addPhysics2DBody(world, b);
256
+ addPhysics2DJoint(world, createPhysics2DRevoluteJoint({ bodyA: a.index, bodyB: b.index }));
257
+ for (let step = 0; step < 60; step += 1) stepPhysics2D(world, 1 / 60);
258
+
259
+ const abi = createPhysics2DAbi();
260
+ const handle = abi.createWorld();
261
+ const commands = createPhysics2DAbiCommandBuffer(4096);
262
+ publish(commands, [a, b]);
263
+ expect(
264
+ writePhysics2DAbiSetJointCommand(commands, 1, 1, 2, createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1 })),
265
+ ).toBe(true);
266
+ expect(abi.execute(handle, commands, createPhysics2DAbiExecutionResult())).toBe(true);
267
+ const joints = createPhysics2DAbiJointBuffer(4);
268
+ expect(abi.readJoints(handle, joints)).toBe(true);
269
+ expect(joints.count).toBe(1);
270
+ });
271
+ });