@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.
@@ -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);
@@ -28,15 +28,40 @@ export class MatterRigidBodyComponent {
28
28
  set angularVelocity(value) {
29
29
  Body.setAngularVelocity(this.nativeBody, value);
30
30
  }
31
+ /**
32
+ * See `IRigidBodyComponent.bodyOptions`'s own doc. `mass`/`friction`/`restitution` are read live
33
+ * off the native matter-js body (a plain JS object - `.mass`/`.friction`/`.restitution` are
34
+ * ordinary fields, resolved to matter-js's own defaults by `Body.create` for whichever of them
35
+ * weren't explicitly given, so this reflects the body's *actual* resolved values, not just
36
+ * whatever was requested). `bodyType`/`ccd` are stored as originally requested rather than
37
+ * derived from the native body: matter-js has no kinematic body concept at all (a requested
38
+ * `kinematic_pos`/`kinematic_vel` degrades to a plain `isStatic` body - see `MatterFactory
39
+ * .transformOptions`'s own doc) and no CCD, so neither is recoverable from - or even meaningfully
40
+ * "live" on - the native body itself; echoing the request instead keeps a level JSON reloaded
41
+ * under a different, kinematic/CCD-capable adapter faithful to what was actually asked for.
42
+ */
43
+ get bodyOptions() {
44
+ return {
45
+ bodyType: this.bodyType,
46
+ mass: this.nativeBody.mass,
47
+ friction: this.nativeBody.friction,
48
+ restitution: this.nativeBody.restitution,
49
+ ccd: this.ccd,
50
+ ownCollisionGroups: this.ownCollisionGroups,
51
+ interactWithCollisionGroups: this.interactWithCollisionGroups,
52
+ };
53
+ }
31
54
  get onCollisionStart() {
32
55
  return this.onCollisionStart$.asObservable();
33
56
  }
34
57
  get onCollisionEnd() {
35
58
  return this.onCollisionEnd$.asObservable();
36
59
  }
37
- constructor(nativeBody, shape) {
60
+ constructor(nativeBody, shape, bodyType = 'dynamic', ccd = false) {
38
61
  this.nativeBody = nativeBody;
39
62
  this.shape = shape;
63
+ this.bodyType = bodyType;
64
+ this.ccd = ccd;
40
65
  this.name = '';
41
66
  this.entity = null;
42
67
  // matter-js has no native kinematic body - a body requested as `kinematic_pos`/`kinematic_vel`
@@ -45,7 +70,7 @@ export class MatterRigidBodyComponent {
45
70
  // rather than what an app originally asked for - the console warning at creation time is what
46
71
  // tells a developer their kinematic request wasn't honored, not this view.
47
72
  this.debugBodySettings = new DebugBody2DSettings(isFinite(this.nativeBody.mass)
48
- ? { type: 'RIGID_DYNAMIC', sleeping: () => this.nativeBody.isSleeping }
73
+ ? { type: 'RIGID_DYNAMIC', sleeping: () => this.isSleeping }
49
74
  : { type: 'RIGID_STATIC' }, this.shape);
50
75
  this._interactWithCGsMask = BitMask.full(16);
51
76
  this._ownCGsMask = BitMask.full(16);
@@ -110,7 +135,7 @@ export class MatterRigidBodyComponent {
110
135
  }
111
136
  clone() {
112
137
  const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
113
- const component = new MatterRigidBodyComponent(clonedBody, this.shape);
138
+ const component = new MatterRigidBodyComponent(clonedBody, this.shape, this.bodyType, this.ccd);
114
139
  component.ownCollisionGroups = this.ownCollisionGroups;
115
140
  component.interactWithCollisionGroups = this.interactWithCollisionGroups;
116
141
  return component;
@@ -147,4 +172,42 @@ export class MatterRigidBodyComponent {
147
172
  Body.setVelocity(this.nativeBody, Pnt2.O);
148
173
  Body.setAngularVelocity(this.nativeBody, 0);
149
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
+ }
150
213
  }
@@ -1,21 +1,61 @@
1
1
  import { Observable, Subject } from 'rxjs';
2
2
  import { Body } from 'matter-js';
3
3
  import { MatterRigidBodyComponent } from './matter-rigid-body.component';
4
+ import { MatterCharacterControllerComponent } from './matter-character-controller.component';
4
5
  import { DebugBody2DSettings, ITrigger2dComponent, Shape2DDescriptor } from '@gg-web-engine/core';
5
6
  import { MatterWorldComponent } from './matter-world.component';
6
7
  import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
7
8
  export declare class MatterTriggerComponent extends MatterRigidBodyComponent implements ITrigger2dComponent<MatterPhysicsTypeDocRepo> {
8
9
  readonly shape: Shape2DDescriptor;
9
10
  protected readonly world: MatterWorldComponent;
10
- get onEntityEntered(): Observable<MatterRigidBodyComponent>;
11
- get onEntityLeft(): Observable<MatterRigidBodyComponent>;
12
- protected readonly onEnter$: Subject<MatterRigidBodyComponent>;
13
- protected readonly onLeft$: Subject<MatterRigidBodyComponent>;
11
+ get onEntityEntered(): Observable<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
12
+ get onEntityLeft(): Observable<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
13
+ protected readonly onEnter$: Subject<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
14
+ protected readonly onLeft$: Subject<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
15
+ /** Character controllers currently overlapping this trigger, as of the last `checkOverlaps()`
16
+ * poll - see that method's own doc for why this needs its own separate polling mechanism instead
17
+ * of the native `collisionStart`/`collisionEnd` events `handleCollisionStart`/`handleCollisionEnd`
18
+ * below rely on for ordinary rigid bodies. */
19
+ protected currentCharacterOverlaps: Set<MatterCharacterControllerComponent>;
14
20
  readonly debugBodySettings: DebugBody2DSettings;
15
21
  protected intersectionsAmount: number;
16
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>;
27
+ /** `Composite.remove` (what `removeFromWorld` calls) never fires a native `collisionEnd` for the
28
+ * body it removes - matter-js simply stops considering that body's pairs on the next step, it
29
+ * doesn't retroactively report the pairs that were active at removal time. Without this, a body
30
+ * removed from the world while still overlapping this trigger would leave `currentOverlaps`
31
+ * (and, for a character, `currentCharacterOverlaps`) permanently stale and `onEntityLeft` would
32
+ * never fire for it. `world.physicsWorld.removed$` fires for every component removal regardless
33
+ * of overlap state, so this only actually acts when the removed component is one this trigger was
34
+ * still tracking as an overlap.
35
+ *
36
+ * The stale entry is dropped synchronously, but `onLeft$` itself is only ever emitted on a
37
+ * microtask from this reaction, never inline. `removed$` fires from inside whatever call stack
38
+ * performed the removal - which can be another component's own in-progress lifecycle operation:
39
+ * `CharacterController2dEntity.recreateCapsule()` (crouch/stand) removes the *old*
40
+ * `characterController` with `dispose: true` before it has assigned the new one, so an `onLeft$`
41
+ * subscriber running inline there (app code driven by `Trigger2dEntity`, e.g. a kill volume
42
+ * resetting `.position` on the character that just left it) would write through the old,
43
+ * already-removed `MatterCharacterControllerComponent` - `Body.setPosition` on a body no longer
44
+ * queried by anything - and the write is silently lost, with the replacement capsule then spawning
45
+ * at the stale position. Deferring the emission keeps it within the same JS turn (well before the
46
+ * next `Engine.update`) but strictly after every synchronous call stack in flight, including the
47
+ * removal's own, has unwound. */
48
+ private removedSub?;
17
49
  private handleCollisionStart;
18
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;
19
59
  constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
20
60
  addToWorld(world: MatterGgWorld): void;
21
61
  removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
@@ -23,6 +63,28 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
23
63
  * `onCollisionStart$`/`onCollisionEnd$` completion (via `super.dispose()`) - this trigger's own
24
64
  * enter/exit subjects are a separate pair this subclass owns and must complete itself. */
25
65
  dispose(): void;
66
+ /**
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
71
+ * deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
72
+ * native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
73
+ * already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
74
+ * to add the poll this needs instead of inventing a second, differently-shaped mechanism: every
75
+ * `MatterCharacterControllerComponent` currently in the world (`world.children`, which - unlike
76
+ * matter's own `Composite` - already tracks it) is tested against this trigger's own body via
77
+ * `Query.collides`, diffed against `currentCharacterOverlaps` to fire `onEntityEntered`/
78
+ * `onEntityLeft` exactly on the enter/exit transitions, the same as the native-event path does for
79
+ * ordinary bodies.
80
+ */
26
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;
27
89
  clone(): MatterTriggerComponent;
28
90
  }
@@ -1,6 +1,7 @@
1
1
  import { map, merge, Subject } from 'rxjs';
2
- import { Body, Events } from 'matter-js';
2
+ import { Body, Detector, Events, Query } from 'matter-js';
3
3
  import { MatterRigidBodyComponent } from './matter-rigid-body.component';
4
+ import { MatterCharacterControllerComponent } from './matter-character-controller.component';
4
5
  import { DebugBody2DSettings } from '@gg-web-engine/core';
5
6
  export class MatterTriggerComponent extends MatterRigidBodyComponent {
6
7
  get onEntityEntered() {
@@ -20,7 +21,13 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
20
21
  }
21
22
  if (body) {
22
23
  let comp = this.world.children.find(c => c.nativeBody === body);
23
- if (comp) {
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
+ }
30
+ this.currentOverlaps.add(comp);
24
31
  this.onEnter$.next(comp);
25
32
  }
26
33
  }
@@ -37,22 +44,56 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
37
44
  }
38
45
  if (body) {
39
46
  let comp = this.world.children.find(c => c.nativeBody === body);
40
- if (comp) {
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
+ }
53
+ this.currentOverlaps.delete(comp);
54
+ this.polledOverlaps.delete(comp);
41
55
  this.onLeft$.next(comp);
42
56
  }
43
57
  }
44
58
  }
45
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
+ }
46
72
  constructor(nativeBody, shape, world) {
47
73
  super(nativeBody, shape);
48
74
  this.shape = shape;
49
75
  this.world = world;
50
76
  this.onEnter$ = new Subject();
51
77
  this.onLeft$ = new Subject();
78
+ /** Character controllers currently overlapping this trigger, as of the last `checkOverlaps()`
79
+ * poll - see that method's own doc for why this needs its own separate polling mechanism instead
80
+ * of the native `collisionStart`/`collisionEnd` events `handleCollisionStart`/`handleCollisionEnd`
81
+ * below rely on for ordinary rigid bodies. */
82
+ this.currentCharacterOverlaps = new Set();
52
83
  this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
53
84
  this.intersectionsAmount = 0;
54
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();
55
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);
56
97
  merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
