@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.
- package/dist/contract.d.ts +6 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +6 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/physics2DAbi.d.ts +11 -0
- package/dist/physics2DAbi.d.ts.map +1 -0
- package/dist/physics2DAbi.js +32 -0
- package/dist/physics2DAbi.js.map +1 -0
- package/dist/physics2DAbiBuffer.d.ts +10 -0
- package/dist/physics2DAbiBuffer.d.ts.map +1 -0
- package/dist/physics2DAbiBuffer.js +81 -0
- package/dist/physics2DAbiBuffer.js.map +1 -0
- package/dist/physics2DAbiCommand.d.ts +17 -0
- package/dist/physics2DAbiCommand.d.ts.map +1 -0
- package/dist/physics2DAbiCommand.js +459 -0
- package/dist/physics2DAbiCommand.js.map +1 -0
- package/dist/physics2DAbiLayout.d.ts +220 -0
- package/dist/physics2DAbiLayout.d.ts.map +1 -0
- package/dist/physics2DAbiLayout.js +250 -0
- package/dist/physics2DAbiLayout.js.map +1 -0
- package/dist/physics2DAbiQuery.d.ts +7 -0
- package/dist/physics2DAbiQuery.d.ts.map +1 -0
- package/dist/physics2DAbiQuery.js +16 -0
- package/dist/physics2DAbiQuery.js.map +1 -0
- package/dist/referencePhysics2DAbi.d.ts +3 -0
- package/dist/referencePhysics2DAbi.d.ts.map +1 -0
- package/dist/referencePhysics2DAbi.js +1089 -0
- package/dist/referencePhysics2DAbi.js.map +1 -0
- package/package.json +50 -0
- package/src/physics2DAbi.test.ts +348 -0
- package/src/physics2DAbiBuffer.test.ts +121 -0
- package/src/physics2DAbiCommand.test.ts +337 -0
- package/src/physics2DAbiLayout.test.ts +293 -0
- package/src/physics2DAbiQuery.test.ts +145 -0
- package/src/referencePhysics2DAbi.test.ts +271 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPhysics2DCollider,
|
|
3
|
+
createPhysics2DDistanceJoint,
|
|
4
|
+
createPhysics2DGearJoint,
|
|
5
|
+
createPhysics2DMouseJoint,
|
|
6
|
+
createPhysics2DPrismaticJoint,
|
|
7
|
+
createPhysics2DPulleyJoint,
|
|
8
|
+
createPhysics2DRevoluteJoint,
|
|
9
|
+
createPhysics2DRopeJoint,
|
|
10
|
+
createPhysics2DSolverConfig,
|
|
11
|
+
createPhysics2DWeldJoint,
|
|
12
|
+
createPhysics2DWheelJoint,
|
|
13
|
+
createRigidBody2D,
|
|
14
|
+
} from '@flighthq/physics2d/contract';
|
|
15
|
+
import type { CollisionBuiltInShape2D, Physics2DJoint } from '@flighthq/types/contract';
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import { createPhysics2DAbiCommandBuffer } from './physics2DAbiBuffer';
|
|
19
|
+
import {
|
|
20
|
+
getPhysics2DAbiSetColliderCommandByteLength,
|
|
21
|
+
writePhysics2DAbiApplyForceAtPointCommand,
|
|
22
|
+
writePhysics2DAbiApplyForceCommand,
|
|
23
|
+
writePhysics2DAbiApplyLinearImpulseAtPointCommand,
|
|
24
|
+
writePhysics2DAbiApplyLinearImpulseCommand,
|
|
25
|
+
writePhysics2DAbiApplyTorqueCommand,
|
|
26
|
+
writePhysics2DAbiDestroyBodyCommand,
|
|
27
|
+
writePhysics2DAbiDestroyColliderCommand,
|
|
28
|
+
writePhysics2DAbiDestroyJointCommand,
|
|
29
|
+
writePhysics2DAbiSetBodyCommand,
|
|
30
|
+
writePhysics2DAbiSetColliderCommand,
|
|
31
|
+
writePhysics2DAbiSetGravityCommand,
|
|
32
|
+
writePhysics2DAbiSetJointCommand,
|
|
33
|
+
writePhysics2DAbiSetSolverConfigCommand,
|
|
34
|
+
writePhysics2DAbiWakeBodyCommand,
|
|
35
|
+
} from './physics2DAbiCommand';
|
|
36
|
+
import {
|
|
37
|
+
Physics2DAbiCommandByteLength,
|
|
38
|
+
Physics2DAbiCommandHeaderByteLength,
|
|
39
|
+
Physics2DAbiCommandKind,
|
|
40
|
+
Physics2DAbiCommandRecordHeaderByteLength,
|
|
41
|
+
Physics2DAbiJointFlag,
|
|
42
|
+
Physics2DAbiJointKind,
|
|
43
|
+
Physics2DAbiShapeHeaderByteLength,
|
|
44
|
+
Physics2DAbiShapeKind,
|
|
45
|
+
} from './physics2DAbiLayout';
|
|
46
|
+
|
|
47
|
+
const MATERIAL = { density: 1, friction: 0.3, restitution: 0.1 };
|
|
48
|
+
|
|
49
|
+
function record(buffer: ReturnType<typeof createPhysics2DAbiCommandBuffer>, at = Physics2DAbiCommandHeaderByteLength) {
|
|
50
|
+
const view = new DataView(buffer.data.buffer);
|
|
51
|
+
return {
|
|
52
|
+
view,
|
|
53
|
+
kind: view.getUint32(at, true),
|
|
54
|
+
byteLength: view.getUint32(at + 4, true),
|
|
55
|
+
objectId: view.getUint32(at + 8, true),
|
|
56
|
+
relatedId: view.getUint32(at + 12, true),
|
|
57
|
+
payload: at + Physics2DAbiCommandRecordHeaderByteLength,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function body() {
|
|
62
|
+
const value = createRigidBody2D('dynamic', 3, -4, 0.5);
|
|
63
|
+
value.velocityX = 1;
|
|
64
|
+
value.velocityY = 2;
|
|
65
|
+
value.angularVelocity = 3;
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('getPhysics2DAbiSetColliderCommandByteLength', () => {
|
|
70
|
+
it('reports the exact length the writer will consume, per shape', () => {
|
|
71
|
+
const shapes: [CollisionBuiltInShape2D, number][] = [
|
|
72
|
+
[{ kind: 'circle', x: 0, y: 0, radius: 1 }, 3],
|
|
73
|
+
[{ kind: 'aabb', minX: -1, minY: -1, maxX: 1, maxY: 1 }, 4],
|
|
74
|
+
[{ kind: 'obb', x: 0, y: 0, halfW: 1, halfH: 2, rotation: 0.5 }, 5],
|
|
75
|
+
[{ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, 5],
|
|
76
|
+
[{ kind: 'polygon', points: [0, 0, 1, 0, 0, 1] }, 6],
|
|
77
|
+
[{ kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 1 }, 4],
|
|
78
|
+
[{ kind: 'point', x: 2, y: 3 }, 2],
|
|
79
|
+
];
|
|
80
|
+
for (const [shape, scalarCount] of shapes) {
|
|
81
|
+
const collider = createPhysics2DCollider(shape, MATERIAL);
|
|
82
|
+
const expected = Physics2DAbiCommandByteLength.SetColliderMinimum + scalarCount * 8;
|
|
83
|
+
expect(getPhysics2DAbiSetColliderCommandByteLength(collider), shape.kind).toBe(expected);
|
|
84
|
+
|
|
85
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
86
|
+
expect(writePhysics2DAbiSetColliderCommand(buffer, 1, 1, collider), shape.kind).toBe(true);
|
|
87
|
+
expect(buffer.byteLength - Physics2DAbiCommandHeaderByteLength, shape.kind).toBe(expected);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('writePhysics2DAbiApplyForceAtPointCommand', () => {
|
|
93
|
+
it('carries the world point in the second pair', () => {
|
|
94
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
95
|
+
expect(writePhysics2DAbiApplyForceAtPointCommand(buffer, 2, 5, 6, 7, 8)).toBe(true);
|
|
96
|
+
const written = record(buffer);
|
|
97
|
+
expect(written.view.getFloat64(written.payload + 16, true)).toBe(7);
|
|
98
|
+
expect(written.view.getFloat64(written.payload + 24, true)).toBe(8);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('writePhysics2DAbiApplyForceCommand', () => {
|
|
103
|
+
it('zeroes the point slots a pointless action does not use', () => {
|
|
104
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
105
|
+
expect(writePhysics2DAbiApplyForceCommand(buffer, 2, 5, 6)).toBe(true);
|
|
106
|
+
const written = record(buffer);
|
|
107
|
+
expect(written.kind).toBe(Physics2DAbiCommandKind.ApplyForce);
|
|
108
|
+
expect(written.view.getFloat64(written.payload, true)).toBe(5);
|
|
109
|
+
expect(written.view.getFloat64(written.payload + 16, true)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('writePhysics2DAbiApplyLinearImpulseAtPointCommand', () => {
|
|
114
|
+
it('uses its own discriminant rather than the pointless one', () => {
|
|
115
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
116
|
+
expect(writePhysics2DAbiApplyLinearImpulseAtPointCommand(buffer, 1, 1, 1, 2, 2)).toBe(true);
|
|
117
|
+
expect(record(buffer).kind).toBe(Physics2DAbiCommandKind.ApplyLinearImpulseAtPoint);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('writePhysics2DAbiApplyLinearImpulseCommand', () => {
|
|
122
|
+
it('uses its own discriminant rather than the force one', () => {
|
|
123
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
124
|
+
expect(writePhysics2DAbiApplyLinearImpulseCommand(buffer, 1, 1, 1)).toBe(true);
|
|
125
|
+
expect(record(buffer).kind).toBe(Physics2DAbiCommandKind.ApplyLinearImpulse);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('writePhysics2DAbiApplyTorqueCommand', () => {
|
|
130
|
+
it('leaves the three unused body-action slots zero', () => {
|
|
131
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
132
|
+
expect(writePhysics2DAbiApplyTorqueCommand(buffer, 1, 12)).toBe(true);
|
|
133
|
+
const written = record(buffer);
|
|
134
|
+
expect(written.view.getFloat64(written.payload, true)).toBe(12);
|
|
135
|
+
expect(written.view.getFloat64(written.payload + 8, true)).toBe(0);
|
|
136
|
+
expect(written.view.getFloat64(written.payload + 16, true)).toBe(0);
|
|
137
|
+
expect(written.view.getFloat64(written.payload + 24, true)).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('writePhysics2DAbiDestroyBodyCommand', () => {
|
|
142
|
+
it('writes a header-only record', () => {
|
|
143
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
144
|
+
expect(writePhysics2DAbiDestroyBodyCommand(buffer, 3)).toBe(true);
|
|
145
|
+
expect(record(buffer).byteLength).toBe(Physics2DAbiCommandRecordHeaderByteLength);
|
|
146
|
+
expect(record(buffer).objectId).toBe(3);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('writePhysics2DAbiDestroyColliderCommand', () => {
|
|
151
|
+
it('writes a header-only record', () => {
|
|
152
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
153
|
+
expect(writePhysics2DAbiDestroyColliderCommand(buffer, 4)).toBe(true);
|
|
154
|
+
expect(record(buffer).kind).toBe(Physics2DAbiCommandKind.DestroyCollider);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('writePhysics2DAbiDestroyJointCommand', () => {
|
|
159
|
+
it('writes a header-only record', () => {
|
|
160
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
161
|
+
expect(writePhysics2DAbiDestroyJointCommand(buffer, 5)).toBe(true);
|
|
162
|
+
expect(record(buffer).kind).toBe(Physics2DAbiCommandKind.DestroyJoint);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('writePhysics2DAbiSetBodyCommand', () => {
|
|
167
|
+
it('writes a fixed-length record carrying the seventeen body scalars', () => {
|
|
168
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
169
|
+
expect(writePhysics2DAbiSetBodyCommand(buffer, 9, body())).toBe(true);
|
|
170
|
+
const written = record(buffer);
|
|
171
|
+
expect(written.kind).toBe(Physics2DAbiCommandKind.SetBody);
|
|
172
|
+
expect(written.byteLength).toBe(Physics2DAbiCommandByteLength.SetBody);
|
|
173
|
+
expect(written.objectId).toBe(9);
|
|
174
|
+
expect(written.relatedId).toBe(0);
|
|
175
|
+
expect(written.view.getFloat64(written.payload + 8, true)).toBe(3);
|
|
176
|
+
expect(written.view.getFloat64(written.payload + 16, true)).toBe(-4);
|
|
177
|
+
expect(written.view.getFloat64(written.payload + 24, true)).toBe(0.5);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('refuses an id outside the non-zero uint32 range', () => {
|
|
181
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
182
|
+
expect(writePhysics2DAbiSetBodyCommand(buffer, 0, body())).toBe(false);
|
|
183
|
+
expect(writePhysics2DAbiSetBodyCommand(buffer, 1.5, body())).toBe(false);
|
|
184
|
+
expect(buffer.commandCount).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('leaves the stream untouched when the record does not fit', () => {
|
|
188
|
+
const buffer = createPhysics2DAbiCommandBuffer(Physics2DAbiCommandHeaderByteLength + 8);
|
|
189
|
+
expect(writePhysics2DAbiSetBodyCommand(buffer, 1, body())).toBe(false);
|
|
190
|
+
expect(buffer.byteLength).toBe(Physics2DAbiCommandHeaderByteLength);
|
|
191
|
+
expect(buffer.commandCount).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('writePhysics2DAbiSetColliderCommand', () => {
|
|
196
|
+
it('names the body in the related id and encodes the shape inline', () => {
|
|
197
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
198
|
+
const collider = createPhysics2DCollider({ kind: 'circle', x: 1, y: 2, radius: 3 }, MATERIAL, true, {
|
|
199
|
+
categoryBits: 5,
|
|
200
|
+
maskBits: 6,
|
|
201
|
+
groupIndex: -7,
|
|
202
|
+
});
|
|
203
|
+
expect(writePhysics2DAbiSetColliderCommand(buffer, 4, 8, collider)).toBe(true);
|
|
204
|
+
const written = record(buffer);
|
|
205
|
+
expect(written.objectId).toBe(4);
|
|
206
|
+
expect(written.relatedId).toBe(8);
|
|
207
|
+
expect(written.view.getUint32(written.payload, true)).toBe(1);
|
|
208
|
+
expect(written.view.getUint32(written.payload + 4, true)).toBe(5);
|
|
209
|
+
expect(written.view.getUint32(written.payload + 8, true)).toBe(6);
|
|
210
|
+
expect(written.view.getInt32(written.payload + 12, true)).toBe(-7);
|
|
211
|
+
expect(written.view.getFloat64(written.payload + 16, true)).toBe(1);
|
|
212
|
+
const shapeAt = written.payload + 40;
|
|
213
|
+
expect(written.view.getUint32(shapeAt, true)).toBe(Physics2DAbiShapeKind.Circle);
|
|
214
|
+
expect(written.view.getUint32(shapeAt + 4, true)).toBe(3);
|
|
215
|
+
expect(written.view.getUint32(shapeAt + 8, true)).toBe(0);
|
|
216
|
+
expect(written.view.getFloat64(shapeAt + Physics2DAbiShapeHeaderByteLength, true)).toBe(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('refuses a filter that will not fit its integer field', () => {
|
|
220
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
221
|
+
const collider = createPhysics2DCollider({ kind: 'circle', x: 0, y: 0, radius: 1 }, MATERIAL);
|
|
222
|
+
collider.filter.categoryBits = -1;
|
|
223
|
+
expect(writePhysics2DAbiSetColliderCommand(buffer, 1, 1, collider)).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('writePhysics2DAbiSetGravityCommand', () => {
|
|
228
|
+
it('carries two components and names no object', () => {
|
|
229
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
230
|
+
expect(writePhysics2DAbiSetGravityCommand(buffer, 1, -9.81)).toBe(true);
|
|
231
|
+
const written = record(buffer);
|
|
232
|
+
expect(written.kind).toBe(Physics2DAbiCommandKind.SetGravity);
|
|
233
|
+
expect(written.objectId).toBe(0);
|
|
234
|
+
expect(written.view.getFloat64(written.payload, true)).toBe(1);
|
|
235
|
+
expect(written.view.getFloat64(written.payload + 8, true)).toBe(-9.81);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('writePhysics2DAbiSetJointCommand', () => {
|
|
240
|
+
it('encodes every built-in kind with its own discriminant', () => {
|
|
241
|
+
const cases: [Physics2DJoint, number][] = [
|
|
242
|
+
[createPhysics2DDistanceJoint({ bodyA: 0, bodyB: 1, length: 2 }), Physics2DAbiJointKind.Distance],
|
|
243
|
+
[createPhysics2DRevoluteJoint({ bodyA: 0, bodyB: 1 }), Physics2DAbiJointKind.Revolute],
|
|
244
|
+
[createPhysics2DPrismaticJoint({ bodyA: 0, bodyB: 1 }), Physics2DAbiJointKind.Prismatic],
|
|
245
|
+
[createPhysics2DWeldJoint({ bodyA: 0, bodyB: 1 }), Physics2DAbiJointKind.Weld],
|
|
246
|
+
[createPhysics2DWheelJoint({ bodyA: 0, bodyB: 1 }), Physics2DAbiJointKind.Wheel],
|
|
247
|
+
[createPhysics2DRopeJoint({ bodyA: 0, bodyB: 1, maxLength: 3 }), Physics2DAbiJointKind.Rope],
|
|
248
|
+
[createPhysics2DMouseJoint({ body: 0, targetX: 1, targetY: 2, maxForce: 10 }), Physics2DAbiJointKind.Mouse],
|
|
249
|
+
[
|
|
250
|
+
createPhysics2DPulleyJoint({
|
|
251
|
+
bodyA: 0,
|
|
252
|
+
bodyB: 1,
|
|
253
|
+
groundAnchorAX: 0,
|
|
254
|
+
groundAnchorAY: 4,
|
|
255
|
+
groundAnchorBX: 2,
|
|
256
|
+
groundAnchorBY: 4,
|
|
257
|
+
ratio: 1,
|
|
258
|
+
constant: 6,
|
|
259
|
+
}),
|
|
260
|
+
Physics2DAbiJointKind.Pulley,
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
createPhysics2DGearJoint({
|
|
264
|
+
bodyA: 0,
|
|
265
|
+
bodyB: 1,
|
|
266
|
+
ratio: 2,
|
|
267
|
+
constant: 0,
|
|
268
|
+
coordinateA: 'angular',
|
|
269
|
+
coordinateB: 'angular',
|
|
270
|
+
}),
|
|
271
|
+
Physics2DAbiJointKind.Gear,
|
|
272
|
+
],
|
|
273
|
+
];
|
|
274
|
+
for (const [joint, kind] of cases) {
|
|
275
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
276
|
+
expect(writePhysics2DAbiSetJointCommand(buffer, 1, 1, 2, joint), joint.kind).toBe(true);
|
|
277
|
+
const written = record(buffer);
|
|
278
|
+
expect(written.byteLength, joint.kind).toBe(Physics2DAbiCommandByteLength.SetJoint);
|
|
279
|
+
expect(written.view.getUint32(written.payload, true), joint.kind).toBe(kind);
|
|
280
|
+
expect(written.view.getUint32(written.payload + 4, true), joint.kind).toBe(1);
|
|
281
|
+
expect(written.view.getUint32(written.payload + 8, true), joint.kind).toBe(2);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('refuses a kind with no wire discriminant rather than writing a zero one', () => {
|
|
286
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
287
|
+
const joint = createPhysics2DDistanceJoint({ bodyA: 0, bodyB: 1, length: 1 });
|
|
288
|
+
expect(writePhysics2DAbiSetJointCommand(buffer, 1, 1, 2, { ...joint, kind: 'acme.Conveyor' })).toBe(false);
|
|
289
|
+
expect(buffer.commandCount).toBe(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('reads a gear coordinate pair out of the flags it shares with the enable bits', () => {
|
|
293
|
+
const buffer = createPhysics2DAbiCommandBuffer(1024);
|
|
294
|
+
const joint = createPhysics2DGearJoint({
|
|
295
|
+
bodyA: 0,
|
|
296
|
+
bodyB: 1,
|
|
297
|
+
ratio: 1,
|
|
298
|
+
constant: 0,
|
|
299
|
+
coordinateA: 'linear',
|
|
300
|
+
coordinateB: 'angular',
|
|
301
|
+
});
|
|
302
|
+
expect(writePhysics2DAbiSetJointCommand(buffer, 1, 1, 2, joint)).toBe(true);
|
|
303
|
+
const flags = record(buffer).view.getUint32(record(buffer).payload + 12, true);
|
|
304
|
+
expect(flags & Physics2DAbiJointFlag.LinearCoordinateA).toBe(Physics2DAbiJointFlag.LinearCoordinateA);
|
|
305
|
+
expect(flags & Physics2DAbiJointFlag.LinearCoordinateB).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('writePhysics2DAbiSetSolverConfigCommand', () => {
|
|
310
|
+
it('packs the three booleans into flags and holds the substep word zero', () => {
|
|
311
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
312
|
+
const config = createPhysics2DSolverConfig();
|
|
313
|
+
expect(writePhysics2DAbiSetSolverConfigCommand(buffer, config)).toBe(true);
|
|
314
|
+
const written = record(buffer);
|
|
315
|
+
expect(written.byteLength).toBe(Physics2DAbiCommandByteLength.SetSolverConfig);
|
|
316
|
+
expect(written.view.getUint32(written.payload + 4, true)).toBe(0);
|
|
317
|
+
expect(written.view.getUint32(written.payload + 16, true)).toBe(config.velocityIterations);
|
|
318
|
+
expect(written.view.getUint32(written.payload + 20, true)).toBe(config.positionIterations);
|
|
319
|
+
expect(written.view.getFloat64(written.payload + 32, true)).toBe(config.sleepLinearThreshold);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('refuses an iteration count that is not a uint32', () => {
|
|
323
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
324
|
+
expect(
|
|
325
|
+
writePhysics2DAbiSetSolverConfigCommand(buffer, { ...createPhysics2DSolverConfig(), velocityIterations: -1 }),
|
|
326
|
+
).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('writePhysics2DAbiWakeBodyCommand', () => {
|
|
331
|
+
it('writes a header-only record and rejects a zero id', () => {
|
|
332
|
+
const buffer = createPhysics2DAbiCommandBuffer(256);
|
|
333
|
+
expect(writePhysics2DAbiWakeBodyCommand(buffer, 0)).toBe(false);
|
|
334
|
+
expect(writePhysics2DAbiWakeBodyCommand(buffer, 6)).toBe(true);
|
|
335
|
+
expect(record(buffer).kind).toBe(Physics2DAbiCommandKind.WakeBody);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { collideContactManifold2D, createCollisionContactManifold2D } from '@flighthq/collision/contract';
|
|
2
|
+
import type { CollisionBuiltInShape2D } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Physics2DAbiBodyFlag,
|
|
7
|
+
Physics2DAbiBodyType,
|
|
8
|
+
Physics2DAbiBodyValue,
|
|
9
|
+
Physics2DAbiBodyValueStride,
|
|
10
|
+
Physics2DAbiCapability,
|
|
11
|
+
Physics2DAbiCommandByteLength,
|
|
12
|
+
Physics2DAbiCommandHeaderByteLength,
|
|
13
|
+
Physics2DAbiCommandHeaderOffset,
|
|
14
|
+
Physics2DAbiCommandKind,
|
|
15
|
+
Physics2DAbiCommandMagic,
|
|
16
|
+
Physics2DAbiCommandRecordHeaderByteLength,
|
|
17
|
+
Physics2DAbiCommandRecordOffset,
|
|
18
|
+
Physics2DAbiContactFlag,
|
|
19
|
+
Physics2DAbiContactId,
|
|
20
|
+
Physics2DAbiContactIdStride,
|
|
21
|
+
Physics2DAbiContactPointValue,
|
|
22
|
+
Physics2DAbiContactPointValueStride,
|
|
23
|
+
Physics2DAbiContactValue,
|
|
24
|
+
Physics2DAbiContactValueStride,
|
|
25
|
+
Physics2DAbiJointCommonValue,
|
|
26
|
+
Physics2DAbiJointFlag,
|
|
27
|
+
Physics2DAbiJointKind,
|
|
28
|
+
Physics2DAbiJointKindValueCount,
|
|
29
|
+
Physics2DAbiJointValue,
|
|
30
|
+
Physics2DAbiJointValueStride,
|
|
31
|
+
Physics2DAbiMaxContactPoints,
|
|
32
|
+
Physics2DAbiQueryValue,
|
|
33
|
+
Physics2DAbiQueryValueStride,
|
|
34
|
+
Physics2DAbiSetBodyPayloadOffset,
|
|
35
|
+
Physics2DAbiSetColliderPayloadOffset,
|
|
36
|
+
Physics2DAbiSetJointPayloadOffset,
|
|
37
|
+
Physics2DAbiSetSolverConfigPayloadOffset,
|
|
38
|
+
Physics2DAbiShapeHeaderByteLength,
|
|
39
|
+
Physics2DAbiShapeHeaderOffset,
|
|
40
|
+
Physics2DAbiShapeKind,
|
|
41
|
+
Physics2DAbiSolverConfigFlag,
|
|
42
|
+
Physics2DAbiSolverConfigValue,
|
|
43
|
+
Physics2DAbiVersion,
|
|
44
|
+
} from './physics2DAbiLayout';
|
|
45
|
+
|
|
46
|
+
describe('Physics2DAbiBodyFlag', () => {
|
|
47
|
+
it('reserves the low two bits for the type and one bit each above it', () => {
|
|
48
|
+
expect(Physics2DAbiBodyFlag.TypeMask).toBe(0b11);
|
|
49
|
+
expect(Object.values(Physics2DAbiBodyType)).toEqual([0, 1, 2]);
|
|
50
|
+
for (const type of Object.values(Physics2DAbiBodyType)) expect(type & Physics2DAbiBodyFlag.TypeMask).toBe(type);
|
|
51
|
+
expect(Physics2DAbiBodyFlag.FixedRotation).toBe(4);
|
|
52
|
+
expect(Physics2DAbiBodyFlag.SleepEnabled).toBe(32);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// These constants ARE the wire contract. A test that recomputed them from the source would agree with
|
|
57
|
+
// any change, so every number here is written out literally: changing one has to be a deliberate edit
|
|
58
|
+
// in two places, which is the only thing that makes a version bump a decision rather than an accident.
|
|
59
|
+
describe('Physics2DAbiBodyValue', () => {
|
|
60
|
+
it('indexes seventeen authored body scalars in wire order', () => {
|
|
61
|
+
expect(Physics2DAbiBodyValueStride).toBe(17);
|
|
62
|
+
expect(Object.values(Physics2DAbiBodyValue)).toEqual([...Array(17).keys()]);
|
|
63
|
+
expect(Physics2DAbiBodyValue.X).toBe(0);
|
|
64
|
+
expect(Physics2DAbiBodyValue.Angle).toBe(2);
|
|
65
|
+
expect(Physics2DAbiBodyValue.Mass).toBe(9);
|
|
66
|
+
expect(Physics2DAbiBodyValue.SleepTimer).toBe(16);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Physics2DAbiCapability', () => {
|
|
71
|
+
it('declares one bit per optional backend feature', () => {
|
|
72
|
+
expect(Physics2DAbiCapability).toEqual({
|
|
73
|
+
ContactHooks: 1,
|
|
74
|
+
PersistentWorlds: 2,
|
|
75
|
+
Queries: 4,
|
|
76
|
+
SelectiveReadback: 8,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Physics2DAbiCommandByteLength', () => {
|
|
82
|
+
it('keeps every record eight-aligned and consistent with its payload', () => {
|
|
83
|
+
for (const [name, byteLength] of Object.entries(Physics2DAbiCommandByteLength)) {
|
|
84
|
+
expect(byteLength % 8, name).toBe(0);
|
|
85
|
+
expect(byteLength, name).toBeGreaterThanOrEqual(Physics2DAbiCommandRecordHeaderByteLength);
|
|
86
|
+
}
|
|
87
|
+
expect(Physics2DAbiCommandByteLength.SetGravity).toBe(Physics2DAbiCommandRecordHeaderByteLength + 2 * 8);
|
|
88
|
+
expect(Physics2DAbiCommandByteLength.SetBody).toBe(
|
|
89
|
+
Physics2DAbiCommandRecordHeaderByteLength + Physics2DAbiSetBodyPayloadOffset.Values + 17 * 8,
|
|
90
|
+
);
|
|
91
|
+
expect(Physics2DAbiCommandByteLength.SetJoint).toBe(
|
|
92
|
+
Physics2DAbiCommandRecordHeaderByteLength + Physics2DAbiSetJointPayloadOffset.KindValues + 9 * 8,
|
|
93
|
+
);
|
|
94
|
+
expect(Physics2DAbiCommandByteLength.SetSolverConfig).toBe(
|
|
95
|
+
Physics2DAbiCommandRecordHeaderByteLength + Physics2DAbiSetSolverConfigPayloadOffset.Values + 6 * 8,
|
|
96
|
+
);
|
|
97
|
+
expect(Physics2DAbiCommandByteLength.BodyAction).toBe(Physics2DAbiCommandRecordHeaderByteLength + 4 * 8);
|
|
98
|
+
expect(Physics2DAbiCommandByteLength.SetColliderMinimum).toBe(
|
|
99
|
+
Physics2DAbiCommandRecordHeaderByteLength +
|
|
100
|
+
Physics2DAbiSetColliderPayloadOffset.Shape +
|
|
101
|
+
Physics2DAbiShapeHeaderByteLength,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Physics2DAbiCommandKind', () => {
|
|
107
|
+
it('assigns each command a stable discriminant', () => {
|
|
108
|
+
expect(Physics2DAbiCommandKind).toEqual({
|
|
109
|
+
SetGravity: 1,
|
|
110
|
+
SetSolverConfig: 2,
|
|
111
|
+
SetBody: 3,
|
|
112
|
+
DestroyBody: 4,
|
|
113
|
+
SetCollider: 5,
|
|
114
|
+
DestroyCollider: 6,
|
|
115
|
+
SetJoint: 7,
|
|
116
|
+
DestroyJoint: 8,
|
|
117
|
+
ApplyForce: 9,
|
|
118
|
+
ApplyForceAtPoint: 10,
|
|
119
|
+
ApplyLinearImpulse: 11,
|
|
120
|
+
ApplyLinearImpulseAtPoint: 12,
|
|
121
|
+
ApplyTorque: 13,
|
|
122
|
+
WakeBody: 14,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Physics2DAbiCommandMagic', () => {
|
|
128
|
+
it('spells P2DA, distinguishing the stream from the 3D one at the header', () => {
|
|
129
|
+
expect(Physics2DAbiCommandMagic).toBe(0x41443250);
|
|
130
|
+
const bytes = new Uint8Array(4);
|
|
131
|
+
new DataView(bytes.buffer).setUint32(0, Physics2DAbiCommandMagic, true);
|
|
132
|
+
expect(String.fromCharCode(...bytes)).toBe('P2DA');
|
|
133
|
+
expect(Physics2DAbiVersion).toBe(1);
|
|
134
|
+
expect(Physics2DAbiCommandHeaderByteLength).toBe(16);
|
|
135
|
+
expect(Physics2DAbiCommandHeaderOffset).toEqual({ Magic: 0, Version: 4, ByteLength: 8, CommandCount: 12 });
|
|
136
|
+
expect(Physics2DAbiCommandRecordOffset).toEqual({ Kind: 0, ByteLength: 4, ObjectId: 8, RelatedId: 12 });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('Physics2DAbiContactFlag', () => {
|
|
141
|
+
it('names the three contact states the standard record carries', () => {
|
|
142
|
+
expect(Physics2DAbiContactFlag).toEqual({ Enabled: 1, Sensor: 2, Touching: 4 });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Physics2DAbiContactValue', () => {
|
|
147
|
+
it('sizes a manifold to the exact planar bound of two points', () => {
|
|
148
|
+
expect(Physics2DAbiMaxContactPoints).toBe(2);
|
|
149
|
+
expect(Physics2DAbiContactIdStride).toBe(4);
|
|
150
|
+
expect(Physics2DAbiContactValueStride).toBe(4);
|
|
151
|
+
expect(Physics2DAbiContactPointValueStride).toBe(7);
|
|
152
|
+
expect(Object.values(Physics2DAbiContactId)).toEqual([0, 1, 2, 3]);
|
|
153
|
+
expect(Object.values(Physics2DAbiContactValue)).toEqual([0, 1, 2, 3]);
|
|
154
|
+
expect(Object.values(Physics2DAbiContactPointValue)).toEqual([0, 1, 2, 3, 4, 5, 6]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Physics2DAbiJointFlag', () => {
|
|
159
|
+
it('overlays the gear coordinate bits on the enable bits, which no kind uses together', () => {
|
|
160
|
+
expect(Physics2DAbiJointFlag.CollideConnected).toBe(1);
|
|
161
|
+
expect(Physics2DAbiJointFlag.Broken).toBe(2);
|
|
162
|
+
// Deliberate aliasing: a record carries exactly one kind, so a gear's coordinates and a revolute's
|
|
163
|
+
// motor/limit bits can share positions 2 and 3 without either ever reading the other's meaning.
|
|
164
|
+
expect(Physics2DAbiJointFlag.LinearCoordinateA).toBe(Physics2DAbiJointFlag.EnableMotor);
|
|
165
|
+
expect(Physics2DAbiJointFlag.LinearCoordinateB).toBe(Physics2DAbiJointFlag.EnableLimit);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Physics2DAbiJointKind', () => {
|
|
170
|
+
it('covers all nine built-in 2D joints', () => {
|
|
171
|
+
expect(Physics2DAbiJointKind).toEqual({
|
|
172
|
+
Distance: 1,
|
|
173
|
+
Revolute: 2,
|
|
174
|
+
Prismatic: 3,
|
|
175
|
+
Weld: 4,
|
|
176
|
+
Wheel: 5,
|
|
177
|
+
Rope: 6,
|
|
178
|
+
Mouse: 7,
|
|
179
|
+
Pulley: 8,
|
|
180
|
+
Gear: 9,
|
|
181
|
+
});
|
|
182
|
+
expect(Physics2DAbiJointKindValueCount).toBe(9);
|
|
183
|
+
expect(Object.values(Physics2DAbiJointCommonValue)).toEqual([0, 1, 2, 3, 4, 5]);
|
|
184
|
+
expect(Object.values(Physics2DAbiJointValue)).toEqual([0, 1, 2]);
|
|
185
|
+
expect(Physics2DAbiJointValueStride).toBe(3);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Physics2DAbiMaxContactPoints', () => {
|
|
190
|
+
it('is not exceeded by any built-in 2D manifold, swept rather than argued', () => {
|
|
191
|
+
// The hook buffer reserves exactly this many point slots, and `writeContact` clamps to it. That the
|
|
192
|
+
// planar bound is two is a geometric argument in a comment; collision exports no 2D counterpart to
|
|
193
|
+
// MAX_COLLISION_CONTACT_POINTS_3D to check it against, so this sweeps the pair matrix instead. If a
|
|
194
|
+
// future shape or manifold change produces a third point, a contact hook would silently see a
|
|
195
|
+
// truncated manifold, and this is what would say so.
|
|
196
|
+
const shapes: CollisionBuiltInShape2D[] = [
|
|
197
|
+
{ kind: 'circle', x: 0, y: 0, radius: 0.6 },
|
|
198
|
+
{ kind: 'aabb', minX: -0.6, minY: -0.6, maxX: 0.6, maxY: 0.6 },
|
|
199
|
+
{ kind: 'obb', x: 0, y: 0, halfW: 0.6, halfH: 0.4, rotation: 0 },
|
|
200
|
+
{ kind: 'capsule', x0: -0.5, y0: 0, x1: 0.5, y1: 0, radius: 0.35 },
|
|
201
|
+
{ kind: 'polygon', points: [-0.6, -0.5, 0.6, -0.5, 0.7, 0.4, -0.4, 0.6] },
|
|
202
|
+
];
|
|
203
|
+
const manifold = createCollisionContactManifold2D();
|
|
204
|
+
let widest = 0;
|
|
205
|
+
let overlaps = 0;
|
|
206
|
+
for (const a of shapes) {
|
|
207
|
+
for (const b of shapes) {
|
|
208
|
+
// Slide one shape across the other on both axes and at several angles, so face-face,
|
|
209
|
+
// face-vertex, and end-on arrangements are all reached rather than one lucky pose.
|
|
210
|
+
for (let step = -12; step <= 12; step += 1) {
|
|
211
|
+
const offset = step * 0.1;
|
|
212
|
+
for (const rotation of [0, 0.37, Math.PI / 4, 1.2]) {
|
|
213
|
+
const moved = translateShape(b, offset, offset * 0.5, rotation);
|
|
214
|
+
if (!collideContactManifold2D(a, moved, manifold)) continue;
|
|
215
|
+
overlaps += 1;
|
|
216
|
+
widest = Math.max(widest, manifold.pointCount);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// The sweep has to actually collide things, or "never exceeded" is a statement about no manifolds.
|
|
222
|
+
expect(overlaps).toBeGreaterThan(100);
|
|
223
|
+
expect(widest).toBe(Physics2DAbiMaxContactPoints);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('Physics2DAbiQueryValue', () => {
|
|
228
|
+
it('carries fraction, point, and normal in five slots', () => {
|
|
229
|
+
expect(Physics2DAbiQueryValueStride).toBe(5);
|
|
230
|
+
expect(Physics2DAbiQueryValue).toEqual({ Fraction: 0, X: 1, Y: 2, NormalX: 3, NormalY: 4 });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Physics2DAbiShapeKind', () => {
|
|
235
|
+
it('covers all seven built-in 2D shapes, including the two area-less ones', () => {
|
|
236
|
+
expect(Physics2DAbiShapeKind).toEqual({
|
|
237
|
+
Circle: 1,
|
|
238
|
+
Aabb: 2,
|
|
239
|
+
Obb: 3,
|
|
240
|
+
Capsule: 4,
|
|
241
|
+
Polygon: 5,
|
|
242
|
+
Segment: 6,
|
|
243
|
+
Point: 7,
|
|
244
|
+
});
|
|
245
|
+
expect(Physics2DAbiShapeHeaderByteLength).toBe(16);
|
|
246
|
+
expect(Physics2DAbiShapeHeaderOffset).toEqual({
|
|
247
|
+
Kind: 0,
|
|
248
|
+
ScalarCount: 4,
|
|
249
|
+
IntegerCount: 8,
|
|
250
|
+
Version: 12,
|
|
251
|
+
Scalars: 16,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Physics2DAbiSolverConfigFlag', () => {
|
|
257
|
+
it('holds the substep word zero, since Physics2D has no substeps', () => {
|
|
258
|
+
expect(Physics2DAbiSolverConfigFlag).toEqual({ AllowSleeping: 1, ContinuousCollision: 2, WarmStarting: 4 });
|
|
259
|
+
expect(Physics2DAbiSetSolverConfigPayloadOffset.Reserved0).toBe(4);
|
|
260
|
+
expect(Object.values(Physics2DAbiSolverConfigValue)).toEqual([0, 1, 2, 3, 4, 5]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Moves a shape by (dx, dy), additionally rotating the kinds that carry an angle so the sweep reaches
|
|
265
|
+
// arrangements an axis-aligned slide never would.
|
|
266
|
+
function translateShape(shape: Readonly<CollisionBuiltInShape2D>, dx: number, dy: number, rotation: number) {
|
|
267
|
+
if (shape.kind === 'circle') return { ...shape, x: shape.x + dx, y: shape.y + dy };
|
|
268
|
+
if (shape.kind === 'aabb') {
|
|
269
|
+
return { ...shape, minX: shape.minX + dx, minY: shape.minY + dy, maxX: shape.maxX + dx, maxY: shape.maxY + dy };
|
|
270
|
+
}
|
|
271
|
+
if (shape.kind === 'obb') return { ...shape, x: shape.x + dx, y: shape.y + dy, rotation };
|
|
272
|
+
if (shape.kind === 'capsule') {
|
|
273
|
+
const cos = Math.cos(rotation);
|
|
274
|
+
const sin = Math.sin(rotation);
|
|
275
|
+
return {
|
|
276
|
+
...shape,
|
|
277
|
+
x0: shape.x0 * cos - shape.y0 * sin + dx,
|
|
278
|
+
y0: shape.x0 * sin + shape.y0 * cos + dy,
|
|
279
|
+
x1: shape.x1 * cos - shape.y1 * sin + dx,
|
|
280
|
+
y1: shape.x1 * sin + shape.y1 * cos + dy,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (shape.kind !== 'polygon') return shape;
|
|
284
|
+
const cos = Math.cos(rotation);
|
|
285
|
+
const sin = Math.sin(rotation);
|
|
286
|
+
const points: number[] = [];
|
|
287
|
+
for (let i = 0; i < shape.points.length; i += 2) {
|
|
288
|
+
const x = shape.points[i];
|
|
289
|
+
const y = shape.points[i + 1];
|
|
290
|
+
points.push(x * cos - y * sin + dx, x * sin + y * cos + dy);
|
|
291
|
+
}
|
|
292
|
+
return { ...shape, points };
|
|
293
|
+
}
|