@gg-web-engine/matter 0.0.73 → 0.0.75

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="../../documentation/assets/logo.png" style="height: 400px; width:400px;" alt=''/>
2
+ <img src="../../documentation/assets/banner.png" width="100%" alt="GG Web Engine"/>
3
3
  </p>
4
4
 
5
5
  ## [Matter.js](https://github.com/liabru/matter-js) integration for [gg-web-engine](https://github.com/AndyGura/gg-web-engine), providing 2D physics simulation
@@ -60,4 +60,30 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
60
60
  removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
61
61
  dispose(): void;
62
62
  resetMotion(): void;
63
+ /**
64
+ * A `kinematic_pos`/`kinematic_vel` request also reports `isStatic: true` here - see this
65
+ * component's own `bodyType`/`bodyOptions` doc for why matter-js can't distinguish that from a
66
+ * genuine `'static'` body natively. Both are equally never simulated as a sleepable dynamic body,
67
+ * so gating on the native `isStatic` flag rather than `this.bodyType === 'static'` is correct for
68
+ * both cases, not just the literal static one.
69
+ *
70
+ * Also always `false` regardless of `isStatic` unless the world's underlying `Matter.Engine` was
71
+ * created with `enableSleeping: true` - this adapter never turns that on itself (see
72
+ * `MatterWorldComponent`'s own doc on its `matterWorld`/engine setup), so a body constructed
73
+ * through this adapter alone never actually falls asleep on its own; `sleep()` below still forces
74
+ * it regardless of that engine setting.
75
+ */
76
+ get isSleeping(): boolean;
77
+ /**
78
+ * No-op on a body that reports `isStatic` (a genuine `'static'` body, or a `kinematic_pos`/
79
+ * `kinematic_vel` request - see `isSleeping`'s own doc for why both are treated the same here).
80
+ */
81
+ wakeUp(): void;
82
+ /**
83
+ * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
84
+ * regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
85
+ * body naturally falling asleep from inactivity (which requires that engine flag), an explicit
86
+ * `Sleeping.set(body, true)` call takes effect either way.
87
+ */
88
+ sleep(): void;
63
89
  }
