@gg-web-engine/ammo 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.
@@ -730,9 +730,7 @@ declare module Ammo {
730
730
  get_primitive_triangle(prim_index: number, triangle: btPrimitiveTriangle): void;
731
731
  }
732
732
  type eGIMPACT_SHAPE_TYPE =
733
- | 'CONST_GIMPACT_COMPOUND_SHAPE'
734
- | 'CONST_GIMPACT_TRIMESH_SHAPE_PART'
735
- | 'CONST_GIMPACT_TRIMESH_SHAPE';
733
+ 'CONST_GIMPACT_COMPOUND_SHAPE' | 'CONST_GIMPACT_TRIMESH_SHAPE_PART' | 'CONST_GIMPACT_TRIMESH_SHAPE';
736
734
  class btTetrahedronShapeEx {
737
735
  constructor();
738
736
  setVertices(v0: btVector3, v1: btVector3, v2: btVector3, v3: btVector3): void;
@@ -1111,10 +1109,7 @@ declare module Ammo {
1111
1109
  | 'FIXED_CONSTRAINT_TYPE'
1112
1110
  | 'MAX_CONSTRAINT_TYPE';
1113
1111
  type btConstraintParams =
1114
- | 'BT_CONSTRAINT_ERP'
1115
- | 'BT_CONSTRAINT_STOP_ERP'
1116
- | 'BT_CONSTRAINT_CFM'
1117
- | 'BT_CONSTRAINT_STOP_CFM';
1112
+ 'BT_CONSTRAINT_ERP' | 'BT_CONSTRAINT_STOP_ERP' | 'BT_CONSTRAINT_CFM' | 'BT_CONSTRAINT_STOP_CFM';
1118
1113
  class btPoint2PointConstraint extends btTypedConstraint {
1119
1114
  constructor(rbA: btRigidBody, rbB: btRigidBody, pivotInA: btVector3, pivotInB: btVector3);
1120
1115
  constructor(rbA: btRigidBody, pivotInA: btVector3);
@@ -731,9 +731,7 @@ declare module Ammo {
731
731
  get_primitive_triangle(prim_index: number, triangle: btPrimitiveTriangle): void;
732
732
  }
733
733
  type eGIMPACT_SHAPE_TYPE =
734
- | 'CONST_GIMPACT_COMPOUND_SHAPE'
735
- | 'CONST_GIMPACT_TRIMESH_SHAPE_PART'
736
- | 'CONST_GIMPACT_TRIMESH_SHAPE';
734
+ 'CONST_GIMPACT_COMPOUND_SHAPE' | 'CONST_GIMPACT_TRIMESH_SHAPE_PART' | 'CONST_GIMPACT_TRIMESH_SHAPE';
737
735
  class btTetrahedronShapeEx {
738
736
  constructor();
739
737
  setVertices(v0: btVector3, v1: btVector3, v2: btVector3, v3: btVector3): void;
@@ -1112,10 +1110,7 @@ declare module Ammo {
1112
1110
  | 'FIXED_CONSTRAINT_TYPE'
1113
1111
  | 'MAX_CONSTRAINT_TYPE';
1114
1112
  type btConstraintParams =
1115
- | 'BT_CONSTRAINT_ERP'
1116
- | 'BT_CONSTRAINT_STOP_ERP'
1117
- | 'BT_CONSTRAINT_CFM'
1118
- | 'BT_CONSTRAINT_STOP_CFM';
1113
+ 'BT_CONSTRAINT_ERP' | 'BT_CONSTRAINT_STOP_ERP' | 'BT_CONSTRAINT_CFM' | 'BT_CONSTRAINT_STOP_CFM';
1119
1114
  class btPoint2PointConstraint extends btTypedConstraint {
1120
1115
  constructor(rbA: btRigidBody, rbB: btRigidBody, pivotInA: btVector3, pivotInB: btVector3);
1121
1116
  constructor(rbA: btRigidBody, pivotInA: btVector3);
@@ -1,12 +1,12 @@
1
- import { CollisionGroup, Gg3dWorld, IEntity, Point3, Point4, Shape3DDescriptor, VisualTypeDocRepo3D } from '@gg-web-engine/core';
1
+ import { CollisionGroup, IEntity, Point3, Point4, Shape3DDescriptor } from '@gg-web-engine/core';
2
2
  import { AmmoWorldComponent } from './ammo-world.component';
3
3
  import Ammo from '../ammo.js/ammo';
4
- import { AmmoPhysicsTypeDocRepo } from '../types';
4
+ import { AmmoGgWorld } from '../types';
5
5
  export declare abstract class AmmoBodyComponent<T extends Ammo.btCollisionObject> {
6
6
  protected readonly world: AmmoWorldComponent;
7
7
  protected _nativeBody: T;
8
8
  readonly shape: Shape3DDescriptor;
9
- protected static nativeBodyReverseMap: Map<number, AmmoBodyComponent<any>>;
9
+ static nativeBodyReverseMap: Map<number, AmmoBodyComponent<any>>;
10
10
  get position(): Point3;
11
11
  set position(value: Point3);
12
12
  get rotation(): Point4;
@@ -25,7 +25,7 @@ export declare abstract class AmmoBodyComponent<T extends Ammo.btCollisionObject
25
25
  set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
26
26
  protected constructor(world: AmmoWorldComponent, _nativeBody: T, shape: Shape3DDescriptor);
27
27
  abstract clone(): AmmoBodyComponent<T>;
28
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
29
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
28
+ addToWorld(world: AmmoGgWorld): void;
29
+ removeFromWorld(world: AmmoGgWorld): void;
30
30
  dispose(): void;
31
31
  }
@@ -1,4 +1,4 @@
1
- import { BitMask, } from '@gg-web-engine/core';
1
+ import { BitMask } from '@gg-web-engine/core';
2
2
  import Ammo from '../ammo.js/ammo';
3
3
  export class AmmoBodyComponent {
4
4
  get position() {
@@ -1,8 +1,8 @@
1
- import { CollisionGroup, Gg3dWorld, IRaycastVehicleComponent, Point3, Point4, RaycastVehicle3dEntity, SuspensionOptions, VisualTypeDocRepo3D, WheelOptions } from '@gg-web-engine/core';
1
+ import { CollisionGroup, IRaycastVehicleComponent, Point3, Point4, RaycastVehicle3dEntity, SuspensionOptions, WheelOptions } from '@gg-web-engine/core';
2
2
  import Ammo from '../ammo.js/ammo';
3
3
  import { AmmoRigidBodyComponent } from './ammo-rigid-body.component';
4
4
  import { AmmoWorldComponent } from './ammo-world.component';
5
- import { AmmoPhysicsTypeDocRepo } from '../types';
5
+ import { AmmoGgWorld, AmmoPhysicsTypeDocRepo } from '../types';
6
6
  export declare class AmmoRaycastVehicleComponent extends AmmoRigidBodyComponent implements IRaycastVehicleComponent<AmmoPhysicsTypeDocRepo> {
7
7
  protected readonly world: AmmoWorldComponent;
8
8
  chassisBody: AmmoRigidBodyComponent;
@@ -19,8 +19,8 @@ export declare class AmmoRaycastVehicleComponent extends AmmoRigidBodyComponent
19
19
  refreshCG(): void;
20
20
  constructor(world: AmmoWorldComponent, chassisBody: AmmoRigidBodyComponent);
21
21
  get wheelSpeed(): number;
22
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
23
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
22
+ addToWorld(world: AmmoGgWorld): void;
23
+ removeFromWorld(world: AmmoGgWorld): void;
24
24
  addWheel(options: WheelOptions, suspensionOptions: SuspensionOptions): void;
25
25
  setSteering(wheelIndex: number, steering: number): void;
26
26
  applyEngineForce(wheelIndex: number, force: number): void;
@@ -1,8 +1,8 @@
1
1
  import { AmmoWorldComponent } from './ammo-world.component';
2
2
  import Ammo from '../ammo.js/ammo';
3
3
  import { AmmoBodyComponent } from './ammo-body.component';
4
- import { DebugBody3DSettings, Entity3d, Gg3dWorld, IRigidBody3dComponent, Point3, Shape3DDescriptor, VisualTypeDocRepo3D } from '@gg-web-engine/core';
5
- import { AmmoPhysicsTypeDocRepo } from '../types';
4
+ import { DebugBody3DSettings, Entity3d, IRigidBody3dComponent, Point3, Shape3DDescriptor } from '@gg-web-engine/core';
5
+ import { AmmoGgWorld, AmmoPhysicsTypeDocRepo } from '../types';
6
6
  export declare class AmmoRigidBodyComponent extends AmmoBodyComponent<Ammo.btRigidBody> implements IRigidBody3dComponent<AmmoPhysicsTypeDocRepo> {
7
7
  protected readonly world: AmmoWorldComponent;
8
8
  protected _nativeBody: Ammo.btRigidBody;
@@ -15,8 +15,8 @@ export declare class AmmoRigidBodyComponent extends AmmoBodyComponent<Ammo.btRig
15
15
  readonly debugBodySettings: DebugBody3DSettings;
16
16
  constructor(world: AmmoWorldComponent, _nativeBody: Ammo.btRigidBody, shape: Shape3DDescriptor);
17
17
  clone(): AmmoRigidBodyComponent;
18
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
19
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
18
+ addToWorld(world: AmmoGgWorld): void;
19
+ removeFromWorld(world: AmmoGgWorld): void;
20
20
  refreshCG(): void;
21
21
  resetMotion(): void;
22
22
  }
@@ -1,6 +1,6 @@
1
1
  import Ammo from '../ammo.js/ammo';
2
2
  import { AmmoBodyComponent } from './ammo-body.component';
3
- import { DebugBody3DSettings, } from '@gg-web-engine/core';
3
+ import { DebugBody3DSettings } from '@gg-web-engine/core';
4
4
  import { first } from 'rxjs';
5
5
  export class AmmoRigidBodyComponent extends AmmoBodyComponent {
6
6
  get linearVelocity() {
@@ -1,10 +1,10 @@
1
- import { DebugBody3DSettings, Gg3dWorld, IEntity, ITrigger3dComponent, Shape3DDescriptor, VisualTypeDocRepo3D } from '@gg-web-engine/core';
1
+ import { DebugBody3DSettings, IEntity, ITrigger3dComponent, Shape3DDescriptor } from '@gg-web-engine/core';
2
2
  import { AmmoWorldComponent } from './ammo-world.component';
3
3
  import { Observable, Subject } from 'rxjs';
4
4
  import Ammo from '../ammo.js/ammo';
5
5
  import { AmmoBodyComponent } from './ammo-body.component';
6
6
  import { AmmoRigidBodyComponent } from './ammo-rigid-body.component';
7
- import { AmmoPhysicsTypeDocRepo } from '../types';
7
+ import { AmmoGgWorld, AmmoPhysicsTypeDocRepo } from '../types';
8
8
  export declare class AmmoTriggerComponent extends AmmoBodyComponent<Ammo.btPairCachingGhostObject> implements ITrigger3dComponent<AmmoPhysicsTypeDocRepo> {
9
9
  protected readonly world: AmmoWorldComponent;
10
10
  protected _nativeBody: Ammo.btPairCachingGhostObject;
@@ -14,13 +14,13 @@ export declare class AmmoTriggerComponent extends AmmoBodyComponent<Ammo.btPairC
14
14
  get onEntityEntered(): Observable<AmmoRigidBodyComponent>;
15
15
  get onEntityLeft(): Observable<AmmoRigidBodyComponent | null>;
16
16
  constructor(world: AmmoWorldComponent, _nativeBody: Ammo.btPairCachingGhostObject, shape: Shape3DDescriptor);
17
+ protected readonly overlaps: Set<Ammo.btCollisionObject>;
17
18
  protected readonly onEnter$: Subject<number>;
18
19
  protected readonly onLeft$: Subject<number>;
19
- protected readonly overlaps: Set<Ammo.btCollisionObject>;
20
20
  checkOverlaps(): void;
21
21
  clone(): AmmoTriggerComponent;
22
- addToWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
23
- removeFromWorld(world: Gg3dWorld<VisualTypeDocRepo3D, AmmoPhysicsTypeDocRepo>): void;
22
+ addToWorld(world: AmmoGgWorld): void;
23
+ removeFromWorld(world: AmmoGgWorld): void;
24
24
  refreshCG(): void;
25
25
  dispose(): void;
26
26
  }
@@ -1,4 +1,4 @@
1
- import { DebugBody3DSettings, } from '@gg-web-engine/core';
1
+ import { DebugBody3DSettings } from '@gg-web-engine/core';
2
2
  import { filter, map, Subject } from 'rxjs';
3
3
  import Ammo from '../ammo.js/ammo';
4
4
  import { AmmoBodyComponent } from './ammo-body.component';
@@ -16,9 +16,9 @@ export class AmmoTriggerComponent extends AmmoBodyComponent {
16
16
  this.shape = shape;
17
17
  this.entity = null;
18
18
  this.debugBodySettings = new DebugBody3DSettings({ type: 'TRIGGER', activated: () => this.overlaps.size > 0 }, this.shape);
19
+ this.overlaps = new Set();
19
20
  this.onEnter$ = new Subject();
20
21
  this.onLeft$ = new Subject();
21
- this.overlaps = new Set();
22
22
  }
23
23
  checkOverlaps() {
24
24
  const numOverlappingObjects = this.nativeBody.getNumOverlappingObjects();
@@ -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 { Subject } from 'rxjs';
3
3
  import Ammo from '../ammo.js/ammo';
4
4
  import { AmmoFactory } from '../ammo-factory';
@@ -35,5 +35,6 @@ export declare class AmmoWorldComponent implements IPhysicsWorld3dComponent<Ammo
35
35
  protected lockedCollisionGroups: number[];
36
36
  registerCollisionGroup(): CollisionGroup;
37
37
  deregisterCollisionGroup(group: CollisionGroup): void;
38
+ raycast(options: RaycastOptions<Point3>): RaycastResult<Point3, AmmoRigidBodyComponent | AmmoTriggerComponent>;
38
39
  dispose(): void;
39
40
  }
@@ -7,10 +7,12 @@ 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, } from '@gg-web-engine/core';
10
11
  import { Subject } from 'rxjs';
11
12
  import Ammo from '../ammo.js/ammo';
12
13
  import { AmmoFactory } from '../ammo-factory';
13
14
  import { AmmoLoader } from '../ammo-loader';
15
+ import { AmmoBodyComponent } from './ammo-body.component';
14
16
  export class AmmoWorldComponent {
15
17
  get factory() {
16
18
  if (!this._factory) {
@@ -89,6 +91,46 @@ export class AmmoWorldComponent {
89
91
  deregisterCollisionGroup(group) {
90
92
  this.lockedCollisionGroups = this.lockedCollisionGroups.filter(x => x !== group);
91
93
  }
94
+ raycast(options) {
95
+ if (!this._dynamicAmmoWorld) {
96
+ return { hasHit: false };
97
+ }
98
+ const from = new Ammo.btVector3(options.from.x, options.from.y, options.from.z);
99
+ const to = new Ammo.btVector3(options.to.x, options.to.y, options.to.z);
100
+ const rayCallback = new Ammo.ClosestRayResultCallback(from, to);
101
+ if (options.collisionFilterGroups) {
102
+ rayCallback.set_m_collisionFilterGroup(BitMask.pack(options.collisionFilterGroups, 16));
103
+ }
104
+ if (options.collisionFilterMask !== undefined) {
105
+ rayCallback.set_m_collisionFilterMask(BitMask.pack(options.collisionFilterMask, 16));
106
+ }
107
+ this._dynamicAmmoWorld.rayTest(from, to, rayCallback);
108
+ const hasHit = rayCallback.hasHit();
109
+ const result = { hasHit };
110
+ if (hasHit) {
111
+ result.hitBody = AmmoBodyComponent.nativeBodyReverseMap.get(Ammo.getPointer(rayCallback.get_m_collisionObject()));
112
+ const hitPointAmmo = rayCallback.get_m_hitPointWorld();
113
+ result.hitPoint = {
114
+ x: hitPointAmmo.x(),
115
+ y: hitPointAmmo.y(),
116
+ z: hitPointAmmo.z(),
117
+ };
118
+ const hitNormalAmmo = rayCallback.get_m_hitNormalWorld();
119
+ result.hitNormal = {
120
+ x: hitNormalAmmo.x(),
121
+ y: hitNormalAmmo.y(),
122
+ z: hitNormalAmmo.z(),
123
+ };
124
+ const dx = result.hitPoint.x - options.from.x;
125
+ const dy = result.hitPoint.y - options.from.y;
126
+ const dz = result.hitPoint.z - options.from.z;
127
+ result.hitDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
128
+ }
129
+ Ammo.destroy(from);
130
+ Ammo.destroy(to);
131
+ Ammo.destroy(rayCallback);
132
+ return result;
133
+ }
92
134
  dispose() {
93
135
  this.afterTick$.complete();
94
136
  Ammo.destroy(this._dynamicAmmoWorld);
package/dist/types.d.ts CHANGED
@@ -3,6 +3,8 @@ import { AmmoTriggerComponent } from './components/ammo-trigger.component';
3
3
  import { AmmoRaycastVehicleComponent } from './components/ammo-raycast-vehicle.component';
4
4
  import { AmmoFactory } from './ammo-factory';
5
5
  import { AmmoLoader } from './ammo-loader';
6
+ import { Gg3dWorld, Gg3dWorldSceneTypeDocPPatch, Gg3dWorldTypeDocPPatch } from '@gg-web-engine/core';
7
+ import { AmmoWorldComponent } from './components/ammo-world.component';
6
8
  export type AmmoPhysicsTypeDocRepo = {
7
9
  factory: AmmoFactory;
8
10
  loader: AmmoLoader;
@@ -10,3 +12,6 @@ export type AmmoPhysicsTypeDocRepo = {
10
12
  trigger: AmmoTriggerComponent;
11
13
  raycastVehicle: AmmoRaycastVehicleComponent;
12
14
  };
15
+ export type AmmoTypeDoc = Gg3dWorldTypeDocPPatch<AmmoPhysicsTypeDocRepo>;
16
+ export type AmmoSceneTypeDoc = Gg3dWorldSceneTypeDocPPatch<AmmoPhysicsTypeDocRepo, AmmoWorldComponent>;
17
+ export type AmmoGgWorld = Gg3dWorld<AmmoTypeDoc, AmmoSceneTypeDoc>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/ammo",
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",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  },
36
36
  "devDependencies": {
37
- "@gg-web-engine/core": "0.0.57",
37
+ "@gg-web-engine/core": "0.0.59",
38
38
  "@types/jest": "^29.5.12",
39
39
  "jest": "^29.7.0",
40
40
  "jest-environment-jsdom": "^29.7.0",
@@ -45,7 +45,7 @@
45
45
  "typescript": "~5.5.4"
46
46
  },
47
47
  "peerDependencies": {
48
- "@gg-web-engine/core": "0.0.57",
48
+ "@gg-web-engine/core": "0.0.59",
49
49
  "mini-signals": "2.0.0",
50
50
  "rxjs": "7.8.1"
51
51
  },
@@ -97,6 +97,7 @@ describe(`AmmoTriggerComponent`, () => {
97
97
  trigger.onEntityLeft.subscribe(((obj) => {
98
98
  exitRegistered = obj === ball;
99
99
  }));
100
+ world.simulate(1);
100
101
  trigger.checkOverlaps();
101
102
  trigger.removeFromWorld({ physicsWorld: world } as any);
102
103
  expect(exitRegistered).toBe(true);
@@ -114,8 +115,10 @@ describe(`AmmoTriggerComponent`, () => {
114
115
  trigger.onEntityLeft.subscribe(((obj) => {
115
116
  exitRegistered = obj === ball;
116
117
  }));
118
+ world.simulate(1);
117
119
  trigger.checkOverlaps();
118
120
  ball.removeFromWorld({ physicsWorld: world } as any);
121
+ world.simulate(1);
119
122
  trigger.checkOverlaps();
120
123
  expect(exitRegistered).toBe(true);
121
124
  });
@@ -0,0 +1,296 @@
1
+ import { Point3, RaycastOptions } from '@gg-web-engine/core';
2
+ import { AmmoWorldComponent } from '../../src';
3
+
4
+ describe('AmmoWorldComponent', () => {
5
+ let world: AmmoWorldComponent;
6
+
7
+ beforeEach(async () => {
8
+ if (world) {
9
+ world.dispose();
10
+ }
11
+ world = new AmmoWorldComponent();
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
@@ -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
  }