@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.
@@ -19,12 +19,103 @@ describe(`MatterTriggerComponent`, () => {
19
19
  world.dispose();
20
20
  });
21
21
 
22
- // FIXME matter.js logic is kinda not intuitive. Spawning objects on some coordinates seems to cause collisions with
23
- // all the objects that intersect the line between (0, 0) and desired position, sensors are flying away to the
24
- // infinity just by setting it's position and other wonderful miracles. Investigate why these tests fail and try to fix.
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.skip(`should detect object intersection`, async () => {
27
- const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
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.skip(`should detect end of object intersection`, async () => {
56
- const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
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.skip(`should fire object intersection if spawned inside`, async () => {
78
- const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
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.skip(`should fire end of object intersection if trigger removed`, async () => {
95
- const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
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.skip(`should fire end of object intersection if object removed`, async () => {
113
- const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
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.skip('Raycast', () => {
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: 'SQUARE', dimensions: { x: 1, y: 1 } },
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: 'SQUARE', dimensions: { x: 2, y: 2 } },
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: 'SQUARE', dimensions: { x: 2, y: 2 } },
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: 'SQUARE', dimensions: { x: 2, y: 2 } },
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: 'SQUARE', dimensions: { x: 2, y: 2 } },
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: 'SQUARE', dimensions: { x: 1, y: 1 } },
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: 'SQUARE', dimensions: { x: 1, y: 1 } },
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: 'SQUARE', dimensions: { x: 4, y: 4 } },
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
  });