@@ -1,5 +1,5 @@
1
1
  import { BitMask, DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
2
- import { Body, Composite, Vector } from 'matter-js';
2
+ import { Body, Composite, Sleeping, Vector } from 'matter-js';
3
3
  import { Subject } from 'rxjs';
4
4
  // FIXME why this needs to be introduced? investigate units in matter.js
5
5
  const MATTER_VELOCITY_SCALE = 0.0166667;
@@ -14,7 +14,7 @@ export class MatterRigidBodyComponent {
14
14
  return this.nativeBody.angle;
15
15
  }
16
16
  set rotation(value) {
17
- this.nativeBody.angle = value;
17
+ Body.setAngle(this.nativeBody, value);
18
18
  }
19
19
  get linearVelocity() {
20
20
  return Pnt2.scalarMult(this.nativeBody.velocity, 1 / MATTER_VELOCITY_SCALE);
@@ -70,7 +70,7 @@ export class MatterRigidBodyComponent {
70
70
  // rather than what an app originally asked for - the console warning at creation time is what
71
71
  // tells a developer their kinematic request wasn't honored, not this view.
72
72
  this.debugBodySettings = new DebugBody2DSettings(isFinite(this.nativeBody.mass)
73
- ? { type: 'RIGID_DYNAMIC', sleeping: () => this.nativeBody.isSleeping }
73
+ ? { type: 'RIGID_DYNAMIC', sleeping: () => this.isSleeping }
74
74
  : { type: 'RIGID_STATIC' }, this.shape);
75
75
  this._interactWithCGsMask = BitMask.full(16);
76
76
  this._ownCGsMask = BitMask.full(16);
@@ -172,4 +172,42 @@ export class MatterRigidBodyComponent {
172
172
  Body.setVelocity(this.nativeBody, Pnt2.O);
173
173
  Body.setAngularVelocity(this.nativeBody, 0);
174
174
  }
175
+ /**
176
+ * A `kinematic_pos`/`kinematic_vel` request also reports `isStatic: true` here - see this
177
+ * component's own `bodyType`/`bodyOptions` doc for why matter-js can't distinguish that from a
178
+ * genuine `'static'` body natively. Both are equally never simulated as a sleepable dynamic body,
179
+ * so gating on the native `isStatic` flag rather than `this.bodyType === 'static'` is correct for
180
+ * both cases, not just the literal static one.
181
+ *
182
+ * Also always `false` regardless of `isStatic` unless the world's underlying `Matter.Engine` was
183
+ * created with `enableSleeping: true` - this adapter never turns that on itself (see
184
+ * `MatterWorldComponent`'s own doc on its `matterWorld`/engine setup), so a body constructed
185
+ * through this adapter alone never actually falls asleep on its own; `sleep()` below still forces
186
+ * it regardless of that engine setting.
187
+ */
188
+ get isSleeping() {
189
+ return !this.nativeBody.isStatic && this.nativeBody.isSleeping;
190
+ }
191
+ /**
192
+ * No-op on a body that reports `isStatic` (a genuine `'static'` body, or a `kinematic_pos`/
193
+ * `kinematic_vel` request - see `isSleeping`'s own doc for why both are treated the same here).
194
+ */
195
+ wakeUp() {
196
+ if (this.nativeBody.isStatic) {
197
+ return;
198
+ }
199
+ Sleeping.set(this.nativeBody, false);
200
+ }
201
+ /**
202
+ * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
203
+ * regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
204
+ * body naturally falling asleep from inactivity (which requires that engine flag), an explicit
205
+ * `Sleeping.set(body, true)` call takes effect either way.
206
+ */
207
+ sleep() {
208
+ if (this.nativeBody.isStatic) {
209
+ return;
210
+ }
211
+ Sleeping.set(this.nativeBody, true);
212
+ }
175
213
  }
@@ -20,6 +20,10 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
20
20
  readonly debugBodySettings: DebugBody2DSettings;
21
21
  protected intersectionsAmount: number;
22
22
  protected currentOverlaps: Set<MatterRigidBodyComponent>;
23
+ /** The subset of `currentOverlaps` whose exit `checkOverlaps()` detects by polling, because matter's
24
+ * detector doesn't pair them with this (static) trigger body - see {@link isPolled}. A body moves
25
+ * back to the native-event path once a native `collisionStart` reports it (it woke up inside). */
26
+ protected polledOverlaps: Set<MatterRigidBodyComponent>;
23
27
  /** `Composite.remove` (what `removeFromWorld` calls) never fires a native `collisionEnd` for the
24
28
  * body it removes - matter-js simply stops considering that body's pairs on the next step, it
25
29
  * doesn't retroactively report the pairs that were active at removal time. Without this, a body
@@ -44,6 +48,14 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
44
48
  private removedSub?;
45
49
  private handleCollisionStart;
46
50
  private handleCollisionEnd;
51
+ /**
52
+ * Whether matter's detector never pairs `comp` with this trigger: it skips every pair whose bodies
53
+ * are both static or sleeping, and the trigger body is static. That covers sleeping bodies and
54
+ * kinematic ones (built as static here, see `MatterFactory`). Bodies requested as `'static'` are
55
+ * level geometry and are never reported.
56
+ */
57
+ protected isPolled(comp: MatterRigidBodyComponent): boolean;
58
+ protected overlaps(comp: MatterRigidBodyComponent): boolean;
47
59
  constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
48
60
  addToWorld(world: MatterGgWorld): void;
49
61
  removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
@@ -52,9 +64,10 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
52
64
  * enter/exit subjects are a separate pair this subclass owns and must complete itself. */
53
65
  dispose(): void;
54
66
  /**
55
- * Regular rigid-body overlaps are handled entirely by `handleCollisionStart`/`handleCollisionEnd`
56
- * above, off matter's own native `collisionStart`/`collisionEnd` engine events - so this used to be
57
- * a pure no-op for matter-js. A `MatterCharacterControllerComponent`'s own phantom body is
67
+ * Awake dynamic bodies are handled by `handleCollisionStart`/`handleCollisionEnd` above, off
68
+ * matter's own native `collisionStart`/`collisionEnd` engine events; kinematic and sleeping ones,
69
+ * which matter never pairs with this static body, are polled by `checkPolledBodyOverlaps()`.
70
+ * A `MatterCharacterControllerComponent`'s own phantom body is
58
71
  * deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
59
72
  * native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
60
73
  * already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
@@ -66,5 +79,12 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
66
79
  * ordinary bodies.
67
80
  */
68
81
  checkOverlaps(): void;
82
+ /**
83
+ * Enter/exit of the rigid bodies matter's detector never pairs with this trigger (see
84
+ * {@link isPolled}): a kinematic platform moving in or out, a body asleep inside. Like the
85
+ * character poll below, `Query.collides` ignores `collisionFilter`, so `Detector.canCollide` is
86
+ * checked by hand.
87
+ */
88
+ protected checkPolledBodyOverlaps(): void;
69
89
  clone(): MatterTriggerComponent;
70
90
  }
@@ -22,6 +22,11 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
22
22
  if (body) {
23
23
  let comp = this.world.children.find(c => c.nativeBody === body);
24
24
  if (comp instanceof MatterRigidBodyComponent) {
25
+ if (this.currentOverlaps.has(comp)) {
26
+ // already inside via polling (it was asleep or kinematic): the native pair takes over
27
+ this.polledOverlaps.delete(comp);
28
+ continue;
29
+ }
25
30
  this.currentOverlaps.add(comp);
26
31
  this.onEnter$.next(comp);
27
32
  }
@@ -39,13 +44,31 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
39
44
  }
40
45
  if (body) {
41
46
  let comp = this.world.children.find(c => c.nativeBody === body);
42
- if (comp instanceof MatterRigidBodyComponent) {
47
+ if (comp instanceof MatterRigidBodyComponent && this.currentOverlaps.has(comp)) {
48
+ if (this.isPolled(comp) && this.overlaps(comp)) {
49
+ // a body falling asleep inside: matter drops the pair without it having left
50
+ this.polledOverlaps.add(comp);
51
+ continue;
52
+ }
43
53
  this.currentOverlaps.delete(comp);
54
+ this.polledOverlaps.delete(comp);
44
55
  this.onLeft$.next(comp);
45
56
  }
46
57
  }
47
58
  }
48
59
  }
