@gg-web-engine/rapier3d 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.
@@ -1,8 +1,8 @@
1
- import { Gg3dWorld, IRaycastVehicleComponent, IVisualScene3dComponent, Point3, Point4, SuspensionOptions, VisualTypeDocRepo3D, WheelOptions } from '@gg-web-engine/core';
1
+ import { IRaycastVehicleComponent, Point3, Point4, SuspensionOptions, WheelOptions } from '@gg-web-engine/core';
2
2
  import { DynamicRayCastVehicleController } from '@dimforge/rapier3d-compat';
3
3
  import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
4
4
  import { Rapier3dWorldComponent } from './rapier-3d-world.component';
5
- import { Rapier3dPhysicsTypeDocRepo } from '../types';
5
+ import { Rapier3dGgWorld, Rapier3dPhysicsTypeDocRepo } from '../types';
6
6
  export declare class Rapier3dRaycastVehicleComponent extends Rapier3dRigidBodyComponent implements IRaycastVehicleComponent<Rapier3dPhysicsTypeDocRepo> {
7
7
  protected readonly world: Rapier3dWorldComponent;
8
8
  private chassisBody;
@@ -11,8 +11,8 @@ export declare class Rapier3dRaycastVehicleComponent extends Rapier3dRigidBodyCo
11
11
  private wheelDescr;
12
12
  constructor(world: Rapier3dWorldComponent, chassisBody: Rapier3dRigidBodyComponent);
13
13
  get wheelSpeed(): number;
14
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, Rapier3dPhysicsTypeDocRepo, IVisualScene3dComponent, Rapier3dWorldComponent>): void;
15
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, Rapier3dPhysicsTypeDocRepo, IVisualScene3dComponent, Rapier3dWorldComponent>): void;
14
+ addToWorld(world: Rapier3dGgWorld): void;
15
+ removeFromWorld(world: Rapier3dGgWorld): void;
16
16
  addWheel(options: WheelOptions, suspensionOptions: SuspensionOptions): void;
17
17
  setSteering(wheelIndex: number, steering: number): void;
18
18
  applyEngineForce(wheelIndex: number, force: number): void;
@@ -1,7 +1,7 @@
1
- import { Body3DOptions, CollisionGroup, DebugBody3DSettings, Entity3d, Gg3dWorld, IRigidBody3dComponent, Point3, Point4, Shape3DDescriptor, VisualTypeDocRepo3D } from '@gg-web-engine/core';
1
+ import { Body3DOptions, CollisionGroup, DebugBody3DSettings, Entity3d, IRigidBody3dComponent, Point3, Point4, Shape3DDescriptor } from '@gg-web-engine/core';
2
2
  import { Collider, ColliderDesc, RigidBody, RigidBodyDesc } from '@dimforge/rapier3d-compat';
3
3
  import { Rapier3dWorldComponent } from './rapier-3d-world.component';
4
- import { Rapier3dPhysicsTypeDocRepo } from '../types';
4
+ import { Rapier3dGgWorld, Rapier3dPhysicsTypeDocRepo } from '../types';
5
5
  import { InteractionGroups } from '@dimforge/rapier3d-compat/geometry/interaction_groups';
6
6
  export declare class Rapier3dRigidBodyComponent implements IRigidBody3dComponent<Rapier3dPhysicsTypeDocRepo> {
7
7
  protected readonly world: Rapier3dWorldComponent;
@@ -37,8 +37,8 @@ export declare class Rapier3dRigidBodyComponent implements IRigidBody3dComponent
37
37
  get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
38
38
  set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
39
39
  clone(): Rapier3dRigidBodyComponent;
40
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, Rapier3dPhysicsTypeDocRepo>): void;
41
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, Rapier3dPhysicsTypeDocRepo>): void;
40
+ addToWorld(world: Rapier3dGgWorld): void;
41
+ removeFromWorld(world: Rapier3dGgWorld): void;
42
42
  resetMotion(): void;
43
43
  dispose(): void;
44
44
  }
@@ -1,22 +1,24 @@
1
1
  import { Observable, Subject } from 'rxjs';
2
2
  import { ColliderDesc, RigidBodyDesc } from '@dimforge/rapier3d-compat';
3
3
  import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
4
- import { DebugBody3DSettings, Gg3dWorld, ITrigger3dComponent, Shape3DDescriptor, VisualTypeDocRepo3D } from '@gg-web-engine/core';
4
+ import { DebugBody3DSettings, ITrigger3dComponent, Shape3DDescriptor } from '@gg-web-engine/core';
5
5
  import { Rapier3dWorldComponent } from './rapier-3d-world.component';
