@gg-web-engine/matter 0.0.66 → 0.0.68
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-rigid-body.component.d.ts +18 -1
- package/dist/components/matter-rigid-body.component.js +45 -4
- package/dist/components/matter-trigger.component.d.ts +5 -4
- package/dist/components/matter-trigger.component.js +11 -10
- package/dist/components/matter-world.component.d.ts +30 -0
- package/dist/components/matter-world.component.js +113 -3
- package/package.json +3 -3
- package/test/components/matter-rigid-body-collision.spec.ts +187 -0
- package/test/components/matter-world.component.spec.ts +114 -65
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CollisionGroup, DebugBody2DSettings, Entity2d, IRigidBody2dComponent, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
1
|
+
import { CollisionEvent, CollisionGroup, DebugBody2DSettings, Entity2d, IRigidBody2dComponent, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
2
2
|
import { Body } from 'matter-js';
|
|
3
|
+
import { Observable, Subject } from 'rxjs';
|
|
3
4
|
import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
|
|
4
5
|
export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<MatterPhysicsTypeDocRepo> {
|
|
5
6
|
nativeBody: Body;
|
|
@@ -17,7 +18,23 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
|
|
|
17
18
|
readonly debugBodySettings: DebugBody2DSettings;
|
|
18
19
|
protected _interactWithCGsMask: number;
|
|
19
20
|
protected _ownCGsMask: number;
|
|
21
|
+
get onCollisionStart(): Observable<CollisionEvent<Point2, MatterRigidBodyComponent>>;
|
|
22
|
+
get onCollisionEnd(): Observable<MatterRigidBodyComponent | null>;
|
|
23
|
+
protected readonly onCollisionStart$: Subject<CollisionEvent<Point2, MatterRigidBodyComponent>>;
|
|
24
|
+
protected readonly onCollisionEnd$: Subject<MatterRigidBodyComponent | null>;
|
|
25
|
+
/** Other rigid bodies this body is currently touching (non-sensor contact only), tracked so
|
|
26
|
+
* `removeFromWorld` can tell them apart to emit `onCollisionEnd(null)` per that member's
|
|
27
|
+
* documented "other body removed while still in contact" case. Maintained exclusively via
|
|
28
|
+
* `notifyCollisionStart`/`notifyCollisionEnd`, called by `MatterWorldComponent`'s single
|
|
29
|
+
* world-wide `collisionStart`/`collisionEnd` listener - not touched directly by anything else. */
|
|
30
|
+
protected readonly currentContacts: Set<MatterRigidBodyComponent>;
|
|
20
31
|
constructor(nativeBody: Body, shape: Shape2DDescriptor);
|
|
32
|
+
/** @internal called by `MatterWorldComponent`'s global `collisionStart` listener - not part of
|
|
33
|
+
* this component's public API surface. */
|
|
34
|
+
notifyCollisionStart(collisionEvent: CollisionEvent<Point2, MatterRigidBodyComponent>): void;
|
|
35
|
+
/** @internal called by `MatterWorldComponent`'s global `collisionEnd` listener - not part of
|
|
36
|
+
* this component's public API surface. */
|
|
37
|
+
notifyCollisionEnd(otherBody: MatterRigidBodyComponent | null): void;
|
|
21
38
|
get interactWithCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
22
39
|
set interactWithCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
23
40
|
get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BitMask, DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
|
|
2
2
|
import { Body, Composite, Vector } from 'matter-js';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
3
4
|
// FIXME why this needs to be introduced? investigate units in matter.js
|
|
4
5
|
const MATTER_VELOCITY_SCALE = 0.0166667;
|
|
5
6
|
export class MatterRigidBodyComponent {
|
|
@@ -27,6 +28,12 @@ export class MatterRigidBodyComponent {
|
|
|
27
28
|
set angularVelocity(value) {
|
|
28
29
|
Body.setAngularVelocity(this.nativeBody, value);
|
|
29
30
|
}
|
|
31
|
+
get onCollisionStart() {
|
|
32
|
+
return this.onCollisionStart$.asObservable();
|
|
33
|
+
}
|
|
34
|
+
get onCollisionEnd() {
|
|
35
|
+
return this.onCollisionEnd$.asObservable();
|
|
36
|
+
}
|
|
30
37
|
constructor(nativeBody, shape) {
|
|
31
38
|
this.nativeBody = nativeBody;
|
|
32
39
|
this.shape = shape;
|
|
@@ -37,8 +44,30 @@ export class MatterRigidBodyComponent {
|
|
|
37
44
|
: { type: 'RIGID_STATIC' }, this.shape);
|
|
38
45
|
this._interactWithCGsMask = BitMask.full(16);
|
|
39
46
|
this._ownCGsMask = BitMask.full(16);
|
|
47
|
+
this.onCollisionStart$ = new Subject();
|
|
48
|
+
this.onCollisionEnd$ = new Subject();
|
|
49
|
+
/** Other rigid bodies this body is currently touching (non-sensor contact only), tracked so
|
|
50
|
+
* `removeFromWorld` can tell them apart to emit `onCollisionEnd(null)` per that member's
|
|
51
|
+
* documented "other body removed while still in contact" case. Maintained exclusively via
|
|
52
|
+
* `notifyCollisionStart`/`notifyCollisionEnd`, called by `MatterWorldComponent`'s single
|
|
53
|
+
* world-wide `collisionStart`/`collisionEnd` listener - not touched directly by anything else. */
|
|
54
|
+
this.currentContacts = new Set();
|
|
40
55
|
this.updateCollisionFilter();
|
|
41
56
|
}
|
|
57
|
+
/** @internal called by `MatterWorldComponent`'s global `collisionStart` listener - not part of
|
|
58
|
+
* this component's public API surface. */
|
|
59
|
+
notifyCollisionStart(collisionEvent) {
|
|
60
|
+
this.currentContacts.add(collisionEvent.otherBody);
|
|
61
|
+
this.onCollisionStart$.next(collisionEvent);
|
|
62
|
+
}
|
|
63
|
+
/** @internal called by `MatterWorldComponent`'s global `collisionEnd` listener - not part of
|
|
64
|
+
* this component's public API surface. */
|
|
65
|
+
notifyCollisionEnd(otherBody) {
|
|
66
|
+
if (otherBody) {
|
|
67
|
+
this.currentContacts.delete(otherBody);
|
|
68
|
+
}
|
|
69
|
+
this.onCollisionEnd$.next(otherBody);
|
|
70
|
+
}
|
|
42
71
|
get interactWithCollisionGroups() {
|
|
43
72
|
return BitMask.unpack(this._interactWithCGsMask, 16);
|
|
44
73
|
}
|
|
@@ -88,15 +117,27 @@ export class MatterRigidBodyComponent {
|
|
|
88
117
|
removeFromWorld(world, dispose = false) {
|
|
89
118
|
Composite.remove(world.physicsWorld.matterWorld, this.nativeBody);
|
|
90
119
|
world.physicsWorld.removed$.next(this);
|
|
120
|
+
// this body is leaving the world while still touching others - per `onCollisionEnd`'s own
|
|
121
|
+
// contract, each of those bodies sees `null` (no further contact geometry is available), not a
|
|
122
|
+
// dangling reference to a body no longer in the world.
|
|
123
|
+
for (const other of this.currentContacts) {
|
|
124
|
+
other.notifyCollisionEnd(null);
|
|
125
|
+
other.currentContacts.delete(this);
|
|
126
|
+
}
|
|
127
|
+
this.currentContacts.clear();
|
|
91
128
|
if (dispose) {
|
|
92
129
|
this.dispose();
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
132
|
// matter.js bodies are plain JS objects with no native/WASM handle - ordinary GC reclaims them
|
|
96
|
-
// once `Composite.remove` above drops the engine's own reference, so there's nothing to free
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
dispose()
|
|
133
|
+
// once `Composite.remove` above drops the engine's own reference, so there's nothing to free
|
|
134
|
+
// beyond completing this component's own RxJS subjects. `MatterTriggerComponent` overrides this
|
|
135
|
+
// to also complete its own `onEnter$`/`onLeft$` subjects, calling back into this via `super.
|
|
136
|
+
// dispose()`.
|
|
137
|
+
dispose() {
|
|
138
|
+
this.onCollisionStart$.complete();
|
|
139
|
+
this.onCollisionEnd$.complete();
|
|
140
|
+
}
|
|
100
141
|
resetMotion() {
|
|
101
142
|
Body.setVelocity(this.nativeBody, Pnt2.O);
|
|
102
143
|
Body.setAngularVelocity(this.nativeBody, 0);
|
|
@@ -14,13 +14,14 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
14
14
|
readonly debugBodySettings: DebugBody2DSettings;
|
|
15
15
|
protected intersectionsAmount: number;
|
|
16
16
|
protected currentOverlaps: Set<MatterRigidBodyComponent>;
|
|
17
|
-
private
|
|
18
|
-
private
|
|
17
|
+
private handleCollisionStart;
|
|
18
|
+
private handleCollisionEnd;
|
|
19
19
|
constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
|
|
20
20
|
addToWorld(world: MatterGgWorld): void;
|
|
21
21
|
removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
|
|
22
|
-
/** Completes `onEnter$`/`onLeft$`
|
|
23
|
-
*
|
|
22
|
+
/** Completes `onEnter$`/`onLeft$` on top of `MatterRigidBodyComponent.dispose()`'s own
|
|
23
|
+
* `onCollisionStart$`/`onCollisionEnd$` completion (via `super.dispose()`) - this trigger's own
|
|
24
|
+
* enter/exit subjects are a separate pair this subclass owns and must complete itself. */
|
|
24
25
|
dispose(): void;
|
|
25
26
|
checkOverlaps(): void;
|
|
26
27
|
clone(): MatterTriggerComponent;
|
|
@@ -9,7 +9,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
9
9
|
get onEntityLeft() {
|
|
10
10
|
return this.onLeft$.asObservable();
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
handleCollisionStart(event) {
|
|
13
13
|
for (const pair of event.pairs) {
|
|
14
14
|
let body = null;
|
|
15
15
|
if (pair.bodyA === this.nativeBody) {
|
|
@@ -26,7 +26,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
handleCollisionEnd(event) {
|
|
30
30
|
for (const pair of event.pairs) {
|
|
31
31
|
let body = null;
|
|
32
32
|
if (pair.bodyA === this.nativeBody) {
|
|
@@ -61,8 +61,8 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
61
61
|
this.intersectionsAmount--;
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
64
|
+
this.handleCollisionStart = this.handleCollisionStart.bind(this);
|
|
65
|
+
this.handleCollisionEnd = this.handleCollisionEnd.bind(this);
|
|
66
66
|
}
|
|
67
67
|
addToWorld(world) {
|
|
68
68
|
if (world.physicsWorld != this.world) {
|
|
@@ -71,20 +71,21 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
71
71
|
this.intersectionsAmount = 0;
|
|
72
72
|
this.currentOverlaps.clear();
|
|
73
73
|
super.addToWorld(world);
|
|
74
|
-
Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.
|
|
75
|
-
Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.
|
|
74
|
+
Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
75
|
+
Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
76
76
|
}
|
|
77
77
|
removeFromWorld(world, dispose) {
|
|
78
|
-
Events.off(world.physicsWorld.matterEngine, 'collisionStart', this.
|
|
79
|
-
Events.off(world.physicsWorld.matterEngine, 'collisionEnd', this.
|
|
78
|
+
Events.off(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
79
|
+
Events.off(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
80
80
|
for (const body of this.currentOverlaps) {
|
|
81
81
|
this.onLeft$.next(body);
|
|
82
82
|
}
|
|
83
83
|
this.currentOverlaps.clear();
|
|
84
84
|
super.removeFromWorld(world, dispose);
|
|
85
85
|
}
|
|
86
|
-
/** Completes `onEnter$`/`onLeft$`
|
|
87
|
-
*
|
|
86
|
+
/** Completes `onEnter$`/`onLeft$` on top of `MatterRigidBodyComponent.dispose()`'s own
|
|
87
|
+
* `onCollisionStart$`/`onCollisionEnd$` completion (via `super.dispose()`) - this trigger's own
|
|
88
|
+
* enter/exit subjects are a separate pair this subclass owns and must complete itself. */
|
|
88
89
|
dispose() {
|
|
89
90
|
this.onEnter$.complete();
|
|
90
91
|
this.onLeft$.complete();
|
|
@@ -13,12 +13,42 @@ export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<Ma
|
|
|
13
13
|
readonly added$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
|
|
14
14
|
readonly removed$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
|
|
15
15
|
readonly children: (MatterRigidBodyComponent | MatterTriggerComponent)[];
|
|
16
|
+
/** Mirrors the rapier packages' `handleIdEntityMap` pattern: `Body.id` (matter-js's own
|
|
17
|
+
* globally-unique numeric id, assigned once per body via `Body.nextId` and stable for its whole
|
|
18
|
+
* 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>;
|
|
16
21
|
private _gravity;
|
|
17
22
|
get gravity(): Point2;
|
|
18
23
|
set gravity(value: Point2);
|
|
19
24
|
readonly mainCollisionGroup: CollisionGroup;
|
|
20
25
|
constructor();
|
|
21
26
|
init(): Promise<void>;
|
|
27
|
+
private findRigidBody;
|
|
28
|
+
/**
|
|
29
|
+
* Wires `IRigidBody2dComponent.onCollisionStart` for every non-sensor pair in the world, off
|
|
30
|
+
* matter-js's own `Matter.Events` 'collisionStart' (fired once per engine, not per-body - see
|
|
31
|
+
* `gg-engine-physics-adapter-matter` for why it's registered here rather than per component).
|
|
32
|
+
* `pair.isSensor` (true whenever either side is a trigger's underlying body, which always has
|
|
33
|
+
* `isSensor: true` - see `MatterTriggerComponent`'s constructor) is skipped entirely, so a
|
|
34
|
+
* trigger overlapping a rigid body only ever fires the trigger's own `onEntityEntered`, never
|
|
35
|
+
* this.
|
|
36
|
+
*
|
|
37
|
+
* Impulse: at the point this event fires, matter-js hasn't run `Resolver.solveVelocity` yet for
|
|
38
|
+
* this step (that happens later in `Engine.update`, whose event order is
|
|
39
|
+
* collisionStart → position solve → velocity solve → collisionActive → collisionEnd) - so a
|
|
40
|
+
* freshly-created pair's `pair.contacts[*].normalImpulse`/`tangentImpulse` are still their
|
|
41
|
+
* just-initialized `0`, not yet a meaningful number, for every pair this event could ever
|
|
42
|
+
* report. Rather than reading always-zero data, `impulse` here is estimated as
|
|
43
|
+
* `|relativeVelocity| * min(massA, massB)` (a rough "how hard did they hit" proxy - a static
|
|
44
|
+
* body's `mass` is `Infinity`, so `min` naturally reduces to the dynamic side's mass when one
|
|
45
|
+
* side is static).
|
|
46
|
+
*/
|
|
47
|
+
private handleCollisionStart;
|
|
48
|
+
/** Wires `IRigidBody2dComponent.onCollisionEnd` for every non-sensor pair that just stopped
|
|
49
|
+
* touching - see `handleCollisionStart`'s doc for why sensor pairs are skipped and why this is
|
|
50
|
+
* registered once per engine rather than per-body. */
|
|
51
|
+
private handleCollisionEnd;
|
|
22
52
|
protected lockedCollisionGroups: number[];
|
|
23
53
|
registerCollisionGroup(): CollisionGroup;
|
|
24
54
|
deregisterCollisionGroup(group: CollisionGroup): void;
|
|
@@ -7,9 +7,33 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import {
|
|
10
|
+
import { Pnt2, } from '@gg-web-engine/core';
|
|
11
|
+
import { Engine, Events } from 'matter-js';
|
|
11
12
|
import { MatterFactory } from '../matter-factory';
|
|
12
13
|
import { Subject } from 'rxjs';
|
|
14
|
+
/**
|
|
15
|
+
* Averages matter-js's collision support points into a single representative contact point for
|
|
16
|
+
* `CollisionEvent.position`. `collision.supports` is *always* a fixed-length-2 array
|
|
17
|
+
* (`[null, null]` initially - see `Collision.js`'s `Collision.create`), but `@types/matter-js`
|
|
18
|
+
* 0.20.2 types it as plain `Vector[]` with no way to tell which slots are actually populated -
|
|
19
|
+
* only the first `collision.supportCount` entries are ever real, the rest can be `null` (or stale
|
|
20
|
+
* leftovers from a previous collision reusing the same record). Reading past `supportCount` (or
|
|
21
|
+
* trusting `.length`, always 2) crashes on the `null` slot - always slice to `supportCount` first.
|
|
22
|
+
*/
|
|
23
|
+
function averageSupports(collision) {
|
|
24
|
+
// `supportCount` is real at runtime (see `Collision.js`) but missing from `@types/matter-js`
|
|
25
|
+
// 0.20.2's `Collision` typing entirely - cast narrowly just for this one field.
|
|
26
|
+
const supportCount = collision.supportCount;
|
|
27
|
+
const supports = collision.supports.slice(0, supportCount);
|
|
28
|
+
if (supports.length === 0) {
|
|
29
|
+
return Pnt2.O;
|
|
30
|
+
}
|
|
31
|
+
let sum = Pnt2.O;
|
|
32
|
+
for (const support of supports) {
|
|
33
|
+
sum = Pnt2.add(sum, support);
|
|
34
|
+
}
|
|
35
|
+
return Pnt2.scalarMult(sum, 1 / supports.length);
|
|
36
|
+
}
|
|
13
37
|
// TODO probably should be configurable in world
|
|
14
38
|
const MATTER_WORLD_SCALE = 0.0001;
|
|
15
39
|
export class MatterWorldComponent {
|
|
@@ -34,21 +58,105 @@ export class MatterWorldComponent {
|
|
|
34
58
|
this.added$ = new Subject();
|
|
35
59
|
this.removed$ = new Subject();
|
|
36
60
|
this.children = [];
|
|
61
|
+
/** Mirrors the rapier packages' `handleIdEntityMap` pattern: `Body.id` (matter-js's own
|
|
62
|
+
* globally-unique numeric id, assigned once per body via `Body.nextId` and stable for its whole
|
|
63
|
+
* lifetime) to component, kept in sync alongside `children` so `findRigidBody` - called once per
|
|
64
|
+
* collision pair, per step - is an O(1) lookup instead of an O(n) `Array.find` scan. */
|
|
65
|
+
this.handleIdEntityMap = new Map();
|
|
37
66
|
this._gravity = { x: 0, y: 9.82 };
|
|
38
67
|
this.mainCollisionGroup = 0;
|
|
39
68
|
this.lockedCollisionGroups = [];
|
|
40
69
|
this.lastDelta = 0;
|
|
41
|
-
this.added$.subscribe(c =>
|
|
42
|
-
|
|
70
|
+
this.added$.subscribe(c => {
|
|
71
|
+
this.children.push(c);
|
|
72
|
+
this.handleIdEntityMap.set(c.nativeBody.id, c);
|
|
73
|
+
});
|
|
74
|
+
this.removed$.subscribe(c => {
|
|
75
|
+
this.children.splice(this.children.indexOf(c), 1);
|
|
76
|
+
this.handleIdEntityMap.delete(c.nativeBody.id);
|
|
77
|
+
});
|
|
43
78
|
this.factory = new MatterFactory(this);
|
|
79
|
+
this.handleCollisionStart = this.handleCollisionStart.bind(this);
|
|
80
|
+
this.handleCollisionEnd = this.handleCollisionEnd.bind(this);
|
|
44
81
|
}
|
|
45
82
|
init() {
|
|
46
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
84
|
this.matterEngine_ = Engine.create({
|
|
48
85
|
gravity: Object.assign(Object.assign({}, this._gravity), { scale: MATTER_WORLD_SCALE }),
|
|
49
86
|
});
|
|
87
|
+
Events.on(this.matterEngine_, 'collisionStart', this.handleCollisionStart);
|
|
88
|
+
Events.on(this.matterEngine_, 'collisionEnd', this.handleCollisionEnd);
|
|
50
89
|
});
|
|
51
90
|
}
|
|
91
|
+
findRigidBody(nativeBody) {
|
|
92
|
+
return this.handleIdEntityMap.get(nativeBody.id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wires `IRigidBody2dComponent.onCollisionStart` for every non-sensor pair in the world, off
|
|
96
|
+
* matter-js's own `Matter.Events` 'collisionStart' (fired once per engine, not per-body - see
|
|
97
|
+
* `gg-engine-physics-adapter-matter` for why it's registered here rather than per component).
|
|
98
|
+
* `pair.isSensor` (true whenever either side is a trigger's underlying body, which always has
|
|
99
|
+
* `isSensor: true` - see `MatterTriggerComponent`'s constructor) is skipped entirely, so a
|
|
100
|
+
* trigger overlapping a rigid body only ever fires the trigger's own `onEntityEntered`, never
|
|
101
|
+
* this.
|
|
102
|
+
*
|
|
103
|
+
* Impulse: at the point this event fires, matter-js hasn't run `Resolver.solveVelocity` yet for
|
|
104
|
+
* this step (that happens later in `Engine.update`, whose event order is
|
|
105
|
+
* collisionStart → position solve → velocity solve → collisionActive → collisionEnd) - so a
|
|
106
|
+
* freshly-created pair's `pair.contacts[*].normalImpulse`/`tangentImpulse` are still their
|
|
107
|
+
* just-initialized `0`, not yet a meaningful number, for every pair this event could ever
|
|
108
|
+
* report. Rather than reading always-zero data, `impulse` here is estimated as
|
|
109
|
+
* `|relativeVelocity| * min(massA, massB)` (a rough "how hard did they hit" proxy - a static
|
|
110
|
+
* body's `mass` is `Infinity`, so `min` naturally reduces to the dynamic side's mass when one
|
|
111
|
+
* side is static).
|
|
112
|
+
*/
|
|
113
|
+
handleCollisionStart(event) {
|
|
114
|
+
for (const pair of event.pairs) {
|
|
115
|
+
if (pair.isSensor) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const compA = this.findRigidBody(pair.bodyA);
|
|
119
|
+
const compB = this.findRigidBody(pair.bodyB);
|
|
120
|
+
if (!compA || !compB) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const position = averageSupports(pair.collision);
|
|
124
|
+
// `pair.collision.normal` is normalized, but - despite `Collision.js`'s own inline comment
|
|
125
|
+
// claiming it's "facing away from bodyA" - empirically (verified against a floor/falling-ball
|
|
126
|
+
// scenario with known relative positions) it always ends up facing the *opposite* way: away
|
|
127
|
+
// from `pair.bodyB`, towards `pair.bodyA`. `Collision.js`'s flip check
|
|
128
|
+
// (`if (normalX * deltaX + normalY * deltaY >= 0) { negate }`, where `delta = bodyB.position -
|
|
129
|
+
// bodyA.position`) guarantees the *final* normal always has a non-positive dot product with
|
|
130
|
+
// the A→B delta - i.e. it points away from B, not away from A as the comment says. So it's
|
|
131
|
+
// `compB`'s event that gets the raw value here, and `compA`'s that needs the negation - the
|
|
132
|
+
// opposite pairing a literal reading of the upstream comment would suggest.
|
|
133
|
+
const normalFromB = Pnt2.clone(pair.collision.normal);
|
|
134
|
+
const normalFromA = Pnt2.neg(normalFromB);
|
|
135
|
+
const relativeVelocityForA = Pnt2.sub(compB.linearVelocity, compA.linearVelocity);
|
|
136
|
+
const relativeVelocityForB = Pnt2.neg(relativeVelocityForA);
|
|
137
|
+
const impulse = Pnt2.len(relativeVelocityForA) * Math.min(compA.nativeBody.mass, compB.nativeBody.mass);
|
|
138
|
+
const baseEvent = { position, impulse };
|
|
139
|
+
compA.notifyCollisionStart(Object.assign(Object.assign({}, baseEvent), { otherBody: compB, normal: normalFromA, relativeVelocity: relativeVelocityForA }));
|
|
140
|
+
compB.notifyCollisionStart(Object.assign(Object.assign({}, baseEvent), { otherBody: compA, normal: normalFromB, relativeVelocity: relativeVelocityForB }));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Wires `IRigidBody2dComponent.onCollisionEnd` for every non-sensor pair that just stopped
|
|
144
|
+
* touching - see `handleCollisionStart`'s doc for why sensor pairs are skipped and why this is
|
|
145
|
+
* registered once per engine rather than per-body. */
|
|
146
|
+
handleCollisionEnd(event) {
|
|
147
|
+
for (const pair of event.pairs) {
|
|
148
|
+
if (pair.isSensor) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const compA = this.findRigidBody(pair.bodyA);
|
|
152
|
+
const compB = this.findRigidBody(pair.bodyB);
|
|
153
|
+
if (!compA || !compB) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
compA.notifyCollisionEnd(compB);
|
|
157
|
+
compB.notifyCollisionEnd(compA);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
52
160
|
registerCollisionGroup() {
|
|
53
161
|
for (let i = 1; i < 16; i++) {
|
|
54
162
|
if (!this.lockedCollisionGroups.includes(i)) {
|
|
@@ -69,6 +177,8 @@ export class MatterWorldComponent {
|
|
|
69
177
|
throw new Error('Ray casting is not implemented for Matter.js');
|
|
70
178
|
}
|
|
71
179
|
dispose() {
|
|
180
|
+
Events.off(this.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
181
|
+
Events.off(this.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
72
182
|
Engine.clear(this.matterEngine);
|
|
73
183
|
}
|
|
74
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/matter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68",
|
|
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
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/matter-js": "0.20.2",
|
|
33
|
-
"@gg-web-engine/core": "0.0.
|
|
33
|
+
"@gg-web-engine/core": "0.0.68",
|
|
34
34
|
"@types/jest": "^30.0.0",
|
|
35
35
|
"@types/node": "^26.4.0",
|
|
36
36
|
"jest": "^30.5.0",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typescript": "~6.0.3"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@gg-web-engine/core": "0.0.
|
|
45
|
+
"@gg-web-engine/core": "0.0.68",
|
|
46
46
|
"matter-js": "0.20.0",
|
|
47
47
|
"rxjs": "7.8.2"
|
|
48
48
|
},
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { CollisionEvent, Pnt2, Point2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterRigidBodyComponent, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterRigidBodyComponent collision events', () => {
|
|
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('should fire onCollisionStart reciprocally, with a sane position/normal, when a falling body lands on a floor', () => {
|
|
23
|
+
// static floor, top edge sits at y = -5
|
|
24
|
+
const floor = factory.createRigidBody(
|
|
25
|
+
{ shape: { shape: 'SQUARE', dimensions: { x: 50, y: 10 } }, body: { dynamic: false, mass: 0 } },
|
|
26
|
+
{ position: { x: 0, y: 0 } },
|
|
27
|
+
);
|
|
28
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
29
|
+
|
|
30
|
+
const ball = factory.createRigidBody(
|
|
31
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
32
|
+
{ position: { x: 0, y: -20 } },
|
|
33
|
+
);
|
|
34
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
35
|
+
ball.linearVelocity = { x: 0, y: 10 };
|
|
36
|
+
ball.nativeBody.frictionAir = 0;
|
|
37
|
+
|
|
38
|
+
const ballEvents: CollisionEvent<Point2, MatterRigidBodyComponent>[] = [];
|
|
39
|
+
const floorEvents: CollisionEvent<Point2, MatterRigidBodyComponent>[] = [];
|
|
40
|
+
ball.onCollisionStart.subscribe(e => ballEvents.push(e));
|
|
41
|
+
floor.onCollisionStart.subscribe(e => floorEvents.push(e));
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < 200 && ballEvents.length === 0; i++) {
|
|
44
|
+
world.simulate(16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
expect(ballEvents.length).toBeGreaterThan(0);
|
|
48
|
+
expect(floorEvents.length).toBeGreaterThan(0);
|
|
49
|
+
|
|
50
|
+
const ballHit = ballEvents[0];
|
|
51
|
+
expect(ballHit.otherBody).toBe(floor);
|
|
52
|
+
expect(ballHit.impulse).toBeGreaterThan(0);
|
|
53
|
+
// ball fell straight down onto a flat floor - normal (pointing away from the ball, towards the
|
|
54
|
+
// floor) should point roughly downward (+y, matter.js's default gravity/screen convention)
|
|
55
|
+
expect(ballHit.normal.y).toBeGreaterThan(0.5);
|
|
56
|
+
|
|
57
|
+
const floorHit = floorEvents[0];
|
|
58
|
+
expect(floorHit.otherBody).toBe(ball);
|
|
59
|
+
// reciprocal event: normal seen by the floor should point the opposite way (towards the ball)
|
|
60
|
+
expect(floorHit.normal.y).toBeLessThan(-0.5);
|
|
61
|
+
expect(floorHit.normal.x).toBeCloseTo(-ballHit.normal.x, 5);
|
|
62
|
+
expect(floorHit.normal.y).toBeCloseTo(-ballHit.normal.y, 5);
|
|
63
|
+
// relative velocity is opposite too (otherBody velocity relative to self, on each side)
|
|
64
|
+
expect(floorHit.relativeVelocity.x).toBeCloseTo(-ballHit.relativeVelocity.x, 5);
|
|
65
|
+
expect(floorHit.relativeVelocity.y).toBeCloseTo(-ballHit.relativeVelocity.y, 5);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should not keep re-firing onCollisionStart while a body rests stably on another', () => {
|
|
69
|
+
const floor = factory.createRigidBody(
|
|
70
|
+
{ shape: { shape: 'SQUARE', dimensions: { x: 50, y: 10 } }, body: { dynamic: false, mass: 0 } },
|
|
71
|
+
{ position: { x: 0, y: 0 } },
|
|
72
|
+
);
|
|
73
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
74
|
+
|
|
75
|
+
const ball = factory.createRigidBody(
|
|
76
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
77
|
+
{ position: { x: 0, y: -20 } },
|
|
78
|
+
);
|
|
79
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
80
|
+
ball.linearVelocity = { x: 0, y: 10 };
|
|
81
|
+
ball.nativeBody.frictionAir = 0;
|
|
82
|
+
|
|
83
|
+
world.gravity = { x: 0, y: 1 };
|
|
84
|
+
|
|
85
|
+
let startCount = 0;
|
|
86
|
+
ball.onCollisionStart.subscribe(() => startCount++);
|
|
87
|
+
|
|
88
|
+
// simulate until it lands and settles
|
|
89
|
+
for (let i = 0; i < 200; i++) {
|
|
90
|
+
world.simulate(16);
|
|
91
|
+
}
|
|
92
|
+
expect(startCount).toBeGreaterThan(0);
|
|
93
|
+
const countAfterLanding = startCount;
|
|
94
|
+
|
|
95
|
+
// continue simulating - resting contact must not re-fire onCollisionStart
|
|
96
|
+
for (let i = 0; i < 100; i++) {
|
|
97
|
+
world.simulate(16);
|
|
98
|
+
}
|
|
99
|
+
expect(startCount).toBe(countAfterLanding);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should fire onCollisionEnd when a body is knocked away and separates', () => {
|
|
103
|
+
const floor = factory.createRigidBody(
|
|
104
|
+
{ shape: { shape: 'SQUARE', dimensions: { x: 50, y: 10 } }, body: { dynamic: false, mass: 0 } },
|
|
105
|
+
{ position: { x: 0, y: 0 } },
|
|
106
|
+
);
|
|
107
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
108
|
+
|
|
109
|
+
const ball = factory.createRigidBody(
|
|
110
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
111
|
+
{ position: { x: 0, y: -20 } },
|
|
112
|
+
);
|
|
113
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
114
|
+
ball.linearVelocity = { x: 0, y: 10 };
|
|
115
|
+
ball.nativeBody.frictionAir = 0;
|
|
116
|
+
|
|
117
|
+
let endEvents: (MatterRigidBodyComponent | null)[] = [];
|
|
118
|
+
ball.onCollisionEnd.subscribe(e => endEvents.push(e));
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < 200 && endEvents.length === 0; i++) {
|
|
121
|
+
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
|
+
}
|
|
127
|
+
|
|
128
|
+
expect(endEvents.length).toBeGreaterThan(0);
|
|
129
|
+
expect(endEvents[0]).toBe(floor);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should emit onCollisionEnd(null) on the remaining body when the other body is removed from the world while still touching', () => {
|
|
133
|
+
const floor = factory.createRigidBody(
|
|
134
|
+
{ shape: { shape: 'SQUARE', dimensions: { x: 50, y: 10 } }, body: { dynamic: false, mass: 0 } },
|
|
135
|
+
{ position: { x: 0, y: 0 } },
|
|
136
|
+
);
|
|
137
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
138
|
+
|
|
139
|
+
const ball = factory.createRigidBody(
|
|
140
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
141
|
+
{ position: { x: 0, y: -20 } },
|
|
142
|
+
);
|
|
143
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
144
|
+
ball.linearVelocity = { x: 0, y: 10 };
|
|
145
|
+
ball.nativeBody.frictionAir = 0;
|
|
146
|
+
|
|
147
|
+
let floorEndEvents: (MatterRigidBodyComponent | null)[] = [];
|
|
148
|
+
floor.onCollisionEnd.subscribe(e => floorEndEvents.push(e));
|
|
149
|
+
let ballStarted = false;
|
|
150
|
+
ball.onCollisionStart.subscribe(() => (ballStarted = true));
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < 200 && !ballStarted; i++) {
|
|
153
|
+
world.simulate(16);
|
|
154
|
+
}
|
|
155
|
+
expect(ballStarted).toBe(true);
|
|
156
|
+
expect(floorEndEvents.length).toBe(0);
|
|
157
|
+
|
|
158
|
+
ball.removeFromWorld({ physicsWorld: world } as any);
|
|
159
|
+
expect(floorEndEvents.length).toBe(1);
|
|
160
|
+
expect(floorEndEvents[0]).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should not fire the rigid body onCollisionStart for a trigger overlapping a rigid body', () => {
|
|
164
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 50, y: 10 } }, { position: { x: 0, y: 0 } });
|
|
165
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
166
|
+
|
|
167
|
+
const ball = factory.createRigidBody(
|
|
168
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
169
|
+
{ position: { x: 0, y: -20 } },
|
|
170
|
+
);
|
|
171
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
172
|
+
ball.linearVelocity = { x: 0, y: 10 };
|
|
173
|
+
ball.nativeBody.frictionAir = 0;
|
|
174
|
+
|
|
175
|
+
let ballStartEvents = 0;
|
|
176
|
+
let triggerEnterEvents = 0;
|
|
177
|
+
ball.onCollisionStart.subscribe(() => ballStartEvents++);
|
|
178
|
+
trigger.onEntityEntered.subscribe(() => triggerEnterEvents++);
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < 200; i++) {
|
|
181
|
+
world.simulate(16);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
expect(triggerEnterEvents).toBeGreaterThan(0);
|
|
185
|
+
expect(ballStartEvents).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -18,13 +18,14 @@ describe('MatterWorldComponent', () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
describe('Rigid bodies', () => {
|
|
21
|
-
|
|
22
21
|
it('should simulate inertial motion of rigid body', () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const circle = world.factory.createRigidBody(
|
|
23
|
+
{
|
|
24
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
25
|
+
body: { dynamic: true, mass: 5 },
|
|
26
|
+
},
|
|
27
|
+
{ position: { x: -5, y: 0 } },
|
|
28
|
+
);
|
|
28
29
|
circle.addToWorld({ physicsWorld: world } as any);
|
|
29
30
|
circle.linearVelocity = { x: 1, y: 0 };
|
|
30
31
|
circle.nativeBody.frictionAir = 0;
|
|
@@ -37,19 +38,24 @@ describe('MatterWorldComponent', () => {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
it('should simulate collision of two rigid bodies', () => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const circle0 = world.factory.createRigidBody(
|
|
42
|
+
{
|
|
43
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
44
|
+
body: { dynamic: true, mass: 5 },
|
|
45
|
+
},
|
|
46
|
+
{ position: { x: -5, y: 0 } },
|
|
47
|
+
);
|
|
45
48
|
circle0.addToWorld({ physicsWorld: world } as any);
|
|
46
49
|
circle0.linearVelocity = { x: 1, y: 0 };
|
|
47
50
|
circle0.nativeBody.frictionAir = 0;
|
|
48
51
|
|
|
49
|
-
const circle1 = world.factory.createRigidBody(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const circle1 = world.factory.createRigidBody(
|
|
53
|
+
{
|
|
54
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
55
|
+
body: { dynamic: true, mass: 5 },
|
|
56
|
+
},
|
|
57
|
+
{ position: { x: 5, y: 0 } },
|
|
58
|
+
);
|
|
53
59
|
circle1.addToWorld({ physicsWorld: world } as any);
|
|
54
60
|
circle1.linearVelocity = { x: -1, y: 0 };
|
|
55
61
|
circle1.nativeBody.frictionAir = 0;
|
|
@@ -62,22 +68,42 @@ describe('MatterWorldComponent', () => {
|
|
|
62
68
|
expect(circle1.position.x).toBeGreaterThan(0);
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
it('keeps handleIdEntityMap (the O(1) findRigidBody lookup) in sync with children on add/remove', () => {
|
|
72
|
+
const circle = world.factory.createRigidBody(
|
|
73
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { dynamic: true, mass: 1 } },
|
|
74
|
+
{ position: { x: 0, y: 0 } },
|
|
75
|
+
);
|
|
76
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
77
|
+
|
|
78
|
+
expect(world.handleIdEntityMap.get(circle.nativeBody.id)).toBe(circle);
|
|
79
|
+
|
|
80
|
+
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
81
|
+
|
|
82
|
+
expect(world.handleIdEntityMap.has(circle.nativeBody.id)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
65
85
|
it('should not simulate collision of two rigid bodies with different collision groups', () => {
|
|
66
86
|
const cg0 = world.registerCollisionGroup();
|
|
67
|
-
const circle0 = world.factory.createRigidBody(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
const circle0 = world.factory.createRigidBody(
|
|
88
|
+
{
|
|
89
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
90
|
+
body: { dynamic: true, mass: 5 },
|
|
91
|
+
},
|
|
92
|
+
{ position: { x: -5, y: 0 } },
|
|
93
|
+
);
|
|
71
94
|
circle0.addToWorld({ physicsWorld: world } as any);
|
|
72
95
|
circle0.linearVelocity = { x: 1, y: 0 };
|
|
73
96
|
circle0.nativeBody.frictionAir = 0;
|
|
74
97
|
circle0.ownCollisionGroups = circle0.interactWithCollisionGroups = [cg0];
|
|
75
98
|
|
|
76
99
|
const cg1 = world.registerCollisionGroup();
|
|
77
|
-
const circle1 = world.factory.createRigidBody(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
const circle1 = world.factory.createRigidBody(
|
|
101
|
+
{
|
|
102
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
103
|
+
body: { dynamic: true, mass: 5 },
|
|
104
|
+
},
|
|
105
|
+
{ position: { x: 5, y: 0 } },
|
|
106
|
+
);
|
|
81
107
|
circle1.addToWorld({ physicsWorld: world } as any);
|
|
82
108
|
circle1.linearVelocity = { x: -1, y: 0 };
|
|
83
109
|
circle1.nativeBody.frictionAir = 0;
|
|
@@ -93,13 +119,15 @@ describe('MatterWorldComponent', () => {
|
|
|
93
119
|
});
|
|
94
120
|
|
|
95
121
|
describe.skip('Raycast', () => {
|
|
96
|
-
|
|
97
122
|
it('should return no hit when ray does not intersect any object', () => {
|
|
98
123
|
// Create a square far away from the ray
|
|
99
|
-
const square = world.factory.createRigidBody(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
const square = world.factory.createRigidBody(
|
|
125
|
+
{
|
|
126
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
127
|
+
body: { dynamic: false, mass: 0 },
|
|
128
|
+
},
|
|
129
|
+
{ position: { x: 10, y: 10 } },
|
|
130
|
+
);
|
|
103
131
|
square.addToWorld({ physicsWorld: world } as any);
|
|
104
132
|
|
|
105
133
|
world.simulate(1);
|
|
@@ -119,10 +147,13 @@ describe('MatterWorldComponent', () => {
|
|
|
119
147
|
|
|
120
148
|
it('should detect hit when ray intersects an object', () => {
|
|
121
149
|
// Create a square in the path of the ray
|
|
122
|
-
const square = world.factory.createRigidBody(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
const square = world.factory.createRigidBody(
|
|
151
|
+
{
|
|
152
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
153
|
+
body: { dynamic: false, mass: 0 },
|
|
154
|
+
},
|
|
155
|
+
{ position: { x: 0, y: -5 } },
|
|
156
|
+
);
|
|
126
157
|
square.addToWorld({ physicsWorld: world } as any);
|
|
127
158
|
|
|
128
159
|
world.simulate(1);
|
|
@@ -153,15 +184,18 @@ describe('MatterWorldComponent', () => {
|
|
|
153
184
|
const group2 = world.registerCollisionGroup();
|
|
154
185
|
|
|
155
186
|
// Create a square that only belongs to group1
|
|
156
|
-
const square1 = world.factory.createRigidBody(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
const square1 = world.factory.createRigidBody(
|
|
188
|
+
{
|
|
189
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
190
|
+
body: {
|
|
191
|
+
dynamic: false,
|
|
192
|
+
mass: 0,
|
|
193
|
+
ownCollisionGroups: [group1],
|
|
194
|
+
interactWithCollisionGroups: [group1, group2],
|
|
195
|
+
},
|
|
163
196
|
},
|
|
164
|
-
|
|
197
|
+
{ position: { x: 0, y: -5 } },
|
|
198
|
+
);
|
|
165
199
|
square1.addToWorld({ physicsWorld: world } as any);
|
|
166
200
|
|
|
167
201
|
world.simulate(1);
|
|
@@ -192,10 +226,13 @@ describe('MatterWorldComponent', () => {
|
|
|
192
226
|
|
|
193
227
|
it('should calculate hit distance correctly', () => {
|
|
194
228
|
// Create a square at a known distance
|
|
195
|
-
const square = world.factory.createRigidBody(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
229
|
+
const square = world.factory.createRigidBody(
|
|
230
|
+
{
|
|
231
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
232
|
+
body: { dynamic: false, mass: 0 },
|
|
233
|
+
},
|
|
234
|
+
{ position: { x: 0, y: -5 } },
|
|
235
|
+
);
|
|
199
236
|
square.addToWorld({ physicsWorld: world } as any);
|
|
200
237
|
|
|
201
238
|
world.simulate(1);
|
|
@@ -220,15 +257,18 @@ describe('MatterWorldComponent', () => {
|
|
|
220
257
|
const group2 = world.registerCollisionGroup();
|
|
221
258
|
|
|
222
259
|
// Create a square that belongs to group1
|
|
223
|
-
const square = world.factory.createRigidBody(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
260
|
+
const square = world.factory.createRigidBody(
|
|
261
|
+
{
|
|
262
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
263
|
+
body: {
|
|
264
|
+
dynamic: false,
|
|
265
|
+
mass: 0,
|
|
266
|
+
ownCollisionGroups: [group1],
|
|
267
|
+
interactWithCollisionGroups: [group1, group2],
|
|
268
|
+
},
|
|
230
269
|
},
|
|
231
|
-
|
|
270
|
+
{ position: { x: 0, y: -5 } },
|
|
271
|
+
);
|
|
232
272
|
square.addToWorld({ physicsWorld: world } as any);
|
|
233
273
|
|
|
234
274
|
world.simulate(1);
|
|
@@ -247,16 +287,22 @@ describe('MatterWorldComponent', () => {
|
|
|
247
287
|
|
|
248
288
|
it('should return correct hit body', () => {
|
|
249
289
|
// Create two squares at different positions
|
|
250
|
-
const square1 = world.factory.createRigidBody(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
290
|
+
const square1 = world.factory.createRigidBody(
|
|
291
|
+
{
|
|
292
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
293
|
+
body: { dynamic: false, mass: 0 },
|
|
294
|
+
},
|
|
295
|
+
{ position: { x: 0, y: -3 } },
|
|
296
|
+
);
|
|
254
297
|
square1.addToWorld({ physicsWorld: world } as any);
|
|
255
298
|
|
|
256
|
-
const square2 = world.factory.createRigidBody(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
299
|
+
const square2 = world.factory.createRigidBody(
|
|
300
|
+
{
|
|
301
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
302
|
+
body: { dynamic: false, mass: 0 },
|
|
303
|
+
},
|
|
304
|
+
{ position: { x: 0, y: -7 } },
|
|
305
|
+
);
|
|
260
306
|
square2.addToWorld({ physicsWorld: world } as any);
|
|
261
307
|
|
|
262
308
|
world.simulate(1);
|
|
@@ -275,10 +321,13 @@ describe('MatterWorldComponent', () => {
|
|
|
275
321
|
|
|
276
322
|
it('should handle edge case with ray starting inside an object', () => {
|
|
277
323
|
// Create a square
|
|
278
|
-
const square = world.factory.createRigidBody(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
324
|
+
const square = world.factory.createRigidBody(
|
|
325
|
+
{
|
|
326
|
+
shape: { shape: 'SQUARE', dimensions: { x: 4, y: 4 } },
|
|
327
|
+
body: { dynamic: false, mass: 0 },
|
|
328
|
+
},
|
|
329
|
+
{ position: { x: 0, y: 0 } },
|
|
330
|
+
);
|
|
282
331
|
square.addToWorld({ physicsWorld: world } as any);
|
|
283
332
|
|
|
284
333
|
world.simulate(1);
|
|
@@ -298,4 +347,4 @@ describe('MatterWorldComponent', () => {
|
|
|
298
347
|
expect(result).toBeDefined();
|
|
299
348
|
});
|
|
300
349
|
});
|
|
301
|
-
});
|
|
350
|
+
});
|