57
98
  if (enter) {
58
99
  this.intersectionsAmount++;
@@ -70,17 +111,39 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
70
111
  }
71
112
  this.intersectionsAmount = 0;
72
113
  this.currentOverlaps.clear();
114
+ this.polledOverlaps.clear();
73
115
  super.addToWorld(world);
74
116
  Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
75
117
  Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
118
+ this.removedSub = world.physicsWorld.removed$.subscribe(c => {
119
+ if (c === this) {
120
+ return;
121
+ }
122
+ if (c instanceof MatterCharacterControllerComponent) {
123
+ if (this.currentCharacterOverlaps.delete(c)) {
124
+ queueMicrotask(() => this.onLeft$.next(c));
125
+ }
126
+ }
127
+ else if (c instanceof MatterRigidBodyComponent && this.currentOverlaps.delete(c)) {
128
+ this.polledOverlaps.delete(c);
129
+ queueMicrotask(() => this.onLeft$.next(c));
130
+ }
131
+ });
76
132
  }
77
133
  removeFromWorld(world, dispose) {
134
+ var _a;
78
135
  Events.off(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
79
136
  Events.off(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
137
+ (_a = this.removedSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
80
138
  for (const body of this.currentOverlaps) {
81
139
  this.onLeft$.next(body);
82
140
  }
83
141
  this.currentOverlaps.clear();
142
+ this.polledOverlaps.clear();
143
+ for (const character of this.currentCharacterOverlaps) {
144
+ this.onLeft$.next(character);
145
+ }
146
+ this.currentCharacterOverlaps.clear();
84
147
  super.removeFromWorld(world, dispose);
85
148
  }
86
149
  /** Completes `onEnter$`/`onLeft$` on top of `MatterRigidBodyComponent.dispose()`'s own
@@ -91,8 +154,80 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
91
154
  this.onLeft$.complete();
92
155
  super.dispose();
93
156
  }
157
+ /**
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
162
+ * deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
163
+ * native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
164
+ * already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
165
+ * to add the poll this needs instead of inventing a second, differently-shaped mechanism: every
166
+ * `MatterCharacterControllerComponent` currently in the world (`world.children`, which - unlike
167
+ * matter's own `Composite` - already tracks it) is tested against this trigger's own body via
168
+ * `Query.collides`, diffed against `currentCharacterOverlaps` to fire `onEntityEntered`/
169
+ * `onEntityLeft` exactly on the enter/exit transitions, the same as the native-event path does for
170
+ * ordinary bodies.
171
+ */
94
172
  checkOverlaps() {
95
- // do nothing, for matter.js we handle this differently
173
+ this.checkPolledBodyOverlaps();
174
+ // `Query.collides` tests raw geometry only and knows nothing about `collisionFilter` (see
175
+ // `MatterCharacterControllerComponent.collectObstacles`'s own doc on this same gap) - call
176
+ // matter's own `Detector.canCollide` directly so a character controller whose collision groups
177
+ // wouldn't ordinarily interact with this trigger isn't falsely reported entering it just because
178
+ // this poll bypasses the broadphase that would otherwise exclude it.
179
+ const characters = this.world.children.filter((c) => c instanceof MatterCharacterControllerComponent &&
180
+ Detector.canCollide(this.nativeBody.collisionFilter, c.nativeBody.collisionFilter));
181
+ const stillOverlapping = new Set();
182
+ if (characters.length > 0) {
183
+ const collisions = Query.collides(this.nativeBody, characters.map(c => c.nativeBody));
184
+ for (const collision of collisions) {
185
+ const otherNative = collision.parentA === this.nativeBody ? collision.parentB : collision.parentA;
186
+ const comp = characters.find(c => c.nativeBody === otherNative);
187
+ if (comp) {
188
+ stillOverlapping.add(comp);
189
+ }
190
+ }
191
+ }
192
+ for (const comp of stillOverlapping) {
193
+ if (!this.currentCharacterOverlaps.has(comp)) {
194
+ this.currentCharacterOverlaps.add(comp);
195
+ this.onEnter$.next(comp);
196
+ }
197
+ }
198
+ for (const comp of this.currentCharacterOverlaps) {
199
+ if (!stillOverlapping.has(comp)) {
200
+ this.currentCharacterOverlaps.delete(comp);
201
+ this.onLeft$.next(comp);
202
+ }
203
+ }
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
+ }
96
231
  }
97
232
  clone() {
98
233
  const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
@@ -5,24 +5,40 @@ import { MatterPhysicsTypeDocRepo } from '../types';
5
5
  import { Subject } from 'rxjs';
6
6
  import { MatterRigidBodyComponent } from './matter-rigid-body.component';
7
7
  import { MatterTriggerComponent } from './matter-trigger.component';
8
+ import { MatterCharacterControllerComponent } from './matter-character-controller.component';
9
+ type MatterWorldChild = MatterRigidBodyComponent | MatterTriggerComponent | MatterCharacterControllerComponent;
8
10
  export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<MatterPhysicsTypeDocRepo> {
9
11
  protected matterEngine_: Engine | null;
10
12
  get matterEngine(): Engine | null;
11
13
  get matterWorld(): World | null;
12
14
  readonly factory: MatterFactory;
13
- readonly added$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
14
- readonly removed$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
15
- readonly children: (MatterRigidBodyComponent | MatterTriggerComponent)[];
15
+ readonly added$: Subject<MatterWorldChild>;
16
+ readonly removed$: Subject<MatterWorldChild>;
17
+ readonly children: MatterWorldChild[];
16
18
  /** Mirrors the rapier packages' `handleIdEntityMap` pattern: `Body.id` (matter-js's own
17
19
  * globally-unique numeric id, assigned once per body via `Body.nextId` and stable for its whole
18
20
  * lifetime) to component, kept in sync alongside `children` so `findRigidBody` - called once per
19
- * collision pair, per step - is an O(1) lookup instead of an O(n) `Array.find` scan. */
20
- readonly handleIdEntityMap: Map<number, MatterRigidBodyComponent>;
21
+ * collision pair, per step - is an O(1) lookup instead of an O(n) `Array.find` scan. Also used by
22
+ * `MatterCharacterControllerComponent.pushDynamicBodies` to resolve a native body it just bumped
23
+ * into back to its owning component. */
24
+ readonly handleIdEntityMap: Map<number, MatterWorldChild>;
21
25
  private _gravity;
22
26
  get gravity(): Point2;
23
27
  set gravity(value: Point2);
24
28
  readonly mainCollisionGroup: CollisionGroup;
25
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
+ */
26
42
  init(): Promise<void>;
27
43
  private findRigidBody;
28
44
  /**
@@ -54,6 +70,29 @@ export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<Ma
54
70
  deregisterCollisionGroup(group: CollisionGroup): void;
55
71
  private lastDelta;
56
72
  simulate(delta: number): void;
73
+ /**
74
+ * matter-js has no native raycast query. Built on a true parametric ray-vs-polygon intersection
75
+ * (`segmentIntersection`/`bodyPolygons`), not `Matter.Query.ray` - that helper is only a thin
76
+ * wrapper over `Query.collides` (a full-geometry SAT test of a synthetic, very thin rectangle body
77
+ * against candidates), which reports an approximate overlap contact point, not a true "where does
78
+ * the ray segment first cross this body's boundary" point; verified via this adapter's own
79
+ * regression suite (`MatterWorldComponent.spec.ts`'s `Raycast` block expects a precise hit point at
80
+ * a known box edge) that the SAT-approximated version produces a visibly wrong point.
81
+ *
82
+ * `Query.collides`/`Collision.collides` test raw geometry only and know nothing about
83
+ * `collisionFilter` (the same limitation `MatterCharacterControllerComponent.collectObstacles`'s
84
+ * own doc describes) - candidates are pre-filtered here via `Detector.canCollide` itself, called
85
+ * directly against a synthetic filter built from `options.collisionFilterGroups`/
86
+ * `collisionFilterMask`. Sensor bodies (triggers) are excluded from candidates entirely, mirroring
87
+ * `collectObstacles`'s own exclusion - a trigger never physically blocks anything, so it shouldn't
88
+ * register as a raycast hit either.
89
+ *
90
+ * `hitNormal` is derived from whichever polygon edge the closest intersection landed on (rotated
91
+ * 90°, sign chosen to point back towards `options.from`) - not from a matter-js collision object at
92
+ * all, sidestepping the sign-convention pitfall `handleCollisionStart`'s own doc describes for
93
+ * `pair.collision.normal`.
94
+ */
57
95
  raycast(options: RaycastOptions<Point2>): RaycastResult<Point2, MatterRigidBodyComponent>;
58
96
  dispose(): void;
59
97
  }
98
+ export {};