6
- import { Rapier3dPhysicsTypeDocRepo } from '../types';
6
+ import { Rapier3dGgWorld, Rapier3dPhysicsTypeDocRepo } from '../types';
7
7
  export declare class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent implements ITrigger3dComponent<Rapier3dPhysicsTypeDocRepo> {
8
8
  protected readonly world: Rapier3dWorldComponent;
9
9
  protected _colliderDescr: ColliderDesc[];
10
10
  readonly shape: Shape3DDescriptor;
11
11
  protected _bodyDescr: RigidBodyDesc;
12
+ readonly debugBodySettings: DebugBody3DSettings;
12
13
  get onEntityEntered(): Observable<Rapier3dRigidBodyComponent>;
13
14
  get onEntityLeft(): Observable<Rapier3dRigidBodyComponent>;
15
+ protected readonly overlaps: Set<Rapier3dRigidBodyComponent>;
14
16
  protected readonly onEnter$: Subject<Rapier3dRigidBodyComponent>;
15
17
  protected readonly onLeft$: Subject<Rapier3dRigidBodyComponent>;
16
- readonly debugBodySettings: DebugBody3DSettings;
17
- protected intersectionsAmount: number;
18
18
  constructor(world: Rapier3dWorldComponent, _colliderDescr: ColliderDesc[], shape: Shape3DDescriptor, _bodyDescr: RigidBodyDesc);
19
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, Rapier3dPhysicsTypeDocRepo>): void;
19
+ addToWorld(world: Rapier3dGgWorld): void;
20
+ removeFromWorld(world: Rapier3dGgWorld): void;
20
21
  checkOverlaps(): void;
21
22
  clone(): Rapier3dTriggerComponent;
23
+ dispose(): void;
22
24
  }
@@ -1,6 +1,6 @@
1
- import { map, merge, Subject } from 'rxjs';
1
+ import { Subject } from 'rxjs';
2
2
  import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
3
- import { DebugBody3DSettings, } from '@gg-web-engine/core';
3
+ import { DebugBody3DSettings } from '@gg-web-engine/core';
4
4
  export class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
