@gg-web-engine/matter 0.0.72 → 0.0.74
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/components/matter-character-controller.component.d.ts +191 -0
- package/dist/components/matter-character-controller.component.js +498 -0
- package/dist/components/matter-rigid-body.component.d.ts +43 -2
- package/dist/components/matter-rigid-body.component.js +68 -5
- package/dist/components/matter-trigger.component.d.ts +66 -4
- package/dist/components/matter-trigger.component.js +139 -4
- package/dist/components/matter-world.component.d.ts +44 -5
- package/dist/components/matter-world.component.js +131 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/matter-factory.d.ts +8 -1
- package/dist/matter-factory.js +116 -11
- package/dist/types.d.ts +2 -0
- package/package.json +5 -4
- package/test/components/matter-character-controller.component.spec.ts +298 -0
- package/test/components/matter-rigid-body-collision.spec.ts +27 -13
- package/test/components/matter-rigid-body-sleep.spec.ts +79 -0
- package/test/components/matter-rigid-body.component.spec.ts +98 -0
- package/test/components/matter-trigger.component.spec.ts +135 -13
- package/test/components/matter-world.component.spec.ts +44 -9
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { CharacterController2dOptions, Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterCharacterControllerComponent', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O; // this component never relies on gravity - see the interface's own doc
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const CHAR_OPTIONS: CharacterController2dOptions = { radius: 0.4, centersDistance: 1.0 };
|
|
23
|
+
// center-to-feet distance for the capsule above: radius + centersDistance / 2
|
|
24
|
+
const HALF_HEIGHT = CHAR_OPTIONS.radius + CHAR_OPTIONS.centersDistance / 2;
|
|
25
|
+
|
|
26
|
+
// A static floor slab spanning [centerX - sizeX/2, centerX + sizeX/2] on X, with its top face (the
|
|
27
|
+
// side facing `up` = `{x:0,y:-1}`, i.e. the smaller-Y side) at world-space `topY`.
|
|
28
|
+
const addFloor = (centerX: number, sizeX: number, topY: number, thickness = 1) => {
|
|
29
|
+
const floor = factory.createRigidBody(
|
|
30
|
+
{
|
|
31
|
+
shape: { shape: 'BOX', dimensions: { x: sizeX, y: thickness } },
|
|
32
|
+
body: { bodyType: 'static', mass: 0 },
|
|
33
|
+
},
|
|
34
|
+
{ position: { x: centerX, y: topY + thickness / 2 } },
|
|
35
|
+
);
|
|
36
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
37
|
+
return floor;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A static vertical wall whose near face is at world-space X `x`.
|
|
41
|
+
const addWall = (x: number, thickness = 1) => {
|
|
42
|
+
const wall = factory.createRigidBody(
|
|
43
|
+
{
|
|
44
|
+
shape: { shape: 'BOX', dimensions: { x: thickness, y: 20 } },
|
|
45
|
+
body: { bodyType: 'static', mass: 0 },
|
|
46
|
+
},
|
|
47
|
+
{ position: { x: x + thickness / 2, y: 0 } },
|
|
48
|
+
);
|
|
49
|
+
wall.addToWorld({ physicsWorld: world } as any);
|
|
50
|
+
return wall;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Unlike Ammo/Rapier, matter-js has no broad-phase-registration lag at all - `Composite.add`
|
|
54
|
+
// synchronously updates `Composite.allBodies`, which is exactly what this component's own
|
|
55
|
+
// `Query.collides`-based queries read - so there is no "world hasn't stepped yet" pitfall here to
|
|
56
|
+
// work around with a settle call. Kept only as a documented no-op for structural parity with the
|
|
57
|
+
// other adapters' test suites.
|
|
58
|
+
const settleWorld = () => world.simulate(0);
|
|
59
|
+
|
|
60
|
+
it('should create a character controller and add it to the world', () => {
|
|
61
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
62
|
+
expect(character.radius).toBe(0.4);
|
|
63
|
+
expect(character.centersDistance).toBe(1.0);
|
|
64
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
65
|
+
expect(world.children).toContain(character);
|
|
66
|
+
expect(character.position).toEqual({ x: 0, y: -5 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should settle on a flat floor and report grounded state', () => {
|
|
70
|
+
addFloor(0, 20, 0);
|
|
71
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
72
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
73
|
+
settleWorld();
|
|
74
|
+
|
|
75
|
+
expect(character.isGrounded).toBe(false);
|
|
76
|
+
|
|
77
|
+
character.move({ x: 0, y: 10 });
|
|
78
|
+
|
|
79
|
+
// resolved fully synchronously - no world.simulate() call happened after settleWorld() above
|
|
80
|
+
expect(character.isGrounded).toBe(true);
|
|
81
|
+
expect(character.position.y).toBeCloseTo(-HALF_HEIGHT, 1);
|
|
82
|
+
expect(character.groundNormal).not.toBeNull();
|
|
83
|
+
expect(character.groundNormal!.y).toBeLessThan(-0.9);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should slide to a stop against a wall instead of passing through it', () => {
|
|
87
|
+
addFloor(0, 20, 0);
|
|
88
|
+
addWall(2);
|
|
89
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
90
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
91
|
+
});
|
|
92
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
93
|
+
settleWorld();
|
|
94
|
+
// settle onto the floor first
|
|
95
|
+
character.move({ x: 0, y: 1 });
|
|
96
|
+
expect(character.isGrounded).toBe(true);
|
|
97
|
+
|
|
98
|
+
character.move({ x: 5, y: 0 });
|
|
99
|
+
|
|
100
|
+
// wall's near face is at x=2; the capsule (radius 0.4) plus its skin offset must stop short of it
|
|
101
|
+
expect(character.position.x).toBeLessThan(1.6);
|
|
102
|
+
expect(character.position.x).toBeGreaterThan(0.5);
|
|
103
|
+
// remained grounded throughout - sliding along the wall, not falling
|
|
104
|
+
expect(character.isGrounded).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it(
|
|
108
|
+
'two character controllers should block each other instead of passing through (regression: a ' +
|
|
109
|
+
"character's own phantom body is never added to Composite/engine.world, so it was invisible to " +
|
|
110
|
+
"any other character's obstacle collection, which only ever enumerated Composite.allBodies)",
|
|
111
|
+
() => {
|
|
112
|
+
addFloor(0, 20, 0);
|
|
113
|
+
const characterA = factory.createCharacterController(CHAR_OPTIONS, {
|
|
114
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
115
|
+
});
|
|
116
|
+
const characterB = factory.createCharacterController(CHAR_OPTIONS, {
|
|
117
|
+
position: { x: 3, y: -(HALF_HEIGHT + 0.5) },
|
|
118
|
+
});
|
|
119
|
+
characterA.addToWorld({ physicsWorld: world } as any);
|
|
120
|
+
characterB.addToWorld({ physicsWorld: world } as any);
|
|
121
|
+
settleWorld();
|
|
122
|
+
characterA.move({ x: 0, y: 1 });
|
|
123
|
+
characterB.move({ x: 0, y: 1 });
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < 20; i++) {
|
|
126
|
+
characterA.move({ x: 0.2, y: 0 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// characterB sits at x=3 and never moves - characterA must be blocked well short of it, not
|
|
130
|
+
// walk straight through to make its full requested 4 units of horizontal progress.
|
|
131
|
+
expect(characterA.position.x).toBeLessThan(2.5);
|
|
132
|
+
expect(characterB.position.x).toBe(3);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it(
|
|
137
|
+
'walks straight through a body added to ignoredBodies instead of sliding to a stop against it ' +
|
|
138
|
+
"(regression: collision groups alone can't express excluding just one specific body while both " +
|
|
139
|
+
'it and the character still need to collide with the rest of the world - see ' +
|
|
140
|
+
'`ICharacterController2dComponent.ignoredBodies`\'s doc)',
|
|
141
|
+
() => {
|
|
142
|
+
addFloor(0, 20, 0);
|
|
143
|
+
const wall = addWall(2);
|
|
144
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
145
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
146
|
+
});
|
|
147
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
148
|
+
character.ignoredBodies.add(wall);
|
|
149
|
+
settleWorld();
|
|
150
|
+
character.move({ x: 0, y: 1 });
|
|
151
|
+
|
|
152
|
+
character.move({ x: 5, y: 0 });
|
|
153
|
+
|
|
154
|
+
// unlike the baseline test above, nothing stopped it short of the wall's x=2 near face
|
|
155
|
+
expect(character.position.x).toBeCloseTo(5);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
it('removing a body from ignoredBodies makes it block the character again', () => {
|
|
160
|
+
addFloor(0, 20, 0);
|
|
161
|
+
const wall = addWall(2);
|
|
162
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
163
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
164
|
+
});
|
|
165
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
166
|
+
character.ignoredBodies.add(wall);
|
|
167
|
+
character.ignoredBodies.delete(wall);
|
|
168
|
+
settleWorld();
|
|
169
|
+
character.move({ x: 0, y: 1 });
|
|
170
|
+
|
|
171
|
+
character.move({ x: 5, y: 0 });
|
|
172
|
+
|
|
173
|
+
expect(character.position.x).toBeLessThan(1.6);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should step up a ledge shorter than maxStepHeight without getting stuck', () => {
|
|
177
|
+
const maxStepHeight = 0.3;
|
|
178
|
+
const stepHeight = 0.2; // below maxStepHeight
|
|
179
|
+
addFloor(-5, 10, 0); // lower floor, spans x in [-10, 0]
|
|
180
|
+
addFloor(5, 10, -stepHeight); // upper ledge, spans x in [0, 10], step wall at x=0
|
|
181
|
+
|
|
182
|
+
const character = factory.createCharacterController(
|
|
183
|
+
{ ...CHAR_OPTIONS, maxStepHeight },
|
|
184
|
+
{ position: { x: -5, y: -(HALF_HEIGHT + 0.5) } },
|
|
185
|
+
);
|
|
186
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
187
|
+
settleWorld();
|
|
188
|
+
character.move({ x: 0, y: 1 });
|
|
189
|
+
expect(character.isGrounded).toBe(true);
|
|
190
|
+
expect(character.position.y).toBeCloseTo(-HALF_HEIGHT, 1);
|
|
191
|
+
|
|
192
|
+
// walk forward across the step in small increments, each with a slight downward bias so the
|
|
193
|
+
// character does not need to rely on gravity (which this component never applies itself)
|
|
194
|
+
for (let i = 0; i < 60; i++) {
|
|
195
|
+
character.move({ x: 0.15, y: 0.05 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
expect(character.isGrounded).toBe(true);
|
|
199
|
+
expect(character.position.x).toBeGreaterThan(2); // made meaningful forward progress past the step
|
|
200
|
+
expect(character.position.y).toBeCloseTo(-(stepHeight + HALF_HEIGHT), 1); // stepped up onto the ledge
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('a small upward move (a jump takeoff tick) actually rises instead of being snapped back to the floor, even though it stays well within the default snapToGroundDistance of 0.3', () => {
|
|
204
|
+
addFloor(0, 20, 0);
|
|
205
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
206
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
207
|
+
});
|
|
208
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
209
|
+
settleWorld();
|
|
210
|
+
character.move({ x: 0, y: 1 });
|
|
211
|
+
expect(character.isGrounded).toBe(true);
|
|
212
|
+
const groundedY = character.position.y;
|
|
213
|
+
|
|
214
|
+
// one tick's worth of a typical jumpSpeed (5 units/s) at 60fps - well under the 0.3 snapToGroundDistance
|
|
215
|
+
character.move({ x: 0, y: -0.083 });
|
|
216
|
+
|
|
217
|
+
expect(character.position.y).toBeCloseTo(groundedY - 0.083, 2);
|
|
218
|
+
expect(character.isGrounded).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("move()'s effects must be visible immediately, without an extra world.step() in between", () => {
|
|
222
|
+
addFloor(0, 20, 0);
|
|
223
|
+
addWall(1);
|
|
224
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
225
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.01) },
|
|
226
|
+
});
|
|
227
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
228
|
+
settleWorld();
|
|
229
|
+
|
|
230
|
+
// no world.simulate() call below this point - move() alone must fully resolve state
|
|
231
|
+
character.move({ x: 10, y: 0 });
|
|
232
|
+
|
|
233
|
+
expect(character.position.x).toBeLessThan(0.6);
|
|
234
|
+
expect(character.position.x).toBeGreaterThan(-0.1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should clone with the same shape/options but independent from the world', () => {
|
|
238
|
+
const character = factory.createCharacterController(
|
|
239
|
+
{ radius: 0.3, centersDistance: 0.8, maxStepHeight: 0.25 },
|
|
240
|
+
{ position: { x: 1, y: 2 } },
|
|
241
|
+
);
|
|
242
|
+
const clone = character.clone();
|
|
243
|
+
expect(clone.radius).toBe(character.radius);
|
|
244
|
+
expect(clone.centersDistance).toBe(character.centersDistance);
|
|
245
|
+
expect(clone.position).toEqual(character.position);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should clone from the CURRENT position/rotation, not the construction-time spawn point', () => {
|
|
249
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
250
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
251
|
+
settleWorld();
|
|
252
|
+
// move the character away from its construction-time position via the position setter, the
|
|
253
|
+
// same code path that leaves any construction-time transform stale once the component is added
|
|
254
|
+
// to a world and driven directly
|
|
255
|
+
character.position = { x: 10, y: 20 };
|
|
256
|
+
|
|
257
|
+
const clone = character.clone();
|
|
258
|
+
|
|
259
|
+
expect(clone.position).toEqual(character.position);
|
|
260
|
+
expect(clone.position).not.toEqual({ x: 0, y: -5 });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it(
|
|
264
|
+
'should clone from the CURRENT up/ownCollisionGroups/interactWithCollisionGroups, not whatever ' +
|
|
265
|
+
'was passed at construction time (regression: clone() used to rebuild from the frozen ' +
|
|
266
|
+
"construction-time options object, silently discarding any of these three fields' live " +
|
|
267
|
+
'mutations via their own setters)',
|
|
268
|
+
() => {
|
|
269
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
270
|
+
character.up = { x: 1, y: 0 };
|
|
271
|
+
character.ownCollisionGroups = [world.registerCollisionGroup()];
|
|
272
|
+
character.interactWithCollisionGroups = [world.registerCollisionGroup()];
|
|
273
|
+
|
|
274
|
+
const clone = character.clone();
|
|
275
|
+
|
|
276
|
+
expect(clone.up).toEqual(character.up);
|
|
277
|
+
expect(clone.ownCollisionGroups).toEqual(character.ownCollisionGroups);
|
|
278
|
+
expect(clone.interactWithCollisionGroups).toEqual(character.interactWithCollisionGroups);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
it('move() before addToWorld() should be a silent no-op, not throw', () => {
|
|
283
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
284
|
+
expect(() => character.move({ x: 1, y: 0 })).not.toThrow();
|
|
285
|
+
expect(character.position).toEqual({ x: 0, y: -5 });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should normalize a non-unit-length up vector, matching the 3D adapter', () => {
|
|
289
|
+
const character = factory.createCharacterController(
|
|
290
|
+
{ ...CHAR_OPTIONS, up: { x: 0, y: 2 } },
|
|
291
|
+
{ position: { x: 0, y: -5 } },
|
|
292
|
+
);
|
|
293
|
+
expect(character.up).toEqual({ x: 0, y: 1 });
|
|
294
|
+
|
|
295
|
+
character.up = { x: 0, y: 3 };
|
|
296
|
+
expect(character.up).toEqual({ x: 0, y: 1 });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -22,7 +22,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
22
22
|
it('should fire onCollisionStart reciprocally, with a sane position/normal, when a falling body lands on a floor', () => {
|
|
23
23
|
// static floor, top edge sits at y = -5
|
|
24
24
|
const floor = factory.createRigidBody(
|
|
25
|
-
{ shape: { shape: '
|
|
25
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
26
26
|
{ position: { x: 0, y: 0 } },
|
|
27
27
|
);
|
|
28
28
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
@@ -67,18 +67,25 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
67
67
|
|
|
68
68
|
it('should not keep re-firing onCollisionStart while a body rests stably on another', () => {
|
|
69
69
|
const floor = factory.createRigidBody(
|
|
70
|
-
{ shape: { shape: '
|
|
70
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
71
71
|
{ position: { x: 0, y: 0 } },
|
|
72
72
|
);
|
|
73
73
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
74
74
|
|
|
75
|
+
// A BOX, not a CIRCLE: a circle has no flat resting face, so under this engine's default (no
|
|
76
|
+
// sleeping - see `gg-engine-physics-adapter-matter`'s own note) it keeps finding tiny amounts of
|
|
77
|
+
// torque to roll away on indefinitely instead of ever truly settling, which is exactly the kind
|
|
78
|
+
// of never-stops-touching-and-un-touching motion this test's own premise ("rests stably") needs
|
|
79
|
+
// to rule out by construction. Also left at matter's own default `frictionAir` (not zeroed like
|
|
80
|
+
// the other tests in this file) - some velocity damping is what actually lets a resting body's
|
|
81
|
+
// energy bleed off instead of slowly building up over many hundreds of steps (see this file's own
|
|
82
|
+
// history for how thoroughly this was verified empirically).
|
|
75
83
|
const ball = factory.createRigidBody(
|
|
76
|
-
{ shape: { shape: '
|
|
84
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
77
85
|
{ position: { x: 0, y: -20 } },
|
|
78
86
|
);
|
|
79
87
|
ball.addToWorld({ physicsWorld: world } as any);
|
|
80
88
|
ball.linearVelocity = { x: 0, y: 10 };
|
|
81
|
-
ball.nativeBody.frictionAir = 0;
|
|
82
89
|
|
|
83
90
|
world.gravity = { x: 0, y: 1 };
|
|
84
91
|
|
|
@@ -101,28 +108,35 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
101
108
|
|
|
102
109
|
it('should fire onCollisionEnd when a body is knocked away and separates', () => {
|
|
103
110
|
const floor = factory.createRigidBody(
|
|
104
|
-
{ shape: { shape: '
|
|
111
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
105
112
|
{ position: { x: 0, y: 0 } },
|
|
106
113
|
);
|
|
107
114
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
108
115
|
|
|
116
|
+
// BOX, not CIRCLE - see the previous test's own doc for why.
|
|
109
117
|
const ball = factory.createRigidBody(
|
|
110
|
-
{ shape: { shape: '
|
|
118
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
111
119
|
{ position: { x: 0, y: -20 } },
|
|
112
120
|
);
|
|
113
121
|
ball.addToWorld({ physicsWorld: world } as any);
|
|
114
122
|
ball.linearVelocity = { x: 0, y: 10 };
|
|
115
|
-
ball.nativeBody.frictionAir = 0;
|
|
116
123
|
|
|
117
124
|
let endEvents: (MatterRigidBodyComponent | null)[] = [];
|
|
125
|
+
// Knock it away the instant it actually touches the floor (rather than after some fixed,
|
|
126
|
+
// speed/distance-dependent step count guessed to be "enough time to have touched by now") - the
|
|
127
|
+
// exact number of steps a fall of this distance/speed takes to make real contact isn't this
|
|
128
|
+
// test's concern and shouldn't be baked into a magic constant.
|
|
129
|
+
let knocked = false;
|
|
130
|
+
ball.onCollisionStart.subscribe(() => {
|
|
131
|
+
if (!knocked) {
|
|
132
|
+
knocked = true;
|
|
133
|
+
ball.linearVelocity = { x: 0, y: -10 };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
118
136
|
ball.onCollisionEnd.subscribe(e => endEvents.push(e));
|
|
119
137
|
|
|
120
138
|
for (let i = 0; i < 200 && endEvents.length === 0; i++) {
|
|
121
139
|
world.simulate(16);
|
|
122
|
-
if (i === 50) {
|
|
123
|
-
// knock it straight back up, away from the floor, once it's had a chance to touch
|
|
124
|
-
ball.linearVelocity = { x: 0, y: -10 };
|
|
125
|
-
}
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
expect(endEvents.length).toBeGreaterThan(0);
|
|
@@ -131,7 +145,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
131
145
|
|
|
132
146
|
it('should emit onCollisionEnd(null) on the remaining body when the other body is removed from the world while still touching', () => {
|
|
133
147
|
const floor = factory.createRigidBody(
|
|
134
|
-
{ shape: { shape: '
|
|
148
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
135
149
|
{ position: { x: 0, y: 0 } },
|
|
136
150
|
);
|
|
137
151
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
@@ -161,7 +175,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
161
175
|
});
|
|
162
176
|
|
|
163
177
|
it('should not fire the rigid body onCollisionStart for a trigger overlapping a rigid body', () => {
|
|
164
|
-
const trigger = factory.createTrigger({ shape: '
|
|
178
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 50, y: 10 } }, { position: { x: 0, y: 0 } });
|
|
165
179
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
166
180
|
|
|
167
181
|
const ball = factory.createRigidBody(
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterRigidBodyComponent sleep API', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reports a fresh dynamic body as not sleeping', () => {
|
|
23
|
+
const body = factory.createRigidBody(
|
|
24
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
25
|
+
{ position: { x: 0, y: 0 } },
|
|
26
|
+
);
|
|
27
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
28
|
+
|
|
29
|
+
expect(body.isSleeping).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sleep() forces isSleeping true, wakeUp() flips it back - regardless of the world engine\'s enableSleeping setting', () => {
|
|
33
|
+
const body = factory.createRigidBody(
|
|
34
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
35
|
+
{ position: { x: 0, y: 0 } },
|
|
36
|
+
);
|
|
37
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
38
|
+
|
|
39
|
+
body.sleep();
|
|
40
|
+
expect(body.isSleeping).toBe(true);
|
|
41
|
+
|
|
42
|
+
body.wakeUp();
|
|
43
|
+
expect(body.isSleeping).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('a position write on a sleeping body does NOT wake it back up - unlike every other adapter', () => {
|
|
47
|
+
const body = factory.createRigidBody(
|
|
48
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
49
|
+
{ position: { x: 0, y: 0 } },
|
|
50
|
+
);
|
|
51
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
52
|
+
|
|
53
|
+
body.sleep();
|
|
54
|
+
expect(body.isSleeping).toBe(true);
|
|
55
|
+
|
|
56
|
+
body.position = { x: 1, y: 2 };
|
|
57
|
+
expect(body.isSleeping).toBe(true);
|
|
58
|
+
// the write itself is not silently dropped - a caller that must write state on a sleeping
|
|
59
|
+
// body without waking it (the whole point of this adapter's setters preserving sleep state)
|
|
60
|
+
// still sees the new position take effect.
|
|
61
|
+
expect(body.position).toEqual({ x: 1, y: 2 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('a static body always reports not sleeping, and sleep()/wakeUp() are no-ops', () => {
|
|
65
|
+
const floor = factory.createRigidBody(
|
|
66
|
+
{ shape: { shape: 'BOX', dimensions: { x: 10, y: 1 } }, body: { bodyType: 'static' } },
|
|
67
|
+
{ position: { x: 0, y: 0 } },
|
|
68
|
+
);
|
|
69
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
70
|
+
|
|
71
|
+
expect(floor.isSleeping).toBe(false);
|
|
72
|
+
|
|
73
|
+
floor.sleep();
|
|
74
|
+
expect(floor.isSleeping).toBe(false);
|
|
75
|
+
|
|
76
|
+
floor.wakeUp();
|
|
77
|
+
expect(floor.isSleeping).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterRigidBodyComponent.bodyOptions', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads back the mass/friction/restitution a dynamic body was actually created with', () => {
|
|
23
|
+
const body = factory.createRigidBody(
|
|
24
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 5, friction: 0.3, restitution: 0.7 } },
|
|
25
|
+
{ position: { x: 0, y: 0 } },
|
|
26
|
+
);
|
|
27
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
28
|
+
|
|
29
|
+
expect(body.bodyOptions.bodyType).toBe('dynamic');
|
|
30
|
+
expect(body.bodyOptions.mass).toBe(5);
|
|
31
|
+
expect(body.bodyOptions.friction).toBe(0.3);
|
|
32
|
+
expect(body.bodyOptions.restitution).toBe(0.7);
|
|
33
|
+
expect(body.bodyOptions.ccd).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('echoes the originally-requested bodyType/ccd even though matter-js degrades both', () => {
|
|
37
|
+
const body = factory.createRigidBody(
|
|
38
|
+
{ shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } }, body: { bodyType: 'kinematic_pos', mass: 1, ccd: true } },
|
|
39
|
+
{ position: { x: 0, y: 0 } },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(body.bodyOptions.bodyType).toBe('kinematic_pos');
|
|
43
|
+
expect(body.bodyOptions.ccd).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reflects a mutated ownCollisionGroups/interactWithCollisionGroups live', () => {
|
|
47
|
+
const body = factory.createRigidBody(
|
|
48
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'static', mass: 0 } },
|
|
49
|
+
{ position: { x: 0, y: 0 } },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
body.ownCollisionGroups = [3];
|
|
53
|
+
body.interactWithCollisionGroups = [5, 6];
|
|
54
|
+
|
|
55
|
+
expect(body.bodyOptions.ownCollisionGroups).toEqual([3]);
|
|
56
|
+
expect(body.bodyOptions.interactWithCollisionGroups).toEqual([5, 6]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('MatterRigidBodyComponent.rotation', () => {
|
|
61
|
+
let world: MatterWorldComponent;
|
|
62
|
+
let factory: MatterFactory;
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
if (world) {
|
|
66
|
+
world.dispose();
|
|
67
|
+
}
|
|
68
|
+
world = new MatterWorldComponent();
|
|
69
|
+
factory = new MatterFactory(world);
|
|
70
|
+
await world.init();
|
|
71
|
+
world.gravity = Pnt2.O;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
world.dispose();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rotates via Body.setAngle, preserving angular velocity and actually rotating the vertices', () => {
|
|
79
|
+
const body = factory.createRigidBody(
|
|
80
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 1 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
81
|
+
{ position: { x: 0, y: 0 } },
|
|
82
|
+
);
|
|
83
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
84
|
+
|
|
85
|
+
body.rotation = Math.PI / 2;
|
|
86
|
+
world.simulate(60);
|
|
87
|
+
|
|
88
|
+
expect(body.rotation).toBeCloseTo(Math.PI / 2);
|
|
89
|
+
expect(body.angularVelocity).toBeCloseTo(0);
|
|
90
|
+
|
|
91
|
+
// A 2x1 box rotated 90 degrees should report swapped width/height bounds - proof the
|
|
92
|
+
// underlying vertices were actually rotated, not just the `.angle` field bumped.
|
|
93
|
+
const width = body.nativeBody.bounds.max.x - body.nativeBody.bounds.min.x;
|
|
94
|
+
const height = body.nativeBody.bounds.max.y - body.nativeBody.bounds.min.y;
|
|
95
|
+
expect(width).toBeCloseTo(1);
|
|
96
|
+
expect(height).toBeCloseTo(2);
|
|
97
|
+
});
|
|
98
|
+
});
|