@gg-web-engine/matter 0.0.57 → 0.0.59
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 +7 -5
- package/dist/components/matter-rigid-body.component.js +41 -10
- package/dist/components/matter-trigger.component.d.ts +24 -0
- package/dist/components/matter-trigger.component.js +96 -0
- package/dist/components/matter-world.component.d.ts +10 -5
- package/dist/components/matter-world.component.js +25 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/matter-factory.d.ts +5 -1
- package/dist/matter-factory.js +24 -1
- package/dist/types.d.ts +7 -1
- package/package.json +30 -4
- package/test/components/matter-trigger.component.spec.ts +131 -0
- package/test/components/matter-world.component.spec.ts +301 -0
- package/tsconfig.json +4 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { CollisionGroup, DebugBody2DSettings, Entity2d,
|
|
1
|
+
import { CollisionGroup, DebugBody2DSettings, Entity2d, IRigidBody2dComponent, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
2
2
|
import { Body } from 'matter-js';
|
|
3
|
-
import { MatterPhysicsTypeDocRepo } from '../types';
|
|
4
|
-
import { MatterWorldComponent } from './matter-world.component';
|
|
3
|
+
import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
|
|
5
4
|
export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<MatterPhysicsTypeDocRepo> {
|
|
6
5
|
nativeBody: Body;
|
|
7
6
|
readonly shape: Shape2DDescriptor;
|
|
@@ -16,14 +15,17 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
|
|
|
16
15
|
name: string;
|
|
17
16
|
entity: Entity2d | null;
|
|
18
17
|
readonly debugBodySettings: DebugBody2DSettings;
|
|
18
|
+
protected _interactWithCGsMask: number;
|
|
19
|
+
protected _ownCGsMask: number;
|
|
19
20
|
constructor(nativeBody: Body, shape: Shape2DDescriptor);
|
|
20
21
|
get interactWithCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
21
22
|
set interactWithCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
22
23
|
get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
23
24
|
set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
25
|
+
protected updateCollisionFilter(): void;
|
|
24
26
|
clone(): MatterRigidBodyComponent;
|
|
25
|
-
addToWorld(world:
|
|
26
|
-
removeFromWorld(world:
|
|
27
|
+
addToWorld(world: MatterGgWorld): void;
|
|
28
|
+
removeFromWorld(world: MatterGgWorld): void;
|
|
27
29
|
dispose(): void;
|
|
28
30
|
resetMotion(): void;
|
|
29
31
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
|
|
1
|
+
import { BitMask, DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
|
|
2
2
|
import { Body, Composite, Vector } from 'matter-js';
|
|
3
|
+
// FIXME why this needs to be introduced? investigate units in matter.js
|
|
4
|
+
const MATTER_VELOCITY_SCALE = 0.0166667;
|
|
3
5
|
export class MatterRigidBodyComponent {
|
|
4
6
|
get position() {
|
|
5
|
-
return this.nativeBody.position;
|
|
7
|
+
return Pnt2.clone(this.nativeBody.position);
|
|
6
8
|
}
|
|
7
9
|
set position(value) {
|
|
8
10
|
Body.setPosition(this.nativeBody, Vector.create(value.x, value.y));
|
|
@@ -14,10 +16,10 @@ export class MatterRigidBodyComponent {
|
|
|
14
16
|
this.nativeBody.angle = value;
|
|
15
17
|
}
|
|
16
18
|
get linearVelocity() {
|
|
17
|
-
return Pnt2.
|
|
19
|
+
return Pnt2.scalarMult(this.nativeBody.velocity, 1 / MATTER_VELOCITY_SCALE);
|
|
18
20
|
}
|
|
19
21
|
set linearVelocity(value) {
|
|
20
|
-
Body.setVelocity(this.nativeBody, Vector.create(value.x, value.y));
|
|
22
|
+
Body.setVelocity(this.nativeBody, Vector.create(value.x * MATTER_VELOCITY_SCALE, value.y * MATTER_VELOCITY_SCALE));
|
|
21
23
|
}
|
|
22
24
|
get angularVelocity() {
|
|
23
25
|
return this.nativeBody.angularVelocity;
|
|
@@ -33,22 +35,51 @@ export class MatterRigidBodyComponent {
|
|
|
33
35
|
this.debugBodySettings = new DebugBody2DSettings(isFinite(this.nativeBody.mass)
|
|
34
36
|
? { type: 'RIGID_DYNAMIC', sleeping: () => this.nativeBody.isSleeping }
|
|
35
37
|
: { type: 'RIGID_STATIC' }, this.shape);
|
|
38
|
+
this._interactWithCGsMask = BitMask.full(16);
|
|
39
|
+
this._ownCGsMask = BitMask.full(16);
|
|
40
|
+
this.updateCollisionFilter();
|
|
36
41
|
}
|
|
37
42
|
get interactWithCollisionGroups() {
|
|
38
|
-
|
|
43
|
+
return BitMask.unpack(this._interactWithCGsMask, 16);
|
|
39
44
|
}
|
|
40
45
|
set interactWithCollisionGroups(value) {
|
|
41
|
-
|
|
46
|
+
let mask;
|
|
47
|
+
if (value === 'all') {
|
|
48
|
+
mask = BitMask.full(16);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
mask = BitMask.pack(value, 16);
|
|
52
|
+
}
|
|
53
|
+
if (this._interactWithCGsMask !== mask) {
|
|
54
|
+
this._interactWithCGsMask = mask;
|
|
55
|
+
this.updateCollisionFilter();
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
58
|
get ownCollisionGroups() {
|
|
44
|
-
|
|
59
|
+
return BitMask.unpack(this._ownCGsMask, 16);
|
|
45
60
|
}
|
|
46
61
|
set ownCollisionGroups(value) {
|
|
47
|
-
|
|
62
|
+
let mask;
|
|
63
|
+
if (value === 'all') {
|
|
64
|
+
mask = BitMask.full(16);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
mask = BitMask.pack(value, 16);
|
|
68
|
+
}
|
|
69
|
+
if (this._ownCGsMask !== mask) {
|
|
70
|
+
this._ownCGsMask = mask;
|
|
71
|
+
this.updateCollisionFilter();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
updateCollisionFilter() {
|
|
75
|
+
this.nativeBody.collisionFilter = Object.assign(Object.assign({}, this.nativeBody.collisionFilter), { category: this._ownCGsMask, mask: this._interactWithCGsMask });
|
|
48
76
|
}
|
|
49
77
|
clone() {
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
|
|
79
|
+
const component = new MatterRigidBodyComponent(clonedBody, this.shape);
|
|
80
|
+
component.ownCollisionGroups = this.ownCollisionGroups;
|
|
81
|
+
component.interactWithCollisionGroups = this.interactWithCollisionGroups;
|
|
82
|
+
return component;
|
|
52
83
|
}
|
|
53
84
|
addToWorld(world) {
|
|
54
85
|
Composite.add(world.physicsWorld.matterWorld, this.nativeBody);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Observable, Subject } from 'rxjs';
|
|
2
|
+
import { Body } from 'matter-js';
|
|
3
|
+
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
4
|
+
import { DebugBody2DSettings, ITrigger2dComponent, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
5
|
+
import { MatterWorldComponent } from './matter-world.component';
|
|
6
|
+
import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
|
|
7
|
+
export declare class MatterTriggerComponent extends MatterRigidBodyComponent implements ITrigger2dComponent<MatterPhysicsTypeDocRepo> {
|
|
8
|
+
readonly shape: Shape2DDescriptor;
|
|
9
|
+
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>;
|
|
14
|
+
readonly debugBodySettings: DebugBody2DSettings;
|
|
15
|
+
protected intersectionsAmount: number;
|
|
16
|
+
protected currentOverlaps: Set<MatterRigidBodyComponent>;
|
|
17
|
+
private onCollisionStart;
|
|
18
|
+
private onCollisionEnd;
|
|
19
|
+
constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
|
|
20
|
+
addToWorld(world: MatterGgWorld): void;
|
|
21
|
+
removeFromWorld(world: MatterGgWorld): void;
|
|
22
|
+
checkOverlaps(): void;
|
|
23
|
+
clone(): MatterTriggerComponent;
|
|
24
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { map, merge, Subject } from 'rxjs';
|
|
2
|
+
import { Body, Events } from 'matter-js';
|
|
3
|
+
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
4
|
+
import { DebugBody2DSettings } from '@gg-web-engine/core';
|
|
5
|
+
export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
6
|
+
get onEntityEntered() {
|
|
7
|
+
return this.onEnter$.asObservable();
|
|
8
|
+
}
|
|
9
|
+
get onEntityLeft() {
|
|
10
|
+
return this.onLeft$.asObservable();
|
|
11
|
+
}
|
|
12
|
+
onCollisionStart(event) {
|
|
13
|
+
for (const pair of event.pairs) {
|
|
14
|
+
let body = null;
|
|
15
|
+
if (pair.bodyA === this.nativeBody) {
|
|
16
|
+
body = pair.bodyB;
|
|
17
|
+
}
|
|
18
|
+
else if (pair.bodyB === this.nativeBody) {
|
|
19
|
+
body = pair.bodyA;
|
|
20
|
+
}
|
|
21
|
+
if (body) {
|
|
22
|
+
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
23
|
+
if (comp) {
|
|
24
|
+
this.onEnter$.next(comp);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
onCollisionEnd(event) {
|
|
30
|
+
for (const pair of event.pairs) {
|
|
31
|
+
let body = null;
|
|
32
|
+
if (pair.bodyA === this.nativeBody) {
|
|
33
|
+
body = pair.bodyB;
|
|
34
|
+
}
|
|
35
|
+
else if (pair.bodyB === this.nativeBody) {
|
|
36
|
+
body = pair.bodyA;
|
|
37
|
+
}
|
|
38
|
+
if (body) {
|
|
39
|
+
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
40
|
+
if (comp) {
|
|
41
|
+
this.onLeft$.next(comp);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
constructor(nativeBody, shape, world) {
|
|
47
|
+
super(nativeBody, shape);
|
|
48
|
+
this.shape = shape;
|
|
49
|
+
this.world = world;
|
|
50
|
+
this.onEnter$ = new Subject();
|
|
51
|
+
this.onLeft$ = new Subject();
|
|
52
|
+
this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
|
|
53
|
+
this.intersectionsAmount = 0;
|
|
54
|
+
this.currentOverlaps = new Set();
|
|
55
|
+
this.nativeBody.isSensor = true;
|
|
56
|
+
merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
|
|
57
|
+
if (enter) {
|
|
58
|
+
this.intersectionsAmount++;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.intersectionsAmount--;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.onCollisionStart = this.onCollisionStart.bind(this);
|
|
65
|
+
this.onCollisionEnd = this.onCollisionEnd.bind(this);
|
|
66
|
+
}
|
|
67
|
+
addToWorld(world) {
|
|
68
|
+
if (world.physicsWorld != this.world) {
|
|
69
|
+
throw new Error('Matter bodies cannot be shared between different worlds');
|
|
70
|
+
}
|
|
71
|
+
this.intersectionsAmount = 0;
|
|
72
|
+
this.currentOverlaps.clear();
|
|
73
|
+
super.addToWorld(world);
|
|
74
|
+
Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.onCollisionStart);
|
|
75
|
+
Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.onCollisionEnd);
|
|
76
|
+
}
|
|
77
|
+
removeFromWorld(world) {
|
|
78
|
+
Events.off(world.physicsWorld.matterEngine, 'collisionStart', this.onCollisionStart);
|
|
79
|
+
Events.off(world.physicsWorld.matterEngine, 'collisionEnd', this.onCollisionEnd);
|
|
80
|
+
for (const body of this.currentOverlaps) {
|
|
81
|
+
this.onLeft$.next(body);
|
|
82
|
+
}
|
|
83
|
+
this.currentOverlaps.clear();
|
|
84
|
+
super.removeFromWorld(world);
|
|
85
|
+
}
|
|
86
|
+
checkOverlaps() {
|
|
87
|
+
// do nothing, for matter.js we handle this differently
|
|
88
|
+
}
|
|
89
|
+
clone() {
|
|
90
|
+
const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
|
|
91
|
+
const component = new MatterTriggerComponent(clonedBody, this.shape, this.world);
|
|
92
|
+
component.ownCollisionGroups = this.ownCollisionGroups;
|
|
93
|
+
component.interactWithCollisionGroups = this.interactWithCollisionGroups;
|
|
94
|
+
return component;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import { CollisionGroup, IPhysicsWorld2dComponent, Point2 } from '@gg-web-engine/core';
|
|
1
|
+
import { CollisionGroup, IPhysicsWorld2dComponent, Point2, RaycastOptions, RaycastResult } from '@gg-web-engine/core';
|
|
2
2
|
import { Engine, World } from 'matter-js';
|
|
3
3
|
import { MatterFactory } from '../matter-factory';
|
|
4
4
|
import { MatterPhysicsTypeDocRepo } from '../types';
|
|
5
5
|
import { Subject } from 'rxjs';
|
|
6
6
|
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
7
|
+
import { MatterTriggerComponent } from './matter-trigger.component';
|
|
7
8
|
export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<MatterPhysicsTypeDocRepo> {
|
|
8
|
-
protected
|
|
9
|
+
protected matterEngine_: Engine | null;
|
|
10
|
+
get matterEngine(): Engine | null;
|
|
9
11
|
get matterWorld(): World | null;
|
|
10
12
|
readonly factory: MatterFactory;
|
|
11
|
-
readonly added$: Subject<MatterRigidBodyComponent>;
|
|
12
|
-
readonly removed$: Subject<MatterRigidBodyComponent>;
|
|
13
|
-
readonly children: MatterRigidBodyComponent[];
|
|
13
|
+
readonly added$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
|
|
14
|
+
readonly removed$: Subject<MatterRigidBodyComponent | MatterTriggerComponent>;
|
|
15
|
+
readonly children: (MatterRigidBodyComponent | MatterTriggerComponent)[];
|
|
14
16
|
private _gravity;
|
|
15
17
|
get gravity(): Point2;
|
|
16
18
|
set gravity(value: Point2);
|
|
17
19
|
readonly mainCollisionGroup: CollisionGroup;
|
|
18
20
|
constructor();
|
|
19
21
|
init(): Promise<void>;
|
|
22
|
+
protected lockedCollisionGroups: number[];
|
|
20
23
|
registerCollisionGroup(): CollisionGroup;
|
|
21
24
|
deregisterCollisionGroup(group: CollisionGroup): void;
|
|
25
|
+
private lastDelta;
|
|
22
26
|
simulate(delta: number): void;
|
|
27
|
+
raycast(options: RaycastOptions<Point2>): RaycastResult<Point2, MatterRigidBodyComponent>;
|
|
23
28
|
dispose(): void;
|
|
24
29
|
}
|
|
@@ -10,8 +10,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { Engine } from 'matter-js';
|
|
11
11
|
import { MatterFactory } from '../matter-factory';
|
|
12
12
|
import { Subject } from 'rxjs';
|
|
13
|
-
// TODO
|
|
13
|
+
// TODO probably should be configurable in world
|
|
14
|
+
const MATTER_WORLD_SCALE = 0.0001;
|
|
14
15
|
export class MatterWorldComponent {
|
|
16
|
+
get matterEngine() {
|
|
17
|
+
return this.matterEngine_;
|
|
18
|
+
}
|
|
15
19
|
get matterWorld() {
|
|
16
20
|
return this.matterEngine && this.matterEngine.world;
|
|
17
21
|
}
|
|
@@ -26,29 +30,43 @@ export class MatterWorldComponent {
|
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
constructor() {
|
|
29
|
-
this.
|
|
30
|
-
this.factory = new MatterFactory();
|
|
33
|
+
this.matterEngine_ = null;
|
|
31
34
|
this.added$ = new Subject();
|
|
32
35
|
this.removed$ = new Subject();
|
|
33
36
|
this.children = [];
|
|
34
37
|
this._gravity = { x: 0, y: 9.82 };
|
|
35
38
|
this.mainCollisionGroup = 0;
|
|
39
|
+
this.lockedCollisionGroups = [];
|
|
40
|
+
this.lastDelta = 0;
|
|
36
41
|
this.added$.subscribe(c => this.children.push(c));
|
|
37
42
|
this.removed$.subscribe(c => this.children.splice(this.children.indexOf(c), 1));
|
|
43
|
+
this.factory = new MatterFactory(this);
|
|
38
44
|
}
|
|
39
45
|
init() {
|
|
40
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
-
this.
|
|
47
|
+
this.matterEngine_ = Engine.create({
|
|
48
|
+
gravity: Object.assign(Object.assign({}, this._gravity), { scale: MATTER_WORLD_SCALE }),
|
|
49
|
+
});
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
registerCollisionGroup() {
|
|
45
|
-
|
|
53
|
+
for (let i = 1; i < 16; i++) {
|
|
54
|
+
if (!this.lockedCollisionGroups.includes(i)) {
|
|
55
|
+
this.lockedCollisionGroups.push(i);
|
|
56
|
+
return i;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error('App tries to register 17th collision group, but Matter.js supports only 16');
|
|
46
60
|
}
|
|
47
61
|
deregisterCollisionGroup(group) {
|
|
48
|
-
|
|
62
|
+
this.lockedCollisionGroups = this.lockedCollisionGroups.filter(x => x !== group);
|
|
49
63
|
}
|
|
50
64
|
simulate(delta) {
|
|
51
|
-
Engine.update(this.matterEngine, delta);
|
|
65
|
+
Engine.update(this.matterEngine, delta, this.lastDelta > 0 ? delta / this.lastDelta : 1);
|
|
66
|
+
this.lastDelta = delta;
|
|
67
|
+
}
|
|
68
|
+
raycast(options) {
|
|
69
|
+
throw new Error('Ray casting is not implemented for Matter.js');
|
|
52
70
|
}
|
|
53
71
|
dispose() {
|
|
54
72
|
Engine.clear(this.matterEngine);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/matter-factory.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { BodyShape2DDescriptor, IPhysicsBody2dComponentFactory, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
2
2
|
import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
|
|
3
|
+
import { MatterTriggerComponent } from './components/matter-trigger.component';
|
|
4
|
+
import { MatterWorldComponent } from './components/matter-world.component';
|
|
3
5
|
import { MatterPhysicsTypeDocRepo } from './types';
|
|
4
6
|
export declare class MatterFactory implements IPhysicsBody2dComponentFactory<MatterPhysicsTypeDocRepo> {
|
|
7
|
+
protected readonly world: MatterWorldComponent;
|
|
8
|
+
constructor(world: MatterWorldComponent);
|
|
5
9
|
createRigidBody(descriptor: BodyShape2DDescriptor, transform?: {
|
|
6
10
|
position?: Point2;
|
|
7
11
|
rotation?: number;
|
|
@@ -9,6 +13,6 @@ export declare class MatterFactory implements IPhysicsBody2dComponentFactory<Mat
|
|
|
9
13
|
createTrigger(descriptor: Shape2DDescriptor, transform?: {
|
|
10
14
|
position?: Point2;
|
|
11
15
|
rotation?: number;
|
|
12
|
-
}):
|
|
16
|
+
}): MatterTriggerComponent;
|
|
13
17
|
private transformOptions;
|
|
14
18
|
}
|
package/dist/matter-factory.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
|
|
2
|
+
import { MatterTriggerComponent } from './components/matter-trigger.component';
|
|
2
3
|
import { Bodies, Vector } from 'matter-js';
|
|
3
4
|
export class MatterFactory {
|
|
5
|
+
constructor(world) {
|
|
6
|
+
this.world = world;
|
|
7
|
+
}
|
|
4
8
|
createRigidBody(descriptor, transform) {
|
|
5
9
|
var _a, _b;
|
|
6
10
|
let nativeBody = null;
|
|
@@ -20,7 +24,26 @@ export class MatterFactory {
|
|
|
20
24
|
return new MatterRigidBodyComponent(nativeBody, descriptor.shape);
|
|
21
25
|
}
|
|
22
26
|
createTrigger(descriptor, transform) {
|
|
23
|
-
|
|
27
|
+
var _a, _b;
|
|
28
|
+
let nativeBody = null;
|
|
29
|
+
switch (descriptor.shape) {
|
|
30
|
+
case 'SQUARE':
|
|
31
|
+
nativeBody = Bodies.rectangle(0, 0, descriptor.dimensions.x, descriptor.dimensions.y, { isSensor: true });
|
|
32
|
+
break;
|
|
33
|
+
case 'CIRCLE':
|
|
34
|
+
nativeBody = Bodies.circle(0, 0, descriptor.radius, { isSensor: true });
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (!nativeBody) {
|
|
38
|
+
throw new Error(`Shape "${descriptor.shape}" not implemented for Matter.js`);
|
|
39
|
+
}
|
|
40
|
+
nativeBody.position.x = ((_a = transform === null || transform === void 0 ? void 0 : transform.position) === null || _a === void 0 ? void 0 : _a.x) || 0;
|
|
41
|
+
nativeBody.position.y = ((_b = transform === null || transform === void 0 ? void 0 : transform.position) === null || _b === void 0 ? void 0 : _b.y) || 0;
|
|
42
|
+
nativeBody.angle = (transform === null || transform === void 0 ? void 0 : transform.rotation) || 0;
|
|
43
|
+
if (!this.world) {
|
|
44
|
+
throw new Error('MatterFactory: World not set. Make sure the factory is created by MatterWorldComponent.');
|
|
45
|
+
}
|
|
46
|
+
return new MatterTriggerComponent(nativeBody, descriptor, this.world);
|
|
24
47
|
}
|
|
25
48
|
transformOptions(options) {
|
|
26
49
|
const res = {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { MatterFactory } from './matter-factory';
|
|
2
2
|
import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
|
|
3
|
+
import { MatterTriggerComponent } from './components/matter-trigger.component';
|
|
4
|
+
import { Gg2dWorld, Gg2dWorldSceneTypeDocPPatch, Gg2dWorldTypeDocPPatch } from '@gg-web-engine/core';
|
|
5
|
+
import { MatterWorldComponent } from './components/matter-world.component';
|
|
3
6
|
export type MatterPhysicsTypeDocRepo = {
|
|
4
7
|
factory: MatterFactory;
|
|
5
8
|
rigidBody: MatterRigidBodyComponent;
|
|
6
|
-
trigger:
|
|
9
|
+
trigger: MatterTriggerComponent;
|
|
7
10
|
};
|
|
11
|
+
export type MatterTypeDoc = Gg2dWorldTypeDocPPatch<MatterPhysicsTypeDocRepo>;
|
|
12
|
+
export type MatterSceneTypeDoc = Gg2dWorldSceneTypeDocPPatch<MatterPhysicsTypeDocRepo, MatterWorldComponent>;
|
|
13
|
+
export type MatterGgWorld = Gg2dWorld<MatterTypeDoc, MatterSceneTypeDoc>;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/matter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "jest",
|
|
9
9
|
"prettier-format": "prettier --config ../core/.prettierrc 'src/**/*.ts' --write",
|
|
10
10
|
"prepublish": "rm -rf ./dist/ && tsc",
|
|
11
11
|
"build": "tsc"
|
|
@@ -30,15 +30,41 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/matter-js": "0.19.7",
|
|
33
|
-
"@gg-web-engine/core": "0.0.
|
|
33
|
+
"@gg-web-engine/core": "0.0.59",
|
|
34
|
+
"@types/jest": "^29.5.12",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
34
37
|
"matter-js": "0.20.0",
|
|
35
38
|
"prettier": "^3.3.3",
|
|
36
39
|
"rxjs": "7.8.1",
|
|
40
|
+
"ts-jest": "^29.2.4",
|
|
37
41
|
"typescript": "~5.5.4"
|
|
38
42
|
},
|
|
39
43
|
"peerDependencies": {
|
|
40
|
-
"@gg-web-engine/core": "0.0.
|
|
44
|
+
"@gg-web-engine/core": "0.0.59",
|
|
41
45
|
"matter-js": "0.20.0",
|
|
42
46
|
"rxjs": "7.8.1"
|
|
47
|
+
},
|
|
48
|
+
"jest": {
|
|
49
|
+
"rootDir": ".",
|
|
50
|
+
"testRegex": "\\.spec\\.ts$",
|
|
51
|
+
"transform": {
|
|
52
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
|
53
|
+
},
|
|
54
|
+
"moduleNameMapper": {
|
|
55
|
+
"@gg-web-engine/core": "<rootDir>/../core/src/index.ts"
|
|
56
|
+
},
|
|
57
|
+
"preset": "ts-jest/presets/default",
|
|
58
|
+
"collectCoverageFrom": [
|
|
59
|
+
"**/*.ts",
|
|
60
|
+
"!**/*.module.ts"
|
|
61
|
+
],
|
|
62
|
+
"coveragePathIgnorePatterns": [
|
|
63
|
+
"<rootDir>/node_modules/",
|
|
64
|
+
"<rootDir>/dist/",
|
|
65
|
+
"<rootDir>/test/"
|
|
66
|
+
],
|
|
67
|
+
"coverageDirectory": "./coverage",
|
|
68
|
+
"testEnvironment": "jsdom"
|
|
43
69
|
}
|
|
44
70
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe(`MatterTriggerComponent`, () => {
|
|
5
|
+
|
|
6
|
+
let world: MatterWorldComponent;
|
|
7
|
+
let factory: MatterFactory;
|
|
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
|
+
// FIXME matter.js logic is kinda not intuitive. Spawning objects on some coordinates seems to cause collisions with
|
|
23
|
+
// all the objects that intersect the line between (0, 0) and desired position, sensors are flying away to the
|
|
24
|
+
// infinity just by setting it's position and other wonderful miracles. Investigate why these tests fail and try to fix.
|
|
25
|
+
// Note: this test is identical to rapier2d trigger test, and it is expected
|
|
26
|
+
it.skip(`should detect object intersection`, async () => {
|
|
27
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
28
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
29
|
+
const circle = factory.createRigidBody({
|
|
30
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
31
|
+
body: { dynamic: true, mass: 1 },
|
|
32
|
+
}, { position: { x: 0, y: 12 } });
|
|
33
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
34
|
+
circle.linearVelocity = { x: 0, y: -10 };
|
|
35
|
+
circle.nativeBody.frictionAir = 0;
|
|
36
|
+
let enterRegistered = false;
|
|
37
|
+
let exitRegistered = false;
|
|
38
|
+
trigger.onEntityEntered.subscribe(((obj) => {
|
|
39
|
+
enterRegistered = obj === circle;
|
|
40
|
+
}));
|
|
41
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
42
|
+
exitRegistered = obj === circle;
|
|
43
|
+
}));
|
|
44
|
+
world.simulate(500);
|
|
45
|
+
trigger.checkOverlaps(); // trigger entity performs that on tick
|
|
46
|
+
expect(enterRegistered).toBe(false);
|
|
47
|
+
expect(exitRegistered).toBe(false);
|
|
48
|
+
|
|
49
|
+
world.simulate(500);
|
|
50
|
+
trigger.checkOverlaps();
|
|
51
|
+
expect(enterRegistered).toBe(true);
|
|
52
|
+
expect(exitRegistered).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.skip(`should detect end of object intersection`, async () => {
|
|
56
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
57
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
58
|
+
const circle = factory.createRigidBody({
|
|
59
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
60
|
+
body: { dynamic: true, mass: 1 },
|
|
61
|
+
}, { position: { x: 0, y: 12 } });
|
|
62
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
63
|
+
circle.linearVelocity = { x: 0, y: -10 };
|
|
64
|
+
circle.nativeBody.frictionAir = 0;
|
|
65
|
+
let exitRegistered = false;
|
|
66
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
67
|
+
exitRegistered = obj === circle;
|
|
68
|
+
}));
|
|
69
|
+
world.simulate(1000);
|
|
70
|
+
trigger.checkOverlaps();
|
|
71
|
+
expect(exitRegistered).toBe(false);
|
|
72
|
+
world.simulate(1000);
|
|
73
|
+
trigger.checkOverlaps();
|
|
74
|
+
expect(exitRegistered).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.skip(`should fire object intersection if spawned inside`, async () => {
|
|
78
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
79
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
80
|
+
const circle = factory.createRigidBody({
|
|
81
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
82
|
+
body: { dynamic: true, mass: 1 },
|
|
83
|
+
}, { position: Pnt2.O });
|
|
84
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
85
|
+
let enterRegistered = false;
|
|
86
|
+
trigger.onEntityEntered.subscribe(((obj) => {
|
|
87
|
+
enterRegistered = obj === circle;
|
|
88
|
+
}));
|
|
89
|
+
world.simulate(1000);
|
|
90
|
+
trigger.checkOverlaps();
|
|
91
|
+
expect(enterRegistered).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.skip(`should fire end of object intersection if trigger removed`, async () => {
|
|
95
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
96
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
97
|
+
const circle = factory.createRigidBody({
|
|
98
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
99
|
+
body: { dynamic: true, mass: 1 },
|
|
100
|
+
}, { position: Pnt2.O });
|
|
101
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
102
|
+
let exitRegistered = false;
|
|
103
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
104
|
+
exitRegistered = obj === circle;
|
|
105
|
+
}));
|
|
106
|
+
world.simulate(1);
|
|
107
|
+
trigger.checkOverlaps();
|
|
108
|
+
trigger.removeFromWorld({ physicsWorld: world } as any);
|
|
109
|
+
expect(exitRegistered).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.skip(`should fire end of object intersection if object removed`, async () => {
|
|
113
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
114
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
115
|
+
const circle = factory.createRigidBody({
|
|
116
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
117
|
+
body: { dynamic: true, mass: 1 },
|
|
118
|
+
}, { position: Pnt2.O });
|
|
119
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
120
|
+
let exitRegistered = false;
|
|
121
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
122
|
+
exitRegistered = obj === circle;
|
|
123
|
+
}));
|
|
124
|
+
world.simulate(1);
|
|
125
|
+
trigger.checkOverlaps();
|
|
126
|
+
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
127
|
+
world.simulate(1);
|
|
128
|
+
trigger.checkOverlaps();
|
|
129
|
+
expect(exitRegistered).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Point2, RaycastOptions } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterWorldComponent', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
if (world) {
|
|
9
|
+
world.dispose();
|
|
10
|
+
}
|
|
11
|
+
world = new MatterWorldComponent();
|
|
12
|
+
await world.init();
|
|
13
|
+
world.gravity = { x: 0, y: 0 }; // Set gravity to zero for predictable physics
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
world.dispose();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Rigid bodies', () => {
|
|
21
|
+
|
|
22
|
+
it('should simulate inertial motion of rigid body', () => {
|
|
23
|
+
|
|
24
|
+
const circle = world.factory.createRigidBody({
|
|
25
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
26
|
+
body: { dynamic: true, mass: 5 },
|
|
27
|
+
}, { position: { x: -5, y: 0 } });
|
|
28
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
29
|
+
circle.linearVelocity = { x: 1, y: 0 };
|
|
30
|
+
circle.nativeBody.frictionAir = 0;
|
|
31
|
+
|
|
32
|
+
// simulate for 6 seconds
|
|
33
|
+
for (let i = 0; i < 100; i++) {
|
|
34
|
+
world.simulate(60);
|
|
35
|
+
}
|
|
36
|
+
expect(circle.position.x).toBeCloseTo(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should simulate collision of two rigid bodies', () => {
|
|
40
|
+
|
|
41
|
+
const circle0 = world.factory.createRigidBody({
|
|
42
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
43
|
+
body: { dynamic: true, mass: 5 },
|
|
44
|
+
}, { position: { x: -5, y: 0 } });
|
|
45
|
+
circle0.addToWorld({ physicsWorld: world } as any);
|
|
46
|
+
circle0.linearVelocity = { x: 1, y: 0 };
|
|
47
|
+
circle0.nativeBody.frictionAir = 0;
|
|
48
|
+
|
|
49
|
+
const circle1 = world.factory.createRigidBody({
|
|
50
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
51
|
+
body: { dynamic: true, mass: 5 },
|
|
52
|
+
}, { position: { x: 5, y: 0 } });
|
|
53
|
+
circle1.addToWorld({ physicsWorld: world } as any);
|
|
54
|
+
circle1.linearVelocity = { x: -1, y: 0 };
|
|
55
|
+
circle1.nativeBody.frictionAir = 0;
|
|
56
|
+
|
|
57
|
+
// simulate for 6 seconds
|
|
58
|
+
for (let i = 0; i < 100; i++) {
|
|
59
|
+
world.simulate(60);
|
|
60
|
+
}
|
|
61
|
+
expect(circle0.position.x).toBeLessThan(0);
|
|
62
|
+
expect(circle1.position.x).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not simulate collision of two rigid bodies with different collision groups', () => {
|
|
66
|
+
const cg0 = world.registerCollisionGroup();
|
|
67
|
+
const circle0 = world.factory.createRigidBody({
|
|
68
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
69
|
+
body: { dynamic: true, mass: 5 },
|
|
70
|
+
}, { position: { x: -5, y: 0 } });
|
|
71
|
+
circle0.addToWorld({ physicsWorld: world } as any);
|
|
72
|
+
circle0.linearVelocity = { x: 1, y: 0 };
|
|
73
|
+
circle0.nativeBody.frictionAir = 0;
|
|
74
|
+
circle0.ownCollisionGroups = circle0.interactWithCollisionGroups = [cg0];
|
|
75
|
+
|
|
76
|
+
const cg1 = world.registerCollisionGroup();
|
|
77
|
+
const circle1 = world.factory.createRigidBody({
|
|
78
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
79
|
+
body: { dynamic: true, mass: 5 },
|
|
80
|
+
}, { position: { x: 5, y: 0 } });
|
|
81
|
+
circle1.addToWorld({ physicsWorld: world } as any);
|
|
82
|
+
circle1.linearVelocity = { x: -1, y: 0 };
|
|
83
|
+
circle1.nativeBody.frictionAir = 0;
|
|
84
|
+
circle1.ownCollisionGroups = circle1.interactWithCollisionGroups = [cg1];
|
|
85
|
+
|
|
86
|
+
// simulate for 6 seconds
|
|
87
|
+
for (let i = 0; i < 100; i++) {
|
|
88
|
+
world.simulate(60);
|
|
89
|
+
}
|
|
90
|
+
expect(circle0.position.x).toBeCloseTo(1);
|
|
91
|
+
expect(circle1.position.x).toBeCloseTo(-1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe.skip('Raycast', () => {
|
|
96
|
+
|
|
97
|
+
it('should return no hit when ray does not intersect any object', () => {
|
|
98
|
+
// Create a square far away from the ray
|
|
99
|
+
const square = world.factory.createRigidBody({
|
|
100
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
101
|
+
body: { dynamic: false, mass: 0 },
|
|
102
|
+
}, { position: { x: 10, y: 10 } });
|
|
103
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
104
|
+
|
|
105
|
+
world.simulate(1);
|
|
106
|
+
|
|
107
|
+
// Cast a ray that doesn't hit anything
|
|
108
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
109
|
+
from: { x: 0, y: 0 },
|
|
110
|
+
to: { x: 0, y: -10 },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = world.raycast(raycastOptions);
|
|
114
|
+
expect(result.hasHit).toBe(false);
|
|
115
|
+
expect(result.hitPoint).toBeUndefined();
|
|
116
|
+
expect(result.hitNormal).toBeUndefined();
|
|
117
|
+
expect(result.hitDistance).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should detect hit when ray intersects an object', () => {
|
|
121
|
+
// Create a square in the path of the ray
|
|
122
|
+
const square = world.factory.createRigidBody({
|
|
123
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
124
|
+
body: { dynamic: false, mass: 0 },
|
|
125
|
+
}, { position: { x: 0, y: -5 } });
|
|
126
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
127
|
+
|
|
128
|
+
world.simulate(1);
|
|
129
|
+
|
|
130
|
+
// Cast a ray that hits the square
|
|
131
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
132
|
+
from: { x: 0, y: 0 },
|
|
133
|
+
to: { x: 0, y: -10 },
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = world.raycast(raycastOptions);
|
|
137
|
+
expect(result.hasHit).toBe(true);
|
|
138
|
+
expect(result.hitBody).toBe(square);
|
|
139
|
+
expect(result.hitPoint).toBeDefined();
|
|
140
|
+
expect(result.hitNormal).toBeDefined();
|
|
141
|
+
expect(result.hitDistance).toBeDefined();
|
|
142
|
+
|
|
143
|
+
// The hit should be at approximately y = -4 (box at y = -5 with size 2)
|
|
144
|
+
expect(result.hitPoint!.y).toBeCloseTo(-4, 0.1);
|
|
145
|
+
|
|
146
|
+
// The normal should point up (towards the ray origin)
|
|
147
|
+
expect(result.hitNormal!.y).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should respect collision filtering', () => {
|
|
151
|
+
// Register collision groups
|
|
152
|
+
const group1 = world.registerCollisionGroup();
|
|
153
|
+
const group2 = world.registerCollisionGroup();
|
|
154
|
+
|
|
155
|
+
// Create a square that only belongs to group1
|
|
156
|
+
const square1 = world.factory.createRigidBody({
|
|
157
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
158
|
+
body: {
|
|
159
|
+
dynamic: false,
|
|
160
|
+
mass: 0,
|
|
161
|
+
ownCollisionGroups: [group1],
|
|
162
|
+
interactWithCollisionGroups: [group1, group2],
|
|
163
|
+
},
|
|
164
|
+
}, { position: { x: 0, y: -5 } });
|
|
165
|
+
square1.addToWorld({ physicsWorld: world } as any);
|
|
166
|
+
|
|
167
|
+
world.simulate(1);
|
|
168
|
+
|
|
169
|
+
// Ray that only checks against group2 should not hit
|
|
170
|
+
const rayOptions1: RaycastOptions<Point2> = {
|
|
171
|
+
from: { x: 0, y: 0 },
|
|
172
|
+
to: { x: 0, y: -10 },
|
|
173
|
+
collisionFilterGroups: [group2],
|
|
174
|
+
collisionFilterMask: [group2],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result1 = world.raycast(rayOptions1);
|
|
178
|
+
expect(result1.hasHit).toBe(false);
|
|
179
|
+
|
|
180
|
+
// Ray that checks against group1 should hit
|
|
181
|
+
const rayOptions2: RaycastOptions<Point2> = {
|
|
182
|
+
from: { x: 0, y: 0 },
|
|
183
|
+
to: { x: 0, y: -10 },
|
|
184
|
+
collisionFilterGroups: [group1],
|
|
185
|
+
collisionFilterMask: [group1],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result2 = world.raycast(rayOptions2);
|
|
189
|
+
expect(result2.hasHit).toBe(true);
|
|
190
|
+
expect(result2.hitBody).toBe(square1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should calculate hit distance correctly', () => {
|
|
194
|
+
// Create a square at a known distance
|
|
195
|
+
const square = world.factory.createRigidBody({
|
|
196
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
197
|
+
body: { dynamic: false, mass: 0 },
|
|
198
|
+
}, { position: { x: 0, y: -5 } });
|
|
199
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
200
|
+
|
|
201
|
+
world.simulate(1);
|
|
202
|
+
|
|
203
|
+
// Cast a ray from origin to y = -10
|
|
204
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
205
|
+
from: { x: 0, y: 0 },
|
|
206
|
+
to: { x: 0, y: -10 },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = world.raycast(raycastOptions);
|
|
210
|
+
expect(result.hasHit).toBe(true);
|
|
211
|
+
|
|
212
|
+
// The hit distance should be approximately 4 units
|
|
213
|
+
// (from origin to the edge of the square at y = -4)
|
|
214
|
+
expect(result.hitDistance).toBeCloseTo(4, 0.1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle array of collision groups correctly', () => {
|
|
218
|
+
// Register collision groups
|
|
219
|
+
const group1 = world.registerCollisionGroup();
|
|
220
|
+
const group2 = world.registerCollisionGroup();
|
|
221
|
+
|
|
222
|
+
// Create a square that belongs to group1
|
|
223
|
+
const square = world.factory.createRigidBody({
|
|
224
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
225
|
+
body: {
|
|
226
|
+
dynamic: false,
|
|
227
|
+
mass: 0,
|
|
228
|
+
ownCollisionGroups: [group1],
|
|
229
|
+
interactWithCollisionGroups: [group1, group2],
|
|
230
|
+
},
|
|
231
|
+
}, { position: { x: 0, y: -5 } });
|
|
232
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
233
|
+
|
|
234
|
+
world.simulate(1);
|
|
235
|
+
|
|
236
|
+
// Ray that checks against both groups should hit
|
|
237
|
+
const rayOptions: RaycastOptions<Point2> = {
|
|
238
|
+
from: { x: 0, y: 0 },
|
|
239
|
+
to: { x: 0, y: -10 },
|
|
240
|
+
collisionFilterGroups: [group1, group2],
|
|
241
|
+
collisionFilterMask: [group1, group2],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = world.raycast(rayOptions);
|
|
245
|
+
expect(result.hasHit).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should return correct hit body', () => {
|
|
249
|
+
// Create two squares at different positions
|
|
250
|
+
const square1 = world.factory.createRigidBody({
|
|
251
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
252
|
+
body: { dynamic: false, mass: 0 },
|
|
253
|
+
}, { position: { x: 0, y: -3 } });
|
|
254
|
+
square1.addToWorld({ physicsWorld: world } as any);
|
|
255
|
+
|
|
256
|
+
const square2 = world.factory.createRigidBody({
|
|
257
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
258
|
+
body: { dynamic: false, mass: 0 },
|
|
259
|
+
}, { position: { x: 0, y: -7 } });
|
|
260
|
+
square2.addToWorld({ physicsWorld: world } as any);
|
|
261
|
+
|
|
262
|
+
world.simulate(1);
|
|
263
|
+
|
|
264
|
+
// Cast a ray that should hit square1 first
|
|
265
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
266
|
+
from: { x: 0, y: 0 },
|
|
267
|
+
to: { x: 0, y: -10 },
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const result = world.raycast(raycastOptions);
|
|
271
|
+
expect(result.hasHit).toBe(true);
|
|
272
|
+
expect(result.hitBody).toBe(square1);
|
|
273
|
+
expect(result.hitPoint!.y).toBeCloseTo(-2.5, 0.1); // square1 edge is at y = -2.5
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle edge case with ray starting inside an object', () => {
|
|
277
|
+
// Create a square
|
|
278
|
+
const square = world.factory.createRigidBody({
|
|
279
|
+
shape: { shape: 'SQUARE', dimensions: { x: 4, y: 4 } },
|
|
280
|
+
body: { dynamic: false, mass: 0 },
|
|
281
|
+
}, { position: { x: 0, y: 0 } });
|
|
282
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
283
|
+
|
|
284
|
+
world.simulate(1);
|
|
285
|
+
|
|
286
|
+
// Cast a ray from inside the square
|
|
287
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
288
|
+
from: { x: 0, y: 0 }, // Center of the square
|
|
289
|
+
to: { x: 0, y: -10 },
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const result = world.raycast(raycastOptions);
|
|
293
|
+
|
|
294
|
+
// Behavior may vary depending on physics engine implementation
|
|
295
|
+
// Some engines might not detect hits when starting inside an object
|
|
296
|
+
// Others might detect the exit point
|
|
297
|
+
// We just verify the method doesn't crash and returns a valid result
|
|
298
|
+
expect(result).toBeDefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"extends": "./../../tsconfig.base.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"baseUrl": "./src/",
|
|
5
|
-
"outDir": "./dist/"
|
|
5
|
+
"outDir": "./dist/",
|
|
6
|
+
"rootDir": "./src/",
|
|
7
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
|
6
8
|
},
|
|
7
9
|
"include": ["src/*.ts", "src/**/*.ts"],
|
|
10
|
+
"references": [{ "path": "../core" }]
|
|
8
11
|
}
|