60
+ /**
61
+ * Whether matter's detector never pairs `comp` with this trigger: it skips every pair whose bodies
62
+ * are both static or sleeping, and the trigger body is static. That covers sleeping bodies and
63
+ * kinematic ones (built as static here, see `MatterFactory`). Bodies requested as `'static'` are
64
+ * level geometry and are never reported.
65
+ */
66
+ isPolled(comp) {
67
+ return comp.bodyType === 'kinematic_pos' || comp.bodyType === 'kinematic_vel' || comp.nativeBody.isSleeping;
68
+ }
69
+ overlaps(comp) {
70
+ return Query.collides(this.nativeBody, [comp.nativeBody]).length > 0;
71
+ }
49
72
  constructor(nativeBody, shape, world) {
50
73
  super(nativeBody, shape);
51
74
  this.shape = shape;
@@ -60,7 +83,17 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
60
83
  this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
61
84
  this.intersectionsAmount = 0;
62
85
  this.currentOverlaps = new Set();
86
+ /** The subset of `currentOverlaps` whose exit `checkOverlaps()` detects by polling, because matter's
87
+ * detector doesn't pair them with this (static) trigger body - see {@link isPolled}. A body moves
88
+ * back to the native-event path once a native `collisionStart` reports it (it woke up inside). */
89
+ this.polledOverlaps = new Set();
63
90
  this.nativeBody.isSensor = true;
91
+ // A trigger is a fixed volume: left non-static, matter-js integrates its sensor body under
92
+ // gravity like any dynamic body, and since a sensor never collides it falls through the level
93
+ // (overlapping, and reporting, whatever it passes on the way down). Static also keeps
94
+ // static-static pairs (trigger vs level geometry) out of matter's broadphase entirely - which
95
+ // drops kinematic and sleeping bodies too, so `checkOverlaps()` polls those.
96
+ Body.setStatic(this.nativeBody, true);
64
97
  merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
65
98
  if (enter) {
66
99
  this.intersectionsAmount++;
@@ -78,6 +111,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
78
111
  }
79
112
  this.intersectionsAmount = 0;
80
113
  this.currentOverlaps.clear();
114
+ this.polledOverlaps.clear();
81
115
  super.addToWorld(world);
82
116
  Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
83
117
  Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
@@ -91,6 +125,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
91
125
  }