5
5
  get onEntityEntered() {
6
6
  return this.onEnter$.asObservable();
@@ -14,29 +14,28 @@ export class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
14
14
  this._colliderDescr = _colliderDescr;
15
15
  this.shape = shape;
16
16
  this._bodyDescr = _bodyDescr;
17
+ this.debugBodySettings = new DebugBody3DSettings({ 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 DebugBody3DSettings({ 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('Rapier3D bodies cannot be shared between different worlds');
33
25
  }
34
- this.intersectionsAmount = 0;
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 Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
49
48
  }
50
49
  if (!otherBody)
51
50
  return;
52
- (started ? this.onEnter$ : this.onLeft$).next(otherBody);
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 Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
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, IPhysicsWorld3dComponent, Point3 } from '@gg-web-engine/core';
1
+ import { CollisionGroup, IPhysicsWorld3dComponent, Point3, RaycastOptions, RaycastResult } from '@gg-web-engine/core';
2
2
  import { EventQueue, World } from '@dimforge/rapier3d-compat';
3
3
  import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
4
4
  import { Rapier3dFactory } from '../rapier-3d-factory';
@@ -28,5 +28,6 @@ export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<
28
28
  protected lockedCollisionGroups: number[];
29
29
  registerCollisionGroup(): CollisionGroup;
30
30
  deregisterCollisionGroup(group: CollisionGroup): void;
31
+ raycast(options: RaycastOptions<Point3>): RaycastResult<Point3, Rapier3dRigidBodyComponent>;
31
32
  dispose(): void;
32
33
  }
@@ -7,6 +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 { BitMask, Pnt3, } from '@gg-web-engine/core';
10
11
  import { EventQueue, init, Vector3, World } from '@dimforge/rapier3d-compat';
11
12
  import { Rapier3dFactory } from '../rapier-3d-factory';
12
13
  import { Rapier3dLoader } from '../rapier-3d-loader';
@@ -88,6 +89,48 @@ export class Rapier3dWorldComponent {
88
89
  deregisterCollisionGroup(group) {
89
90
  this.lockedCollisionGroups = this.lockedCollisionGroups.filter(x => x !== group);
90
91
  }
92
+ raycast(options) {
93
+ if (!this._nativeWorld) {
94
+ return { hasHit: false };
95
+ }
96
+ const origin = options.from;
97
+ let direction = Pnt3.sub(options.to, options.from);
98
+ const rayLength = Pnt3.len(direction);
99
+ direction = Pnt3.norm(direction);
100
+ const ray = {
101
+ origin: new Vector3(...Pnt3.spr(origin)),
102
+ dir: new Vector3(...Pnt3.spr(direction)),
103
+ pointAt: (t) => {
104
+ return new Vector3(origin.x + direction.x * t, origin.y + direction.y * t, origin.z + direction.z * t);
105
+ },
106
+ };
107
+ const mask = (groups) => {
108
+ return groups ? BitMask.pack(groups, 16) : BitMask.full(16);
109
+ };
110
+ const hit = this._nativeWorld.castRay(ray, rayLength, true, undefined, (mask(options.collisionFilterGroups) << 16) | mask(options.collisionFilterMask));
111
+ if (!hit) {
112
+ return { hasHit: false };
113
+ }
114
+ const result = {
115
+ hasHit: true,
116
+ };
117
+ const collider = hit.collider;
118
+ if (collider) {
119
+ const rigidBody = collider.parent();
120
+ if (!rigidBody) {
121
+ return { hasHit: false };
122
+ }
123
+ result.hitBody = this.handleIdEntityMap.get(rigidBody.handle);
124
+ result.hitDistance = hit.timeOfImpact;
125
+ result.hitPoint = Pnt3.add(origin, Pnt3.scalarMult(direction, hit.timeOfImpact));
126
+ const rayIntersection = hit.collider.castRayAndGetNormal(ray, hit.timeOfImpact, true);
127
+ result.hitNormal = Pnt3.clone((rayIntersection === null || rayIntersection === void 0 ? void 0 : rayIntersection.normal) || Pnt3.O);
128
+ }
129
+ else {
130
+ result.hasHit = false;
131
+ }
132
+ return result;
133
+ }
91
134
  dispose() {
92
135
  var _a;
93
136
  (_a = this._nativeWorld) === null || _a === void 0 ? void 0 : _a.free();
package/dist/types.d.ts CHANGED
@@ -2,6 +2,8 @@ import { Rapier3dFactory } from './rapier-3d-factory';
2
2
  import { Rapier3dLoader } from './rapier-3d-loader';
3
3
  import { Rapier3dRigidBodyComponent } from './components/rapier-3d-rigid-body.component';
4
4
  import { Rapier3dTriggerComponent } from './components/rapier-3d-trigger.component';
5
+ import { Gg3dWorld, Gg3dWorldSceneTypeDocPPatch, Gg3dWorldTypeDocPPatch } from '@gg-web-engine/core';
6
+ import { Rapier3dWorldComponent } from './components/rapier-3d-world.component';
5
7
  export type Rapier3dPhysicsTypeDocRepo = {
6
8
  factory: Rapier3dFactory;
7
9
  loader: Rapier3dLoader;
@@ -9,3 +11,6 @@ export type Rapier3dPhysicsTypeDocRepo = {
9
11
  trigger: Rapier3dTriggerComponent;
10
12
  raycastVehicle: never;
11
13
  };
14
+ export type Rapier3dTypeDoc = Gg3dWorldTypeDocPPatch<Rapier3dPhysicsTypeDocRepo>;
15
+ export type Rapier3dSceneTypeDoc = Gg3dWorldSceneTypeDocPPatch<Rapier3dPhysicsTypeDocRepo, Rapier3dWorldComponent>;
16
+ export type Rapier3dGgWorld = Gg3dWorld<Rapier3dTypeDoc, Rapier3dSceneTypeDoc>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/rapier3d",
3
- "version": "0.0.57",
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/rapier3d-compat": "0.0.0-50223ff-20240729",
31
- "@gg-web-engine/core": "0.0.57",
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/rapier3d-compat": "0.0.0-50223ff-20240729",
42
- "@gg-web-engine/core": "0.0.57",
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 { Rapier3dFactory, Rapier3dWorldComponent } from '../../src';
2
1
  import { Pnt3 } from '@gg-web-engine/core';
2
+ import { Rapier3dFactory, Rapier3dWorldComponent } from '../../src';
3
3
 
4
4
  describe(`Rapier3dTriggerComponent`, () => {
5
5
 
@@ -85,40 +85,41 @@ describe(`Rapier3dTriggerComponent`, () => {
85
85
  expect(enterRegistered).toBe(true);
86
86
  });
87
87
 
88
- // TODO implement functionality below
89
- // it(`should fire end of object intersection if trigger removed`, async () => {
90
- // const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10, z: 10 } });
91
- // trigger.addToWorld({ physicsWorld: world } as any);
92
- // const ball = factory.createRigidBody({
93
- // shape: { shape: 'SPHERE', radius: 1 },
94
- // body: { dynamic: true, mass: 1 },
95
- // }, { position: { x: 0, y: 0, z: 0 } });
96
- // ball.addToWorld({ physicsWorld: world } as any);
97
- // let exitRegistered = false;
98
- // trigger.onEntityLeft.subscribe(((obj) => {
99
- // exitRegistered = obj === ball;
100
- // }));
101
- // world.simulate(1000);
102
- // trigger.checkOverlaps();
103
- // trigger.removeFromWorld({ physicsWorld: world } as any);
104
- // expect(exitRegistered).toBe(true);
105
- // });
106
- //
107
- // it(`should fire end of object intersection if object removed`, async () => {
108
- // const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10, z: 10 } });
109
- // trigger.addToWorld({ physicsWorld: world } as any);
110
- // const ball = factory.createRigidBody({
111
- // shape: { shape: 'SPHERE', radius: 1 },
112
- // body: { dynamic: true, mass: 1 },
113
- // }, { position: { x: 0, y: 0, z: 0 } });
114
- // ball.addToWorld({ physicsWorld: world } as any);
115
- // let exitRegistered = false;
116
- // trigger.onEntityLeft.subscribe(((obj) => {
117
- // exitRegistered = obj === ball;
118
- // }));
119
- // world.simulate(1000);
120
- // trigger.checkOverlaps();
121
- // ball.removeFromWorld({ physicsWorld: world } as any);
122
- // expect(exitRegistered).toBe(true);
123
- // });
88
+ it(`should fire end of object intersection if trigger removed`, async () => {
89
+ const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10, z: 10 } });
90
+ trigger.addToWorld({ physicsWorld: world } as any);
91
+ const ball = factory.createRigidBody({
92
+ shape: { shape: 'SPHERE', radius: 1 },
93
+ body: { dynamic: true, mass: 1 },
94
+ }, { position: { x: 0, y: 0, z: 0 } });
95
+ ball.addToWorld({ physicsWorld: world } as any);
96
+ let exitRegistered = false;
97
+ trigger.onEntityLeft.subscribe(((obj) => {
98
+ exitRegistered = obj === ball;
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: 'BOX', dimensions: { x: 10, y: 10, z: 10 } });
108
+ trigger.addToWorld({ physicsWorld: world } as any);
109
+ const ball = factory.createRigidBody({
110
+ shape: { shape: 'SPHERE', radius: 1 },
111
+ body: { dynamic: true, mass: 1 },
112
+ }, { position: { x: 0, y: 0, z: 0 } });
113
+ ball.addToWorld({ physicsWorld: world } as any);
114
+ let exitRegistered = false;
115
+ trigger.onEntityLeft.subscribe(((obj) => {
116
+ exitRegistered = obj === ball;
117
+ }));
118
+ world.simulate(1);
119
+ trigger.checkOverlaps();
120
+ ball.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 { Point3, RaycastOptions } from '@gg-web-engine/core';
2
+ import { Rapier3dWorldComponent } from '../../src';
3
+
4
+ describe('Rapier3dWorldComponent', () => {
5
+ let world: Rapier3dWorldComponent;
6
+
7
+ beforeEach(async () => {
8
+ if (world) {
9
+ world.dispose();
10
+ }
11
+ world = new Rapier3dWorldComponent();
12
+ await world.init();
13
+ world.gravity = { x: 0, y: 0, z: 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 ball = world.factory.createRigidBody({
25
+ shape: { shape: 'SPHERE', radius: 1 },
26
+ body: { dynamic: true, mass: 5 },
27
+ }, { position: { x: -5, y: 0, z: 0 } });
28
+ ball.addToWorld({ physicsWorld: world } as any);
29
+ ball.linearVelocity = { x: 1, y: 0, z: 0 };
30
+
31
+ // simulate for 6 seconds
32
+ for (let i = 0; i < 100; i++) {
33
+ world.simulate(60);
34
+ }
35
+ expect(ball.position.x).toBeCloseTo(1);
36
+ });
37
+
38
+ it('should simulate collision of two rigid bodies', () => {
39
+
40
+ const ball0 = world.factory.createRigidBody({
41
+ shape: { shape: 'SPHERE', radius: 1 },
42
+ body: { dynamic: true, mass: 5 },
43
+ }, { position: { x: -5, y: 0, z: 0 } });
44
+ ball0.addToWorld({ physicsWorld: world } as any);
45
+ ball0.linearVelocity = { x: 1, y: 0, z: 0 };
46
+
47
+ const ball1 = world.factory.createRigidBody({
48
+ shape: { shape: 'SPHERE', radius: 1 },
49
+ body: { dynamic: true, mass: 5 },
50
+ }, { position: { x: 5, y: 0, z: 0 } });
51
+ ball1.addToWorld({ physicsWorld: world } as any);
52
+ ball1.linearVelocity = { x: -1, y: 0, z: 0 };
53
+
54
+ // simulate for 6 seconds
55
+ for (let i = 0; i < 100; i++) {
56
+ world.simulate(60);
57
+ }
58
+ expect(ball0.position.x).toBeLessThan(0);
59
+ expect(ball1.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 ball0 = world.factory.createRigidBody({
65
+ shape: { shape: 'SPHERE', radius: 1 },
66
+ body: { dynamic: true, mass: 5 },
67
+ }, { position: { x: -5, y: 0, z: 0 } });
68
+ ball0.addToWorld({ physicsWorld: world } as any);
69
+ ball0.linearVelocity = { x: 1, y: 0, z: 0 };
70
+ ball0.ownCollisionGroups = ball0.interactWithCollisionGroups = [cg0];
71
+
72
+ const cg1 = world.registerCollisionGroup();
73
+ const ball1 = world.factory.createRigidBody({
74
+ shape: { shape: 'SPHERE', radius: 1 },
75
+ body: { dynamic: true, mass: 5 },
76
+ }, { position: { x: 5, y: 0, z: 0 } });
77
+ ball1.addToWorld({ physicsWorld: world } as any);
78
+ ball1.linearVelocity = { x: -1, y: 0, z: 0 };
79
+ ball1.ownCollisionGroups = ball1.interactWithCollisionGroups = [cg1];
80
+
81
+ // simulate for 6 seconds
82
+ for (let i = 0; i < 100; i++) {
83
+ world.simulate(60);
84
+ }
85
+ expect(ball0.position.x).toBeCloseTo(1);
86
+ expect(ball1.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 box far away from the ray
94
+ const box = world.factory.createRigidBody({
95
+ shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } },
96
+ body: { dynamic: false, mass: 0 },
97
+ }, { position: { x: 10, y: 10, z: 10 } });
98
+ box.addToWorld({ physicsWorld: world } as any);
99
+
100
+ world.simulate(1);
101
+
102
+ // Cast a ray that doesn't hit anything
103
+ const raycastOptions: RaycastOptions<Point3> = {
104
+ from: { x: 0, y: 0, z: 0 },
105
+ to: { x: 0, y: 0, z: -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 box in the path of the ray
117
+ const box = world.factory.createRigidBody({
118
+ shape: { shape: 'BOX', dimensions: { x: 2, y: 2, z: 2 } },
119
+ body: { dynamic: false, mass: 0 },
120
+ }, { position: { x: 0, y: 0, z: -5 } });
121
+ box.addToWorld({ physicsWorld: world } as any);
122
+
123
+ world.simulate(1);
124
+
125
+ // Cast a ray that hits the box
126
+ const raycastOptions: RaycastOptions<Point3> = {
127
+ from: { x: 0, y: 0, z: 0 },
128
+ to: { x: 0, y: 0, z: -10 },
129
+ };
130
+
131
+ const result = world.raycast(raycastOptions);
132
+ expect(result.hasHit).toBe(true);
133
+ expect(result.hitBody).toBe(box);
134
+ expect(result.hitPoint).toBeDefined();
135
+ expect(result.hitNormal).toBeDefined();
136
+ expect(result.hitDistance).toBeDefined();
137
+
138
+ // The hit should be at approximately z = -4 (box at z = -5 with size 2)
139
+ expect(result.hitPoint!.z).toBeCloseTo(-4, 0.1);
140
+
141
+ // The normal should point up (towards the ray origin)
142
+ expect(result.hitNormal!.z).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 box that only belongs to group1
151
+ const box1 = world.factory.createRigidBody({
152
+ shape: { shape: 'BOX', dimensions: { x: 2, y: 2, z: 2 } },
153
+ body: {
154
+ dynamic: false,
155
+ mass: 0,
156
+ ownCollisionGroups: [group1],
157
+ interactWithCollisionGroups: [group1, group2],
158
+ },
159
+ }, { position: { x: 0, y: 0, z: -5 } });
160
+ box1.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<Point3> = {
166
+ from: { x: 0, y: 0, z: 0 },
167
+ to: { x: 0, y: 0, z: -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<Point3> = {
177
+ from: { x: 0, y: 0, z: 0 },
178
+ to: { x: 0, y: 0, z: -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(box1);
186
+ });
187
+
188
+ it('should calculate hit distance correctly', () => {
189
+ // Create a box at a known distance
190
+ const box = world.factory.createRigidBody({
191
+ shape: { shape: 'BOX', dimensions: { x: 2, y: 2, z: 2 } },
192
+ body: { dynamic: false, mass: 0 },
193
+ }, { position: { x: 0, y: 0, z: -5 } });
194
+ box.addToWorld({ physicsWorld: world } as any);
195
+
196
+ world.simulate(1);
197
+
198
+ // Cast a ray from origin to z = -10
199
+ const raycastOptions: RaycastOptions<Point3> = {
200
+ from: { x: 0, y: 0, z: 0 },
201
+ to: { x: 0, y: 0, z: -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 box at z = -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 box that belongs to group1
218
+ const box = world.factory.createRigidBody({
219
+ shape: { shape: 'BOX', dimensions: { x: 2, y: 2, z: 2 } },
220
+ body: {
221
+ dynamic: false,
222
+ mass: 0,
223
+ ownCollisionGroups: [group1],
224
+ interactWithCollisionGroups: [group1, group2],
225
+ },
226
+ }, { position: { x: 0, y: 0, z: -5 } });
227
+ box.addToWorld({ physicsWorld: world } as any);
228
+
229
+ world.simulate(1);
230
+
231
+ // Ray that checks against both groups should hit
232
+ const rayOptions: RaycastOptions<Point3> = {
233
+ from: { x: 0, y: 0, z: 0 },
234
+ to: { x: 0, y: 0, z: -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 boxes at different positions
245
+ const box1 = world.factory.createRigidBody({
246
+ shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } },
247
+ body: { dynamic: false, mass: 0 },
248
+ }, { position: { x: 0, y: 0, z: -3 } });
249
+ box1.addToWorld({ physicsWorld: world } as any);
250
+
251
+ const box2 = world.factory.createRigidBody({
252
+ shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } },
253
+ body: { dynamic: false, mass: 0 },
254
+ }, { position: { x: 0, y: 0, z: -7 } });
255
+ box2.addToWorld({ physicsWorld: world } as any);
256
+
257
+ world.simulate(1);
258
+
259
+ // Cast a ray that should hit box1 first
260
+ const raycastOptions: RaycastOptions<Point3> = {
261
+ from: { x: 0, y: 0, z: 0 },
262
+ to: { x: 0, y: 0, z: -10 },
263
+ };
264
+
265
+ const result = world.raycast(raycastOptions);
266
+ expect(result.hasHit).toBe(true);
267
+ expect(result.hitBody).toBe(box1);
268
+ expect(result.hitPoint!.z).toBeCloseTo(-2.5, 0.1); // box1 edge is at z = -2.5
269
+ });
270
+
271
+ it('should handle edge case with ray starting inside an object', () => {
272
+ // Create a box
273
+ const box = world.factory.createRigidBody({
274
+ shape: { shape: 'BOX', dimensions: { x: 4, y: 4, z: 4 } },
275
+ body: { dynamic: false, mass: 0 },
276
+ }, { position: { x: 0, y: 0, z: 0 } });
277
+ box.addToWorld({ physicsWorld: world } as any);
278
+
279
+ world.simulate(1);
280
+
281
+ // Cast a ray from inside the box
282
+ const raycastOptions: RaycastOptions<Point3> = {
283
+ from: { x: 0, y: 0, z: 0 }, // Center of the box
284
+ to: { x: 0, y: 0, z: -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
@@ -3,6 +3,9 @@
3
3
  "compilerOptions": {
4
4
  "baseUrl": "./src/",
5
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
  }