@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
|
@@ -19,12 +19,103 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
19
19
|
world.dispose();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
it(`stays where it was placed under gravity, and never reports static level geometry`, async () => {
|
|
23
|
+
world.gravity = { x: 0, y: 9.82 };
|
|
24
|
+
const ground = factory.createRigidBody(
|
|
25
|
+
{ shape: { shape: 'BOX', dimensions: { x: 100, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
26
|
+
{ position: { x: 0, y: 50 } },
|
|
27
|
+
);
|
|
28
|
+
ground.addToWorld({ physicsWorld: world } as any);
|
|
29
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } }, { position: { x: 0, y: 0 } });
|
|
30
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
31
|
+
let entered = 0;
|
|
32
|
+
trigger.onEntityEntered.subscribe(() => entered++);
|
|
33
|
+
for (let i = 0; i < 300; i++) {
|
|
34
|
+
world.simulate(16);
|
|
35
|
+
trigger.checkOverlaps();
|
|
36
|
+
}
|
|
37
|
+
expect(trigger.position).toEqual({ x: 0, y: 0 });
|
|
38
|
+
expect(entered).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it(`detects a kinematic body moving in and out`, async () => {
|
|
42
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
43
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
44
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
45
|
+
const platform = factory.createRigidBody(
|
|
46
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'kinematic_pos', mass: 0 } },
|
|
47
|
+
{ position: { x: 20, y: 0 } },
|
|
48
|
+
);
|
|
49
|
+
warn.mockRestore();
|
|
50
|
+
platform.addToWorld({ physicsWorld: world } as any);
|
|
51
|
+
const events: string[] = [];
|
|
52
|
+
trigger.onEntityEntered.subscribe(obj => events.push(obj === platform ? 'enter' : 'other'));
|
|
53
|
+
trigger.onEntityLeft.subscribe(obj => events.push(obj === platform ? 'left' : 'other'));
|
|
54
|
+
const step = () => {
|
|
55
|
+
world.simulate(16);
|
|
56
|
+
trigger.checkOverlaps();
|
|
57
|
+
};
|
|
58
|
+
step();
|
|
59
|
+
platform.position = { x: 0, y: 0 };
|
|
60
|
+
step();
|
|
61
|
+
step();
|
|
62
|
+
expect(events).toEqual(['enter']);
|
|
63
|
+
platform.position = { x: 20, y: 0 };
|
|
64
|
+
step();
|
|
65
|
+
expect(events).toEqual(['enter', 'left']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it(`keeps a body that falls asleep inside, and reports it leaving after waking up`, async () => {
|
|
69
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
70
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
71
|
+
const box = factory.createRigidBody(
|
|
72
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
73
|
+
{ position: Pnt2.O },
|
|
74
|
+
);
|
|
75
|
+
box.addToWorld({ physicsWorld: world } as any);
|
|
76
|
+
const events: string[] = [];
|
|
77
|
+
trigger.onEntityEntered.subscribe(obj => events.push(obj === box ? 'enter' : 'other'));
|
|
78
|
+
trigger.onEntityLeft.subscribe(obj => events.push(obj === box ? 'left' : 'other'));
|
|
79
|
+
const step = () => {
|
|
80
|
+
world.simulate(16);
|
|
81
|
+
trigger.checkOverlaps();
|
|
82
|
+
};
|
|
83
|
+
step();
|
|
84
|
+
expect(events).toEqual(['enter']);
|
|
85
|
+
box.sleep();
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
step();
|
|
88
|
+
}
|
|
89
|
+
expect(events).toEqual(['enter']);
|
|
90
|
+
box.wakeUp();
|
|
91
|
+
step();
|
|
92
|
+
step();
|
|
93
|
+
expect(events).toEqual(['enter']);
|
|
94
|
+
box.position = { x: 20, y: 0 };
|
|
95
|
+
step();
|
|
96
|
+
step();
|
|
97
|
+
expect(events).toEqual(['enter', 'left']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it(`detects a body that is put to sleep before entering`, async () => {
|
|
101
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
102
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
103
|
+
const box = factory.createRigidBody(
|
|
104
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
105
|
+
{ position: Pnt2.O },
|
|
106
|
+
);
|
|
107
|
+
box.sleep();
|
|
108
|
+
box.addToWorld({ physicsWorld: world } as any);
|
|
109
|
+
let entered = 0;
|
|
110
|
+
trigger.onEntityEntered.subscribe(() => entered++);
|
|
111
|
+
world.simulate(16);
|
|
112
|
+
trigger.checkOverlaps();
|
|
113
|
+
expect(entered).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
25
116
|
// Note: this test is identical to rapier2d trigger test, and it is expected
|
|
26
|
-
it
|
|
27
|
-
const trigger = factory.createTrigger({ shape: '
|
|
117
|
+
it(`should detect object intersection`, async () => {
|
|
118
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
28
119
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
29
120
|
const circle = factory.createRigidBody({
|
|
30
121
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -52,8 +143,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
52
143
|
expect(exitRegistered).toBe(false);
|
|
53
144
|
});
|
|
54
145
|
|
|
55
|
-
it
|
|
56
|
-
const trigger = factory.createTrigger({ shape: '
|
|
146
|
+
it(`should detect end of object intersection`, async () => {
|
|
147
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
57
148
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
58
149
|
const circle = factory.createRigidBody({
|
|
59
150
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -74,8 +165,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
74
165
|
expect(exitRegistered).toBe(true);
|
|
75
166
|
});
|
|
76
167
|
|
|
77
|
-
it
|
|
78
|
-
const trigger = factory.createTrigger({ shape: '
|
|
168
|
+
it(`should fire object intersection if spawned inside`, async () => {
|
|
169
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
79
170
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
80
171
|
const circle = factory.createRigidBody({
|
|
81
172
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -91,8 +182,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
91
182
|
expect(enterRegistered).toBe(true);
|
|
92
183
|
});
|
|
93
184
|
|
|
94
|
-
it
|
|
95
|
-
const trigger = factory.createTrigger({ shape: '
|
|
185
|
+
it(`should fire end of object intersection if trigger removed`, async () => {
|
|
186
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
96
187
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
97
188
|
const circle = factory.createRigidBody({
|
|
98
189
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -109,8 +200,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
109
200
|
expect(exitRegistered).toBe(true);
|
|
110
201
|
});
|
|
111
202
|
|
|
112
|
-
it
|
|
113
|
-
const trigger = factory.createTrigger({ shape: '
|
|
203
|
+
it(`should fire end of object intersection if object removed`, async () => {
|
|
204
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
114
205
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
115
206
|
const circle = factory.createRigidBody({
|
|
116
207
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -124,8 +215,39 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
124
215
|
world.simulate(1);
|
|
125
216
|
trigger.checkOverlaps();
|
|
126
217
|
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
218
|
+
// the removed$ reaction drops the overlap synchronously but emits onEntityLeft on a microtask
|
|
219
|
+
// (see MatterTriggerComponent.removedSub's own doc) - nothing a later step/checkOverlaps() poll
|
|
220
|
+
// could still notice, so give that microtask a chance to run
|
|
221
|
+
await Promise.resolve();
|
|
222
|
+
world.simulate(1);
|
|
223
|
+
trigger.checkOverlaps();
|
|
224
|
+
expect(exitRegistered).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it(`should fire end of object intersection on the next microtask when the object is removed, with no step or checkOverlaps() poll`, async () => {
|
|
228
|
+
// Composite.remove never fires a native collisionEnd for the body it removes, so the only thing
|
|
229
|
+
// that can fire onEntityLeft for it is the trigger's own removed$ reaction. That reaction must
|
|
230
|
+
// not emit inline from inside the removal's own call stack (a removal can happen partway
|
|
231
|
+
// through an unrelated component's own lifecycle operation, e.g. a character's crouch/stand
|
|
232
|
+
// capsule swap - see MatterTriggerComponent.removedSub's own doc), so the emission is deferred
|
|
233
|
+
// to a microtask: not yet observable synchronously after removeFromWorld, observable right after
|
|
234
|
+
// one microtask hop, with no simulate()/checkOverlaps() call in between at all.
|
|
235
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
236
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
237
|
+
const circle = factory.createRigidBody({
|
|
238
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
239
|
+
body: { bodyType: 'dynamic', mass: 1 },
|
|
240
|
+
}, { position: Pnt2.O });
|
|
241
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
127
242
|
world.simulate(1);
|
|
128
243
|
trigger.checkOverlaps();
|
|
244
|
+
let exitRegistered = false;
|
|
245
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
246
|
+
exitRegistered = obj === circle;
|
|
247
|
+
}));
|
|
248
|
+
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
249
|
+
expect(exitRegistered).toBe(false);
|
|
250
|
+
await Promise.resolve();
|
|
129
251
|
expect(exitRegistered).toBe(true);
|
|
130
252
|
});
|
|
131
253
|
});
|
|
@@ -118,12 +118,12 @@ describe('MatterWorldComponent', () => {
|
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
describe
|
|
121
|
+
describe('Raycast', () => {
|
|
122
122
|
it('should return no hit when ray does not intersect any object', () => {
|
|
123
123
|
// Create a square far away from the ray
|
|
124
124
|
const square = world.factory.createRigidBody(
|
|
125
125
|
{
|
|
126
|
-
shape: { shape: '
|
|
126
|
+
shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } },
|
|
127
127
|
body: { bodyType: 'static', mass: 0 },
|
|
128
128
|
},
|
|
129
129
|
{ position: { x: 10, y: 10 } },
|
|
@@ -149,7 +149,7 @@ describe('MatterWorldComponent', () => {
|
|
|
149
149
|
// Create a square in the path of the ray
|
|
150
150
|
const square = world.factory.createRigidBody(
|
|
151
151
|
{
|
|
152
|
-
shape: { shape: '
|
|
152
|
+
shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } },
|
|
153
153
|
body: { bodyType: 'static', mass: 0 },
|
|
154
154
|
},
|
|
155
155
|
{ position: { x: 0, y: -5 } },
|
|
@@ -186,7 +186,7 @@ describe('MatterWorldComponent', () => {
|
|
|
186
186
|
// Create a square that only belongs to group1
|
|
187
187
|
const square1 = world.factory.createRigidBody(
|
|
188
188
|
{
|
|
189
|
-
shape: { shape: '
|
|
189
|
+
shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } },
|
|
190
190
|
body: {
|
|
191
191
|
bodyType: 'static',
|
|
192
192
|
mass: 0,
|
|
@@ -228,7 +228,7 @@ describe('MatterWorldComponent', () => {
|
|
|
228
228
|
// Create a square at a known distance
|
|
229
229
|
const square = world.factory.createRigidBody(
|
|
230
230
|
{
|
|
231
|
-
shape: { shape: '
|
|
231
|
+
shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } },
|
|
232
232
|
body: { bodyType: 'static', mass: 0 },
|
|
233
233
|
},
|
|
234
234
|
{ position: { x: 0, y: -5 } },
|
|
@@ -259,7 +259,7 @@ describe('MatterWorldComponent', () => {
|
|
|
259
259
|
// Create a square that belongs to group1
|
|
260
260
|
const square = world.factory.createRigidBody(
|
|
261
261
|
{
|
|
262
|
-
shape: { shape: '
|
|
262
|
+
shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } },
|
|
263
263
|
body: {
|
|
264
264
|
bodyType: 'static',
|
|
265
265
|
mass: 0,
|
|
@@ -289,7 +289,7 @@ describe('MatterWorldComponent', () => {
|
|
|
289
289
|
// Create two squares at different positions
|
|
290
290
|
const square1 = world.factory.createRigidBody(
|
|
291
291
|
{
|
|
292
|
-
shape: { shape: '
|
|
292
|
+
shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } },
|
|
293
293
|
body: { bodyType: 'static', mass: 0 },
|
|
294
294
|
},
|
|
295
295
|
{ position: { x: 0, y: -3 } },
|
|
@@ -298,7 +298,7 @@ describe('MatterWorldComponent', () => {
|
|
|
298
298
|
|
|
299
299
|
const square2 = world.factory.createRigidBody(
|
|
300
300
|
{
|
|
301
|
-
shape: { shape: '
|
|
301
|
+
shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } },
|
|
302
302
|
body: { bodyType: 'static', mass: 0 },
|
|
303
303
|
},
|
|
304
304
|
{ position: { x: 0, y: -7 } },
|
|
@@ -323,7 +323,7 @@ describe('MatterWorldComponent', () => {
|
|
|
323
323
|
// Create a square
|
|
324
324
|
const square = world.factory.createRigidBody(
|
|
325
325
|
{
|
|
326
|
-
shape: { shape: '
|
|
326
|
+
shape: { shape: 'BOX', dimensions: { x: 4, y: 4 } },
|
|
327
327
|
body: { bodyType: 'static', mass: 0 },
|
|
328
328
|
},
|
|
329
329
|
{ position: { x: 0, y: 0 } },
|
|
@@ -346,5 +346,40 @@ describe('MatterWorldComponent', () => {
|
|
|
346
346
|
// We just verify the method doesn't crash and returns a valid result
|
|
347
347
|
expect(result).toBeDefined();
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
it('should never resolve a hit against a trigger, even when the ray starts inside it', () => {
|
|
351
|
+
// A trigger is a sensor with no collision response (ITrigger2dComponent) and must never
|
|
352
|
+
// obstruct a raycast - a world-enclosing trigger (a map-bounds kill volume) would otherwise
|
|
353
|
+
// be "hit" at ~0 distance by every ray cast anywhere inside it. raycast() excludes sensor
|
|
354
|
+
// bodies from its candidate set outright (see its own doc).
|
|
355
|
+
const trigger = world.factory.createTrigger({ shape: 'BOX', dimensions: { x: 100, y: 100 } });
|
|
356
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
357
|
+
world.simulate(1);
|
|
358
|
+
|
|
359
|
+
// from well inside the trigger's volume, hitting nothing else
|
|
360
|
+
const result = world.raycast({
|
|
361
|
+
from: { x: 0, y: 0 },
|
|
362
|
+
to: { x: 0, y: 10 },
|
|
363
|
+
});
|
|
364
|
+
expect(result.hasHit).toBe(false);
|
|
365
|
+
|
|
366
|
+
// a real solid body behind/through the trigger must still be hit normally
|
|
367
|
+
const square = world.factory.createRigidBody(
|
|
368
|
+
{
|
|
369
|
+
shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } },
|
|
370
|
+
body: { bodyType: 'static', mass: 0 },
|
|
371
|
+
},
|
|
372
|
+
{ position: { x: 0, y: -5 } },
|
|
373
|
+
);
|
|
374
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
375
|
+
world.simulate(1);
|
|
376
|
+
|
|
377
|
+
const throughResult = world.raycast({
|
|
378
|
+
from: { x: 0, y: 0 },
|
|
379
|
+
to: { x: 0, y: -10 },
|
|
380
|
+
});
|
|
381
|
+
expect(throughResult.hasHit).toBe(true);
|
|
382
|
+
expect(throughResult.hitBody).toBe(square);
|
|
383
|
+
});
|
|
349
384
|
});
|
|
350
385
|
});
|