92
126
  }
93
127
  else if (c instanceof MatterRigidBodyComponent && this.currentOverlaps.delete(c)) {
128
+ this.polledOverlaps.delete(c);
94
129
  queueMicrotask(() => this.onLeft$.next(c));
95
130
  }
96
131
  });
@@ -104,6 +139,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
104
139
  this.onLeft$.next(body);
105
140
  }
106
141
  this.currentOverlaps.clear();
142
+ this.polledOverlaps.clear();
107
143
  for (const character of this.currentCharacterOverlaps) {
108
144
  this.onLeft$.next(character);
109
145
  }
@@ -119,9 +155,10 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
119
155
  super.dispose();
120
156
  }
121
157
  /**
122
- * Regular rigid-body overlaps are handled entirely by `handleCollisionStart`/`handleCollisionEnd`
123
- * above, off matter's own native `collisionStart`/`collisionEnd` engine events - so this used to be
124
- * a pure no-op for matter-js. A `MatterCharacterControllerComponent`'s own phantom body is
158
+ * Awake dynamic bodies are handled by `handleCollisionStart`/`handleCollisionEnd` above, off
159
+ * matter's own native `collisionStart`/`collisionEnd` engine events; kinematic and sleeping ones,
160
+ * which matter never pairs with this static body, are polled by `checkPolledBodyOverlaps()`.
161
+ * A `MatterCharacterControllerComponent`'s own phantom body is
125
162
  * deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
126
163
  * native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
127
164
  * already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
@@ -133,6 +170,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
133
170
  * ordinary bodies.
134
171
  */
135
172
  checkOverlaps() {
173
+ this.checkPolledBodyOverlaps();
136
174
  // `Query.collides` tests raw geometry only and knows nothing about `collisionFilter` (see
137
175
  // `MatterCharacterControllerComponent.collectObstacles`'s own doc on this same gap) - call
138
176
  // matter's own `Detector.canCollide` directly so a character controller whose collision groups
@@ -164,6 +202,33 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
164
202
  }
165
203
  }
166
204
  }
