@gg-web-engine/rapier2d 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/rapier-2d-rigid-body.component.d.ts +4 -4
- package/dist/components/rapier-2d-rigid-body.component.js +1 -0
- package/dist/components/rapier-2d-trigger.component.d.ts +7 -5
- package/dist/components/rapier-2d-trigger.component.js +30 -14
- package/dist/components/rapier-2d-world.component.d.ts +2 -1
- package/dist/components/rapier-2d-world.component.js +43 -1
- package/dist/types.d.ts +5 -0
- package/package.json +3 -3
- package/test/components/rapier-2d-trigger.component.spec.ts +51 -50
- package/test/components/rapier-2d-world.component.spec.ts +296 -0
- package/tsconfig.json +3 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Body2DOptions, CollisionGroup, DebugBody2DSettings, Entity2d,
|
|
1
|
+
import { Body2DOptions, CollisionGroup, DebugBody2DSettings, Entity2d, IRigidBody2dComponent, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
2
2
|
import { Collider, ColliderDesc, InteractionGroups, RigidBody, RigidBodyDesc } from '@dimforge/rapier2d-compat';
|
|
3
3
|
import { Rapier2dWorldComponent } from './rapier-2d-world.component';
|
|
4
|
-
import { Rapier2dPhysicsTypeDocRepo } from '../types';
|
|
4
|
+
import { Rapier2dGgWorld, Rapier2dPhysicsTypeDocRepo } from '../types';
|
|
5
5
|
export declare class Rapier2dRigidBodyComponent implements IRigidBody2dComponent<Rapier2dPhysicsTypeDocRepo> {
|
|
6
6
|
protected readonly world: Rapier2dWorldComponent;
|
|
7
7
|
protected _colliderDescr: ColliderDesc[];
|
|
@@ -36,8 +36,8 @@ export declare class Rapier2dRigidBodyComponent implements IRigidBody2dComponent
|
|
|
36
36
|
get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
37
37
|
set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
38
38
|
clone(): Rapier2dRigidBodyComponent;
|
|
39
|
-
addToWorld(world:
|
|
40
|
-
removeFromWorld(world:
|
|
39
|
+
addToWorld(world: Rapier2dGgWorld): void;
|
|
40
|
+
removeFromWorld(world: Rapier2dGgWorld): void;
|
|
41
41
|
resetMotion(): void;
|
|
42
42
|
dispose(): void;
|
|
43
43
|
}
|
|
@@ -127,6 +127,7 @@ export class Rapier2dRigidBodyComponent {
|
|
|
127
127
|
const col = this.world.nativeWorld.createCollider(c, this._nativeBody);
|
|
128
128
|
col.setFriction(this._colliderOptions.friction);
|
|
129
129
|
col.setRestitution(this._colliderOptions.restitution);
|
|
130
|
+
col.setCollisionGroups(this.collisionGroups);
|
|
130
131
|
return col;
|
|
131
132
|
});
|
|
132
133
|
this.world.handleIdEntityMap.set(this._nativeBody.handle, this);
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { Observable, Subject } from 'rxjs';
|
|
2
2
|
import { ColliderDesc, RigidBodyDesc } from '@dimforge/rapier2d-compat';
|
|
3
3
|
import { Rapier2dRigidBodyComponent } from './rapier-2d-rigid-body.component';
|
|
4
|
-
import { DebugBody2DSettings,
|
|
4
|
+
import { DebugBody2DSettings, ITrigger2dComponent, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
5
5
|
import { Rapier2dWorldComponent } from './rapier-2d-world.component';
|
|
6
|
-
import { Rapier2dPhysicsTypeDocRepo } from '../types';
|
|
6
|
+
import { Rapier2dGgWorld, Rapier2dPhysicsTypeDocRepo } from '../types';
|
|
7
7
|
export declare class Rapier2dTriggerComponent extends Rapier2dRigidBodyComponent implements ITrigger2dComponent<Rapier2dPhysicsTypeDocRepo> {
|
|
8
8
|
protected readonly world: Rapier2dWorldComponent;
|
|
9
9
|
protected _colliderDescr: ColliderDesc[];
|
|
10
10
|
readonly shape: Shape2DDescriptor;
|
|
11
11
|
protected _bodyDescr: RigidBodyDesc;
|
|
12
|
+
readonly debugBodySettings: DebugBody2DSettings;
|
|
12
13
|
get onEntityEntered(): Observable<Rapier2dRigidBodyComponent>;
|
|
13
14
|
get onEntityLeft(): Observable<Rapier2dRigidBodyComponent>;
|
|
15
|
+
protected readonly overlaps: Set<Rapier2dRigidBodyComponent>;
|
|
14
16
|
protected readonly onEnter$: Subject<Rapier2dRigidBodyComponent>;
|
|
15
17
|
protected readonly onLeft$: Subject<Rapier2dRigidBodyComponent>;
|
|
16
|
-
readonly debugBodySettings: DebugBody2DSettings;
|
|
17
|
-
protected intersectionsAmount: number;
|
|
18
18
|
constructor(world: Rapier2dWorldComponent, _colliderDescr: ColliderDesc[], shape: Shape2DDescriptor, _bodyDescr: RigidBodyDesc);
|
|
19
|
-
addToWorld(world:
|
|
19
|
+
addToWorld(world: Rapier2dGgWorld): void;
|
|
20
|
+
removeFromWorld(world: Rapier2dGgWorld): void;
|
|
20
21
|
checkOverlaps(): void;
|
|
21
22
|
clone(): Rapier2dTriggerComponent;
|
|
23
|
+
dispose(): void;
|
|
22
24
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Subject } from 'rxjs';
|
|
2
2
|
import { Rapier2dRigidBodyComponent } from './rapier-2d-rigid-body.component';
|
|
3
|
-
import { DebugBody2DSettings
|
|
3
|
+
import { DebugBody2DSettings } from '@gg-web-engine/core';
|
|
4
4
|
export class Rapier2dTriggerComponent extends Rapier2dRigidBodyComponent {
|
|
5
5
|
get onEntityEntered() {
|
|
6
6
|
return this.onEnter$.asObservable();
|
|
@@ -14,29 +14,28 @@ export class Rapier2dTriggerComponent extends Rapier2dRigidBodyComponent {
|
|
|
14
14
|
this._colliderDescr = _colliderDescr;
|
|
15
15
|
this.shape = shape;
|
|
16
16
|
this._bodyDescr = _bodyDescr;
|
|
17
|
+
this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.overlaps.size > 0 }, this.shape);
|
|
18
|
+
this.overlaps = new Set();
|
|
17
19
|
this.onEnter$ = new Subject();
|
|
18
20
|
this.onLeft$ = new Subject();
|
|
19
|
-
this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
|
|
20
|
-
this.intersectionsAmount = 0;
|
|
21
|
-
merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
|
|
22
|
-
if (enter) {
|
|
23
|
-
this.intersectionsAmount++;
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
this.intersectionsAmount--;
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
21
|
}
|
|
30
22
|
addToWorld(world) {
|
|
31
23
|
if (world.physicsWorld != this.world) {
|
|
32
24
|
throw new Error('Rapier2D bodies cannot be shared between different worlds');
|
|
33
25
|
}
|
|
34
|
-
this.
|
|
26
|
+
this.overlaps.clear();
|
|
35
27
|
this._nativeBody = this.world.nativeWorld.createRigidBody(this._bodyDescr);
|
|
36
28
|
this._nativeBodyColliders = this._colliderDescr.map(c => this.world.nativeWorld.createCollider(c, this._nativeBody));
|
|
37
29
|
this.world.handleIdEntityMap.set(this._nativeBody.handle, this);
|
|
38
30
|
this.world.added$.next(this);
|
|
39
31
|
}
|
|
32
|
+
removeFromWorld(world) {
|
|
33
|
+
for (const body of this.overlaps) {
|
|
34
|
+
this.onLeft$.next(body);
|
|
35
|
+
}
|
|
36
|
+
this.overlaps.clear();
|
|
37
|
+
super.removeFromWorld(world);
|
|
38
|
+
}
|
|
40
39
|
checkOverlaps() {
|
|
41
40
|
this.world.eventQueue.drainCollisionEvents((h1, h2, started) => {
|
|
42
41
|
var _a, _b;
|
|
@@ -49,8 +48,21 @@ export class Rapier2dTriggerComponent extends Rapier2dRigidBodyComponent {
|
|
|
49
48
|
}
|
|
50
49
|
if (!otherBody)
|
|
51
50
|
return;
|
|
52
|
-
(started
|
|
51
|
+
if (started) {
|
|
52
|
+
this.overlaps.add(otherBody);
|
|
53
|
+
this.onEnter$.next(otherBody);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.overlaps.delete(otherBody);
|
|
57
|
+
this.onLeft$.next(otherBody);
|
|
58
|
+
}
|
|
53
59
|
});
|
|
60
|
+
for (const body of this.overlaps.keys()) {
|
|
61
|
+
if (!body.nativeBody) {
|
|
62
|
+
this.overlaps.delete(body);
|
|
63
|
+
this.onLeft$.next(body);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
clone() {
|
|
56
68
|
const [colliderDescr, sd, bd] = super.factoryProps;
|
|
@@ -59,4 +71,8 @@ export class Rapier2dTriggerComponent extends Rapier2dRigidBodyComponent {
|
|
|
59
71
|
component.interactWithCollisionGroups = this.interactWithCollisionGroups;
|
|
60
72
|
return component;
|
|
61
73
|
}
|
|
74
|
+
dispose() {
|
|
75
|
+
this.overlaps.clear();
|
|
76
|
+
super.dispose();
|
|
77
|
+
}
|
|
62
78
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
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 { EventQueue, World } from '@dimforge/rapier2d-compat';
|
|
3
3
|
import { Rapier2dRigidBodyComponent } from './rapier-2d-rigid-body.component';
|
|
4
4
|
import { Rapier2dFactory } from '../rapier-2d-factory';
|
|
@@ -26,5 +26,6 @@ export declare class Rapier2dWorldComponent implements IPhysicsWorld2dComponent<
|
|
|
26
26
|
protected lockedCollisionGroups: number[];
|
|
27
27
|
registerCollisionGroup(): CollisionGroup;
|
|
28
28
|
deregisterCollisionGroup(group: CollisionGroup): void;
|
|
29
|
+
raycast(options: RaycastOptions<Point2>): RaycastResult<Point2, Rapier2dRigidBodyComponent>;
|
|
29
30
|
dispose(): void;
|
|
30
31
|
}
|
|
@@ -7,7 +7,7 @@ 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 { Pnt2 } from '@gg-web-engine/core';
|
|
10
|
+
import { BitMask, Pnt2, Pnt3, } from '@gg-web-engine/core';
|
|
11
11
|
import { EventQueue, init, Vector2, World } from '@dimforge/rapier2d-compat';
|
|
12
12
|
import { Rapier2dFactory } from '../rapier-2d-factory';
|
|
13
13
|
import { Subject } from 'rxjs';
|
|
@@ -80,6 +80,48 @@ export class Rapier2dWorldComponent {
|
|
|
80
80
|
deregisterCollisionGroup(group) {
|
|
81
81
|
this.lockedCollisionGroups = this.lockedCollisionGroups.filter(x => x !== group);
|
|
82
82
|
}
|
|
83
|
+
raycast(options) {
|
|
84
|
+
if (!this._nativeWorld) {
|
|
85
|
+
return { hasHit: false };
|
|
86
|
+
}
|
|
87
|
+
const origin = options.from;
|
|
88
|
+
let direction = Pnt2.sub(options.to, options.from);
|
|
89
|
+
const rayLength = Pnt2.len(direction);
|
|
90
|
+
direction = Pnt2.norm(direction);
|
|
91
|
+
const ray = {
|
|
92
|
+
origin: new Vector2(...Pnt2.spr(origin)),
|
|
93
|
+
dir: new Vector2(...Pnt2.spr(direction)),
|
|
94
|
+
pointAt: (t) => {
|
|
95
|
+
return new Vector2(origin.x + direction.x * t, origin.y + direction.y * t);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const mask = (groups) => {
|
|
99
|
+
return groups ? BitMask.pack(groups, 16) : BitMask.full(16);
|
|
100
|
+
};
|
|
101
|
+
const hit = this._nativeWorld.castRay(ray, rayLength, true, undefined, (mask(options.collisionFilterGroups) << 16) | mask(options.collisionFilterMask));
|
|
102
|
+
if (!hit) {
|
|
103
|
+
return { hasHit: false };
|
|
104
|
+
}
|
|
105
|
+
const result = {
|
|
106
|
+
hasHit: true,
|
|
107
|
+
};
|
|
108
|
+
const collider = hit.collider;
|
|
109
|
+
if (collider) {
|
|
110
|
+
const rigidBody = collider.parent();
|
|
111
|
+
if (!rigidBody) {
|
|
112
|
+
return { hasHit: false };
|
|
113
|
+
}
|
|
114
|
+
result.hitBody = this.handleIdEntityMap.get(rigidBody.handle);
|
|
115
|
+
result.hitDistance = hit.timeOfImpact;
|
|
116
|
+
result.hitPoint = Pnt2.add(origin, Pnt2.scalarMult(direction, hit.timeOfImpact));
|
|
117
|
+
const rayIntersection = hit.collider.castRayAndGetNormal(ray, hit.timeOfImpact, true);
|
|
118
|
+
result.hitNormal = Pnt2.clone((rayIntersection === null || rayIntersection === void 0 ? void 0 : rayIntersection.normal) || Pnt3.O);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result.hasHit = false;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
83
125
|
dispose() {
|
|
84
126
|
var _a;
|
|
85
127
|
(_a = this._nativeWorld) === null || _a === void 0 ? void 0 : _a.free();
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Rapier2dFactory } from './rapier-2d-factory';
|
|
2
2
|
import { Rapier2dRigidBodyComponent } from './components/rapier-2d-rigid-body.component';
|
|
3
3
|
import { Rapier2dTriggerComponent } from './components/rapier-2d-trigger.component';
|
|
4
|
+
import { Gg2dWorld, Gg2dWorldSceneTypeDocPPatch, Gg2dWorldTypeDocPPatch } from '@gg-web-engine/core';
|
|
5
|
+
import { Rapier2dWorldComponent } from './components/rapier-2d-world.component';
|
|
4
6
|
export type Rapier2dPhysicsTypeDocRepo = {
|
|
5
7
|
factory: Rapier2dFactory;
|
|
6
8
|
rigidBody: Rapier2dRigidBodyComponent;
|
|
7
9
|
trigger: Rapier2dTriggerComponent;
|
|
8
10
|
};
|
|
11
|
+
export type Rapier2dTypeDoc = Gg2dWorldTypeDocPPatch<Rapier2dPhysicsTypeDocRepo>;
|
|
12
|
+
export type Rapier2dSceneTypeDoc = Gg2dWorldSceneTypeDocPPatch<Rapier2dPhysicsTypeDocRepo, Rapier2dWorldComponent>;
|
|
13
|
+
export type Rapier2dGgWorld = Gg2dWorld<Rapier2dTypeDoc, Rapier2dSceneTypeDoc>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/rapier2d",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"homepage": "https://github.com/AndyGura/gg-web-engine#readme",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@dimforge/rapier2d-compat": "0.0.0-50223ff-20240729",
|
|
31
|
-
"@gg-web-engine/core": "0.0.
|
|
31
|
+
"@gg-web-engine/core": "0.0.59",
|
|
32
32
|
"@types/jest": "^29.5.12",
|
|
33
33
|
"jest": "^29.7.0",
|
|
34
34
|
"jest-environment-jsdom": "^29.7.0",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@dimforge/rapier2d-compat": "0.0.0-50223ff-20240729",
|
|
42
|
-
"@gg-web-engine/core": "0.0.
|
|
42
|
+
"@gg-web-engine/core": "0.0.59",
|
|
43
43
|
"rxjs": "7.8.1"
|
|
44
44
|
},
|
|
45
45
|
"jest": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Rapier2dFactory, Rapier2dWorldComponent } from '../../src';
|
|
2
1
|
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { Rapier2dFactory, Rapier2dWorldComponent } from '../../src';
|
|
3
3
|
|
|
4
4
|
describe(`Rapier2dTriggerComponent`, () => {
|
|
5
5
|
|
|
@@ -22,19 +22,19 @@ describe(`Rapier2dTriggerComponent`, () => {
|
|
|
22
22
|
it(`should detect object intersection`, async () => {
|
|
23
23
|
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
24
24
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
25
|
-
const
|
|
25
|
+
const circle = factory.createRigidBody({
|
|
26
26
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
27
27
|
body: { dynamic: true, mass: 1 },
|
|
28
28
|
}, { position: { x: 0, y: 12 } });
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
30
|
+
circle.linearVelocity = { x: 0, y: -10 };
|
|
31
31
|
let enterRegistered = false;
|
|
32
32
|
let exitRegistered = false;
|
|
33
33
|
trigger.onEntityEntered.subscribe(((obj) => {
|
|
34
|
-
enterRegistered = obj ===
|
|
34
|
+
enterRegistered = obj === circle;
|
|
35
35
|
}));
|
|
36
36
|
trigger.onEntityLeft.subscribe(((obj) => {
|
|
37
|
-
exitRegistered = obj ===
|
|
37
|
+
exitRegistered = obj === circle;
|
|
38
38
|
}));
|
|
39
39
|
world.simulate(500);
|
|
40
40
|
trigger.checkOverlaps(); // trigger entity performs that on tick
|
|
@@ -50,15 +50,15 @@ describe(`Rapier2dTriggerComponent`, () => {
|
|
|
50
50
|
it(`should detect end of object intersection`, async () => {
|
|
51
51
|
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
52
52
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
53
|
-
const
|
|
53
|
+
const circle = factory.createRigidBody({
|
|
54
54
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
55
55
|
body: { dynamic: true, mass: 1 },
|
|
56
56
|
}, { position: { x: 0, y: 12 } });
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
58
|
+
circle.linearVelocity = { x: 0, y: -10 };
|
|
59
59
|
let exitRegistered = false;
|
|
60
60
|
trigger.onEntityLeft.subscribe(((obj) => {
|
|
61
|
-
exitRegistered = obj ===
|
|
61
|
+
exitRegistered = obj === circle;
|
|
62
62
|
}));
|
|
63
63
|
world.simulate(1000);
|
|
64
64
|
trigger.checkOverlaps();
|
|
@@ -71,54 +71,55 @@ describe(`Rapier2dTriggerComponent`, () => {
|
|
|
71
71
|
it(`should fire object intersection if spawned inside`, async () => {
|
|
72
72
|
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
73
73
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
74
|
-
const
|
|
74
|
+
const circle = factory.createRigidBody({
|
|
75
75
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
76
76
|
body: { dynamic: true, mass: 1 },
|
|
77
|
-
}, { position:
|
|
78
|
-
|
|
77
|
+
}, { position: Pnt2.O });
|
|
78
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
79
79
|
let enterRegistered = false;
|
|
80
80
|
trigger.onEntityEntered.subscribe(((obj) => {
|
|
81
|
-
enterRegistered = obj ===
|
|
81
|
+
enterRegistered = obj === circle;
|
|
82
82
|
}));
|
|
83
83
|
world.simulate(1000);
|
|
84
84
|
trigger.checkOverlaps();
|
|
85
85
|
expect(enterRegistered).toBe(true);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
it(`should fire end of object intersection if trigger removed`, async () => {
|
|
89
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
90
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
91
|
+
const circle = factory.createRigidBody({
|
|
92
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
93
|
+
body: { dynamic: true, mass: 1 },
|
|
94
|
+
}, { position: Pnt2.O });
|
|
95
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
96
|
+
let exitRegistered = false;
|
|
97
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
98
|
+
exitRegistered = obj === circle;
|
|
99
|
+
}));
|
|
100
|
+
world.simulate(1);
|
|
101
|
+
trigger.checkOverlaps();
|
|
102
|
+
trigger.removeFromWorld({ physicsWorld: world } as any);
|
|
103
|
+
expect(exitRegistered).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it(`should fire end of object intersection if object removed`, async () => {
|
|
107
|
+
const trigger = factory.createTrigger({ shape: 'SQUARE', dimensions: { x: 10, y: 10 } });
|
|
108
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
109
|
+
const circle = factory.createRigidBody({
|
|
110
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
111
|
+
body: { dynamic: true, mass: 1 },
|
|
112
|
+
}, { position: Pnt2.O });
|
|
113
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
114
|
+
let exitRegistered = false;
|
|
115
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
116
|
+
exitRegistered = obj === circle;
|
|
117
|
+
}));
|
|
118
|
+
world.simulate(1);
|
|
119
|
+
trigger.checkOverlaps();
|
|
120
|
+
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
121
|
+
world.simulate(1);
|
|
122
|
+
trigger.checkOverlaps();
|
|
123
|
+
expect(exitRegistered).toBe(true);
|
|
124
|
+
});
|
|
124
125
|
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { Point2, RaycastOptions } from '@gg-web-engine/core';
|
|
2
|
+
import { Rapier2dWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('Rapier2dWorldComponent', () => {
|
|
5
|
+
let world: Rapier2dWorldComponent;
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
if (world) {
|
|
9
|
+
world.dispose();
|
|
10
|
+
}
|
|
11
|
+
world = new Rapier2dWorldComponent();
|
|
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
|
+
|
|
31
|
+
// simulate for 6 seconds
|
|
32
|
+
for (let i = 0; i < 100; i++) {
|
|
33
|
+
world.simulate(60);
|
|
34
|
+
}
|
|
35
|
+
expect(circle.position.x).toBeCloseTo(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should simulate collision of two rigid bodies', () => {
|
|
39
|
+
|
|
40
|
+
const circle0 = world.factory.createRigidBody({
|
|
41
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
42
|
+
body: { dynamic: true, mass: 5 },
|
|
43
|
+
}, { position: { x: -5, y: 0 } });
|
|
44
|
+
circle0.addToWorld({ physicsWorld: world } as any);
|
|
45
|
+
circle0.linearVelocity = { x: 1, y: 0 };
|
|
46
|
+
|
|
47
|
+
const circle1 = world.factory.createRigidBody({
|
|
48
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
49
|
+
body: { dynamic: true, mass: 5 },
|
|
50
|
+
}, { position: { x: 5, y: 0 } });
|
|
51
|
+
circle1.addToWorld({ physicsWorld: world } as any);
|
|
52
|
+
circle1.linearVelocity = { x: -1, y: 0 };
|
|
53
|
+
|
|
54
|
+
// simulate for 6 seconds
|
|
55
|
+
for (let i = 0; i < 100; i++) {
|
|
56
|
+
world.simulate(60);
|
|
57
|
+
}
|
|
58
|
+
expect(circle0.position.x).toBeLessThan(0);
|
|
59
|
+
expect(circle1.position.x).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should not simulate collision of two rigid bodies with different collision groups', () => {
|
|
63
|
+
const cg0 = world.registerCollisionGroup();
|
|
64
|
+
const circle0 = world.factory.createRigidBody({
|
|
65
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
66
|
+
body: { dynamic: true, mass: 5 },
|
|
67
|
+
}, { position: { x: -5, y: 0 } });
|
|
68
|
+
circle0.addToWorld({ physicsWorld: world } as any);
|
|
69
|
+
circle0.linearVelocity = { x: 1, y: 0 };
|
|
70
|
+
circle0.ownCollisionGroups = circle0.interactWithCollisionGroups = [cg0];
|
|
71
|
+
|
|
72
|
+
const cg1 = world.registerCollisionGroup();
|
|
73
|
+
const circle1 = world.factory.createRigidBody({
|
|
74
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
75
|
+
body: { dynamic: true, mass: 5 },
|
|
76
|
+
}, { position: { x: 5, y: 0 } });
|
|
77
|
+
circle1.addToWorld({ physicsWorld: world } as any);
|
|
78
|
+
circle1.linearVelocity = { x: -1, y: 0 };
|
|
79
|
+
circle1.ownCollisionGroups = circle1.interactWithCollisionGroups = [cg1];
|
|
80
|
+
|
|
81
|
+
// simulate for 6 seconds
|
|
82
|
+
for (let i = 0; i < 100; i++) {
|
|
83
|
+
world.simulate(60);
|
|
84
|
+
}
|
|
85
|
+
expect(circle0.position.x).toBeCloseTo(1);
|
|
86
|
+
expect(circle1.position.x).toBeCloseTo(-1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Raycast', () => {
|
|
91
|
+
|
|
92
|
+
it('should return no hit when ray does not intersect any object', () => {
|
|
93
|
+
// Create a square far away from the ray
|
|
94
|
+
const square = world.factory.createRigidBody({
|
|
95
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
96
|
+
body: { dynamic: false, mass: 0 },
|
|
97
|
+
}, { position: { x: 10, y: 10 } });
|
|
98
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
99
|
+
|
|
100
|
+
world.simulate(1);
|
|
101
|
+
|
|
102
|
+
// Cast a ray that doesn't hit anything
|
|
103
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
104
|
+
from: { x: 0, y: 0 },
|
|
105
|
+
to: { x: 0, y: -10 },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = world.raycast(raycastOptions);
|
|
109
|
+
expect(result.hasHit).toBe(false);
|
|
110
|
+
expect(result.hitPoint).toBeUndefined();
|
|
111
|
+
expect(result.hitNormal).toBeUndefined();
|
|
112
|
+
expect(result.hitDistance).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should detect hit when ray intersects an object', () => {
|
|
116
|
+
// Create a square in the path of the ray
|
|
117
|
+
const square = world.factory.createRigidBody({
|
|
118
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
119
|
+
body: { dynamic: false, mass: 0 },
|
|
120
|
+
}, { position: { x: 0, y: -5 } });
|
|
121
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
122
|
+
|
|
123
|
+
world.simulate(1);
|
|
124
|
+
|
|
125
|
+
// Cast a ray that hits the square
|
|
126
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
127
|
+
from: { x: 0, y: 0 },
|
|
128
|
+
to: { x: 0, y: -10 },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = world.raycast(raycastOptions);
|
|
132
|
+
expect(result.hasHit).toBe(true);
|
|
133
|
+
expect(result.hitBody).toBe(square);
|
|
134
|
+
expect(result.hitPoint).toBeDefined();
|
|
135
|
+
expect(result.hitNormal).toBeDefined();
|
|
136
|
+
expect(result.hitDistance).toBeDefined();
|
|
137
|
+
|
|
138
|
+
// The hit should be at approximately y = -4 (box at y = -5 with size 2)
|
|
139
|
+
expect(result.hitPoint!.y).toBeCloseTo(-4, 0.1);
|
|
140
|
+
|
|
141
|
+
// The normal should point up (towards the ray origin)
|
|
142
|
+
expect(result.hitNormal!.y).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should respect collision filtering', () => {
|
|
146
|
+
// Register collision groups
|
|
147
|
+
const group1 = world.registerCollisionGroup();
|
|
148
|
+
const group2 = world.registerCollisionGroup();
|
|
149
|
+
|
|
150
|
+
// Create a square that only belongs to group1
|
|
151
|
+
const square1 = world.factory.createRigidBody({
|
|
152
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
153
|
+
body: {
|
|
154
|
+
dynamic: false,
|
|
155
|
+
mass: 0,
|
|
156
|
+
ownCollisionGroups: [group1],
|
|
157
|
+
interactWithCollisionGroups: [group1, group2],
|
|
158
|
+
},
|
|
159
|
+
}, { position: { x: 0, y: -5 } });
|
|
160
|
+
square1.addToWorld({ physicsWorld: world } as any);
|
|
161
|
+
|
|
162
|
+
world.simulate(1);
|
|
163
|
+
|
|
164
|
+
// Ray that only checks against group2 should not hit
|
|
165
|
+
const rayOptions1: RaycastOptions<Point2> = {
|
|
166
|
+
from: { x: 0, y: 0 },
|
|
167
|
+
to: { x: 0, y: -10 },
|
|
168
|
+
collisionFilterGroups: [group2],
|
|
169
|
+
collisionFilterMask: [group2],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const result1 = world.raycast(rayOptions1);
|
|
173
|
+
expect(result1.hasHit).toBe(false);
|
|
174
|
+
|
|
175
|
+
// Ray that checks against group1 should hit
|
|
176
|
+
const rayOptions2: RaycastOptions<Point2> = {
|
|
177
|
+
from: { x: 0, y: 0 },
|
|
178
|
+
to: { x: 0, y: -10 },
|
|
179
|
+
collisionFilterGroups: [group1],
|
|
180
|
+
collisionFilterMask: [group1],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result2 = world.raycast(rayOptions2);
|
|
184
|
+
expect(result2.hasHit).toBe(true);
|
|
185
|
+
expect(result2.hitBody).toBe(square1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should calculate hit distance correctly', () => {
|
|
189
|
+
// Create a square at a known distance
|
|
190
|
+
const square = world.factory.createRigidBody({
|
|
191
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
192
|
+
body: { dynamic: false, mass: 0 },
|
|
193
|
+
}, { position: { x: 0, y: -5 } });
|
|
194
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
195
|
+
|
|
196
|
+
world.simulate(1);
|
|
197
|
+
|
|
198
|
+
// Cast a ray from origin to y = -10
|
|
199
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
200
|
+
from: { x: 0, y: 0 },
|
|
201
|
+
to: { x: 0, y: -10 },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = world.raycast(raycastOptions);
|
|
205
|
+
expect(result.hasHit).toBe(true);
|
|
206
|
+
|
|
207
|
+
// The hit distance should be approximately 4 units
|
|
208
|
+
// (from origin to the edge of the square at y = -4)
|
|
209
|
+
expect(result.hitDistance).toBeCloseTo(4, 0.1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle array of collision groups correctly', () => {
|
|
213
|
+
// Register collision groups
|
|
214
|
+
const group1 = world.registerCollisionGroup();
|
|
215
|
+
const group2 = world.registerCollisionGroup();
|
|
216
|
+
|
|
217
|
+
// Create a square that belongs to group1
|
|
218
|
+
const square = world.factory.createRigidBody({
|
|
219
|
+
shape: { shape: 'SQUARE', dimensions: { x: 2, y: 2 } },
|
|
220
|
+
body: {
|
|
221
|
+
dynamic: false,
|
|
222
|
+
mass: 0,
|
|
223
|
+
ownCollisionGroups: [group1],
|
|
224
|
+
interactWithCollisionGroups: [group1, group2],
|
|
225
|
+
},
|
|
226
|
+
}, { position: { x: 0, y: -5 } });
|
|
227
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
228
|
+
|
|
229
|
+
world.simulate(1);
|
|
230
|
+
|
|
231
|
+
// Ray that checks against both groups should hit
|
|
232
|
+
const rayOptions: RaycastOptions<Point2> = {
|
|
233
|
+
from: { x: 0, y: 0 },
|
|
234
|
+
to: { x: 0, y: -10 },
|
|
235
|
+
collisionFilterGroups: [group1, group2],
|
|
236
|
+
collisionFilterMask: [group1, group2],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const result = world.raycast(rayOptions);
|
|
240
|
+
expect(result.hasHit).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return correct hit body', () => {
|
|
244
|
+
// Create two squares at different positions
|
|
245
|
+
const square1 = world.factory.createRigidBody({
|
|
246
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
247
|
+
body: { dynamic: false, mass: 0 },
|
|
248
|
+
}, { position: { x: 0, y: -3 } });
|
|
249
|
+
square1.addToWorld({ physicsWorld: world } as any);
|
|
250
|
+
|
|
251
|
+
const square2 = world.factory.createRigidBody({
|
|
252
|
+
shape: { shape: 'SQUARE', dimensions: { x: 1, y: 1 } },
|
|
253
|
+
body: { dynamic: false, mass: 0 },
|
|
254
|
+
}, { position: { x: 0, y: -7 } });
|
|
255
|
+
square2.addToWorld({ physicsWorld: world } as any);
|
|
256
|
+
|
|
257
|
+
world.simulate(1);
|
|
258
|
+
|
|
259
|
+
// Cast a ray that should hit square1 first
|
|
260
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
261
|
+
from: { x: 0, y: 0 },
|
|
262
|
+
to: { x: 0, y: -10 },
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const result = world.raycast(raycastOptions);
|
|
266
|
+
expect(result.hasHit).toBe(true);
|
|
267
|
+
expect(result.hitBody).toBe(square1);
|
|
268
|
+
expect(result.hitPoint!.y).toBeCloseTo(-2.5, 0.1); // square1 edge is at y = -2.5
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle edge case with ray starting inside an object', () => {
|
|
272
|
+
// Create a square
|
|
273
|
+
const square = world.factory.createRigidBody({
|
|
274
|
+
shape: { shape: 'SQUARE', dimensions: { x: 4, y: 4 } },
|
|
275
|
+
body: { dynamic: false, mass: 0 },
|
|
276
|
+
}, { position: { x: 0, y: 0 } });
|
|
277
|
+
square.addToWorld({ physicsWorld: world } as any);
|
|
278
|
+
|
|
279
|
+
world.simulate(1);
|
|
280
|
+
|
|
281
|
+
// Cast a ray from inside the square
|
|
282
|
+
const raycastOptions: RaycastOptions<Point2> = {
|
|
283
|
+
from: { x: 0, y: 0 }, // Center of the square
|
|
284
|
+
to: { x: 0, y: -10 },
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const result = world.raycast(raycastOptions);
|
|
288
|
+
|
|
289
|
+
// Behavior may vary depending on physics engine implementation
|
|
290
|
+
// Some engines might not detect hits when starting inside an object
|
|
291
|
+
// Others might detect the exit point
|
|
292
|
+
// We just verify the method doesn't crash and returns a valid result
|
|
293
|
+
expect(result).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
package/tsconfig.json
CHANGED