205
+ /**
206
+ * Enter/exit of the rigid bodies matter's detector never pairs with this trigger (see
207
+ * {@link isPolled}): a kinematic platform moving in or out, a body asleep inside. Like the
208
+ * character poll below, `Query.collides` ignores `collisionFilter`, so `Detector.canCollide` is
209
+ * checked by hand.
210
+ */
211
+ checkPolledBodyOverlaps() {
212
+ for (const comp of this.world.children) {
213
+ if (comp instanceof MatterRigidBodyComponent &&
214
+ !(comp instanceof MatterTriggerComponent) &&
215
+ !this.currentOverlaps.has(comp) &&
216
+ this.isPolled(comp) &&
217
+ Detector.canCollide(this.nativeBody.collisionFilter, comp.nativeBody.collisionFilter) &&
218
+ this.overlaps(comp)) {
219
+ this.currentOverlaps.add(comp);
220
+ this.polledOverlaps.add(comp);
221
+ this.onEnter$.next(comp);
222
+ }
223
+ }
224
+ for (const comp of this.polledOverlaps) {
225
+ if (!this.overlaps(comp)) {
226
+ this.polledOverlaps.delete(comp);
227
+ this.currentOverlaps.delete(comp);
228
+ this.onLeft$.next(comp);
229
+ }
230
+ }
231
+ }
167
232
  clone() {
168
233
  const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
169
234
  const component = new MatterTriggerComponent(clonedBody, this.shape, this.world);
@@ -27,6 +27,18 @@ export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<Ma
27
27
  set gravity(value: Point2);
28
28
  readonly mainCollisionGroup: CollisionGroup;
29
29
  constructor();
30
+ /**
31
+ * `Engine.create` is never given `enableSleeping: true` here, matching matter-js's own default -
32
+ * so a body this world creates never *naturally* falls asleep from prolonged inactivity, no matter
33
+ * how long it rests. `IRigidBodyComponent.sleep()`/`wakeUp()` still work regardless of this flag
34
+ * (`Matter.Sleeping.set` writes a body's `isSleeping` flag directly, and `Engine.update`'s own
35
+ * per-body integration skip checks that flag unconditionally, not gated behind
36
+ * `engine.enableSleeping` - only the *automatic* fall-asleep-from-rest/wake-on-collision behavior
37
+ * is gated behind that flag) - only automatic, inactivity-driven sleeping is unavailable under the
38
+ * current setup. A future change enabling it should re-check
39
+ * `gg-engine-physics-adapter-matter`'s own note on this before assuming every existing
40
+ * position/velocity setter still behaves the same way once bodies can sleep on their own.
41
+ */
30
42
  init(): Promise<void>;
31
43
  private findRigidBody;
32
44
  /**
@@ -118,6 +118,18 @@ export class MatterWorldComponent {
118
118
  this.handleCollisionStart = this.handleCollisionStart.bind(this);
119
119
  this.handleCollisionEnd = this.handleCollisionEnd.bind(this);
120
120
  }
121
+ /**
122
+ * `Engine.create` is never given `enableSleeping: true` here, matching matter-js's own default -
123
+ * so a body this world creates never *naturally* falls asleep from prolonged inactivity, no matter
124
+ * how long it rests. `IRigidBodyComponent.sleep()`/`wakeUp()` still work regardless of this flag
125
+ * (`Matter.Sleeping.set` writes a body's `isSleeping` flag directly, and `Engine.update`'s own
126
+ * per-body integration skip checks that flag unconditionally, not gated behind
127
+ * `engine.enableSleeping` - only the *automatic* fall-asleep-from-rest/wake-on-collision behavior
128
+ * is gated behind that flag) - only automatic, inactivity-driven sleeping is unavailable under the
129
+ * current setup. A future change enabling it should re-check
130
+ * `gg-engine-physics-adapter-matter`'s own note on this before assuming every existing
131
+ * position/velocity setter still behaves the same way once bodies can sleep on their own.
132
+ */
121
133
  init() {
122
134
  return __awaiter(this, void 0, void 0, function* () {
123
135
  this.matterEngine_ = Engine.create({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/matter",
3
- "version": "0.0.73",
3
+ "version": "0.0.75",
4
4
  "description": "An attempt to create open source game engine for browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "poly-decomp": "^0.3.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@gg-web-engine/core": "0.0.73",
33
+ "@gg-web-engine/core": "0.0.75",
34
34
  "@types/jest": "^30.0.0",
35
35
  "@types/matter-js": "0.20.2",
36
36
  "@types/node": "^26.4.0",
@@ -43,7 +43,7 @@
43
43
  "typescript": "~6.0.3"
44
44
  },
45
45
  "peerDependencies": {
46
- "@gg-web-engine/core": "0.0.73",
46
+ "@gg-web-engine/core": "0.0.75",
47
47
  "matter-js": "0.20.0",
48
48
  "rxjs": "7.8.2"
49
49
  },
@@ -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
+ });
@@ -56,3 +56,43 @@ describe('MatterRigidBodyComponent.bodyOptions', () => {
56
56
  expect(body.bodyOptions.interactWithCollisionGroups).toEqual([5, 6]);
57
57
  });
58
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
+ });
@@ -19,6 +19,100 @@ describe(`MatterTriggerComponent`, () => {
19
19
  world.dispose();
20
20
  });
21
21
 
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
+
22
116
  // Note: this test is identical to rapier2d trigger test, and it is expected
23
117
  it(`should detect object intersection`, async () => {
24
118
  const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });