From 42b51503f319d122a10cd5551c6e2e62fc6531e2 Mon Sep 17 00:00:00 2001 From: Josh Field Date: Tue, 6 Aug 2024 08:01:27 +1000 Subject: [PATCH 01/20] =?UTF-8?q?Revert=20"Physics=20reactivity=20for=20co?= =?UTF-8?q?llider=20component=20updates=20and=20order=20dependenc=E2=80=A6?= =?UTF-8?q?"=20(#10876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 33afb3b987eb9f5b1747257b7e8510f78914857f. --- .../src/physics/classes/Physics.test.tsx | 5334 ++++++++--------- .../spatial/src/physics/classes/Physics.ts | 100 +- .../components/ColliderComponent.test.ts | 812 +-- .../physics/components/ColliderComponent.tsx | 6 +- .../components/RigidBodyComponent.test.tsx | 1006 ++-- .../components/TriggerComponent.test.ts | 452 +- .../src/physics/systems/PhysicsSystem.test.ts | 880 +-- .../src/physics/systems/TriggerSystem.test.ts | 474 +- 8 files changed, 4532 insertions(+), 4532 deletions(-) diff --git a/packages/spatial/src/physics/classes/Physics.test.tsx b/packages/spatial/src/physics/classes/Physics.test.tsx index da0b14adcb..8fd2154c72 100644 --- a/packages/spatial/src/physics/classes/Physics.test.tsx +++ b/packages/spatial/src/physics/classes/Physics.test.tsx @@ -1,2667 +1,2667 @@ -// /* -// CPAL-1.0 License - -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. - -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. - -// The Original Code is Ethereal Engine. - -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. - -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ -// import '../../..' - -// import { RigidBodyType, ShapeType, TempContactForceEvent, Vector, World } from '@dimforge/rapier3d-compat' -// import assert from 'assert' -// import sinon from 'sinon' -// import { BoxGeometry, Mesh, Quaternion, Vector3 } from 'three' - -// import { -// getComponent, -// getMutableComponent, -// getOptionalComponent, -// hasComponent, -// removeComponent, -// setComponent -// } from '@etherealengine/ecs/src/ComponentFunctions' -// import { destroyEngine } from '@etherealengine/ecs/src/Engine' -// import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' -// import { getState } from '@etherealengine/hyperflux' - -// import { createEngine } from '@etherealengine/ecs/src/Engine' -// import { ObjectDirection, Vector3_Zero } from '../../common/constants/MathConstants' -// import { TransformComponent } from '../../transform/components/TransformComponent' -// import { computeTransformMatrix } from '../../transform/systems/TransformSystem' -// import { ColliderComponent } from '../components/ColliderComponent' -// import { CollisionComponent } from '../components/CollisionComponent' -// import { -// RigidBodyComponent, -// RigidBodyFixedTagComponent, -// getTagComponentForRigidBody -// } from '../components/RigidBodyComponent' -// import { TriggerComponent } from '../components/TriggerComponent' -// import { AllCollisionMask, CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' -// import { getInteractionGroups } from '../functions/getInteractionGroups' - -// import { -// Entity, -// EntityUUID, -// SystemDefinitions, -// UUIDComponent, -// UndefinedEntity, -// removeEntity -// } from '@etherealengine/ecs' -// import { act, render } from '@testing-library/react' -// import React from 'react' -// import { MeshComponent } from '../../renderer/components/MeshComponent' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent } from '../../transform/components/EntityTree' -// import { PhysicsSystem } from '../PhysicsModule' -// import { -// BodyTypes, -// ColliderDescOptions, -// ColliderHitEvent, -// CollisionEvents, -// SceneQueryType, -// Shapes -// } from '../types/PhysicsTypes' -// import { Physics, PhysicsWorld, RapierWorldState } from './Physics' - -// const Rotation_Zero = { x: 0, y: 0, z: 0, w: 1 } - -// const Epsilon = 0.001 -// function floatApproxEq(A: number, B: number, epsilon = Epsilon): boolean { -// return Math.abs(A - B) < epsilon -// } -// export function assertFloatApproxEq(A: number, B: number, epsilon = Epsilon) { -// assert.ok(floatApproxEq(A, B, epsilon), `Numbers are not approximately equal: ${A} : ${B} : ${A - B}`) -// } - -// export function assertFloatApproxNotEq(A: number, B: number, epsilon = Epsilon) { -// assert.ok(!floatApproxEq(A, B, epsilon), `Numbers are approximately equal: ${A} : ${B} : ${A - B}`) -// } - -// export function assertVecApproxEq(A, B, elems: number, epsilon = Epsilon) { -// // @note Also used by RigidBodyComponent.test.ts -// assertFloatApproxEq(A.x, B.x, epsilon) -// assertFloatApproxEq(A.y, B.y, epsilon) -// assertFloatApproxEq(A.z, B.z, epsilon) -// if (elems > 3) assertFloatApproxEq(A.w, B.w, epsilon) -// } - -// /** -// * @description -// * Triggers an assert if one or many of the (x,y,z,w) members of `@param A` is not equal to `@param B`. -// * Does nothing for members that are equal */ -// export function assertVecAnyApproxNotEq(A, B, elems: number, epsilon = Epsilon) { -// // @note Also used by PhysicsSystem.test.ts -// !floatApproxEq(A.x, B.x, epsilon) && assertFloatApproxNotEq(A.x, B.x, epsilon) -// !floatApproxEq(A.y, B.y, epsilon) && assertFloatApproxNotEq(A.y, B.y, epsilon) -// !floatApproxEq(A.z, B.z, epsilon) && assertFloatApproxNotEq(A.z, B.z, epsilon) -// if (elems > 3) !floatApproxEq(A.w, B.w, epsilon) && assertFloatApproxEq(A.w, B.w, epsilon) -// } - -// export function assertVecAllApproxNotEq(A, B, elems: number, epsilon = Epsilon) { -// // @note Also used by RigidBodyComponent.test.ts -// assertFloatApproxNotEq(A.x, B.x, epsilon) -// assertFloatApproxNotEq(A.y, B.y, epsilon) -// assertFloatApproxNotEq(A.z, B.z, epsilon) -// if (elems > 3) assertFloatApproxNotEq(A.w, B.w, epsilon) -// } - -// export const boxDynamicConfig = { -// shapeType: ShapeType.Cuboid, -// bodyType: RigidBodyType.Fixed, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask | CollisionGroups.Avatars | CollisionGroups.Ground, -// friction: 1, -// restitution: 0, -// isTrigger: false, -// spawnPosition: new Vector3(0, 0.25, 5), -// spawnScale: new Vector3(0.5, 0.25, 0.5) -// } as ColliderDescOptions - -// describe('Physics : External API', () => { -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity: Entity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// physicsWorld.timestep = 1 / 60 -// }) - -// afterEach(() => { -// return destroyEngine() -// }) - -// it('should create & remove rigidBody', async () => { -// const entity = createEntity() -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(entity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity, ColliderComponent, { shape: Shapes.Sphere }) - -// assert.deepEqual(physicsWorld.bodies.len(), 1) -// assert.deepEqual(physicsWorld.colliders.len(), 1) - -// removeComponent(entity, RigidBodyComponent) - -// assert.deepEqual(physicsWorld.bodies.len(), 0) -// }) - -// it('component type should match rigid body type', async () => { -// const entity = createEntity() - -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(entity, ColliderComponent, { shape: Shapes.Sphere }) - -// const rigidBodyComponent = getTagComponentForRigidBody(BodyTypes.Fixed) -// assert.deepEqual(rigidBodyComponent, RigidBodyFixedTagComponent) -// }) - -// /** -// // @todo External API test for `setRigidBodyType` -// it("should change the entity's RigidBody type", async () => {}) -// */ - -// it('should create accurate InteractionGroups', async () => { -// const collisionGroup = 0x0001 -// const collisionMask = 0x0003 -// const interactionGroups = getInteractionGroups(collisionGroup, collisionMask) - -// assert.deepEqual(interactionGroups, 65539) -// }) - -// it('should generate a collision event', async () => { -// const entity1 = createEntity() -// const entity2 = createEntity() -// setComponent(entity1, TransformComponent) -// setComponent(entity1, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(entity2, TransformComponent) -// setComponent(entity2, EntityTreeComponent, { parentEntity: physicsWorldEntity }) - -// setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity1, ColliderComponent, { -// shape: Shapes.Sphere, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// }) -// setComponent(entity2, ColliderComponent, { -// shape: Shapes.Sphere, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// }) - -// const collisionEventQueue = Physics.createCollisionEventQueue() -// const drainCollisions = Physics.drainCollisionEventQueue(physicsWorld) - -// physicsWorld.step(collisionEventQueue) -// collisionEventQueue.drainCollisionEvents(drainCollisions) - -// const rigidBody1 = physicsWorld.Rigidbodies.get(entity1)! -// const rigidBody2 = physicsWorld.Rigidbodies.get(entity2)! - -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.COLLISION_START) - -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.COLLISION_START) - -// rigidBody2.setTranslation({ x: 0, y: 0, z: 15 }, true) - -// physicsWorld.step(collisionEventQueue) -// collisionEventQueue.drainCollisionEvents(drainCollisions) - -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.COLLISION_END) - -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.COLLISION_END) -// }) - -// it('should generate a trigger event', async () => { -// //force nested reactors to run -// const { rerender, unmount } = render(<>) - -// const entity1 = createEntity() -// const entity2 = createEntity() - -// setComponent(entity1, CollisionComponent) -// setComponent(entity2, CollisionComponent) - -// setComponent(entity1, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(entity1, TransformComponent) -// setComponent(entity2, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(entity2, TransformComponent) - -// setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity1, ColliderComponent, { -// shape: Shapes.Sphere, -// collisionLayer: CollisionGroups.Default, -// collisionMask: AllCollisionMask -// }) -// setComponent(entity2, ColliderComponent, { -// shape: Shapes.Sphere, -// collisionLayer: CollisionGroups.Default, -// collisionMask: AllCollisionMask -// }) -// setComponent(entity2, TriggerComponent) - -// await act(() => rerender(<>)) - -// const collisionEventQueue = Physics.createCollisionEventQueue() -// const drainCollisions = Physics.drainCollisionEventQueue(physicsWorld) - -// physicsWorld.step(collisionEventQueue) -// collisionEventQueue.drainCollisionEvents(drainCollisions) - -// const rigidBody1 = physicsWorld.Rigidbodies.get(entity1)! -// const rigidBody2 = physicsWorld.Rigidbodies.get(entity2)! - -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.TRIGGER_START) - -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.TRIGGER_START) - -// rigidBody2.setTranslation({ x: 0, y: 0, z: 15 }, true) - -// physicsWorld.step(collisionEventQueue) -// collisionEventQueue.drainCollisionEvents(drainCollisions) - -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) -// assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.TRIGGER_END) - -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) -// assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.TRIGGER_END) -// }) -// }) - -// describe('Physics : Rapier->ECS API', () => { -// describe('createWorld', () => { -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// }) - -// afterEach(() => { -// return destroyEngine() -// }) - -// it('should create a world object with the default gravity when not specified', () => { -// const world = Physics.createWorld('world' as EntityUUID) -// assert(getState(RapierWorldState)['world']) -// assert.ok(world instanceof World, 'The create world has an incorrect type.') -// const Expected = new Vector3(0.0, -9.81, 0.0) -// assertVecApproxEq(world.gravity, Expected, 3) -// Physics.destroyWorld('world' as EntityUUID) -// assert(!getState(RapierWorldState)['world']) -// }) - -// it('should create a world object with a different gravity value when specified', () => { -// const expected = { x: 0.0, y: -5.0, z: 0.0 } -// const world = Physics.createWorld('world' as EntityUUID, { gravity: expected, substeps: 2 }) -// assertVecApproxEq(world.gravity, expected, 3) -// assert.equal(world.substeps, 2) -// }) -// }) - -// describe('Rigidbodies', () => { -// describe('createRigidBody', () => { -// const position = new Vector3(1, 2, 3) -// const rotation = new Quaternion(0.2, 0.3, 0.5, 0.0).normalize() - -// const scale = new Vector3(10, 10, 10) -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity: Entity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent, { position: position, scale: scale, rotation: rotation }) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic, canSleep: true, gravityScale: 0 }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should create a rigidBody successfully', () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(body) -// }) - -// it("shouldn't mark the entity transform as dirty", () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.ok(TransformComponent.dirtyTransforms[testEntity] == false) -// }) - -// it('should assign the correct RigidBodyType enum', () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.equal(body.bodyType(), RigidBodyType.Dynamic) -// }) - -// it("should assign the entity's position to the rigidBody.translation property", () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.translation(), position, 3) -// }) - -// it("should assign the entity's rotation to the rigidBody.rotation property", () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body!.rotation(), rotation, 4) -// }) - -// it('should create a body with no Linear Velocity', () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.linvel(), Vector3_Zero, 3) -// }) - -// it('should create a body with no Angular Velocity', () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.angvel(), Vector3_Zero, 3) -// }) - -// it("should store the entity in the body's userData property", () => { -// Physics.createRigidBody(physicsWorld, testEntity) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.deepEqual(body.userData, { entity: testEntity }) -// }) -// }) - -// describe('removeRigidbody', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// Physics.createRigidBody(physicsWorld, testEntity) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should successfully remove the body from the RigidBodies map', () => { -// let body = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(body) -// Physics.removeRigidbody(physicsWorld, testEntity) -// body = physicsWorld.Rigidbodies.get(testEntity) -// assert.equal(body, undefined) -// }) -// }) - -// describe('isSleeping', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// Physics.createRigidBody(physicsWorld, testEntity) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should return the correct values', () => { -// const noBodyEntity = createEntity() -// assert.equal( -// Physics.isSleeping(physicsWorld, noBodyEntity), -// true, -// 'Returns true when the entity does not have a RigidBody' -// ) -// assert.equal( -// Physics.isSleeping(physicsWorld, testEntity), -// false, -// "Returns false when the entity is first created and physics haven't been simulated yet" -// ) -// }) -// }) - -// describe('setRigidBodyType', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// Physics.createRigidBody(physicsWorld, testEntity) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should assign the correct RigidBodyType to the entity's body", () => { -// let body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.equal(body.bodyType(), RigidBodyType.Dynamic) -// // Check change to fixed -// Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Fixed) -// body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.notEqual(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed") -// assert.equal(body.bodyType(), RigidBodyType.Fixed, "The RigidBody's type was not changed to Fixed") -// // Check change to dynamic -// Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Dynamic) -// body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.notEqual(body.bodyType(), RigidBodyType.Fixed, "The RigidBody's type was not changed") -// assert.equal(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed to Dynamic") -// // Check change to kinematic -// Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Kinematic) -// body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.notEqual(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed") -// assert.equal( -// body.bodyType(), -// RigidBodyType.KinematicPositionBased, -// "The RigidBody's type was not changed to KinematicPositionBased" -// ) -// }) -// }) - -// describe('setRigidbodyPose', () => { -// const position = new Vector3(1, 2, 3) -// const rotation = new Quaternion(0.1, 0.3, 0.7, 0.0).normalize() -// const linVel = new Vector3(7, 8, 9) -// const angVel = new Vector3(0, 1, 2) -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// Physics.createRigidBody(physicsWorld, testEntity) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should set the body's Translation to the given Position", () => { -// Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.translation(), position, 3) -// }) - -// it("should set the body's Rotation to the given value", () => { -// Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.rotation(), rotation, 4) -// }) - -// it("should set the body's Linear Velocity to the given value", () => { -// Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.linvel(), linVel, 3) -// }) - -// it("should set the body's Angular Velocity to the given value", () => { -// Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assertVecApproxEq(body.angvel(), angVel, 3) -// }) -// }) - -// describe('enabledCcd', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// RigidBodyComponent.reactorMap.get(testEntity)!.stop() -// Physics.createRigidBody(physicsWorld, testEntity) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should enable Continuous Collision Detection on the entity', () => { -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.equal(body.isCcdEnabled(), false) -// Physics.enabledCcd(physicsWorld, testEntity, true) -// assert.equal(body.isCcdEnabled(), true) -// }) - -// it('should disable CCD on the entity when passing `false` to the `enabled` property', () => { -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// Physics.enabledCcd(physicsWorld, testEntity, true) -// assert.equal(body.isCcdEnabled(), true) -// Physics.enabledCcd(physicsWorld, testEntity, false) -// assert.equal(body.isCcdEnabled(), false) -// }) -// }) - -// describe('applyImpulse', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute - -// it('should apply the impulse to the RigidBody of the entity', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const beforeBody = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(beforeBody) -// const before = beforeBody.linvel() -// assertVecApproxEq(before, Vector3_Zero, 3) -// Physics.applyImpulse(physicsWorld, testEntity, testImpulse) -// physicsSystemExecute() -// const afterBody = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(afterBody) -// const after = afterBody.linvel() -// assertVecAllApproxNotEq(after, before, 3) -// }) -// }) - -// describe('lockRotations', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should lock rotations on the entity', () => { -// const impulse = new Vector3(1, 2, 3) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } -// assertVecApproxEq(before, Vector3_Zero, 3) - -// body.applyTorqueImpulse(impulse, false) -// const dummy = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } -// assertVecAllApproxNotEq(before, dummy, 3) - -// Physics.lockRotations(physicsWorld, testEntity, true) -// body.applyTorqueImpulse(impulse, false) -// const after = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } -// assertVecApproxEq(dummy, after, 3) -// }) - -// /** -// // @todo Fix this test when we update to Rapier >= v0.12 -// it('should disable locked rotations on the entity', () => { -// const ExpectedValue = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// assert.notDeepEqual(body.rotation(), ExpectedValue) - -// Physics.lockRotations(testEntity, true) -// body.setRotation(ExpectedValue, false) -// console.log(JSON.stringify(body.rotation()), "BEFORE") -// console.log(JSON.stringify(ExpectedValue), "Expected") -// assertVecAllApproxNotEq(body.rotation(), ExpectedValue, 3) -// // assert.notDeepEqual(body.rotation(), ExpectedValue) - -// Physics.lockRotations(testEntity, true) -// console.log(JSON.stringify(body.rotation()), "AFTEr") -// assertVecApproxEq(body.rotation(), ExpectedValue, 4) -// }) -// */ -// }) - -// describe('setEnabledRotations', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should disable rotations on the X axis for the rigidBody of the entity', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const enabledRotation = [false, true, true] as [boolean, boolean, boolean] -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = body.angvel() -// assertVecApproxEq(before, Vector3_Zero, 3) -// Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) -// body.applyTorqueImpulse(testImpulse, false) -// physicsWorld!.step() -// const after = body.angvel() -// assertFloatApproxEq(after.x, before.x) -// assertFloatApproxNotEq(after.y, before.y) -// assertFloatApproxNotEq(after.z, before.z) -// }) - -// it('should disable rotations on the Y axis for the rigidBody of the entity', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const enabledRotation = [true, false, true] as [boolean, boolean, boolean] -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = body.angvel() -// assertVecApproxEq(before, Vector3_Zero, 3) -// Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) -// body.applyTorqueImpulse(testImpulse, false) -// physicsWorld!.step() -// const after = body.angvel() -// assertFloatApproxNotEq(after.x, before.x) -// assertFloatApproxEq(after.y, before.y) -// assertFloatApproxNotEq(after.z, before.z) -// }) - -// it('should disable rotations on the Z axis for the rigidBody of the entity', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const enabledRotation = [true, true, false] as [boolean, boolean, boolean] -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = body.angvel() -// assertVecApproxEq(before, Vector3_Zero, 3) -// Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) -// body.applyTorqueImpulse(testImpulse, false) -// physicsWorld!.step() -// const after = body.angvel() -// assertFloatApproxNotEq(after.x, before.x) -// assertFloatApproxNotEq(after.y, before.y) -// assertFloatApproxEq(after.z, before.z) -// }) -// }) - -// describe('updatePreviousRigidbodyPose', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should set the previous position of the entity's RigidBodyComponent", () => { -// const Expected = new Vector3(1, 2, 3) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.setTranslation(Expected, false) -// const before = { -// x: RigidBodyComponent.previousPosition.x[testEntity], -// y: RigidBodyComponent.previousPosition.y[testEntity], -// z: RigidBodyComponent.previousPosition.z[testEntity] -// } -// Physics.updatePreviousRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.previousPosition.x[testEntity], -// y: RigidBodyComponent.previousPosition.y[testEntity], -// z: RigidBodyComponent.previousPosition.z[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 3) -// }) - -// it("should set the previous rotation of the entity's RigidBodyComponent", () => { -// const Expected = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.setRotation(Expected, false) -// const before = { -// x: RigidBodyComponent.previousRotation.x[testEntity], -// y: RigidBodyComponent.previousRotation.y[testEntity], -// z: RigidBodyComponent.previousRotation.z[testEntity], -// w: RigidBodyComponent.previousRotation.w[testEntity] -// } -// Physics.updatePreviousRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.previousRotation.x[testEntity], -// y: RigidBodyComponent.previousRotation.y[testEntity], -// z: RigidBodyComponent.previousRotation.z[testEntity], -// w: RigidBodyComponent.previousRotation.w[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 4) -// }) -// }) - -// describe('updateRigidbodyPose', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should set the position of the entity's RigidBodyComponent", () => { -// const position = new Vector3(1, 2, 3) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.setTranslation(position, false) -// const before = { -// x: RigidBodyComponent.position.x[testEntity], -// y: RigidBodyComponent.position.y[testEntity], -// z: RigidBodyComponent.position.z[testEntity] -// } -// Physics.updateRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.position.x[testEntity], -// y: RigidBodyComponent.position.y[testEntity], -// z: RigidBodyComponent.position.z[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 3) -// }) - -// it("should set the rotation of the entity's RigidBodyComponent", () => { -// const rotation = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.setRotation(rotation, false) -// const before = { -// x: RigidBodyComponent.rotation.x[testEntity], -// y: RigidBodyComponent.rotation.y[testEntity], -// z: RigidBodyComponent.rotation.z[testEntity], -// w: RigidBodyComponent.rotation.w[testEntity] -// } -// Physics.updateRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.rotation.x[testEntity], -// y: RigidBodyComponent.rotation.y[testEntity], -// z: RigidBodyComponent.rotation.z[testEntity], -// w: RigidBodyComponent.rotation.w[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 4) -// }) - -// it("should set the linearVelocity of the entity's RigidBodyComponent", () => { -// const impulse = new Vector3(1, 2, 3) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.applyImpulse(impulse, false) -// const before = { -// x: RigidBodyComponent.linearVelocity.x[testEntity], -// y: RigidBodyComponent.linearVelocity.y[testEntity], -// z: RigidBodyComponent.linearVelocity.z[testEntity] -// } -// Physics.updateRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.linearVelocity.x[testEntity], -// y: RigidBodyComponent.linearVelocity.y[testEntity], -// z: RigidBodyComponent.linearVelocity.z[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 3) -// }) - -// it("should set the angularVelocity of the entity's RigidBodyComponent", () => { -// const impulse = new Vector3(1, 2, 3) -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// body.applyTorqueImpulse(impulse, false) -// const before = { -// x: RigidBodyComponent.angularVelocity.x[testEntity], -// y: RigidBodyComponent.angularVelocity.y[testEntity], -// z: RigidBodyComponent.angularVelocity.z[testEntity] -// } -// Physics.updateRigidbodyPose([testEntity]) -// const after = { -// x: RigidBodyComponent.angularVelocity.x[testEntity], -// y: RigidBodyComponent.angularVelocity.y[testEntity], -// z: RigidBodyComponent.angularVelocity.z[testEntity] -// } -// assertVecAllApproxNotEq(before, after, 3) -// }) -// }) - -// describe('setKinematicRigidbodyPose', () => { -// const position = new Vector3(1, 2, 3) -// const rotation = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should set the nextTranslation property of the entity's Kinematic RigidBody", () => { -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = body.nextTranslation() -// Physics.setKinematicRigidbodyPose(physicsWorld, testEntity, position, rotation) -// const after = body.nextTranslation() -// assertVecAllApproxNotEq(before, after, 3) -// }) - -// it("should set the nextRotation property of the entity's Kinematic RigidBody", () => { -// const body = physicsWorld.Rigidbodies.get(testEntity)! -// const before = body.nextRotation() -// Physics.setKinematicRigidbodyPose(physicsWorld, testEntity, position, rotation) -// const after = body.nextRotation() -// assertVecAllApproxNotEq(before, after, 4) -// }) -// }) -// }) // << Rigidbodies - -// describe('Colliders', () => { -// describe('setTrigger', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should mark the collider of the entity as a sensor', () => { -// const collider = physicsWorld.Colliders.get(testEntity)! -// Physics.setTrigger(physicsWorld, testEntity, true) -// assert.ok(collider.isSensor()) -// }) - -// it('should add CollisionGroup.trigger to the interaction groups of the collider when `isTrigger` is passed as true', () => { -// const collider = physicsWorld.Colliders.get(testEntity)! -// Physics.setTrigger(physicsWorld, testEntity, true) -// const triggerInteraction = getInteractionGroups(CollisionGroups.Trigger, 0) // Shift the Trigger bits into the interaction bits, so they don't match with the mask -// const hasTriggerInteraction = Boolean(collider.collisionGroups() & triggerInteraction) // If interactionGroups contains the triggerInteraction bits -// assert.ok(hasTriggerInteraction) -// }) - -// it('should not add CollisionGroup.trigger to the interaction groups of the collider when `isTrigger` is passed as false', () => { -// const collider = physicsWorld.Colliders.get(testEntity)! -// Physics.setTrigger(physicsWorld, testEntity, false) -// const triggerInteraction = getInteractionGroups(CollisionGroups.Trigger, 0) // Shift the Trigger bits into the interaction bits, so they don't match with the mask -// const notTriggerInteraction = !(collider.collisionGroups() & triggerInteraction) // If interactionGroups does not contain the triggerInteraction bits -// assert.ok(notTriggerInteraction) -// }) -// }) // << setTrigger - -// describe('setCollisionLayer', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should set the collider interaction groups to the given value', () => { -// const data = getComponent(testEntity, ColliderComponent) -// const ExpectedLayer = CollisionGroups.Avatars | data.collisionLayer -// const Expected = getInteractionGroups(ExpectedLayer, data.collisionMask) -// const before = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// Physics.setCollisionLayer(physicsWorld, testEntity, ExpectedLayer) -// const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// assert.notEqual(before, Expected) -// assert.equal(after, Expected) -// }) - -// it('should not modify the collision mask of the collider', () => { -// const data = getComponent(testEntity, ColliderComponent) -// const newLayer = CollisionGroups.Avatars -// const Expected = getInteractionGroups(newLayer, data.collisionMask) -// Physics.setCollisionLayer(physicsWorld, testEntity, newLayer) -// const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// assert.equal(after, Expected) -// }) - -// it('should not add CollisionGroups.Trigger to the collider interaction groups if the entity does not have a TriggerComponent', () => { -// Physics.setCollisionLayer(physicsWorld, testEntity, CollisionGroups.Avatars) -// const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// const noTriggerBit = !(after & getInteractionGroups(CollisionGroups.Trigger, 0)) // not collisionLayer contains Trigger -// assert.ok(noTriggerBit) -// }) - -// it('should not modify the CollisionGroups.Trigger bit in the collider interaction groups if the entity has a TriggerComponent', () => { -// const triggerLayer = getInteractionGroups(CollisionGroups.Trigger, 0) // Create the triggerLayer groups bitmask -// setComponent(testEntity, TriggerComponent) -// const beforeGroups = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// const before = getInteractionGroups(beforeGroups & triggerLayer, 0) === triggerLayer // beforeGroups.collisionLayer contains Trigger -// Physics.setCollisionLayer(physicsWorld, testEntity, CollisionGroups.Avatars) -// const afterGroups = physicsWorld.Colliders.get(testEntity)!.collisionGroups() -// const after = getInteractionGroups(afterGroups & triggerLayer, 0) === triggerLayer // afterGroups.collisionLayer contains Trigger -// assert.equal(before, after) -// }) -// }) // setCollisionLayer - -// describe('setCollisionMask', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should set the collider mask to the given value', () => { -// const before = getComponent(testEntity, ColliderComponent) -// const Expected = CollisionGroups.Avatars | before.collisionMask -// Physics.setCollisionMask(physicsWorld, testEntity, Expected) -// const after = getComponent(testEntity, ColliderComponent) -// assert.equal(after.collisionMask, Expected) -// }) - -// it('should not modify the collision layer of the collider', () => { -// const before = getComponent(testEntity, ColliderComponent) -// Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) -// const after = getComponent(testEntity, ColliderComponent) -// assert.equal(before.collisionLayer, after.collisionLayer) -// }) - -// it('should not add CollisionGroups.Trigger to the collider mask if the entity does not have a TriggerComponent', () => { -// Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) -// const after = getComponent(testEntity, ColliderComponent) -// const noTriggerBit = !(after.collisionMask & CollisionGroups.Trigger) // not collisionMask contains Trigger -// assert.ok(noTriggerBit) -// }) - -// it('should not modify the CollisionGroups.Trigger bit in the collider mask if the entity has a TriggerComponent', () => { -// setComponent(testEntity, TriggerComponent) -// const beforeData = getComponent(testEntity, ColliderComponent) -// const before = beforeData.collisionMask & CollisionGroups.Trigger // collisionMask contains Trigger -// Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) - -// const afterData = getComponent(testEntity, ColliderComponent) -// const after = afterData.collisionMask & CollisionGroups.Trigger // collisionMask contains Trigger -// assert.equal(before, after) -// }) -// }) // setCollisionMask - -// describe('setFriction', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should set the friction value on the entity', () => { -// const ExpectedValue = 42 -// const collider = physicsWorld.Colliders.get(testEntity)! -// assert.notEqual(collider.friction(), ExpectedValue) -// Physics.setFriction(physicsWorld, testEntity, ExpectedValue) -// assert.equal(collider.friction(), ExpectedValue) -// }) -// }) // << setFriction - -// describe('setRestitution', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should set the restitution value on the entity', () => { -// const ExpectedValue = 42 -// const collider = physicsWorld.Colliders.get(testEntity)! -// assert.notEqual(collider.restitution(), ExpectedValue) -// Physics.setRestitution(physicsWorld, testEntity, ExpectedValue) -// assert.equal(collider.restitution(), ExpectedValue) -// }) -// }) // << setRestitution - -// describe('setMass', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should set the mass value on the entity', () => { -// const ExpectedValue = 42 -// const collider = physicsWorld.Colliders.get(testEntity)! -// assert.notEqual(collider.mass(), ExpectedValue) -// Physics.setMass(physicsWorld, testEntity, ExpectedValue) -// assert.equal(collider.mass(), ExpectedValue) -// }) -// }) // << setMass - -// describe('getShape', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should return a sphere shape', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Sphere) -// }) - -// it('should return a capsule shape', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Capsule }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Capsule) -// }) - -// it('should return a cylinder shape', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Cylinder }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Cylinder) -// }) - -// it('should return a box shape', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Box) -// }) - -// it('should return a plane shape', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Plane }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Box) // The Shapes.Plane case is implemented as a box in the engine -// }) - -// it('should return undefined for the convex_hull case', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.ConvexHull }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), undefined /** @todo Shapes.ConvexHull */) -// }) - -// it('should return undefined for the mesh case', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), undefined /** @todo Shapes.Mesh */) -// }) - -// /** -// // @todo Heightfield is not supported yet. Triggers an Error exception -// it("should return undefined for the heightfield case", () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Heightfield }) -// Physics.createRigidBody(physicsWorld, testEntity) -// assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Heightfield) -// }) -// */ -// }) // << getShape - -// describe('removeCollider', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should remove the entity's collider", () => { -// const before = physicsWorld.Colliders.get(testEntity) -// assert.notEqual(before, undefined) -// Physics.removeCollider(physicsWorld!, testEntity) -// const after = physicsWorld.Colliders.get(testEntity) -// assert.equal(after, undefined) -// }) -// }) // << removeCollider - -// describe('removeCollidersFromRigidBody', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should remove all Colliders from the RigidBody when called', () => { -// const before = physicsWorld.Rigidbodies.get(testEntity)! -// assert.notEqual(before.numColliders(), 0) -// Physics.removeCollidersFromRigidBody(testEntity, physicsWorld!) -// assert.equal(before.numColliders(), 0) -// }) -// }) // << removeCollidersFromRigidBody - -// describe('createColliderDesc', () => { -// const Default = { -// // Default values returned by `createColliderDesc` when the default values of the components are not changed -// enabled: true, -// shape: { type: 1, halfExtents: { x: 0.5, y: 0.5, z: 0.5 } }, -// massPropsMode: 0, -// density: 1, -// friction: 0.5, -// restitution: 0.5, -// rotation: { x: 0, y: 0, z: 0, w: 1 }, -// translation: { x: 0, y: 0, z: 0 }, -// isSensor: false, -// collisionGroups: 65543, -// solverGroups: 4294967295, -// frictionCombineRule: 0, -// restitutionCombineRule: 0, -// activeCollisionTypes: 60943, -// activeEvents: 1, -// activeHooks: 0, -// mass: 0, -// centerOfMass: { x: 0, y: 0, z: 0 }, -// contactForceEventThreshold: 0, -// principalAngularInertia: { x: 0, y: 0, z: 0 }, -// angularInertiaLocalFrame: { x: 0, y: 0, z: 0, w: 1 } -// } - -// let physicsWorld: PhysicsWorld -// let testEntity = UndefinedEntity -// let rootEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: rootEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// setComponent(testEntity, ColliderComponent) -// rootEntity = createEntity() -// setComponent(rootEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(rootEntity, TransformComponent) -// setComponent(rootEntity, RigidBodyComponent) -// setComponent(rootEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// removeEntity(rootEntity) -// return destroyEngine() -// }) - -// it('should return early if the given `rootEntity` does not have a RigidBody', () => { -// removeComponent(rootEntity, RigidBodyComponent) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result, undefined) -// }) - -// it('should return a descriptor with the expected default values', () => { -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.deepEqual(result, Default) -// }) - -// it('should set the friction to the same value as the ColliderComponent', () => { -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.friction, getComponent(testEntity, ColliderComponent).friction) -// }) - -// it('should set the restitution to the same value as the ColliderComponent', () => { -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.restitution, getComponent(testEntity, ColliderComponent).restitution) -// }) - -// it('should set the collisionGroups to the same value as the ColliderComponent layer and mask', () => { -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// const data = getComponent(testEntity, ColliderComponent) -// assert.equal(result.collisionGroups, getInteractionGroups(data.collisionLayer, data.collisionMask)) -// }) - -// it('should set the sensor property according to whether the entity has a TriggerComponent or not', () => { -// const noTriggerDesc = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(noTriggerDesc.isSensor, hasComponent(testEntity, TriggerComponent)) -// setComponent(testEntity, TriggerComponent) -// const triggerDesc = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(triggerDesc.isSensor, hasComponent(testEntity, TriggerComponent)) -// }) - -// it('should set the shape to a Ball when the ColliderComponent shape is a Sphere', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.Ball) -// }) - -// it('should set the shape to a Cuboid when the ColliderComponent shape is a Box', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.Cuboid) -// }) - -// it('should set the shape to a Cuboid when the ColliderComponent shape is a Plane', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Plane }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.Cuboid) -// }) - -// it('should set the shape to a TriMesh when the ColliderComponent shape is a Mesh', () => { -// setComponent(testEntity, MeshComponent, new Mesh(new BoxGeometry())) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.TriMesh) -// }) - -// it('should set the shape to a ConvexPolyhedron when the ColliderComponent shape is a ConvexHull', () => { -// setComponent(testEntity, MeshComponent, new Mesh(new BoxGeometry())) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.ConvexHull }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.ConvexPolyhedron) -// }) - -// it('should set the shape to a Cylinder when the ColliderComponent shape is a Cylinder', () => { -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Cylinder }) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// assert.equal(result.shape.type, ShapeType.Cylinder) -// }) - -// it('should set the position relative to the parent entity', () => { -// const Expected = new Vector3(1, 2, 3) -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// console.log(JSON.stringify(result)) -// console.log(JSON.stringify(result.translation)) -// assertVecApproxEq(result.translation, Vector3_Zero, 3) -// }) - -// it('should set the rotation relative to the parent entity', () => { -// const Expected = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) -// console.log(JSON.stringify(result.rotation)) -// assertVecApproxEq(result.rotation, Rotation_Zero, 4) -// }) -// }) - -// describe('attachCollider', () => { -// let testEntity = UndefinedEntity -// let rigidbodyEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// rigidbodyEntity = createEntity() -// setComponent(rigidbodyEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(rigidbodyEntity, TransformComponent) -// setComponent(rigidbodyEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(rigidbodyEntity, ColliderComponent, { shape: Shapes.Box }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// removeEntity(rigidbodyEntity) -// return destroyEngine() -// }) - -// it("should return undefined when rigidBodyEntity doesn't have a RigidBodyComponent", () => { -// removeComponent(rigidbodyEntity, RigidBodyComponent) -// const colliderDesc = Physics.createColliderDesc(physicsWorld, testEntity, rigidbodyEntity) -// const result = Physics.attachCollider(physicsWorld!, colliderDesc, rigidbodyEntity, testEntity) -// assert.equal(result, undefined) -// }) - -// it('should add the collider to the physicsWorld.Colliders map', () => { -// ColliderComponent.reactorMap.get(testEntity)!.stop() -// const colliderDesc = Physics.createColliderDesc(physicsWorld, testEntity, rigidbodyEntity) -// const result = Physics.attachCollider(physicsWorld!, colliderDesc, rigidbodyEntity, testEntity)! -// const expected = physicsWorld.Colliders.get(testEntity) -// assert.ok(result) -// assert.ok(expected) -// assert.deepEqual(result.handle, expected.handle) -// }) -// }) - -// describe('setColliderPose', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// const position = new Vector3(1, 2, 3) -// const rotation = new Quaternion(0.5, 0.4, 0.1, 0.0).normalize() - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should assign the entity's position to the collider.translation property", () => { -// Physics.setColliderPose(physicsWorld, testEntity, position, rotation) -// const collider = physicsWorld.Colliders.get(testEntity)! -// // need to step to update the collider's position -// physicsWorld.step() -// assertVecApproxEq(collider.translation(), position, 3, 0.01) -// }) - -// it("should assign the entity's rotation to the collider.rotation property", () => { -// Physics.setColliderPose(physicsWorld, testEntity, position, rotation) -// const collider = physicsWorld.Colliders.get(testEntity)! -// // need to step to update the collider's position -// physicsWorld.step() -// assertVecApproxEq(collider.rotation(), rotation, 4) -// }) -// }) - -// describe('setMassCenter', () => {}) /** @todo The function is not implemented. It is annotated with a todo tag */ -// }) // << Colliders - -// describe('CharacterControllers', () => { -// describe('createCharacterController', () => { -// const Default = { -// offset: 0.01, -// maxSlopeClimbAngle: (60 * Math.PI) / 180, -// minSlopeSlideAngle: (30 * Math.PI) / 180, -// autoStep: { maxHeight: 0.5, minWidth: 0.01, stepOverDynamic: true }, -// enableSnapToGround: 0.1 as number | false -// } - -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should store a character controller in the Controllers map', () => { -// const before = physicsWorld.Controllers.get(testEntity) -// assert.equal(before, undefined) -// Physics.createCharacterController(physicsWorld, testEntity, {}) -// const after = physicsWorld.Controllers.get(testEntity) -// assert.ok(after) -// }) - -// it('should create a the character controller with the expected defaults when they are omitted', () => { -// Physics.createCharacterController(physicsWorld, testEntity, {}) -// const controller = physicsWorld.Controllers.get(testEntity) -// assert.ok(controller) -// assertFloatApproxEq(controller.offset(), Default.offset) -// assertFloatApproxEq(controller.maxSlopeClimbAngle(), Default.maxSlopeClimbAngle) -// assertFloatApproxEq(controller.minSlopeSlideAngle(), Default.minSlopeSlideAngle) -// assertFloatApproxEq(controller.autostepMaxHeight()!, Default.autoStep.maxHeight) -// assertFloatApproxEq(controller.autostepMinWidth()!, Default.autoStep.minWidth) -// assert.equal(controller.autostepEnabled(), Default.autoStep.stepOverDynamic) -// assert.equal(controller.snapToGroundEnabled(), !!Default.enableSnapToGround) -// }) - -// it('should create a the character controller with values different than the defaults when they are specified', () => { -// const Expected = { -// offset: 0.05, -// maxSlopeClimbAngle: (20 * Math.PI) / 180, -// minSlopeSlideAngle: (60 * Math.PI) / 180, -// autoStep: { maxHeight: 0.1, minWidth: 0.05, stepOverDynamic: false }, -// enableSnapToGround: false as number | false -// } -// Physics.createCharacterController(physicsWorld, testEntity, Expected) -// const controller = physicsWorld.Controllers.get(testEntity) -// assert.ok(controller) -// // Compare against the specified values -// assertFloatApproxEq(controller.offset(), Expected.offset) -// assertFloatApproxEq(controller.maxSlopeClimbAngle(), Expected.maxSlopeClimbAngle) -// assertFloatApproxEq(controller.minSlopeSlideAngle(), Expected.minSlopeSlideAngle) -// assertFloatApproxEq(controller.autostepMaxHeight()!, Expected.autoStep.maxHeight) -// assertFloatApproxEq(controller.autostepMinWidth()!, Expected.autoStep.minWidth) -// assert.equal(controller.autostepIncludesDynamicBodies(), Expected.autoStep.stepOverDynamic) -// assert.equal(controller.snapToGroundEnabled(), !!Expected.enableSnapToGround) -// // Compare against the defaults -// assertFloatApproxNotEq(controller.offset(), Default.offset) -// assertFloatApproxNotEq(controller.maxSlopeClimbAngle(), Default.maxSlopeClimbAngle) -// assertFloatApproxNotEq(controller.minSlopeSlideAngle(), Default.minSlopeSlideAngle) -// assertFloatApproxNotEq(controller.autostepMaxHeight()!, Default.autoStep.maxHeight) -// assertFloatApproxNotEq(controller.autostepMinWidth()!, Default.autoStep.minWidth) -// assert.notEqual(controller.autostepIncludesDynamicBodies(), Default.autoStep.stepOverDynamic) -// assert.notEqual(controller.snapToGroundEnabled(), !!Default.enableSnapToGround) -// }) -// }) - -// describe('removeCharacterController', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should remove the character controller from the Controllers map', () => { -// const before = physicsWorld.Controllers.get(testEntity) -// assert.equal(before, undefined) -// Physics.createCharacterController(physicsWorld, testEntity, {}) -// const created = physicsWorld.Controllers.get(testEntity) -// assert.ok(created) -// Physics.removeCharacterController(physicsWorld, testEntity) -// const after = physicsWorld.Controllers.get(testEntity) -// assert.equal(after, undefined) -// }) -// }) - -// describe('computeColliderMovement', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// Physics.createCharacterController(physicsWorld, testEntity, {}) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should change the `computedMovement` value for the entity's Character Controller", () => { -// const movement = new Vector3(1, 2, 3) -// const controller = physicsWorld.Controllers.get(testEntity)! -// const before = controller.computedMovement() -// Physics.computeColliderMovement( -// physicsWorld, -// testEntity, // entity: Entity, -// testEntity, // colliderEntity: Entity, -// movement // desiredTranslation: Vector3, -// // filterGroups?: InteractionGroups, -// // filterPredicate?: (collider: Collider) => boolean -// ) -// const after = controller.computedMovement() -// assertVecAllApproxNotEq(before, after, 3) -// }) -// }) // << computeColliderMovement - -// describe('getComputedMovement', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should return (0,0,0) when the entity does not have a CharacterController', () => { -// const result = new Vector3(1, 2, 3) -// Physics.getComputedMovement(physicsWorld, testEntity, result) -// assertVecApproxEq(result, Vector3_Zero, 3) -// }) - -// it("should return the same value contained in the `computedMovement` value of the entity's Character Controller", () => { -// Physics.createCharacterController(physicsWorld, testEntity, {}) -// const movement = new Vector3(1, 2, 3) -// const controller = physicsWorld.Controllers.get(testEntity)! -// const before = controller.computedMovement() -// Physics.computeColliderMovement( -// physicsWorld, -// testEntity, // entity: Entity, -// testEntity, // colliderEntity: Entity, -// movement // desiredTranslation: Vector3, -// // filterGroups?: InteractionGroups, -// // filterPredicate?: (collider: Collider) => boolean -// ) -// const after = controller.computedMovement() -// assertVecAllApproxNotEq(before, after, 3) -// const result = new Vector3() -// Physics.getComputedMovement(physicsWorld, testEntity, result) -// assertVecAllApproxNotEq(before, result, 3) -// assertVecApproxEq(after, result, 3) -// }) -// }) // << getComputedMovement -// }) // << CharacterControllers - -// describe('Raycasts', () => { -// describe('castRay', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent, { -// position: new Vector3(10, 0, 0), -// scale: new Vector3(10, 10, 10) -// }) -// computeTransformMatrix(testEntity) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(testEntity, ColliderComponent, { -// shape: Shapes.Box, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should cast a ray and hit a rigidbody', async () => { -// physicsWorld!.step() - -// const raycastComponentData = { -// type: SceneQueryType.Closest, -// origin: new Vector3().set(0, 0, 0), -// direction: ObjectDirection.Right, -// maxDistance: 20, -// groups: getInteractionGroups(CollisionGroups.Default, CollisionGroups.Default) -// } -// const hits = Physics.castRay(physicsWorld!, raycastComponentData) - -// assert.deepEqual(hits.length, 1) -// assert.deepEqual(hits[0].normal.x, -1) -// assert.deepEqual(hits[0].distance, 5) -// assert.deepEqual((hits[0].body.userData as any)['entity'], testEntity) -// }) -// }) - -// describe('castRayFromCamera', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent, { -// position: new Vector3(10, 0, 0), -// scale: new Vector3(10, 10, 10) -// }) -// computeTransformMatrix(testEntity) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(testEntity, ColliderComponent, { -// shape: Shapes.Box, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// /* -// it('should cast a ray from a camera and hit a rigidbody', async () => { -// physicsWorld!.step() -// assert.ok(1) -// }) -// */ -// }) // << castRayFromCamera - -// /** -// // @todo Double check the `castShape` implementation before implementing this test -// describe('castShape', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity, TransformComponent, { -// position: new Vector3(10, 0, 0), -// scale: new Vector3(10, 10, 10) -// }) -// computeTransformMatrix(testEntity) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(testEntity, ColliderComponent, { -// shape: Shapes.Box, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// }) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// // @todo This setup is not hitting. Double check the `castShape` implementation before implementing this test -// it('should cast a shape and hit a rigidbody', () => { -// physicsWorld!.step() - -// const collider = physicsWorld.Colliders.get(testEntity)! -// const hits = [] as RaycastHit[] -// const shapecastComponentData :ShapecastArgs= { -// type: SceneQueryType.Closest, // type: SceneQueryType -// hits: hits, // hits: RaycastHit[] -// collider: collider, // collider: Collider -// direction: ObjectDirection.Right, // direction: Vector3 -// maxDistance: 20, // maxDistance: number -// collisionGroups: getInteractionGroups(CollisionGroups.Default, CollisionGroups.Default), // collisionGroups: InteractionGroups -// } -// Physics.castShape(physicsWorld!, shapecastComponentData) - -// assert.deepEqual(hits.length, 1, "The length of the hits array is incorrect.") -// assert.deepEqual(hits[0].normal.x, -1) -// assert.deepEqual(hits[0].distance, 5) -// assert.deepEqual((hits[0].body.userData as any)['entity'], testEntity) -// }) -// }) // << castShape -// */ -// }) // << Raycasts - -// describe('Collisions', () => { -// describe('createCollisionEventQueue', () => { -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// }) - -// afterEach(() => { -// return destroyEngine() -// }) - -// it('should create a collision event queue successfully', () => { -// const queue = Physics.createCollisionEventQueue() -// assert(queue) -// }) -// }) - -// describe('drainCollisionEventQueue', () => { -// const InvalidHandle = 8198123 -// let physicsWorld: PhysicsWorld -// let testEntity1 = UndefinedEntity -// let testEntity2 = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld.timestep = 1 / 60 - -// testEntity1 = createEntity() -// setComponent(testEntity1, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity1, TransformComponent) -// setComponent(testEntity1, RigidBodyComponent) -// setComponent(testEntity1, ColliderComponent) - -// testEntity2 = createEntity() -// setComponent(testEntity2, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity2, TransformComponent) -// setComponent(testEntity2, RigidBodyComponent) -// setComponent(testEntity2, ColliderComponent) -// }) - -// afterEach(() => { -// return destroyEngine() -// }) - -// function assertCollisionEventClosure(closure: any) { -// type CollisionEventClosure = (handle1: number, handle2: number, started: boolean) => void -// function hasCollisionEventClosureShape(closure: any): closure is CollisionEventClosure { -// return typeof closure === 'function' && closure.length === 3 -// } -// assert.ok(closure) -// assert.ok(hasCollisionEventClosureShape(closure)) -// } - -// it('should return a function with the correct shape (handle1: number, handle2: number, started: boolean) => void', () => { -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// }) - -// it('should do nothing if any of the collider handles are not found', () => { -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// physicsWorld.step() -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) - -// assert.ok(!hasComponent(testEntity1, CollisionComponent)) -// event(collider1.handle, InvalidHandle, true) -// assert.ok(!hasComponent(testEntity1, CollisionComponent)) - -// assert.ok(!hasComponent(testEntity2, CollisionComponent)) -// event(collider2!.handle, InvalidHandle, true) -// assert.ok(!hasComponent(testEntity2, CollisionComponent)) -// }) - -// it('should add a CollisionComponent to the entities contained in the userData of the parent rigidBody of each collider (collider.parent())', () => { -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// physicsWorld.step() - -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Get the parents from the API -// const colliderParent1 = collider1.parent() -// const colliderParent2 = collider2.parent() -// assert.ok(colliderParent1) -// assert.ok(colliderParent2) -// // Get the entities from parent.userData -// const entity1 = (colliderParent1.userData as any)['entity'] -// const entity2 = (colliderParent2.userData as any)['entity'] -// assert.equal(testEntity1, entity1) -// assert.equal(testEntity2, entity2) -// // Check before -// assert.ok(!hasComponent(entity1, CollisionComponent)) -// assert.ok(!hasComponent(entity2, CollisionComponent)) - -// // Run and Check after -// event(collider1.handle, collider2.handle, true) -// assert.ok(hasComponent(entity1, CollisionComponent)) -// assert.ok(hasComponent(entity2, CollisionComponent)) -// }) - -// describe('when `started` is set to `true` ...', () => { -// it('... should create a CollisionEvents.COLLISION_START when neither of the colliders is a sensor (aka has a TriggerComponent)', () => { -// const Started = true - -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Check before -// const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) -// const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) -// assert.equal(before1, undefined) -// assert.equal(before2, undefined) -// // setComponent(testEntity1, TriggerComponent) // DONT set the trigger component (testEntity1.body.isSensor() is false) - -// // Run and Check after -// event(collider1.handle, collider2.handle, Started) -// const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) -// const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) -// assert.ok(after1) -// assert.ok(after2) -// assert.equal(after1.type, CollisionEvents.COLLISION_START) -// assert.equal(after2.type, CollisionEvents.COLLISION_START) -// }) - -// it('... should create a CollisionEvents.TRIGGER_START when either one of the colliders is a sensor (aka has a TriggerComponent)', async () => { -// //force nested reactors to run -// const { rerender, unmount } = render(<>) - -// const Started = true - -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Check before -// const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) -// const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) -// assert.equal(before1, undefined) -// assert.equal(before2, undefined) -// setComponent(testEntity1, TriggerComponent) // Set the trigger component (marks testEntity1.body.isSensor() as true) -// await act(() => rerender(<>)) - -// event(collider1.handle, collider2.handle, Started) - -// // Run and Check after -// const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) -// const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) -// assert.ok(after1) -// assert.ok(after2) -// assert.equal(after1.type, CollisionEvents.TRIGGER_START) -// assert.equal(after2.type, CollisionEvents.TRIGGER_START) -// }) - -// it('... should set entity2 in the CollisionComponent of entity1, and entity1 in the CollisionComponent of entity2', () => { -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Check before -// const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) -// const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) -// assert.equal(before1, undefined) -// assert.equal(before2, undefined) - -// // Run and Check after -// event(collider1.handle, collider2.handle, true) -// const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) -// const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) -// assert.ok(after1) -// assert.ok(after2) -// }) -// }) - -// describe('when `started` is set to `false` ...', () => { -// it('... should create a CollisionEvents.TRIGGER_END when either one of the colliders is a sensor', async () => { -// //force nested reactors to run -// const { rerender, unmount } = render(<>) - -// const Started = false - -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Check before -// const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) -// const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) -// assert.equal(before1, undefined) -// assert.equal(before2, undefined) -// setComponent(testEntity1, TriggerComponent) // Set the trigger component (marks testEntity1.body.isSensor() as true) -// await act(() => rerender(<>)) - -// // Run and Check after -// event(collider1.handle, collider2.handle, true) // Run the even twice, so that the entities get each other in their collision components -// event(collider1.handle, collider2.handle, Started) -// const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) -// const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) -// assert.ok(after1) -// assert.ok(after2) -// assert.equal(after1.type, CollisionEvents.TRIGGER_END) -// assert.equal(after2.type, CollisionEvents.TRIGGER_END) -// }) - -// it('... should create a CollisionEvents.COLLISION_END when neither of the colliders is a sensor', () => { -// const Started = false - -// assert.ok(physicsWorld) -// const event = Physics.drainCollisionEventQueue(physicsWorld) -// assertCollisionEventClosure(event) -// // Get the colliders from the API -// const collider1 = physicsWorld.Colliders.get(testEntity1) -// const collider2 = physicsWorld.Colliders.get(testEntity2) -// assert.ok(collider1) -// assert.ok(collider2) -// // Check before -// const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) -// const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) -// assert.equal(before1, undefined) -// assert.equal(before2, undefined) -// // setComponent(testEntity1, TriggerComponent) // DONT set the trigger component (testEntity1.body.isSensor() is false) - -// // Run and Check after -// event(collider1.handle, collider2.handle, true) // Run the even twice, so that the entities get each other in their collision components -// event(collider1.handle, collider2.handle, Started) -// const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) -// const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) -// assert.ok(after1) -// assert.ok(after2) -// assert.equal(after1.type, CollisionEvents.COLLISION_END) -// assert.equal(after2.type, CollisionEvents.COLLISION_END) -// }) -// }) -// }) // << drainCollisionEventQueue - -// describe('drainContactEventQueue', () => { -// let physicsWorld: PhysicsWorld -// let testEntity1 = UndefinedEntity -// let testEntity2 = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// const entity = createEntity() -// setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(entity, SceneComponent) -// setComponent(entity, TransformComponent) -// setComponent(entity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) -// physicsWorld.timestep = 1 / 60 - -// testEntity1 = createEntity() -// setComponent(testEntity1, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity1, TransformComponent) -// setComponent(testEntity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity1, ColliderComponent) -// testEntity2 = createEntity() -// setComponent(testEntity2, EntityTreeComponent, { parentEntity: entity }) -// setComponent(testEntity2, TransformComponent) -// setComponent(testEntity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity2, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity1) -// removeEntity(testEntity2) -// return destroyEngine() -// }) - -// function assertContactEventClosure(closure: any) { -// type ContactEventClosure = (handle1: number, handle2: number, started: boolean) => void -// function hasContactEventClosureShape(closure: any): closure is ContactEventClosure { -// return typeof closure === 'function' && closure.length === 1 -// } -// assert.ok(closure) -// assert.ok(hasContactEventClosureShape(closure)) -// } - -// it('should return a function with the correct shape (event: TempContactForceEvent) => void', () => { -// assert.ok(physicsWorld) -// const closure = Physics.drainContactEventQueue(physicsWorld) -// assertContactEventClosure(closure) -// }) - -// describe('if the collision exists ...', () => { -// const DummyMaxForce = { x: 42, y: 43, z: 44 } -// const DummyTotalForce = { x: 45, y: 46, z: 47 } -// const DummyHit = { -// maxForceDirection: DummyMaxForce, -// totalForce: DummyTotalForce -// } as ColliderHitEvent -// function setDummyCollisionBetween(ent1: Entity, ent2: Entity, hit = DummyHit): void { -// const hits = new Map() -// hits.set(ent2, hit) -// setComponent(ent1, CollisionComponent) -// getMutableComponent(ent1, CollisionComponent).set(hits) -// } - -// const ExpectedMaxForce = { x: 4, y: 5, z: 6 } -// const ExpectedTotalForce = { x: 7, y: 8, z: 9 } - -// it('should store event.maxForceDirection() into the CollisionComponent.maxForceDirection of entity1.collision.get(entity2) if the collision exists', () => { -// // Setup the function spies -// const collider1Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity1)!.handle -// }) -// const collider2Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity2)!.handle -// }) -// const totalForceSpy = sinon.spy((): Vector => { -// return ExpectedTotalForce -// }) -// const maxForceSpy = sinon.spy((): Vector => { -// return ExpectedMaxForce -// }) - -// // Check before -// assert.ok(physicsWorld) -// const event = Physics.drainContactEventQueue(physicsWorld) -// assertContactEventClosure(event) -// assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) -// assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) - -// // Run and Check after -// setDummyCollisionBetween(testEntity1, testEntity2) -// setDummyCollisionBetween(testEntity2, testEntity1) -// event({ -// collider1: collider1Spy as any, -// collider2: collider2Spy as any, -// totalForce: totalForceSpy as any, -// maxForceDirection: maxForceSpy as any -// } as TempContactForceEvent) -// sinon.assert.called(collider1Spy) -// sinon.assert.called(collider2Spy) -// sinon.assert.called(maxForceSpy) -// const after = getComponent(testEntity1, CollisionComponent).get(testEntity2)?.maxForceDirection -// assertVecApproxEq(after, ExpectedMaxForce, 3) -// }) - -// it('should store event.maxForceDirection() into the CollisionComponent.maxForceDirection of entity2.collision.get(entity1) if the collision exists', () => { -// // Setup the function spies -// const collider1Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity1)!.handle -// }) -// const collider2Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity2)!.handle -// }) -// const totalForceSpy = sinon.spy((): Vector => { -// return ExpectedTotalForce -// }) -// const maxForceSpy = sinon.spy((): Vector => { -// return ExpectedMaxForce -// }) - -// // Check before -// assert.ok(physicsWorld) -// const event = Physics.drainContactEventQueue(physicsWorld) -// assertContactEventClosure(event) -// assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) -// assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) - -// // Run and Check after -// setDummyCollisionBetween(testEntity1, testEntity2) -// setDummyCollisionBetween(testEntity2, testEntity1) - -// event({ -// collider1: collider1Spy as any, -// collider2: collider2Spy as any, -// totalForce: totalForceSpy as any, -// maxForceDirection: maxForceSpy as any -// } as TempContactForceEvent) - -// sinon.assert.called(collider1Spy) -// sinon.assert.called(collider2Spy) -// sinon.assert.called(maxForceSpy) -// const after = getComponent(testEntity2, CollisionComponent).get(testEntity1)?.maxForceDirection -// assertVecApproxEq(after, ExpectedMaxForce, 3) -// }) - -// it('should store event.totalForce() into the CollisionComponent.totalForce of entity1.collision.get(entity2) if the collision exists', () => { -// // Setup the function spies -// const collider1Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity1)!.handle -// }) -// const collider2Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity2)!.handle -// }) -// const totalForceSpy = sinon.spy((): Vector => { -// return ExpectedTotalForce -// }) -// const maxForceSpy = sinon.spy((): Vector => { -// return ExpectedMaxForce -// }) - -// // Check before -// assert.ok(physicsWorld) -// const event = Physics.drainContactEventQueue(physicsWorld) -// assertContactEventClosure(event) -// assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) -// assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) -// // Run and Check after -// setDummyCollisionBetween(testEntity1, testEntity2) -// setDummyCollisionBetween(testEntity2, testEntity1) - -// event({ -// collider1: collider1Spy as any, -// collider2: collider2Spy as any, -// totalForce: totalForceSpy as any, -// maxForceDirection: maxForceSpy as any -// } as TempContactForceEvent) - -// sinon.assert.called(collider1Spy) -// sinon.assert.called(collider2Spy) -// sinon.assert.called(totalForceSpy) -// const after = getComponent(testEntity1, CollisionComponent).get(testEntity2)?.totalForce -// assertVecApproxEq(after, ExpectedTotalForce, 3) -// }) - -// it('should store event.totalForce() into the CollisionComponent.totalForce of entity2.collision.get(entity1) if the collision exists', () => { -// // Setup the function spies -// const collider1Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity1)!.handle -// }) -// const collider2Spy = sinon.spy((): number => { -// return physicsWorld.Colliders.get(testEntity2)!.handle -// }) -// const totalForceSpy = sinon.spy((): Vector => { -// return ExpectedTotalForce -// }) -// const maxForceSpy = sinon.spy((): Vector => { -// return ExpectedMaxForce -// }) - -// // Check before -// assert.ok(physicsWorld) -// const event = Physics.drainContactEventQueue(physicsWorld) -// assertContactEventClosure(event) -// assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) -// assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) - -// // Run and Check after -// setDummyCollisionBetween(testEntity1, testEntity2) -// setDummyCollisionBetween(testEntity2, testEntity1) -// event({ -// collider1: collider1Spy as any, -// collider2: collider2Spy as any, -// totalForce: totalForceSpy as any, -// maxForceDirection: maxForceSpy as any -// } as TempContactForceEvent) - -// sinon.assert.called(collider1Spy) -// sinon.assert.called(collider2Spy) -// sinon.assert.called(totalForceSpy) -// const after = getComponent(testEntity2, CollisionComponent).get(testEntity1)?.totalForce -// assertVecApproxEq(after, ExpectedTotalForce, 3) -// }) -// }) -// }) // << drainContactEventQueue -// }) // << Collisions -// }) - -// /** TODO: -// describe("load", () => {}) // @todo Is there a way to check that the wasmInit() call from rapier.js has been run? -// // Character Controller -// describe("getControllerOffset", () => {}) // @deprecated -// */ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ +import '../../..' + +import { RigidBodyType, ShapeType, TempContactForceEvent, Vector, World } from '@dimforge/rapier3d-compat' +import assert from 'assert' +import sinon from 'sinon' +import { BoxGeometry, Mesh, Quaternion, Vector3 } from 'three' + +import { + getComponent, + getMutableComponent, + getOptionalComponent, + hasComponent, + removeComponent, + setComponent +} from '@etherealengine/ecs/src/ComponentFunctions' +import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' +import { getState } from '@etherealengine/hyperflux' + +import { createEngine } from '@etherealengine/ecs/src/Engine' +import { ObjectDirection, Vector3_Zero } from '../../common/constants/MathConstants' +import { TransformComponent } from '../../transform/components/TransformComponent' +import { computeTransformMatrix } from '../../transform/systems/TransformSystem' +import { ColliderComponent } from '../components/ColliderComponent' +import { CollisionComponent } from '../components/CollisionComponent' +import { + RigidBodyComponent, + RigidBodyFixedTagComponent, + getTagComponentForRigidBody +} from '../components/RigidBodyComponent' +import { TriggerComponent } from '../components/TriggerComponent' +import { AllCollisionMask, CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' +import { getInteractionGroups } from '../functions/getInteractionGroups' + +import { + Entity, + EntityUUID, + SystemDefinitions, + UUIDComponent, + UndefinedEntity, + removeEntity +} from '@etherealengine/ecs' +import { act, render } from '@testing-library/react' +import React from 'react' +import { MeshComponent } from '../../renderer/components/MeshComponent' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { PhysicsSystem } from '../PhysicsModule' +import { + BodyTypes, + ColliderDescOptions, + ColliderHitEvent, + CollisionEvents, + SceneQueryType, + Shapes +} from '../types/PhysicsTypes' +import { Physics, PhysicsWorld, RapierWorldState } from './Physics' + +const Rotation_Zero = { x: 0, y: 0, z: 0, w: 1 } + +const Epsilon = 0.001 +function floatApproxEq(A: number, B: number, epsilon = Epsilon): boolean { + return Math.abs(A - B) < epsilon +} +export function assertFloatApproxEq(A: number, B: number, epsilon = Epsilon) { + assert.ok(floatApproxEq(A, B, epsilon), `Numbers are not approximately equal: ${A} : ${B} : ${A - B}`) +} + +export function assertFloatApproxNotEq(A: number, B: number, epsilon = Epsilon) { + assert.ok(!floatApproxEq(A, B, epsilon), `Numbers are approximately equal: ${A} : ${B} : ${A - B}`) +} + +export function assertVecApproxEq(A, B, elems: number, epsilon = Epsilon) { + // @note Also used by RigidBodyComponent.test.ts + assertFloatApproxEq(A.x, B.x, epsilon) + assertFloatApproxEq(A.y, B.y, epsilon) + assertFloatApproxEq(A.z, B.z, epsilon) + if (elems > 3) assertFloatApproxEq(A.w, B.w, epsilon) +} + +/** + * @description + * Triggers an assert if one or many of the (x,y,z,w) members of `@param A` is not equal to `@param B`. + * Does nothing for members that are equal */ +export function assertVecAnyApproxNotEq(A, B, elems: number, epsilon = Epsilon) { + // @note Also used by PhysicsSystem.test.ts + !floatApproxEq(A.x, B.x, epsilon) && assertFloatApproxNotEq(A.x, B.x, epsilon) + !floatApproxEq(A.y, B.y, epsilon) && assertFloatApproxNotEq(A.y, B.y, epsilon) + !floatApproxEq(A.z, B.z, epsilon) && assertFloatApproxNotEq(A.z, B.z, epsilon) + if (elems > 3) !floatApproxEq(A.w, B.w, epsilon) && assertFloatApproxEq(A.w, B.w, epsilon) +} + +export function assertVecAllApproxNotEq(A, B, elems: number, epsilon = Epsilon) { + // @note Also used by RigidBodyComponent.test.ts + assertFloatApproxNotEq(A.x, B.x, epsilon) + assertFloatApproxNotEq(A.y, B.y, epsilon) + assertFloatApproxNotEq(A.z, B.z, epsilon) + if (elems > 3) assertFloatApproxNotEq(A.w, B.w, epsilon) +} + +export const boxDynamicConfig = { + shapeType: ShapeType.Cuboid, + bodyType: RigidBodyType.Fixed, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask | CollisionGroups.Avatars | CollisionGroups.Ground, + friction: 1, + restitution: 0, + isTrigger: false, + spawnPosition: new Vector3(0, 0.25, 5), + spawnScale: new Vector3(0.5, 0.25, 0.5) +} as ColliderDescOptions + +describe('Physics : External API', () => { + let physicsWorld: PhysicsWorld + let physicsWorldEntity: Entity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + physicsWorld.timestep = 1 / 60 + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should create & remove rigidBody', async () => { + const entity = createEntity() + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(entity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity, ColliderComponent, { shape: Shapes.Sphere }) + + assert.deepEqual(physicsWorld.bodies.len(), 1) + assert.deepEqual(physicsWorld.colliders.len(), 1) + + removeComponent(entity, RigidBodyComponent) + + assert.deepEqual(physicsWorld.bodies.len(), 0) + }) + + it('component type should match rigid body type', async () => { + const entity = createEntity() + + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(entity, ColliderComponent, { shape: Shapes.Sphere }) + + const rigidBodyComponent = getTagComponentForRigidBody(BodyTypes.Fixed) + assert.deepEqual(rigidBodyComponent, RigidBodyFixedTagComponent) + }) + + /** + // @todo External API test for `setRigidBodyType` + it("should change the entity's RigidBody type", async () => {}) + */ + + it('should create accurate InteractionGroups', async () => { + const collisionGroup = 0x0001 + const collisionMask = 0x0003 + const interactionGroups = getInteractionGroups(collisionGroup, collisionMask) + + assert.deepEqual(interactionGroups, 65539) + }) + + it('should generate a collision event', async () => { + const entity1 = createEntity() + const entity2 = createEntity() + setComponent(entity1, TransformComponent) + setComponent(entity1, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(entity2, TransformComponent) + setComponent(entity2, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + + setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity1, ColliderComponent, { + shape: Shapes.Sphere, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + }) + setComponent(entity2, ColliderComponent, { + shape: Shapes.Sphere, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + }) + + const collisionEventQueue = Physics.createCollisionEventQueue() + const drainCollisions = Physics.drainCollisionEventQueue(physicsWorld) + + physicsWorld.step(collisionEventQueue) + collisionEventQueue.drainCollisionEvents(drainCollisions) + + const rigidBody1 = physicsWorld.Rigidbodies.get(entity1)! + const rigidBody2 = physicsWorld.Rigidbodies.get(entity2)! + + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.COLLISION_START) + + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.COLLISION_START) + + rigidBody2.setTranslation({ x: 0, y: 0, z: 15 }, true) + + physicsWorld.step(collisionEventQueue) + collisionEventQueue.drainCollisionEvents(drainCollisions) + + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.COLLISION_END) + + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.COLLISION_END) + }) + + it('should generate a trigger event', async () => { + //force nested reactors to run + const { rerender, unmount } = render(<>) + + const entity1 = createEntity() + const entity2 = createEntity() + + setComponent(entity1, CollisionComponent) + setComponent(entity2, CollisionComponent) + + setComponent(entity1, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(entity1, TransformComponent) + setComponent(entity2, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(entity2, TransformComponent) + + setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity1, ColliderComponent, { + shape: Shapes.Sphere, + collisionLayer: CollisionGroups.Default, + collisionMask: AllCollisionMask + }) + setComponent(entity2, ColliderComponent, { + shape: Shapes.Sphere, + collisionLayer: CollisionGroups.Default, + collisionMask: AllCollisionMask + }) + setComponent(entity2, TriggerComponent) + + await act(() => rerender(<>)) + + const collisionEventQueue = Physics.createCollisionEventQueue() + const drainCollisions = Physics.drainCollisionEventQueue(physicsWorld) + + physicsWorld.step(collisionEventQueue) + collisionEventQueue.drainCollisionEvents(drainCollisions) + + const rigidBody1 = physicsWorld.Rigidbodies.get(entity1)! + const rigidBody2 = physicsWorld.Rigidbodies.get(entity2)! + + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.TRIGGER_START) + + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.TRIGGER_START) + + rigidBody2.setTranslation({ x: 0, y: 0, z: 15 }, true) + + physicsWorld.step(collisionEventQueue) + collisionEventQueue.drainCollisionEvents(drainCollisions) + + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodySelf, rigidBody1) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.bodyOther, rigidBody2) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeSelf, rigidBody1.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.shapeOther, rigidBody2.collider(0)) + assert.equal(getComponent(entity1, CollisionComponent).get(entity2)?.type, CollisionEvents.TRIGGER_END) + + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodySelf, rigidBody2) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.bodyOther, rigidBody1) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeSelf, rigidBody2.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.shapeOther, rigidBody1.collider(0)) + assert.equal(getComponent(entity2, CollisionComponent).get(entity1)?.type, CollisionEvents.TRIGGER_END) + }) +}) + +describe('Physics : Rapier->ECS API', () => { + describe('createWorld', () => { + beforeEach(async () => { + createEngine() + await Physics.load() + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should create a world object with the default gravity when not specified', () => { + const world = Physics.createWorld('world' as EntityUUID) + assert(getState(RapierWorldState)['world']) + assert.ok(world instanceof World, 'The create world has an incorrect type.') + const Expected = new Vector3(0.0, -9.81, 0.0) + assertVecApproxEq(world.gravity, Expected, 3) + Physics.destroyWorld('world' as EntityUUID) + assert(!getState(RapierWorldState)['world']) + }) + + it('should create a world object with a different gravity value when specified', () => { + const expected = { x: 0.0, y: -5.0, z: 0.0 } + const world = Physics.createWorld('world' as EntityUUID, { gravity: expected, substeps: 2 }) + assertVecApproxEq(world.gravity, expected, 3) + assert.equal(world.substeps, 2) + }) + }) + + describe('Rigidbodies', () => { + describe('createRigidBody', () => { + const position = new Vector3(1, 2, 3) + const rotation = new Quaternion(0.2, 0.3, 0.5, 0.0).normalize() + + const scale = new Vector3(10, 10, 10) + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity: Entity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent, { position: position, scale: scale, rotation: rotation }) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic, canSleep: true, gravityScale: 0 }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should create a rigidBody successfully', () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(body) + }) + + it("shouldn't mark the entity transform as dirty", () => { + Physics.createRigidBody(physicsWorld, testEntity) + assert.ok(TransformComponent.dirtyTransforms[testEntity] == false) + }) + + it('should assign the correct RigidBodyType enum', () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assert.equal(body.bodyType(), RigidBodyType.Dynamic) + }) + + it("should assign the entity's position to the rigidBody.translation property", () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.translation(), position, 3) + }) + + it("should assign the entity's rotation to the rigidBody.rotation property", () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body!.rotation(), rotation, 4) + }) + + it('should create a body with no Linear Velocity', () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.linvel(), Vector3_Zero, 3) + }) + + it('should create a body with no Angular Velocity', () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.angvel(), Vector3_Zero, 3) + }) + + it("should store the entity in the body's userData property", () => { + Physics.createRigidBody(physicsWorld, testEntity) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assert.deepEqual(body.userData, { entity: testEntity }) + }) + }) + + describe('removeRigidbody', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + Physics.createRigidBody(physicsWorld, testEntity) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should successfully remove the body from the RigidBodies map', () => { + let body = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(body) + Physics.removeRigidbody(physicsWorld, testEntity) + body = physicsWorld.Rigidbodies.get(testEntity) + assert.equal(body, undefined) + }) + }) + + describe('isSleeping', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + Physics.createRigidBody(physicsWorld, testEntity) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should return the correct values', () => { + const noBodyEntity = createEntity() + assert.equal( + Physics.isSleeping(physicsWorld, noBodyEntity), + true, + 'Returns true when the entity does not have a RigidBody' + ) + assert.equal( + Physics.isSleeping(physicsWorld, testEntity), + false, + "Returns false when the entity is first created and physics haven't been simulated yet" + ) + }) + }) + + describe('setRigidBodyType', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + Physics.createRigidBody(physicsWorld, testEntity) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should assign the correct RigidBodyType to the entity's body", () => { + let body = physicsWorld.Rigidbodies.get(testEntity)! + assert.equal(body.bodyType(), RigidBodyType.Dynamic) + // Check change to fixed + Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Fixed) + body = physicsWorld.Rigidbodies.get(testEntity)! + assert.notEqual(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed") + assert.equal(body.bodyType(), RigidBodyType.Fixed, "The RigidBody's type was not changed to Fixed") + // Check change to dynamic + Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Dynamic) + body = physicsWorld.Rigidbodies.get(testEntity)! + assert.notEqual(body.bodyType(), RigidBodyType.Fixed, "The RigidBody's type was not changed") + assert.equal(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed to Dynamic") + // Check change to kinematic + Physics.setRigidBodyType(physicsWorld, testEntity, BodyTypes.Kinematic) + body = physicsWorld.Rigidbodies.get(testEntity)! + assert.notEqual(body.bodyType(), RigidBodyType.Dynamic, "The RigidBody's type was not changed") + assert.equal( + body.bodyType(), + RigidBodyType.KinematicPositionBased, + "The RigidBody's type was not changed to KinematicPositionBased" + ) + }) + }) + + describe('setRigidbodyPose', () => { + const position = new Vector3(1, 2, 3) + const rotation = new Quaternion(0.1, 0.3, 0.7, 0.0).normalize() + const linVel = new Vector3(7, 8, 9) + const angVel = new Vector3(0, 1, 2) + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + Physics.createRigidBody(physicsWorld, testEntity) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the body's Translation to the given Position", () => { + Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.translation(), position, 3) + }) + + it("should set the body's Rotation to the given value", () => { + Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.rotation(), rotation, 4) + }) + + it("should set the body's Linear Velocity to the given value", () => { + Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.linvel(), linVel, 3) + }) + + it("should set the body's Angular Velocity to the given value", () => { + Physics.setRigidbodyPose(physicsWorld, testEntity, position, rotation, linVel, angVel) + const body = physicsWorld.Rigidbodies.get(testEntity)! + assertVecApproxEq(body.angvel(), angVel, 3) + }) + }) + + describe('enabledCcd', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + RigidBodyComponent.reactorMap.get(testEntity)!.stop() + Physics.createRigidBody(physicsWorld, testEntity) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should enable Continuous Collision Detection on the entity', () => { + const body = physicsWorld.Rigidbodies.get(testEntity)! + assert.equal(body.isCcdEnabled(), false) + Physics.enabledCcd(physicsWorld, testEntity, true) + assert.equal(body.isCcdEnabled(), true) + }) + + it('should disable CCD on the entity when passing `false` to the `enabled` property', () => { + const body = physicsWorld.Rigidbodies.get(testEntity)! + Physics.enabledCcd(physicsWorld, testEntity, true) + assert.equal(body.isCcdEnabled(), true) + Physics.enabledCcd(physicsWorld, testEntity, false) + assert.equal(body.isCcdEnabled(), false) + }) + }) + + describe('applyImpulse', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute + + it('should apply the impulse to the RigidBody of the entity', () => { + const testImpulse = new Vector3(1, 2, 3) + const beforeBody = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(beforeBody) + const before = beforeBody.linvel() + assertVecApproxEq(before, Vector3_Zero, 3) + Physics.applyImpulse(physicsWorld, testEntity, testImpulse) + physicsSystemExecute() + const afterBody = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(afterBody) + const after = afterBody.linvel() + assertVecAllApproxNotEq(after, before, 3) + }) + }) + + describe('lockRotations', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should lock rotations on the entity', () => { + const impulse = new Vector3(1, 2, 3) + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } + assertVecApproxEq(before, Vector3_Zero, 3) + + body.applyTorqueImpulse(impulse, false) + const dummy = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } + assertVecAllApproxNotEq(before, dummy, 3) + + Physics.lockRotations(physicsWorld, testEntity, true) + body.applyTorqueImpulse(impulse, false) + const after = { x: body.angvel().x, y: body.angvel().y, z: body.angvel().z } + assertVecApproxEq(dummy, after, 3) + }) + + /** + // @todo Fix this test when we update to Rapier >= v0.12 + it('should disable locked rotations on the entity', () => { + const ExpectedValue = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + const body = physicsWorld.Rigidbodies.get(testEntity)! + assert.notDeepEqual(body.rotation(), ExpectedValue) + + Physics.lockRotations(testEntity, true) + body.setRotation(ExpectedValue, false) + console.log(JSON.stringify(body.rotation()), "BEFORE") + console.log(JSON.stringify(ExpectedValue), "Expected") + assertVecAllApproxNotEq(body.rotation(), ExpectedValue, 3) + // assert.notDeepEqual(body.rotation(), ExpectedValue) + + Physics.lockRotations(testEntity, true) + console.log(JSON.stringify(body.rotation()), "AFTEr") + assertVecApproxEq(body.rotation(), ExpectedValue, 4) + }) + */ + }) + + describe('setEnabledRotations', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should disable rotations on the X axis for the rigidBody of the entity', () => { + const testImpulse = new Vector3(1, 2, 3) + const enabledRotation = [false, true, true] as [boolean, boolean, boolean] + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = body.angvel() + assertVecApproxEq(before, Vector3_Zero, 3) + Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) + body.applyTorqueImpulse(testImpulse, false) + physicsWorld!.step() + const after = body.angvel() + assertFloatApproxEq(after.x, before.x) + assertFloatApproxNotEq(after.y, before.y) + assertFloatApproxNotEq(after.z, before.z) + }) + + it('should disable rotations on the Y axis for the rigidBody of the entity', () => { + const testImpulse = new Vector3(1, 2, 3) + const enabledRotation = [true, false, true] as [boolean, boolean, boolean] + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = body.angvel() + assertVecApproxEq(before, Vector3_Zero, 3) + Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) + body.applyTorqueImpulse(testImpulse, false) + physicsWorld!.step() + const after = body.angvel() + assertFloatApproxNotEq(after.x, before.x) + assertFloatApproxEq(after.y, before.y) + assertFloatApproxNotEq(after.z, before.z) + }) + + it('should disable rotations on the Z axis for the rigidBody of the entity', () => { + const testImpulse = new Vector3(1, 2, 3) + const enabledRotation = [true, true, false] as [boolean, boolean, boolean] + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = body.angvel() + assertVecApproxEq(before, Vector3_Zero, 3) + Physics.setEnabledRotations(physicsWorld, testEntity, enabledRotation) + body.applyTorqueImpulse(testImpulse, false) + physicsWorld!.step() + const after = body.angvel() + assertFloatApproxNotEq(after.x, before.x) + assertFloatApproxNotEq(after.y, before.y) + assertFloatApproxEq(after.z, before.z) + }) + }) + + describe('updatePreviousRigidbodyPose', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the previous position of the entity's RigidBodyComponent", () => { + const Expected = new Vector3(1, 2, 3) + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.setTranslation(Expected, false) + const before = { + x: RigidBodyComponent.previousPosition.x[testEntity], + y: RigidBodyComponent.previousPosition.y[testEntity], + z: RigidBodyComponent.previousPosition.z[testEntity] + } + Physics.updatePreviousRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.previousPosition.x[testEntity], + y: RigidBodyComponent.previousPosition.y[testEntity], + z: RigidBodyComponent.previousPosition.z[testEntity] + } + assertVecAllApproxNotEq(before, after, 3) + }) + + it("should set the previous rotation of the entity's RigidBodyComponent", () => { + const Expected = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.setRotation(Expected, false) + const before = { + x: RigidBodyComponent.previousRotation.x[testEntity], + y: RigidBodyComponent.previousRotation.y[testEntity], + z: RigidBodyComponent.previousRotation.z[testEntity], + w: RigidBodyComponent.previousRotation.w[testEntity] + } + Physics.updatePreviousRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.previousRotation.x[testEntity], + y: RigidBodyComponent.previousRotation.y[testEntity], + z: RigidBodyComponent.previousRotation.z[testEntity], + w: RigidBodyComponent.previousRotation.w[testEntity] + } + assertVecAllApproxNotEq(before, after, 4) + }) + }) + + describe('updateRigidbodyPose', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the position of the entity's RigidBodyComponent", () => { + const position = new Vector3(1, 2, 3) + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.setTranslation(position, false) + const before = { + x: RigidBodyComponent.position.x[testEntity], + y: RigidBodyComponent.position.y[testEntity], + z: RigidBodyComponent.position.z[testEntity] + } + Physics.updateRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.position.x[testEntity], + y: RigidBodyComponent.position.y[testEntity], + z: RigidBodyComponent.position.z[testEntity] + } + assertVecAllApproxNotEq(before, after, 3) + }) + + it("should set the rotation of the entity's RigidBodyComponent", () => { + const rotation = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.setRotation(rotation, false) + const before = { + x: RigidBodyComponent.rotation.x[testEntity], + y: RigidBodyComponent.rotation.y[testEntity], + z: RigidBodyComponent.rotation.z[testEntity], + w: RigidBodyComponent.rotation.w[testEntity] + } + Physics.updateRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.rotation.x[testEntity], + y: RigidBodyComponent.rotation.y[testEntity], + z: RigidBodyComponent.rotation.z[testEntity], + w: RigidBodyComponent.rotation.w[testEntity] + } + assertVecAllApproxNotEq(before, after, 4) + }) + + it("should set the linearVelocity of the entity's RigidBodyComponent", () => { + const impulse = new Vector3(1, 2, 3) + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.applyImpulse(impulse, false) + const before = { + x: RigidBodyComponent.linearVelocity.x[testEntity], + y: RigidBodyComponent.linearVelocity.y[testEntity], + z: RigidBodyComponent.linearVelocity.z[testEntity] + } + Physics.updateRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.linearVelocity.x[testEntity], + y: RigidBodyComponent.linearVelocity.y[testEntity], + z: RigidBodyComponent.linearVelocity.z[testEntity] + } + assertVecAllApproxNotEq(before, after, 3) + }) + + it("should set the angularVelocity of the entity's RigidBodyComponent", () => { + const impulse = new Vector3(1, 2, 3) + const body = physicsWorld.Rigidbodies.get(testEntity)! + body.applyTorqueImpulse(impulse, false) + const before = { + x: RigidBodyComponent.angularVelocity.x[testEntity], + y: RigidBodyComponent.angularVelocity.y[testEntity], + z: RigidBodyComponent.angularVelocity.z[testEntity] + } + Physics.updateRigidbodyPose([testEntity]) + const after = { + x: RigidBodyComponent.angularVelocity.x[testEntity], + y: RigidBodyComponent.angularVelocity.y[testEntity], + z: RigidBodyComponent.angularVelocity.z[testEntity] + } + assertVecAllApproxNotEq(before, after, 3) + }) + }) + + describe('setKinematicRigidbodyPose', () => { + const position = new Vector3(1, 2, 3) + const rotation = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the nextTranslation property of the entity's Kinematic RigidBody", () => { + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = body.nextTranslation() + Physics.setKinematicRigidbodyPose(physicsWorld, testEntity, position, rotation) + const after = body.nextTranslation() + assertVecAllApproxNotEq(before, after, 3) + }) + + it("should set the nextRotation property of the entity's Kinematic RigidBody", () => { + const body = physicsWorld.Rigidbodies.get(testEntity)! + const before = body.nextRotation() + Physics.setKinematicRigidbodyPose(physicsWorld, testEntity, position, rotation) + const after = body.nextRotation() + assertVecAllApproxNotEq(before, after, 4) + }) + }) + }) // << Rigidbodies + + describe('Colliders', () => { + describe('setTrigger', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should mark the collider of the entity as a sensor', () => { + const collider = physicsWorld.Colliders.get(testEntity)! + Physics.setTrigger(physicsWorld, testEntity, true) + assert.ok(collider.isSensor()) + }) + + it('should add CollisionGroup.trigger to the interaction groups of the collider when `isTrigger` is passed as true', () => { + const collider = physicsWorld.Colliders.get(testEntity)! + Physics.setTrigger(physicsWorld, testEntity, true) + const triggerInteraction = getInteractionGroups(CollisionGroups.Trigger, 0) // Shift the Trigger bits into the interaction bits, so they don't match with the mask + const hasTriggerInteraction = Boolean(collider.collisionGroups() & triggerInteraction) // If interactionGroups contains the triggerInteraction bits + assert.ok(hasTriggerInteraction) + }) + + it('should not add CollisionGroup.trigger to the interaction groups of the collider when `isTrigger` is passed as false', () => { + const collider = physicsWorld.Colliders.get(testEntity)! + Physics.setTrigger(physicsWorld, testEntity, false) + const triggerInteraction = getInteractionGroups(CollisionGroups.Trigger, 0) // Shift the Trigger bits into the interaction bits, so they don't match with the mask + const notTriggerInteraction = !(collider.collisionGroups() & triggerInteraction) // If interactionGroups does not contain the triggerInteraction bits + assert.ok(notTriggerInteraction) + }) + }) // << setTrigger + + describe('setCollisionLayer', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the collider interaction groups to the given value', () => { + const data = getComponent(testEntity, ColliderComponent) + const ExpectedLayer = CollisionGroups.Avatars | data.collisionLayer + const Expected = getInteractionGroups(ExpectedLayer, data.collisionMask) + const before = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + Physics.setCollisionLayer(physicsWorld, testEntity, ExpectedLayer) + const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + assert.notEqual(before, Expected) + assert.equal(after, Expected) + }) + + it('should not modify the collision mask of the collider', () => { + const data = getComponent(testEntity, ColliderComponent) + const newLayer = CollisionGroups.Avatars + const Expected = getInteractionGroups(newLayer, data.collisionMask) + Physics.setCollisionLayer(physicsWorld, testEntity, newLayer) + const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + assert.equal(after, Expected) + }) + + it('should not add CollisionGroups.Trigger to the collider interaction groups if the entity does not have a TriggerComponent', () => { + Physics.setCollisionLayer(physicsWorld, testEntity, CollisionGroups.Avatars) + const after = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + const noTriggerBit = !(after & getInteractionGroups(CollisionGroups.Trigger, 0)) // not collisionLayer contains Trigger + assert.ok(noTriggerBit) + }) + + it('should not modify the CollisionGroups.Trigger bit in the collider interaction groups if the entity has a TriggerComponent', () => { + const triggerLayer = getInteractionGroups(CollisionGroups.Trigger, 0) // Create the triggerLayer groups bitmask + setComponent(testEntity, TriggerComponent) + const beforeGroups = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + const before = getInteractionGroups(beforeGroups & triggerLayer, 0) === triggerLayer // beforeGroups.collisionLayer contains Trigger + Physics.setCollisionLayer(physicsWorld, testEntity, CollisionGroups.Avatars) + const afterGroups = physicsWorld.Colliders.get(testEntity)!.collisionGroups() + const after = getInteractionGroups(afterGroups & triggerLayer, 0) === triggerLayer // afterGroups.collisionLayer contains Trigger + assert.equal(before, after) + }) + }) // setCollisionLayer + + describe('setCollisionMask', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the collider mask to the given value', () => { + const before = getComponent(testEntity, ColliderComponent) + const Expected = CollisionGroups.Avatars | before.collisionMask + Physics.setCollisionMask(physicsWorld, testEntity, Expected) + const after = getComponent(testEntity, ColliderComponent) + assert.equal(after.collisionMask, Expected) + }) + + it('should not modify the collision layer of the collider', () => { + const before = getComponent(testEntity, ColliderComponent) + Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) + const after = getComponent(testEntity, ColliderComponent) + assert.equal(before.collisionLayer, after.collisionLayer) + }) + + it('should not add CollisionGroups.Trigger to the collider mask if the entity does not have a TriggerComponent', () => { + Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) + const after = getComponent(testEntity, ColliderComponent) + const noTriggerBit = !(after.collisionMask & CollisionGroups.Trigger) // not collisionMask contains Trigger + assert.ok(noTriggerBit) + }) + + it('should not modify the CollisionGroups.Trigger bit in the collider mask if the entity has a TriggerComponent', () => { + setComponent(testEntity, TriggerComponent) + const beforeData = getComponent(testEntity, ColliderComponent) + const before = beforeData.collisionMask & CollisionGroups.Trigger // collisionMask contains Trigger + Physics.setCollisionMask(physicsWorld, testEntity, CollisionGroups.Avatars) + + const afterData = getComponent(testEntity, ColliderComponent) + const after = afterData.collisionMask & CollisionGroups.Trigger // collisionMask contains Trigger + assert.equal(before, after) + }) + }) // setCollisionMask + + describe('setFriction', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the friction value on the entity', () => { + const ExpectedValue = 42 + const collider = physicsWorld.Colliders.get(testEntity)! + assert.notEqual(collider.friction(), ExpectedValue) + Physics.setFriction(physicsWorld, testEntity, ExpectedValue) + assert.equal(collider.friction(), ExpectedValue) + }) + }) // << setFriction + + describe('setRestitution', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the restitution value on the entity', () => { + const ExpectedValue = 42 + const collider = physicsWorld.Colliders.get(testEntity)! + assert.notEqual(collider.restitution(), ExpectedValue) + Physics.setRestitution(physicsWorld, testEntity, ExpectedValue) + assert.equal(collider.restitution(), ExpectedValue) + }) + }) // << setRestitution + + describe('setMass', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the mass value on the entity', () => { + const ExpectedValue = 42 + const collider = physicsWorld.Colliders.get(testEntity)! + assert.notEqual(collider.mass(), ExpectedValue) + Physics.setMass(physicsWorld, testEntity, ExpectedValue) + assert.equal(collider.mass(), ExpectedValue) + }) + }) // << setMass + + describe('getShape', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should return a sphere shape', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Sphere) + }) + + it('should return a capsule shape', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Capsule }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Capsule) + }) + + it('should return a cylinder shape', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Cylinder }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Cylinder) + }) + + it('should return a box shape', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Box) + }) + + it('should return a plane shape', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Plane }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Box) // The Shapes.Plane case is implemented as a box in the engine + }) + + it('should return undefined for the convex_hull case', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.ConvexHull }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), undefined /** @todo Shapes.ConvexHull */) + }) + + it('should return undefined for the mesh case', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), undefined /** @todo Shapes.Mesh */) + }) + + /** + // @todo Heightfield is not supported yet. Triggers an Error exception + it("should return undefined for the heightfield case", () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Heightfield }) + Physics.createRigidBody(physicsWorld, testEntity) + assert.equal(Physics.getShape(physicsWorld, testEntity), Shapes.Heightfield) + }) + */ + }) // << getShape + + describe('removeCollider', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should remove the entity's collider", () => { + const before = physicsWorld.Colliders.get(testEntity) + assert.notEqual(before, undefined) + Physics.removeCollider(physicsWorld!, testEntity) + const after = physicsWorld.Colliders.get(testEntity) + assert.equal(after, undefined) + }) + }) // << removeCollider + + describe('removeCollidersFromRigidBody', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should remove all Colliders from the RigidBody when called', () => { + const before = physicsWorld.Rigidbodies.get(testEntity)! + assert.notEqual(before.numColliders(), 0) + Physics.removeCollidersFromRigidBody(testEntity, physicsWorld!) + assert.equal(before.numColliders(), 0) + }) + }) // << removeCollidersFromRigidBody + + describe('createColliderDesc', () => { + const Default = { + // Default values returned by `createColliderDesc` when the default values of the components are not changed + enabled: true, + shape: { type: 1, halfExtents: { x: 0.5, y: 0.5, z: 0.5 } }, + massPropsMode: 0, + density: 1, + friction: 0.5, + restitution: 0.5, + rotation: { x: 0, y: 0, z: 0, w: 1 }, + translation: { x: 0, y: 0, z: 0 }, + isSensor: false, + collisionGroups: 65543, + solverGroups: 4294967295, + frictionCombineRule: 0, + restitutionCombineRule: 0, + activeCollisionTypes: 60943, + activeEvents: 1, + activeHooks: 0, + mass: 0, + centerOfMass: { x: 0, y: 0, z: 0 }, + contactForceEventThreshold: 0, + principalAngularInertia: { x: 0, y: 0, z: 0 }, + angularInertiaLocalFrame: { x: 0, y: 0, z: 0, w: 1 } + } + + let physicsWorld: PhysicsWorld + let testEntity = UndefinedEntity + let rootEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: rootEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + setComponent(testEntity, ColliderComponent) + rootEntity = createEntity() + setComponent(rootEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(rootEntity, TransformComponent) + setComponent(rootEntity, RigidBodyComponent) + setComponent(rootEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + removeEntity(rootEntity) + return destroyEngine() + }) + + it('should return early if the given `rootEntity` does not have a RigidBody', () => { + removeComponent(rootEntity, RigidBodyComponent) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result, undefined) + }) + + it('should return a descriptor with the expected default values', () => { + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.deepEqual(result, Default) + }) + + it('should set the friction to the same value as the ColliderComponent', () => { + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.friction, getComponent(testEntity, ColliderComponent).friction) + }) + + it('should set the restitution to the same value as the ColliderComponent', () => { + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.restitution, getComponent(testEntity, ColliderComponent).restitution) + }) + + it('should set the collisionGroups to the same value as the ColliderComponent layer and mask', () => { + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + const data = getComponent(testEntity, ColliderComponent) + assert.equal(result.collisionGroups, getInteractionGroups(data.collisionLayer, data.collisionMask)) + }) + + it('should set the sensor property according to whether the entity has a TriggerComponent or not', () => { + const noTriggerDesc = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(noTriggerDesc.isSensor, hasComponent(testEntity, TriggerComponent)) + setComponent(testEntity, TriggerComponent) + const triggerDesc = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(triggerDesc.isSensor, hasComponent(testEntity, TriggerComponent)) + }) + + it('should set the shape to a Ball when the ColliderComponent shape is a Sphere', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.Ball) + }) + + it('should set the shape to a Cuboid when the ColliderComponent shape is a Box', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.Cuboid) + }) + + it('should set the shape to a Cuboid when the ColliderComponent shape is a Plane', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Plane }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.Cuboid) + }) + + it('should set the shape to a TriMesh when the ColliderComponent shape is a Mesh', () => { + setComponent(testEntity, MeshComponent, new Mesh(new BoxGeometry())) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.TriMesh) + }) + + it('should set the shape to a ConvexPolyhedron when the ColliderComponent shape is a ConvexHull', () => { + setComponent(testEntity, MeshComponent, new Mesh(new BoxGeometry())) + setComponent(testEntity, ColliderComponent, { shape: Shapes.ConvexHull }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.ConvexPolyhedron) + }) + + it('should set the shape to a Cylinder when the ColliderComponent shape is a Cylinder', () => { + setComponent(testEntity, ColliderComponent, { shape: Shapes.Cylinder }) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + assert.equal(result.shape.type, ShapeType.Cylinder) + }) + + it('should set the position relative to the parent entity', () => { + const Expected = new Vector3(1, 2, 3) + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + console.log(JSON.stringify(result)) + console.log(JSON.stringify(result.translation)) + assertVecApproxEq(result.translation, Vector3_Zero, 3) + }) + + it('should set the rotation relative to the parent entity', () => { + const Expected = new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + const result = Physics.createColliderDesc(physicsWorld, testEntity, rootEntity) + console.log(JSON.stringify(result.rotation)) + assertVecApproxEq(result.rotation, Rotation_Zero, 4) + }) + }) + + describe('attachCollider', () => { + let testEntity = UndefinedEntity + let rigidbodyEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + rigidbodyEntity = createEntity() + setComponent(rigidbodyEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(rigidbodyEntity, TransformComponent) + setComponent(rigidbodyEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(rigidbodyEntity, ColliderComponent, { shape: Shapes.Box }) + }) + + afterEach(() => { + removeEntity(testEntity) + removeEntity(rigidbodyEntity) + return destroyEngine() + }) + + it("should return undefined when rigidBodyEntity doesn't have a RigidBodyComponent", () => { + removeComponent(rigidbodyEntity, RigidBodyComponent) + const colliderDesc = Physics.createColliderDesc(physicsWorld, testEntity, rigidbodyEntity) + const result = Physics.attachCollider(physicsWorld!, colliderDesc, rigidbodyEntity, testEntity) + assert.equal(result, undefined) + }) + + it('should add the collider to the physicsWorld.Colliders map', () => { + ColliderComponent.reactorMap.get(testEntity)!.stop() + const colliderDesc = Physics.createColliderDesc(physicsWorld, testEntity, rigidbodyEntity) + const result = Physics.attachCollider(physicsWorld!, colliderDesc, rigidbodyEntity, testEntity)! + const expected = physicsWorld.Colliders.get(testEntity) + assert.ok(result) + assert.ok(expected) + assert.deepEqual(result.handle, expected.handle) + }) + }) + + describe('setColliderPose', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + const position = new Vector3(1, 2, 3) + const rotation = new Quaternion(0.5, 0.4, 0.1, 0.0).normalize() + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should assign the entity's position to the collider.translation property", () => { + Physics.setColliderPose(physicsWorld, testEntity, position, rotation) + const collider = physicsWorld.Colliders.get(testEntity)! + // need to step to update the collider's position + physicsWorld.step() + assertVecApproxEq(collider.translation(), position, 3, 0.01) + }) + + it("should assign the entity's rotation to the collider.rotation property", () => { + Physics.setColliderPose(physicsWorld, testEntity, position, rotation) + const collider = physicsWorld.Colliders.get(testEntity)! + // need to step to update the collider's position + physicsWorld.step() + assertVecApproxEq(collider.rotation(), rotation, 4) + }) + }) + + describe('setMassCenter', () => {}) /** @todo The function is not implemented. It is annotated with a todo tag */ + }) // << Colliders + + describe('CharacterControllers', () => { + describe('createCharacterController', () => { + const Default = { + offset: 0.01, + maxSlopeClimbAngle: (60 * Math.PI) / 180, + minSlopeSlideAngle: (30 * Math.PI) / 180, + autoStep: { maxHeight: 0.5, minWidth: 0.01, stepOverDynamic: true }, + enableSnapToGround: 0.1 as number | false + } + + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should store a character controller in the Controllers map', () => { + const before = physicsWorld.Controllers.get(testEntity) + assert.equal(before, undefined) + Physics.createCharacterController(physicsWorld, testEntity, {}) + const after = physicsWorld.Controllers.get(testEntity) + assert.ok(after) + }) + + it('should create a the character controller with the expected defaults when they are omitted', () => { + Physics.createCharacterController(physicsWorld, testEntity, {}) + const controller = physicsWorld.Controllers.get(testEntity) + assert.ok(controller) + assertFloatApproxEq(controller.offset(), Default.offset) + assertFloatApproxEq(controller.maxSlopeClimbAngle(), Default.maxSlopeClimbAngle) + assertFloatApproxEq(controller.minSlopeSlideAngle(), Default.minSlopeSlideAngle) + assertFloatApproxEq(controller.autostepMaxHeight()!, Default.autoStep.maxHeight) + assertFloatApproxEq(controller.autostepMinWidth()!, Default.autoStep.minWidth) + assert.equal(controller.autostepEnabled(), Default.autoStep.stepOverDynamic) + assert.equal(controller.snapToGroundEnabled(), !!Default.enableSnapToGround) + }) + + it('should create a the character controller with values different than the defaults when they are specified', () => { + const Expected = { + offset: 0.05, + maxSlopeClimbAngle: (20 * Math.PI) / 180, + minSlopeSlideAngle: (60 * Math.PI) / 180, + autoStep: { maxHeight: 0.1, minWidth: 0.05, stepOverDynamic: false }, + enableSnapToGround: false as number | false + } + Physics.createCharacterController(physicsWorld, testEntity, Expected) + const controller = physicsWorld.Controllers.get(testEntity) + assert.ok(controller) + // Compare against the specified values + assertFloatApproxEq(controller.offset(), Expected.offset) + assertFloatApproxEq(controller.maxSlopeClimbAngle(), Expected.maxSlopeClimbAngle) + assertFloatApproxEq(controller.minSlopeSlideAngle(), Expected.minSlopeSlideAngle) + assertFloatApproxEq(controller.autostepMaxHeight()!, Expected.autoStep.maxHeight) + assertFloatApproxEq(controller.autostepMinWidth()!, Expected.autoStep.minWidth) + assert.equal(controller.autostepIncludesDynamicBodies(), Expected.autoStep.stepOverDynamic) + assert.equal(controller.snapToGroundEnabled(), !!Expected.enableSnapToGround) + // Compare against the defaults + assertFloatApproxNotEq(controller.offset(), Default.offset) + assertFloatApproxNotEq(controller.maxSlopeClimbAngle(), Default.maxSlopeClimbAngle) + assertFloatApproxNotEq(controller.minSlopeSlideAngle(), Default.minSlopeSlideAngle) + assertFloatApproxNotEq(controller.autostepMaxHeight()!, Default.autoStep.maxHeight) + assertFloatApproxNotEq(controller.autostepMinWidth()!, Default.autoStep.minWidth) + assert.notEqual(controller.autostepIncludesDynamicBodies(), Default.autoStep.stepOverDynamic) + assert.notEqual(controller.snapToGroundEnabled(), !!Default.enableSnapToGround) + }) + }) + + describe('removeCharacterController', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Mesh }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should remove the character controller from the Controllers map', () => { + const before = physicsWorld.Controllers.get(testEntity) + assert.equal(before, undefined) + Physics.createCharacterController(physicsWorld, testEntity, {}) + const created = physicsWorld.Controllers.get(testEntity) + assert.ok(created) + Physics.removeCharacterController(physicsWorld, testEntity) + const after = physicsWorld.Controllers.get(testEntity) + assert.equal(after, undefined) + }) + }) + + describe('computeColliderMovement', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + Physics.createCharacterController(physicsWorld, testEntity, {}) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should change the `computedMovement` value for the entity's Character Controller", () => { + const movement = new Vector3(1, 2, 3) + const controller = physicsWorld.Controllers.get(testEntity)! + const before = controller.computedMovement() + Physics.computeColliderMovement( + physicsWorld, + testEntity, // entity: Entity, + testEntity, // colliderEntity: Entity, + movement // desiredTranslation: Vector3, + // filterGroups?: InteractionGroups, + // filterPredicate?: (collider: Collider) => boolean + ) + const after = controller.computedMovement() + assertVecAllApproxNotEq(before, after, 3) + }) + }) // << computeColliderMovement + + describe('getComputedMovement', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent, { shape: Shapes.Box }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should return (0,0,0) when the entity does not have a CharacterController', () => { + const result = new Vector3(1, 2, 3) + Physics.getComputedMovement(physicsWorld, testEntity, result) + assertVecApproxEq(result, Vector3_Zero, 3) + }) + + it("should return the same value contained in the `computedMovement` value of the entity's Character Controller", () => { + Physics.createCharacterController(physicsWorld, testEntity, {}) + const movement = new Vector3(1, 2, 3) + const controller = physicsWorld.Controllers.get(testEntity)! + const before = controller.computedMovement() + Physics.computeColliderMovement( + physicsWorld, + testEntity, // entity: Entity, + testEntity, // colliderEntity: Entity, + movement // desiredTranslation: Vector3, + // filterGroups?: InteractionGroups, + // filterPredicate?: (collider: Collider) => boolean + ) + const after = controller.computedMovement() + assertVecAllApproxNotEq(before, after, 3) + const result = new Vector3() + Physics.getComputedMovement(physicsWorld, testEntity, result) + assertVecAllApproxNotEq(before, result, 3) + assertVecApproxEq(after, result, 3) + }) + }) // << getComputedMovement + }) // << CharacterControllers + + describe('Raycasts', () => { + describe('castRay', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent, { + position: new Vector3(10, 0, 0), + scale: new Vector3(10, 10, 10) + }) + computeTransformMatrix(testEntity) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(testEntity, ColliderComponent, { + shape: Shapes.Box, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should cast a ray and hit a rigidbody', async () => { + physicsWorld!.step() + + const raycastComponentData = { + type: SceneQueryType.Closest, + origin: new Vector3().set(0, 0, 0), + direction: ObjectDirection.Right, + maxDistance: 20, + groups: getInteractionGroups(CollisionGroups.Default, CollisionGroups.Default) + } + const hits = Physics.castRay(physicsWorld!, raycastComponentData) + + assert.deepEqual(hits.length, 1) + assert.deepEqual(hits[0].normal.x, -1) + assert.deepEqual(hits[0].distance, 5) + assert.deepEqual((hits[0].body.userData as any)['entity'], testEntity) + }) + }) + + describe('castRayFromCamera', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent, { + position: new Vector3(10, 0, 0), + scale: new Vector3(10, 10, 10) + }) + computeTransformMatrix(testEntity) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(testEntity, ColliderComponent, { + shape: Shapes.Box, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + /* + it('should cast a ray from a camera and hit a rigidbody', async () => { + physicsWorld!.step() + assert.ok(1) + }) + */ + }) // << castRayFromCamera + + /** + // @todo Double check the `castShape` implementation before implementing this test + describe('castShape', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity, TransformComponent, { + position: new Vector3(10, 0, 0), + scale: new Vector3(10, 10, 10) + }) + computeTransformMatrix(testEntity) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(testEntity, ColliderComponent, { + shape: Shapes.Box, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + }) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + // @todo This setup is not hitting. Double check the `castShape` implementation before implementing this test + it('should cast a shape and hit a rigidbody', () => { + physicsWorld!.step() + + const collider = physicsWorld.Colliders.get(testEntity)! + const hits = [] as RaycastHit[] + const shapecastComponentData :ShapecastArgs= { + type: SceneQueryType.Closest, // type: SceneQueryType + hits: hits, // hits: RaycastHit[] + collider: collider, // collider: Collider + direction: ObjectDirection.Right, // direction: Vector3 + maxDistance: 20, // maxDistance: number + collisionGroups: getInteractionGroups(CollisionGroups.Default, CollisionGroups.Default), // collisionGroups: InteractionGroups + } + Physics.castShape(physicsWorld!, shapecastComponentData) + + assert.deepEqual(hits.length, 1, "The length of the hits array is incorrect.") + assert.deepEqual(hits[0].normal.x, -1) + assert.deepEqual(hits[0].distance, 5) + assert.deepEqual((hits[0].body.userData as any)['entity'], testEntity) + }) + }) // << castShape + */ + }) // << Raycasts + + describe('Collisions', () => { + describe('createCollisionEventQueue', () => { + beforeEach(async () => { + createEngine() + await Physics.load() + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should create a collision event queue successfully', () => { + const queue = Physics.createCollisionEventQueue() + assert(queue) + }) + }) + + describe('drainCollisionEventQueue', () => { + const InvalidHandle = 8198123 + let physicsWorld: PhysicsWorld + let testEntity1 = UndefinedEntity + let testEntity2 = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld.timestep = 1 / 60 + + testEntity1 = createEntity() + setComponent(testEntity1, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity1, TransformComponent) + setComponent(testEntity1, RigidBodyComponent) + setComponent(testEntity1, ColliderComponent) + + testEntity2 = createEntity() + setComponent(testEntity2, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity2, TransformComponent) + setComponent(testEntity2, RigidBodyComponent) + setComponent(testEntity2, ColliderComponent) + }) + + afterEach(() => { + return destroyEngine() + }) + + function assertCollisionEventClosure(closure: any) { + type CollisionEventClosure = (handle1: number, handle2: number, started: boolean) => void + function hasCollisionEventClosureShape(closure: any): closure is CollisionEventClosure { + return typeof closure === 'function' && closure.length === 3 + } + assert.ok(closure) + assert.ok(hasCollisionEventClosureShape(closure)) + } + + it('should return a function with the correct shape (handle1: number, handle2: number, started: boolean) => void', () => { + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + }) + + it('should do nothing if any of the collider handles are not found', () => { + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + physicsWorld.step() + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + + assert.ok(!hasComponent(testEntity1, CollisionComponent)) + event(collider1.handle, InvalidHandle, true) + assert.ok(!hasComponent(testEntity1, CollisionComponent)) + + assert.ok(!hasComponent(testEntity2, CollisionComponent)) + event(collider2!.handle, InvalidHandle, true) + assert.ok(!hasComponent(testEntity2, CollisionComponent)) + }) + + it('should add a CollisionComponent to the entities contained in the userData of the parent rigidBody of each collider (collider.parent())', () => { + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + physicsWorld.step() + + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Get the parents from the API + const colliderParent1 = collider1.parent() + const colliderParent2 = collider2.parent() + assert.ok(colliderParent1) + assert.ok(colliderParent2) + // Get the entities from parent.userData + const entity1 = (colliderParent1.userData as any)['entity'] + const entity2 = (colliderParent2.userData as any)['entity'] + assert.equal(testEntity1, entity1) + assert.equal(testEntity2, entity2) + // Check before + assert.ok(!hasComponent(entity1, CollisionComponent)) + assert.ok(!hasComponent(entity2, CollisionComponent)) + + // Run and Check after + event(collider1.handle, collider2.handle, true) + assert.ok(hasComponent(entity1, CollisionComponent)) + assert.ok(hasComponent(entity2, CollisionComponent)) + }) + + describe('when `started` is set to `true` ...', () => { + it('... should create a CollisionEvents.COLLISION_START when neither of the colliders is a sensor (aka has a TriggerComponent)', () => { + const Started = true + + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Check before + const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) + const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) + assert.equal(before1, undefined) + assert.equal(before2, undefined) + // setComponent(testEntity1, TriggerComponent) // DONT set the trigger component (testEntity1.body.isSensor() is false) + + // Run and Check after + event(collider1.handle, collider2.handle, Started) + const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) + const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) + assert.ok(after1) + assert.ok(after2) + assert.equal(after1.type, CollisionEvents.COLLISION_START) + assert.equal(after2.type, CollisionEvents.COLLISION_START) + }) + + it('... should create a CollisionEvents.TRIGGER_START when either one of the colliders is a sensor (aka has a TriggerComponent)', async () => { + //force nested reactors to run + const { rerender, unmount } = render(<>) + + const Started = true + + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Check before + const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) + const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) + assert.equal(before1, undefined) + assert.equal(before2, undefined) + setComponent(testEntity1, TriggerComponent) // Set the trigger component (marks testEntity1.body.isSensor() as true) + await act(() => rerender(<>)) + + event(collider1.handle, collider2.handle, Started) + + // Run and Check after + const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) + const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) + assert.ok(after1) + assert.ok(after2) + assert.equal(after1.type, CollisionEvents.TRIGGER_START) + assert.equal(after2.type, CollisionEvents.TRIGGER_START) + }) + + it('... should set entity2 in the CollisionComponent of entity1, and entity1 in the CollisionComponent of entity2', () => { + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Check before + const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) + const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) + assert.equal(before1, undefined) + assert.equal(before2, undefined) + + // Run and Check after + event(collider1.handle, collider2.handle, true) + const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) + const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) + assert.ok(after1) + assert.ok(after2) + }) + }) + + describe('when `started` is set to `false` ...', () => { + it('... should create a CollisionEvents.TRIGGER_END when either one of the colliders is a sensor', async () => { + //force nested reactors to run + const { rerender, unmount } = render(<>) + + const Started = false + + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Check before + const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) + const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) + assert.equal(before1, undefined) + assert.equal(before2, undefined) + setComponent(testEntity1, TriggerComponent) // Set the trigger component (marks testEntity1.body.isSensor() as true) + await act(() => rerender(<>)) + + // Run and Check after + event(collider1.handle, collider2.handle, true) // Run the even twice, so that the entities get each other in their collision components + event(collider1.handle, collider2.handle, Started) + const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) + const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) + assert.ok(after1) + assert.ok(after2) + assert.equal(after1.type, CollisionEvents.TRIGGER_END) + assert.equal(after2.type, CollisionEvents.TRIGGER_END) + }) + + it('... should create a CollisionEvents.COLLISION_END when neither of the colliders is a sensor', () => { + const Started = false + + assert.ok(physicsWorld) + const event = Physics.drainCollisionEventQueue(physicsWorld) + assertCollisionEventClosure(event) + // Get the colliders from the API + const collider1 = physicsWorld.Colliders.get(testEntity1) + const collider2 = physicsWorld.Colliders.get(testEntity2) + assert.ok(collider1) + assert.ok(collider2) + // Check before + const before1 = getComponent(testEntity1, CollisionComponent)?.get(testEntity2) + const before2 = getComponent(testEntity2, CollisionComponent)?.get(testEntity1) + assert.equal(before1, undefined) + assert.equal(before2, undefined) + // setComponent(testEntity1, TriggerComponent) // DONT set the trigger component (testEntity1.body.isSensor() is false) + + // Run and Check after + event(collider1.handle, collider2.handle, true) // Run the even twice, so that the entities get each other in their collision components + event(collider1.handle, collider2.handle, Started) + const after1 = getComponent(testEntity1, CollisionComponent).get(testEntity2) + const after2 = getComponent(testEntity2, CollisionComponent).get(testEntity1) + assert.ok(after1) + assert.ok(after2) + assert.equal(after1.type, CollisionEvents.COLLISION_END) + assert.equal(after2.type, CollisionEvents.COLLISION_END) + }) + }) + }) // << drainCollisionEventQueue + + describe('drainContactEventQueue', () => { + let physicsWorld: PhysicsWorld + let testEntity1 = UndefinedEntity + let testEntity2 = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + const entity = createEntity() + setComponent(entity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(entity, SceneComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(entity, UUIDComponent)) + physicsWorld.timestep = 1 / 60 + + testEntity1 = createEntity() + setComponent(testEntity1, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity1, TransformComponent) + setComponent(testEntity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity1, ColliderComponent) + testEntity2 = createEntity() + setComponent(testEntity2, EntityTreeComponent, { parentEntity: entity }) + setComponent(testEntity2, TransformComponent) + setComponent(testEntity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity2, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity1) + removeEntity(testEntity2) + return destroyEngine() + }) + + function assertContactEventClosure(closure: any) { + type ContactEventClosure = (handle1: number, handle2: number, started: boolean) => void + function hasContactEventClosureShape(closure: any): closure is ContactEventClosure { + return typeof closure === 'function' && closure.length === 1 + } + assert.ok(closure) + assert.ok(hasContactEventClosureShape(closure)) + } + + it('should return a function with the correct shape (event: TempContactForceEvent) => void', () => { + assert.ok(physicsWorld) + const closure = Physics.drainContactEventQueue(physicsWorld) + assertContactEventClosure(closure) + }) + + describe('if the collision exists ...', () => { + const DummyMaxForce = { x: 42, y: 43, z: 44 } + const DummyTotalForce = { x: 45, y: 46, z: 47 } + const DummyHit = { + maxForceDirection: DummyMaxForce, + totalForce: DummyTotalForce + } as ColliderHitEvent + function setDummyCollisionBetween(ent1: Entity, ent2: Entity, hit = DummyHit): void { + const hits = new Map() + hits.set(ent2, hit) + setComponent(ent1, CollisionComponent) + getMutableComponent(ent1, CollisionComponent).set(hits) + } + + const ExpectedMaxForce = { x: 4, y: 5, z: 6 } + const ExpectedTotalForce = { x: 7, y: 8, z: 9 } + + it('should store event.maxForceDirection() into the CollisionComponent.maxForceDirection of entity1.collision.get(entity2) if the collision exists', () => { + // Setup the function spies + const collider1Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity1)!.handle + }) + const collider2Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity2)!.handle + }) + const totalForceSpy = sinon.spy((): Vector => { + return ExpectedTotalForce + }) + const maxForceSpy = sinon.spy((): Vector => { + return ExpectedMaxForce + }) + + // Check before + assert.ok(physicsWorld) + const event = Physics.drainContactEventQueue(physicsWorld) + assertContactEventClosure(event) + assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) + assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) + + // Run and Check after + setDummyCollisionBetween(testEntity1, testEntity2) + setDummyCollisionBetween(testEntity2, testEntity1) + event({ + collider1: collider1Spy as any, + collider2: collider2Spy as any, + totalForce: totalForceSpy as any, + maxForceDirection: maxForceSpy as any + } as TempContactForceEvent) + sinon.assert.called(collider1Spy) + sinon.assert.called(collider2Spy) + sinon.assert.called(maxForceSpy) + const after = getComponent(testEntity1, CollisionComponent).get(testEntity2)?.maxForceDirection + assertVecApproxEq(after, ExpectedMaxForce, 3) + }) + + it('should store event.maxForceDirection() into the CollisionComponent.maxForceDirection of entity2.collision.get(entity1) if the collision exists', () => { + // Setup the function spies + const collider1Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity1)!.handle + }) + const collider2Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity2)!.handle + }) + const totalForceSpy = sinon.spy((): Vector => { + return ExpectedTotalForce + }) + const maxForceSpy = sinon.spy((): Vector => { + return ExpectedMaxForce + }) + + // Check before + assert.ok(physicsWorld) + const event = Physics.drainContactEventQueue(physicsWorld) + assertContactEventClosure(event) + assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) + assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) + + // Run and Check after + setDummyCollisionBetween(testEntity1, testEntity2) + setDummyCollisionBetween(testEntity2, testEntity1) + + event({ + collider1: collider1Spy as any, + collider2: collider2Spy as any, + totalForce: totalForceSpy as any, + maxForceDirection: maxForceSpy as any + } as TempContactForceEvent) + + sinon.assert.called(collider1Spy) + sinon.assert.called(collider2Spy) + sinon.assert.called(maxForceSpy) + const after = getComponent(testEntity2, CollisionComponent).get(testEntity1)?.maxForceDirection + assertVecApproxEq(after, ExpectedMaxForce, 3) + }) + + it('should store event.totalForce() into the CollisionComponent.totalForce of entity1.collision.get(entity2) if the collision exists', () => { + // Setup the function spies + const collider1Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity1)!.handle + }) + const collider2Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity2)!.handle + }) + const totalForceSpy = sinon.spy((): Vector => { + return ExpectedTotalForce + }) + const maxForceSpy = sinon.spy((): Vector => { + return ExpectedMaxForce + }) + + // Check before + assert.ok(physicsWorld) + const event = Physics.drainContactEventQueue(physicsWorld) + assertContactEventClosure(event) + assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) + assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) + // Run and Check after + setDummyCollisionBetween(testEntity1, testEntity2) + setDummyCollisionBetween(testEntity2, testEntity1) + + event({ + collider1: collider1Spy as any, + collider2: collider2Spy as any, + totalForce: totalForceSpy as any, + maxForceDirection: maxForceSpy as any + } as TempContactForceEvent) + + sinon.assert.called(collider1Spy) + sinon.assert.called(collider2Spy) + sinon.assert.called(totalForceSpy) + const after = getComponent(testEntity1, CollisionComponent).get(testEntity2)?.totalForce + assertVecApproxEq(after, ExpectedTotalForce, 3) + }) + + it('should store event.totalForce() into the CollisionComponent.totalForce of entity2.collision.get(entity1) if the collision exists', () => { + // Setup the function spies + const collider1Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity1)!.handle + }) + const collider2Spy = sinon.spy((): number => { + return physicsWorld.Colliders.get(testEntity2)!.handle + }) + const totalForceSpy = sinon.spy((): Vector => { + return ExpectedTotalForce + }) + const maxForceSpy = sinon.spy((): Vector => { + return ExpectedMaxForce + }) + + // Check before + assert.ok(physicsWorld) + const event = Physics.drainContactEventQueue(physicsWorld) + assertContactEventClosure(event) + assert.equal(getOptionalComponent(testEntity1, CollisionComponent), undefined) + assert.equal(getOptionalComponent(testEntity2, CollisionComponent), undefined) + + // Run and Check after + setDummyCollisionBetween(testEntity1, testEntity2) + setDummyCollisionBetween(testEntity2, testEntity1) + event({ + collider1: collider1Spy as any, + collider2: collider2Spy as any, + totalForce: totalForceSpy as any, + maxForceDirection: maxForceSpy as any + } as TempContactForceEvent) + + sinon.assert.called(collider1Spy) + sinon.assert.called(collider2Spy) + sinon.assert.called(totalForceSpy) + const after = getComponent(testEntity2, CollisionComponent).get(testEntity1)?.totalForce + assertVecApproxEq(after, ExpectedTotalForce, 3) + }) + }) + }) // << drainContactEventQueue + }) // << Collisions +}) + +/** TODO: + describe("load", () => {}) // @todo Is there a way to check that the wasmInit() call from rapier.js has been run? + // Character Controller + describe("getControllerOffset", () => {}) // @deprecated + */ diff --git a/packages/spatial/src/physics/classes/Physics.ts b/packages/spatial/src/physics/classes/Physics.ts index 55e9a26034..be6091dd76 100644 --- a/packages/spatial/src/physics/classes/Physics.ts +++ b/packages/spatial/src/physics/classes/Physics.ts @@ -91,9 +91,9 @@ export type PhysicsWorld = World & { id: EntityUUID substeps: number cameraAttachedRigidbodyEntity: Entity - Colliders: Record - Rigidbodies: Record - Controllers: Record + Colliders: Map + Rigidbodies: Map + Controllers: Map collisionEventQueue: EventQueue drainCollisions: ReturnType drainContacts: ReturnType @@ -115,9 +115,9 @@ function createWorld(id: EntityUUID, args = { gravity: { x: 0.0, y: -9.81, z: 0. world.substeps = args.substeps world.cameraAttachedRigidbodyEntity = UndefinedEntity - const Colliders = {} as Record - const Rigidbodies = {} as Record - const Controllers = {} as Record + const Colliders = new Map() + const Rigidbodies = new Map() + const Controllers = new Map() world.Colliders = Colliders world.Rigidbodies = Rigidbodies @@ -133,13 +133,13 @@ function createWorld(id: EntityUUID, args = { gravity: { x: 0.0, y: -9.81, z: 0. } function destroyWorld(id: EntityUUID) { - const world = getMutableState(RapierWorldState)[id] + const world = getState(RapierWorldState)[id] if (!world) throw new Error('Physics world not found') - world.Colliders.set({}) - world.Rigidbodies.set({}) - world.Controllers.set({}) getMutableState(RapierWorldState)[id].set(none) - world.value.free() + world.Colliders.clear() + world.Rigidbodies.clear() + world.Controllers.clear() + world.free() } function getWorld(entity: Entity) { @@ -193,7 +193,7 @@ function simulate(simulationTimestep: number, kinematicEntities: Entity[]) { // smooth kinematic pose changes const substep = (i + 1) / substeps for (const entity of kinematicEntities) { - if (world.Rigidbodies[entity]) smoothKinematicBody(world, entity, timestep, substep) + if (world.Rigidbodies.has(entity)) smoothKinematicBody(world, entity, timestep, substep) } world.step(collisionEventQueue) collisionEventQueue.drainCollisionEvents(drainCollisions) @@ -259,16 +259,16 @@ function createRigidBody(world: PhysicsWorld, entity: Entity) { const rigidBodyUserdata = { entity: entity } body.userData = rigidBodyUserdata - getMutableState(RapierWorldState)[world.id].Rigidbodies[entity].set(body) + world.Rigidbodies.set(entity, body) } function isSleeping(world: PhysicsWorld, entity: Entity) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) return !rigidBody || rigidBody.isSleeping() } const setRigidBodyType = (world: PhysicsWorld, entity: Entity, type: Body) => { - const rigidbody = world.Rigidbodies[entity] + const rigidbody = world.Rigidbodies.get(entity) if (!rigidbody) return let typeEnum: RigidBodyType = undefined! @@ -298,7 +298,7 @@ function setRigidbodyPose( linearVelocity: Vector3, angularVelocity: Vector3 ) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.setTranslation(position, false) rigidBody.setRotation(rotation, false) @@ -307,14 +307,14 @@ function setRigidbodyPose( } function setKinematicRigidbodyPose(world: PhysicsWorld, entity: Entity, position: Vector3, rotation: Quaternion) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.setNextKinematicTranslation(position) rigidBody.setNextKinematicRotation(rotation) } function enabledCcd(world: PhysicsWorld, entity: Entity, enabled: boolean) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.enableCcd(enabled) } @@ -326,7 +326,7 @@ function enabledCcd(world: PhysicsWorld, entity: Entity, enabled: boolean) { * https://github.com/dimforge/rapier.js/issues/282#issuecomment-2177426589 */ function lockRotations(world: PhysicsWorld, entity: Entity, lock: boolean) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.lockRotations(lock, false) } @@ -335,7 +335,7 @@ function lockRotations(world: PhysicsWorld, entity: Entity, lock: boolean) { * @note `setEnabledRotations(entity, [ true, true, true ])` is the exact same as `lockRotations(entity, true)` */ function setEnabledRotations(world: PhysicsWorld, entity: Entity, enabledRotations: [boolean, boolean, boolean]) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.setEnabledRotations(enabledRotations[0], enabledRotations[1], enabledRotations[2], false) } @@ -344,7 +344,7 @@ function updatePreviousRigidbodyPose(entities: Entity[]) { for (const entity of entities) { const world = getWorld(entity) if (!world) continue - const body = world.Rigidbodies[entity] + const body = world.Rigidbodies.get(entity) if (!body) continue const translation = body.translation() as Vector3 const rotation = body.rotation() as Quaternion @@ -362,7 +362,7 @@ function updateRigidbodyPose(entities: Entity[]) { for (const entity of entities) { const world = getWorld(entity) if (!world) continue - const body = world.Rigidbodies[entity] + const body = world.Rigidbodies.get(entity) if (!body) continue const translation = body.translation() as Vector3 const rotation = body.rotation() as Quaternion @@ -385,21 +385,21 @@ function updateRigidbodyPose(entities: Entity[]) { } function removeRigidbody(world: PhysicsWorld, entity: Entity) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (rigidBody && world.bodies.contains(rigidBody.handle)) { world.removeRigidBody(rigidBody) - getMutableState(RapierWorldState)[world.id].Rigidbodies[entity].set(none) + world.Rigidbodies.delete(entity) } } function applyImpulse(world: PhysicsWorld, entity: Entity, impulse: Vector3) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return rigidBody.applyImpulse(impulse, true) } function createColliderDesc(world: PhysicsWorld, entity: Entity, rootEntity: Entity) { - if (!world.Rigidbodies[rootEntity]) return + if (!world.Rigidbodies.has(rootEntity)) return const mesh = getOptionalComponent(entity, MeshComponent) @@ -538,30 +538,30 @@ function attachCollider( rigidBodyEntity: Entity, colliderEntity: Entity ) { - if (world.Colliders[colliderEntity]) return - const rigidBody = world.Rigidbodies[rigidBodyEntity] // guaranteed will exist + if (world.Colliders.has(colliderEntity)) return + const rigidBody = world.Rigidbodies.get(rigidBodyEntity) // guaranteed will exist if (!rigidBody) return console.error('Rigidbody not found for entity ' + rigidBodyEntity) const collider = world.createCollider(colliderDesc, rigidBody) - getMutableState(RapierWorldState)[world.id].Colliders[colliderEntity].set(collider) + world.Colliders.set(colliderEntity, collider) return collider } function setColliderPose(world: PhysicsWorld, entity: Entity, position: Vector3, rotation: Quaternion) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return collider.setTranslationWrtParent(position) collider.setRotationWrtParent(rotation) } function removeCollider(world: PhysicsWorld, entity: Entity) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return world.removeCollider(collider, false) - getMutableState(RapierWorldState)[world.id].Colliders[entity].set(none) + world.Colliders.delete(entity) } function setTrigger(world: PhysicsWorld, entity: Entity, isTrigger: boolean) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return collider.setSensor(isTrigger) const colliderComponent = getComponent(entity, ColliderComponent) @@ -571,32 +571,32 @@ function setTrigger(world: PhysicsWorld, entity: Entity, isTrigger: boolean) { } function setFriction(world: PhysicsWorld, entity: Entity, friction: number) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return collider.setFriction(friction) } function setRestitution(world: PhysicsWorld, entity: Entity, restitution: number) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return collider.setRestitution(restitution) } function setMass(world: PhysicsWorld, entity: Entity, mass: number) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return collider.setMass(mass) } function setMassCenter(world: PhysicsWorld, entity: Entity, massCenter: Vector3) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return /** @todo */ // collider.setMassProperties(massCenter, collider.mass()) } function setCollisionLayer(world: PhysicsWorld, entity: Entity, collisionLayer: InteractionGroups) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return const colliderComponent = getComponent(entity, ColliderComponent) const _collisionLayer = hasComponent(entity, TriggerComponent) @@ -606,7 +606,7 @@ function setCollisionLayer(world: PhysicsWorld, entity: Entity, collisionLayer: } function setCollisionMask(world: PhysicsWorld, entity: Entity, collisionMask: InteractionGroups) { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return const colliderComponent = getComponent(entity, ColliderComponent) const collisionLayer = hasComponent(entity, TriggerComponent) @@ -616,13 +616,13 @@ function setCollisionMask(world: PhysicsWorld, entity: Entity, collisionMask: In } function getShape(world: PhysicsWorld, entity: Entity): Shape | undefined { - const collider = world.Colliders[entity] + const collider = world.Colliders.get(entity) if (!collider) return return RapierShapeToString[collider.shape.type] } function removeCollidersFromRigidBody(entity: Entity, world: PhysicsWorld) { - const rigidBody = world.Rigidbodies[entity] + const rigidBody = world.Rigidbodies.get(entity) if (!rigidBody) return const numColliders = rigidBody.numColliders() for (let index = 0; index < numColliders; index++) { @@ -648,21 +648,21 @@ function createCharacterController( if (autoStep) characterController.enableAutostep(autoStep.maxHeight, autoStep.minWidth, autoStep.stepOverDynamic) if (enableSnapToGround) characterController.enableSnapToGround(enableSnapToGround) else characterController.disableSnapToGround() - getMutableState(RapierWorldState)[world.id].Controllers[entity].set(characterController) + world.Controllers.set(entity, characterController) } function removeCharacterController(world: PhysicsWorld, entity: Entity) { - const controller = world.Controllers[entity] + const controller = world.Controllers.get(entity) if (!controller) return world.removeCharacterController(controller) - getMutableState(RapierWorldState)[world.id].Controllers[entity].set(none) + world.Controllers.delete(entity) } /** * @deprecated - will be populated on AvatarControllerComponent */ function getControllerOffset(world: PhysicsWorld, entity: Entity) { - const controller = world.Controllers[entity] + const controller = world.Controllers.get(entity) if (!controller) return 0 return controller.offset() } @@ -677,9 +677,9 @@ function computeColliderMovement( filterGroups?: InteractionGroups, filterPredicate?: (collider: Collider) => boolean ) { - const controller = world.Controllers[entity] + const controller = world.Controllers.get(entity) if (!controller) return - const collider = world.Colliders[colliderEntity] + const collider = world.Colliders.get(colliderEntity) if (!collider) return controller.computeColliderMovement( collider, @@ -691,7 +691,7 @@ function computeColliderMovement( } function getComputedMovement(world: PhysicsWorld, entity: Entity, out: Vector3) { - const controller = world.Controllers[entity] + const controller = world.Controllers.get(entity) if (!controller) return out.set(0, 0, 0) return out.copy(controller.computedMovement() as Vector3) } @@ -732,8 +732,8 @@ function castRay(world: PhysicsWorld, raycastQuery: RaycastArgs, filterPredicate const groups = raycastQuery.groups const flags = raycastQuery.flags - const excludeCollider = raycastQuery.excludeCollider && world.Colliders[raycastQuery.excludeCollider] - const excludeRigidBody = raycastQuery.excludeRigidBody && world.Rigidbodies[raycastQuery.excludeRigidBody] + const excludeCollider = raycastQuery.excludeCollider && world.Colliders.get(raycastQuery.excludeCollider) + const excludeRigidBody = raycastQuery.excludeRigidBody && world.Rigidbodies.get(raycastQuery.excludeRigidBody) const hits = [] as RaycastHit[] const hitWithNormal = world.castRayAndGetNormal( diff --git a/packages/spatial/src/physics/components/ColliderComponent.test.ts b/packages/spatial/src/physics/components/ColliderComponent.test.ts index 9809199ea0..2eae254445 100644 --- a/packages/spatial/src/physics/components/ColliderComponent.test.ts +++ b/packages/spatial/src/physics/components/ColliderComponent.test.ts @@ -1,406 +1,406 @@ -// /* -// CPAL-1.0 License - -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. - -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. - -// The Original Code is Ethereal Engine. - -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. - -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ - -// import assert from 'assert' - -// import { -// Entity, -// UUIDComponent, -// UndefinedEntity, -// createEntity, -// destroyEngine, -// getComponent, -// removeComponent, -// removeEntity, -// serializeComponent, -// setComponent -// } from '@etherealengine/ecs' - -// import { createEngine } from '@etherealengine/ecs/src/Engine' -// import { Vector3 } from 'three' -// import { TransformComponent } from '../../SpatialModule' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent, getAncestorWithComponent } from '../../transform/components/EntityTree' -// import { Physics, PhysicsWorld } from '../classes/Physics' -// import { assertVecAllApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' -// import { CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' -// import { BodyTypes, Shapes } from '../types/PhysicsTypes' -// import { ColliderComponent } from './ColliderComponent' -// import { RigidBodyComponent } from './RigidBodyComponent' -// import { TriggerComponent } from './TriggerComponent' - -// export const ColliderComponentDefaults = { -// // also used in TriggerComponent.test.ts -// shape: Shapes.Box, -// mass: 1, -// massCenter: new Vector3(), -// friction: 0.5, -// restitution: 0.5, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// } - -// export function assertColliderComponentEquals(data, expected, testShape = true) { -// // also used in TriggerComponent.test.ts -// testShape && assert.equal(data.shape.type, expected.shape.type) -// assert.equal(data.mass, expected.mass) -// assert.deepEqual(data.massCenter, expected.massCenter) -// assert.equal(data.friction, expected.friction) -// assert.equal(data.restitution, expected.restitution) -// assert.equal(data.collisionLayer, expected.collisionLayer) -// assert.equal(data.collisionMask, expected.collisionMask) -// } - -// function getLayerFromCollisionGroups(groups: number): number { -// return (groups & 0xffff_0000) >> 16 -// } -// function getMaskFromCollisionGroups(groups: number): number { -// return groups & 0x0000_ffff -// } - -// describe('ColliderComponent', () => { -// describe('general functionality', () => { -// let entity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// entity = createEntity() -// setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// }) - -// afterEach(() => { -// removeEntity(entity) -// return destroyEngine() -// }) - -// it('should add collider to rigidbody', () => { -// setComponent(entity, TransformComponent) -// setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(entity, ColliderComponent) - -// const body = physicsWorld.Rigidbodies.get(entity)! -// const collider = physicsWorld.Colliders.get(entity)! - -// assert.equal(body.numColliders(), 1) -// assert(collider) -// assert.equal(collider, body.collider(0)) -// }) - -// it('should remove collider from rigidbody', () => { -// setComponent(entity, TransformComponent) -// setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(entity, ColliderComponent) - -// const body = physicsWorld.Rigidbodies.get(entity)! -// const collider = physicsWorld.Colliders.get(entity)! - -// assert.equal(body.numColliders(), 1) -// assert(collider) -// assert.equal(collider, body.collider(0)) - -// removeComponent(entity, ColliderComponent) - -// assert.equal(body.numColliders(), 0) -// }) - -// it('should add trigger collider', () => { -// setComponent(entity, TransformComponent) - -// setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// setComponent(entity, TriggerComponent) -// setComponent(entity, ColliderComponent) - -// const collider = physicsWorld.Colliders.get(entity)! -// assert.equal(collider!.isSensor(), true) -// }) -// }) - -// describe('IDs', () => { -// it('should initialize the ColliderComponent.name field with the expected value', () => { -// assert.equal(ColliderComponent.name, 'ColliderComponent') -// }) -// it('should initialize the ColliderComponent.jsonID field with the expected value', () => { -// assert.equal(ColliderComponent.jsonID, 'EE_collider') -// }) -// }) - -// describe('onInit', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// testEntity = createEntity() -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should initialize the component with the expected default values', () => { -// const data = getComponent(testEntity, ColliderComponent) -// assertColliderComponentEquals(data, ColliderComponentDefaults) -// }) -// }) // << onInit - -// describe('onSet', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// testEntity = createEntity() -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should change the values of an initialized ColliderComponent', () => { -// const Expected = { -// shape: Shapes.Sphere, -// mass: 2, -// massCenter: new Vector3(1, 2, 3), -// friction: 4.0, -// restitution: 5.0, -// collisionLayer: CollisionGroups.Ground, -// collisionMask: CollisionGroups.Avatars | CollisionGroups.Trigger -// } -// const before = getComponent(testEntity, ColliderComponent) -// assertColliderComponentEquals(before, ColliderComponentDefaults) -// setComponent(testEntity, ColliderComponent, Expected) - -// const data = getComponent(testEntity, ColliderComponent) -// assertColliderComponentEquals(data, Expected) -// }) - -// it('should not change values of an initialized ColliderComponent when the data passed had incorrect types', () => { -// const Incorrect = { -// shape: 1, -// mass: 'mass.incorrect', -// massCenter: 2, -// friction: 'friction.incorrect', -// restitution: 'restitution.incorrect', -// collisionLayer: 'collisionLayer.incorrect', -// collisionMask: 'trigger.incorrect' -// } -// const before = getComponent(testEntity, ColliderComponent) -// assertColliderComponentEquals(before, ColliderComponentDefaults) - -// // @ts-ignore -// setComponent(testEntity, ColliderComponent, Incorrect) -// const data = getComponent(testEntity, ColliderComponent) -// assertColliderComponentEquals(data, ColliderComponentDefaults) -// }) -// }) // << onSet - -// describe('toJSON', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should serialize the component's data correctly", () => { -// const json = serializeComponent(testEntity, ColliderComponent) -// assert.deepEqual(json, ColliderComponentDefaults) -// }) -// }) // << toJson - -// describe('reactor', () => { -// let testEntity = UndefinedEntity -// let parentEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// function createValidAncestor(colliderData = ColliderComponentDefaults as any): Entity { -// const result = createEntity() -// setComponent(result, TransformComponent) -// setComponent(result, ColliderComponent, colliderData) -// setComponent(result, RigidBodyComponent) -// return result -// } - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld!.timestep = 1 / 60 - -// parentEntity = createValidAncestor() -// testEntity = createEntity() -// setComponent(parentEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, EntityTreeComponent, { parentEntity: parentEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// Physics.destroyWorld(physicsWorld.id) -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// describe('should attach and/or remove a collider to the physicsWorld based on the entity and its closest ancestor with a RigidBodyComponent ...', () => { -// it("... when the shape of the entity's collider changes", () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const beforeCollider = physicsWorld.Colliders.get(testEntity) -// assert.ok(beforeCollider) -// const before = beforeCollider.shape -// assert.equal(getComponent(testEntity, ColliderComponent).shape, ColliderComponentDefaults.shape) - -// setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) -// assert.notEqual(getComponent(testEntity, ColliderComponent).shape, ColliderComponentDefaults.shape) -// const after1Collider = physicsWorld.Colliders.get(testEntity)! -// const after1 = after1Collider.shape -// assert.notEqual(beforeCollider.handle, after1Collider.handle) -// assert.notDeepEqual(after1, before) - -// removeComponent(testEntity, ColliderComponent) -// assert.notEqual(getComponent(testEntity, ColliderComponent)?.shape, ColliderComponentDefaults.shape) -// const after2Collider = physicsWorld.Colliders.get(testEntity)! -// assert.equal(after2Collider, undefined) -// }) - -// it("... when the scale of the entity's transform changes", () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const TransformScaleDefault = new Vector3(1, 1, 1) -// const Expected = new Vector3(42, 42, 42) -// const beforeCollider = physicsWorld.Colliders.get(testEntity) -// assert.ok(beforeCollider) -// const before = getComponent(testEntity, TransformComponent).scale.clone() -// assertVecApproxEq(before, TransformScaleDefault, 3) - -// // Apply and check on changes -// setComponent(testEntity, TransformComponent, { scale: Expected }) -// const after1 = getComponent(testEntity, TransformComponent).scale.clone() -// assertVecAllApproxNotEq(before, after1, 3) - -// // Apply and check on component removal -// removeComponent(testEntity, ColliderComponent) -// const after2 = getComponent(testEntity, TransformComponent).scale.clone() -// assert.notEqual(after1, after2) -// const afterCollider = physicsWorld.Colliders.get(testEntity) -// assert.equal(afterCollider, undefined) -// }) - -// it('... when the closest ancestor to the entity, with a RigidBodyComponent, changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const newParent = createValidAncestor() -// assert.notEqual(parentEntity, newParent) - -// removeComponent(testEntity, EntityTreeComponent) -// setComponent(testEntity, EntityTreeComponent, { parentEntity: newParent }) -// const ancestor = getAncestorWithComponent( -// testEntity, -// RigidBodyComponent, -// /*closest*/ true, -// /*includeSelf*/ false -// ) -// assert.equal(ancestor, newParent) -// }) -// }) - -// it('should set the mass of the API data based on the component.mass.value when it changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = 42 -// const before = physicsWorld.Colliders.get(testEntity)!.mass() -// setComponent(testEntity, ColliderComponent, { mass: Expected }) -// const after = physicsWorld.Colliders.get(testEntity)!.mass() -// assert.notEqual(before, after, 'Before and After should not be equal') -// assert.notEqual(before, Expected, 'Before and Expected should not be equal') -// assert.equal(after, Expected, 'After and Expected should be equal') -// }) - -// it('should set the friction of the API data based on the component.friction.value when it changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = 42 -// const before = physicsWorld.Colliders.get(testEntity)!.friction() -// setComponent(testEntity, ColliderComponent, { friction: Expected }) -// const after = physicsWorld.Colliders.get(testEntity)!.friction() -// assert.notEqual(before, after, 'Before and After should not be equal') -// assert.notEqual(before, Expected, 'Before and Expected should not be equal') -// assert.equal(after, Expected, 'After and Expected should be equal') -// }) - -// it('should set the restitution of the API data based on the component.restitution.value when it changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = 42 -// const before = physicsWorld.Colliders.get(testEntity)!.restitution() -// setComponent(testEntity, ColliderComponent, { restitution: Expected }) -// const after = physicsWorld.Colliders.get(testEntity)!.restitution() -// assert.notEqual(before, after, 'Before and After should not be equal') -// assert.notEqual(before, Expected, 'Before and Expected should not be equal') -// assert.equal(after, Expected, 'After and Expected should be equal') -// }) - -// it('should set the collisionLayer of the API data based on the component.collisionLayer.value when it changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = CollisionGroups.Avatars -// const before = getLayerFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) -// setComponent(testEntity, ColliderComponent, { collisionLayer: Expected }) -// const after = getLayerFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) -// assert.notEqual(before, after, 'Before and After should not be equal') -// assert.notEqual(before, Expected, 'Before and Expected should not be equal') -// assert.equal(after, Expected, 'After and Expected should be equal') -// }) - -// it('should set the collisionMask of the API data based on the component.collisionMask.value when it changes', () => { -// assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = CollisionGroups.Avatars -// const before = getMaskFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) -// setComponent(testEntity, ColliderComponent, { collisionMask: Expected }) -// const after = getMaskFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) -// assert.notEqual(before, after, 'Before and After should not be equal') -// assert.notEqual(before, Expected, 'Before and Expected should not be equal') -// assert.equal(after, Expected, 'After and Expected should be equal') -// }) -// }) // << reactor -// }) +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import assert from 'assert' + +import { + Entity, + UUIDComponent, + UndefinedEntity, + createEntity, + destroyEngine, + getComponent, + removeComponent, + removeEntity, + serializeComponent, + setComponent +} from '@etherealengine/ecs' + +import { createEngine } from '@etherealengine/ecs/src/Engine' +import { Vector3 } from 'three' +import { TransformComponent } from '../../SpatialModule' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent, getAncestorWithComponent } from '../../transform/components/EntityTree' +import { Physics, PhysicsWorld } from '../classes/Physics' +import { assertVecAllApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' +import { CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' +import { BodyTypes, Shapes } from '../types/PhysicsTypes' +import { ColliderComponent } from './ColliderComponent' +import { RigidBodyComponent } from './RigidBodyComponent' +import { TriggerComponent } from './TriggerComponent' + +export const ColliderComponentDefaults = { + // also used in TriggerComponent.test.ts + shape: Shapes.Box, + mass: 1, + massCenter: new Vector3(), + friction: 0.5, + restitution: 0.5, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask +} + +export function assertColliderComponentEquals(data, expected, testShape = true) { + // also used in TriggerComponent.test.ts + testShape && assert.equal(data.shape.type, expected.shape.type) + assert.equal(data.mass, expected.mass) + assert.deepEqual(data.massCenter, expected.massCenter) + assert.equal(data.friction, expected.friction) + assert.equal(data.restitution, expected.restitution) + assert.equal(data.collisionLayer, expected.collisionLayer) + assert.equal(data.collisionMask, expected.collisionMask) +} + +function getLayerFromCollisionGroups(groups: number): number { + return (groups & 0xffff_0000) >> 16 +} +function getMaskFromCollisionGroups(groups: number): number { + return groups & 0x0000_ffff +} + +describe('ColliderComponent', () => { + describe('general functionality', () => { + let entity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + entity = createEntity() + setComponent(entity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + }) + + afterEach(() => { + removeEntity(entity) + return destroyEngine() + }) + + it('should add collider to rigidbody', () => { + setComponent(entity, TransformComponent) + setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(entity, ColliderComponent) + + const body = physicsWorld.Rigidbodies.get(entity)! + const collider = physicsWorld.Colliders.get(entity)! + + assert.equal(body.numColliders(), 1) + assert(collider) + assert.equal(collider, body.collider(0)) + }) + + it('should remove collider from rigidbody', () => { + setComponent(entity, TransformComponent) + setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(entity, ColliderComponent) + + const body = physicsWorld.Rigidbodies.get(entity)! + const collider = physicsWorld.Colliders.get(entity)! + + assert.equal(body.numColliders(), 1) + assert(collider) + assert.equal(collider, body.collider(0)) + + removeComponent(entity, ColliderComponent) + + assert.equal(body.numColliders(), 0) + }) + + it('should add trigger collider', () => { + setComponent(entity, TransformComponent) + + setComponent(entity, RigidBodyComponent, { type: BodyTypes.Fixed }) + setComponent(entity, TriggerComponent) + setComponent(entity, ColliderComponent) + + const collider = physicsWorld.Colliders.get(entity)! + assert.equal(collider!.isSensor(), true) + }) + }) + + describe('IDs', () => { + it('should initialize the ColliderComponent.name field with the expected value', () => { + assert.equal(ColliderComponent.name, 'ColliderComponent') + }) + it('should initialize the ColliderComponent.jsonID field with the expected value', () => { + assert.equal(ColliderComponent.jsonID, 'EE_collider') + }) + }) + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, ColliderComponent) + assertColliderComponentEquals(data, ColliderComponentDefaults) + }) + }) // << onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized ColliderComponent', () => { + const Expected = { + shape: Shapes.Sphere, + mass: 2, + massCenter: new Vector3(1, 2, 3), + friction: 4.0, + restitution: 5.0, + collisionLayer: CollisionGroups.Ground, + collisionMask: CollisionGroups.Avatars | CollisionGroups.Trigger + } + const before = getComponent(testEntity, ColliderComponent) + assertColliderComponentEquals(before, ColliderComponentDefaults) + setComponent(testEntity, ColliderComponent, Expected) + + const data = getComponent(testEntity, ColliderComponent) + assertColliderComponentEquals(data, Expected) + }) + + it('should not change values of an initialized ColliderComponent when the data passed had incorrect types', () => { + const Incorrect = { + shape: 1, + mass: 'mass.incorrect', + massCenter: 2, + friction: 'friction.incorrect', + restitution: 'restitution.incorrect', + collisionLayer: 'collisionLayer.incorrect', + collisionMask: 'trigger.incorrect' + } + const before = getComponent(testEntity, ColliderComponent) + assertColliderComponentEquals(before, ColliderComponentDefaults) + + // @ts-ignore + setComponent(testEntity, ColliderComponent, Incorrect) + const data = getComponent(testEntity, ColliderComponent) + assertColliderComponentEquals(data, ColliderComponentDefaults) + }) + }) // << onSet + + describe('toJSON', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should serialize the component's data correctly", () => { + const json = serializeComponent(testEntity, ColliderComponent) + assert.deepEqual(json, ColliderComponentDefaults) + }) + }) // << toJson + + describe('reactor', () => { + let testEntity = UndefinedEntity + let parentEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + function createValidAncestor(colliderData = ColliderComponentDefaults as any): Entity { + const result = createEntity() + setComponent(result, TransformComponent) + setComponent(result, ColliderComponent, colliderData) + setComponent(result, RigidBodyComponent) + return result + } + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld!.timestep = 1 / 60 + + parentEntity = createValidAncestor() + testEntity = createEntity() + setComponent(parentEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, EntityTreeComponent, { parentEntity: parentEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + Physics.destroyWorld(physicsWorld.id) + removeEntity(testEntity) + return destroyEngine() + }) + + describe('should attach and/or remove a collider to the physicsWorld based on the entity and its closest ancestor with a RigidBodyComponent ...', () => { + it("... when the shape of the entity's collider changes", () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const beforeCollider = physicsWorld.Colliders.get(testEntity) + assert.ok(beforeCollider) + const before = beforeCollider.shape + assert.equal(getComponent(testEntity, ColliderComponent).shape, ColliderComponentDefaults.shape) + + setComponent(testEntity, ColliderComponent, { shape: Shapes.Sphere }) + assert.notEqual(getComponent(testEntity, ColliderComponent).shape, ColliderComponentDefaults.shape) + const after1Collider = physicsWorld.Colliders.get(testEntity)! + const after1 = after1Collider.shape + assert.notEqual(beforeCollider.handle, after1Collider.handle) + assert.notDeepEqual(after1, before) + + removeComponent(testEntity, ColliderComponent) + assert.notEqual(getComponent(testEntity, ColliderComponent)?.shape, ColliderComponentDefaults.shape) + const after2Collider = physicsWorld.Colliders.get(testEntity)! + assert.equal(after2Collider, undefined) + }) + + it("... when the scale of the entity's transform changes", () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const TransformScaleDefault = new Vector3(1, 1, 1) + const Expected = new Vector3(42, 42, 42) + const beforeCollider = physicsWorld.Colliders.get(testEntity) + assert.ok(beforeCollider) + const before = getComponent(testEntity, TransformComponent).scale.clone() + assertVecApproxEq(before, TransformScaleDefault, 3) + + // Apply and check on changes + setComponent(testEntity, TransformComponent, { scale: Expected }) + const after1 = getComponent(testEntity, TransformComponent).scale.clone() + assertVecAllApproxNotEq(before, after1, 3) + + // Apply and check on component removal + removeComponent(testEntity, ColliderComponent) + const after2 = getComponent(testEntity, TransformComponent).scale.clone() + assert.notEqual(after1, after2) + const afterCollider = physicsWorld.Colliders.get(testEntity) + assert.equal(afterCollider, undefined) + }) + + it('... when the closest ancestor to the entity, with a RigidBodyComponent, changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const newParent = createValidAncestor() + assert.notEqual(parentEntity, newParent) + + removeComponent(testEntity, EntityTreeComponent) + setComponent(testEntity, EntityTreeComponent, { parentEntity: newParent }) + const ancestor = getAncestorWithComponent( + testEntity, + RigidBodyComponent, + /*closest*/ true, + /*includeSelf*/ false + ) + assert.equal(ancestor, newParent) + }) + }) + + it('should set the mass of the API data based on the component.mass.value when it changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = 42 + const before = physicsWorld.Colliders.get(testEntity)!.mass() + setComponent(testEntity, ColliderComponent, { mass: Expected }) + const after = physicsWorld.Colliders.get(testEntity)!.mass() + assert.notEqual(before, after, 'Before and After should not be equal') + assert.notEqual(before, Expected, 'Before and Expected should not be equal') + assert.equal(after, Expected, 'After and Expected should be equal') + }) + + it('should set the friction of the API data based on the component.friction.value when it changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = 42 + const before = physicsWorld.Colliders.get(testEntity)!.friction() + setComponent(testEntity, ColliderComponent, { friction: Expected }) + const after = physicsWorld.Colliders.get(testEntity)!.friction() + assert.notEqual(before, after, 'Before and After should not be equal') + assert.notEqual(before, Expected, 'Before and Expected should not be equal') + assert.equal(after, Expected, 'After and Expected should be equal') + }) + + it('should set the restitution of the API data based on the component.restitution.value when it changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = 42 + const before = physicsWorld.Colliders.get(testEntity)!.restitution() + setComponent(testEntity, ColliderComponent, { restitution: Expected }) + const after = physicsWorld.Colliders.get(testEntity)!.restitution() + assert.notEqual(before, after, 'Before and After should not be equal') + assert.notEqual(before, Expected, 'Before and Expected should not be equal') + assert.equal(after, Expected, 'After and Expected should be equal') + }) + + it('should set the collisionLayer of the API data based on the component.collisionLayer.value when it changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = CollisionGroups.Avatars + const before = getLayerFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) + setComponent(testEntity, ColliderComponent, { collisionLayer: Expected }) + const after = getLayerFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) + assert.notEqual(before, after, 'Before and After should not be equal') + assert.notEqual(before, Expected, 'Before and Expected should not be equal') + assert.equal(after, Expected, 'After and Expected should be equal') + }) + + it('should set the collisionMask of the API data based on the component.collisionMask.value when it changes', () => { + assert.ok(ColliderComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = CollisionGroups.Avatars + const before = getMaskFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) + setComponent(testEntity, ColliderComponent, { collisionMask: Expected }) + const after = getMaskFromCollisionGroups(physicsWorld.Colliders.get(testEntity)!.collisionGroups()) + assert.notEqual(before, after, 'Before and After should not be equal') + assert.notEqual(before, Expected, 'Before and Expected should not be equal') + assert.equal(after, Expected, 'After and Expected should be equal') + }) + }) // << reactor +}) diff --git a/packages/spatial/src/physics/components/ColliderComponent.tsx b/packages/spatial/src/physics/components/ColliderComponent.tsx index d77fd317cd..b83efb5a3d 100644 --- a/packages/spatial/src/physics/components/ColliderComponent.tsx +++ b/packages/spatial/src/physics/components/ColliderComponent.tsx @@ -83,13 +83,13 @@ export const ColliderComponent = defineComponent({ const component = useComponent(entity, ColliderComponent) const transform = useComponent(entity, TransformComponent) const rigidbodyEntity = useAncestorWithComponent(entity, RigidBodyComponent) + const rigidbodyComponent = useOptionalComponent(rigidbodyEntity, RigidBodyComponent) const physicsWorld = Physics.useWorld(entity) const triggerComponent = useOptionalComponent(entity, TriggerComponent) const hasCollider = useState(false) - const physicsWorldRigidbody = Physics.useWorld(entity)?.Rigidbodies[entity] useEffect(() => { - if (!rigidbodyEntity || !physicsWorld) return + if (!rigidbodyComponent || !physicsWorld) return const colliderDesc = Physics.createColliderDesc(physicsWorld, entity, rigidbodyEntity) if (!colliderDesc) return @@ -101,7 +101,7 @@ export const ColliderComponent = defineComponent({ Physics.removeCollider(physicsWorld, entity) hasCollider.set(false) } - }, [physicsWorld, component.shape, rigidbodyEntity, !!physicsWorldRigidbody, transform.scale]) + }, [physicsWorld, component.shape, rigidbodyEntity, !!rigidbodyComponent, transform.scale]) useLayoutEffect(() => { if (!physicsWorld) return diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx b/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx index 368d08095e..f8ad4a9048 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx +++ b/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx @@ -1,503 +1,503 @@ -// /* -// CPAL-1.0 License - -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. - -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. - -// The Original Code is Ethereal Engine. - -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. - -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ - -// import { act, render } from '@testing-library/react' -// import assert from 'assert' - -// import { RigidBodyType } from '@dimforge/rapier3d-compat' -// import { -// SystemDefinitions, -// UUIDComponent, -// UndefinedEntity, -// createEngine, -// createEntity, -// destroyEngine, -// getComponent, -// hasComponent, -// removeComponent, -// removeEntity, -// serializeComponent, -// setComponent -// } from '@etherealengine/ecs' -// import React from 'react' -// import { Vector3 } from 'three' -// import { PhysicsSystem, TransformComponent } from '../../SpatialModule' -// import { Vector3_Zero } from '../../common/constants/MathConstants' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent } from '../../transform/components/EntityTree' -// import { Physics, PhysicsWorld } from '../classes/Physics' -// import { -// assertFloatApproxEq, -// assertFloatApproxNotEq, -// assertVecAllApproxNotEq, -// assertVecApproxEq -// } from '../classes/Physics.test' -// import { BodyTypes } from '../types/PhysicsTypes' -// import { ColliderComponent } from './ColliderComponent' -// import { -// RigidBodyComponent, -// RigidBodyDynamicTagComponent, -// RigidBodyFixedTagComponent, -// RigidBodyKinematicTagComponent, -// getTagComponentForRigidBody -// } from './RigidBodyComponent' - -// const RigidBodyComponentDefaults = { -// type: BodyTypes.Fixed, -// ccd: false, -// allowRolling: true, -// enabledRotations: [true, true, true] as [boolean, boolean, boolean], -// canSleep: true, -// gravityScale: 1, -// previousPosition: 3, -// previousRotation: 4, -// position: 3, -// rotation: 4, -// targetKinematicPosition: 3, -// targetKinematicRotation: 4, -// linearVelocity: 3, -// angularVelocity: 3, -// targetKinematicLerpMultiplier: 0 -// } - -// export function assertArrayEqual(A: Array, B: Array, err = 'Arrays are not equal') { -// assert.equal(A.length, B.length, err + ': Their lenght is not the same') -// for (let id = 0; id < A.length && id < B.length; id++) { -// assert.deepEqual(A[id], B[id], err + `: Their item[${id}] is not the same`) -// } -// } - -// export function assertArrayNotEqual(A: Array, B: Array, err = 'Arrays are equal') { -// for (let id = 0; id < A.length && id < B.length; id++) { -// assert.notDeepEqual(A[id], B[id], err) -// } -// } - -// export function assertRigidBodyComponentEqual(data, expected = RigidBodyComponentDefaults) { -// assert.equal(data.type, expected.type) -// assert.equal(data.ccd, expected.ccd) -// assert.equal(data.allowRolling, expected.allowRolling) -// assert.equal(data.enabledRotations.length, expected.enabledRotations.length) -// assert.equal(data.enabledRotations[0], expected.enabledRotations[0]) -// assert.equal(data.enabledRotations[1], expected.enabledRotations[1]) -// assert.equal(data.enabledRotations[2], expected.enabledRotations[2]) -// assert.equal(data.canSleep, expected.canSleep) -// assert.equal(data.gravityScale, expected.gravityScale) -// /** -// // @todo Not serialized by the component -// assertVecApproxEq(data.previousPosition, expected.previousPosition, 3) -// assertVecApproxEq(data.previousRotation, expected.previousRotation, 4) -// assertVecApproxEq(data.position, expected.position, 3) -// assertVecApproxEq(data.rotation, expected.rotation, 4) -// assertVecApproxEq(data.targetKinematicPosition, expected.targetKinematicPosition, 3) -// assertVecApproxEq(data.targetKinematicRotation, expected.targetKinematicRotation, 4) -// assertVecApproxEq(data.linearVelocity, expected.linearVelocity, 3) -// assertVecApproxEq(data.angularVelocity, expected.angularVelocity, 3) -// assert.equal(data.targetKinematicLerpMultiplier, expected.targetKinematicLerpMultiplier) -// */ -// } - -// describe('RigidBodyComponent', () => { -// describe('IDs', () => { -// it('should initialize the RigidBodyComponent.name field with the expected value', () => { -// assert.equal(RigidBodyComponent.name, 'RigidBodyComponent') -// }) -// it('should initialize the RigidBodyComponent.jsonID field with the expected value', () => { -// assert.equal(RigidBodyComponent.jsonID, 'EE_rigidbody') -// }) -// }) - -// describe('onInit', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// setComponent(testEntity, RigidBodyComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should initialize the component with the expected default values', () => { -// const data = getComponent(testEntity, RigidBodyComponent) -// assertRigidBodyComponentEqual(data, RigidBodyComponentDefaults) -// }) -// }) // << onInit - -// describe('onSet', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// setComponent(testEntity, RigidBodyComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should change the values of an initialized RigidBodyComponent', () => { -// const Expected = { -// type: BodyTypes.Dynamic, -// ccd: true, -// allowRolling: false, -// canSleep: false, -// gravityScale: 2, -// enabledRotations: [false, false, false] as [boolean, boolean, boolean] -// } -// const before = getComponent(testEntity, RigidBodyComponent) -// assertRigidBodyComponentEqual(before, RigidBodyComponentDefaults) - -// setComponent(testEntity, RigidBodyComponent, Expected) -// const after = getComponent(testEntity, RigidBodyComponent) -// assert.equal(after.type, Expected.type) -// assert.equal(after.ccd, Expected.ccd) -// assert.equal(after.allowRolling, Expected.allowRolling) -// assert.equal(after.canSleep, Expected.canSleep) -// assert.equal(after.gravityScale, Expected.gravityScale) -// assert.equal(after.enabledRotations.length, Expected.enabledRotations.length) -// assert.equal(after.enabledRotations[0], Expected.enabledRotations[0]) -// assert.equal(after.enabledRotations[1], Expected.enabledRotations[1]) -// assert.equal(after.enabledRotations[2], Expected.enabledRotations[2]) -// }) - -// it('should not change values of an initialized RigidBodyComponent when the data passed had incorrect types', () => { -// const Incorrect = { -// type: 1, -// ccd: 'ccd', -// allowRolling: 2, -// canSleep: 3, -// gravityScale: false, -// enabledRotations: [4, 5, 6] -// } -// const before = getComponent(testEntity, RigidBodyComponent) -// assertRigidBodyComponentEqual(before, RigidBodyComponentDefaults) - -// // @ts-ignore Pass an incorrect type to setComponent -// setComponent(testEntity, RigidBodyComponent, Incorrect) -// const after = getComponent(testEntity, RigidBodyComponent) -// assertRigidBodyComponentEqual(after, RigidBodyComponentDefaults) -// }) -// }) // << onSet - -// describe('toJSON', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// setComponent(testEntity, RigidBodyComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should serialize the component's data correctly", () => { -// const Expected = { -// type: 'fixed', -// ccd: false, -// allowRolling: true, -// enabledRotations: [true, true, true], -// canSleep: true, -// gravityScale: 1 -// } -// const json = serializeComponent(testEntity, RigidBodyComponent) -// assert.deepEqual(json, Expected) -// }) -// }) // << toJSON - -// describe('reactor', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let newPhysicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// Physics.destroyWorld(physicsWorld.id) -// // if (newPhysicsWorld) Physics.destroyWorld(newPhysicsWorld.id) -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute - -// it('should create a RigidBody for the entity in the new physicsWorld when the world is changed', async () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const before = physicsWorld.Rigidbodies.get(testEntity)!.handle -// assert.ok(physicsWorld!.bodies.contains(before)) - -// const newPhysicsEntity = createEntity() -// setComponent(newPhysicsEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(newPhysicsEntity, SceneComponent) -// setComponent(newPhysicsEntity, TransformComponent) -// setComponent(newPhysicsEntity, EntityTreeComponent) -// newPhysicsWorld = Physics.createWorld(getComponent(newPhysicsEntity, UUIDComponent)) -// newPhysicsWorld!.timestep = 1 / 60 - -// // Change the world -// setComponent(testEntity, EntityTreeComponent, { parentEntity: newPhysicsEntity }) - -// // Force react lifecycle to update Physics.useWorld -// const { rerender, unmount } = render(<>) -// await act(() => rerender(<>)) - -// // Check the changes -// RigidBodyComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately -// const after = newPhysicsWorld.Rigidbodies.get(testEntity)!.handle -// assert.ok(newPhysicsWorld!.bodies.contains(after)) -// }) - -// it('should set the correct RigidBody type on the API data when component.type changes', () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// const one = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() -// assert.equal(one, RigidBodyType.Dynamic) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// const two = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() -// assert.equal(two, RigidBodyType.Fixed) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) -// const three = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() -// assert.equal(three, RigidBodyType.KinematicPositionBased) -// }) - -// it('should set and remove a RigidBodyDynamicTagComponent on the entity when the component.type changes to dynamic', () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const tag = RigidBodyDynamicTagComponent -// removeComponent(testEntity, RigidBodyComponent) -// assert.equal(hasComponent(testEntity, tag), false) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// assert.equal(hasComponent(testEntity, tag), true) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// assert.equal(hasComponent(testEntity, tag), false) -// }) - -// it('should set and remove a RigidBodyFixedTagComponent on the entity when the component.type changes to fixed', () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const tag = RigidBodyFixedTagComponent -// removeComponent(testEntity, RigidBodyComponent) -// assert.equal(hasComponent(testEntity, tag), false) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// assert.equal(hasComponent(testEntity, tag), true) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// assert.equal(hasComponent(testEntity, tag), false) -// }) - -// it('should set and remove a RigidBodyKinematicTagComponent on the entity when the component.type changes to kinematic', () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const tag = RigidBodyKinematicTagComponent -// removeComponent(testEntity, RigidBodyComponent) -// assert.equal(hasComponent(testEntity, tag), false) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) -// assert.equal(hasComponent(testEntity, tag), true) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// assert.equal(hasComponent(testEntity, tag), false) -// }) - -// it('should enable CCD for the RigidBody on the API data when component.ccd changes', () => { -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const Expected = !RigidBodyComponentDefaults.ccd -// const beforeBody = physicsWorld.Rigidbodies.get(testEntity)! -// assert.ok(beforeBody) -// const beforeAPI = beforeBody.isCcdEnabled() -// assert.equal(beforeAPI, RigidBodyComponentDefaults.ccd) -// const beforeECS = getComponent(testEntity, RigidBodyComponent).ccd -// assert.equal(beforeECS, RigidBodyComponentDefaults.ccd) - -// setComponent(testEntity, RigidBodyComponent, { ccd: Expected }) -// const afterBody = physicsWorld.Rigidbodies.get(testEntity)! -// assert.ok(afterBody) -// const afterAPI = afterBody.isCcdEnabled() -// assert.equal(afterAPI, Expected) -// const afterECS = getComponent(testEntity, RigidBodyComponent).ccd -// assert.equal(afterECS, Expected) -// }) - -// it('should lock/unlock rotations for the RigidBody on the API data when component.allowRolling changes', () => { -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) - -// assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) -// const TorqueImpulse = new Vector3(10, 20, 30) -// const body = physicsWorld.Rigidbodies.get(testEntity)! - -// // Defaults -// const one = getComponent(testEntity, RigidBodyComponent).angularVelocity -// const before = { x: one.x, y: one.y, z: one.z } -// assertVecApproxEq(before, Vector3_Zero, 3) -// const Expected = !RigidBodyComponentDefaults.allowRolling -// assert.notEqual(getComponent(testEntity, RigidBodyComponent).allowRolling, Expected) // Should still be the default - -// // Locked -// setComponent(testEntity, RigidBodyComponent, { allowRolling: Expected }) -// assert.equal(getComponent(testEntity, RigidBodyComponent).allowRolling, Expected) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const two = getComponent(testEntity, RigidBodyComponent).angularVelocity -// const after = { x: two.x, y: two.y, z: two.z } -// assertVecApproxEq(before, after, 3) - -// // Unlocked -// setComponent(testEntity, RigidBodyComponent, { allowRolling: !Expected }) -// assert.equal(getComponent(testEntity, RigidBodyComponent).allowRolling, !Expected) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const three = getComponent(testEntity, RigidBodyComponent).angularVelocity -// const unlocked = { x: three.x, y: three.y, z: three.z } -// assertVecAllApproxNotEq(before, unlocked, 3) -// }) - -// it('should enable/disable rotations for each axis for the RigidBody on the API data when component.enabledRotations changes', () => { -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) - -// const reactor = RigidBodyComponent.reactorMap.get(testEntity)! -// assert.ok(reactor.isRunning) -// const TorqueImpulse = new Vector3(10, 20, 30) -// const body = physicsWorld.Rigidbodies.get(testEntity)! - -// // Defaults -// const one = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxEq(one.x, Vector3_Zero.x) -// assertFloatApproxEq(one.y, Vector3_Zero.y) -// assertFloatApproxEq(one.z, Vector3_Zero.z) - -// // Locked -// const AllLocked = [false, false, false] as [boolean, boolean, boolean] -// assertArrayNotEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllLocked) // Should still be the default -// setComponent(testEntity, RigidBodyComponent, { enabledRotations: AllLocked }) -// assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllLocked) -// reactor.run() -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const two = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxEq(one.x, two.x) -// assertFloatApproxEq(one.y, two.y) -// assertFloatApproxEq(one.z, two.z) - -// // Unlock X -// const XUnlocked = [true, false, false] as [boolean, boolean, boolean] -// setComponent(testEntity, RigidBodyComponent, { enabledRotations: XUnlocked }) -// assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, XUnlocked) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const three = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxNotEq(two.x, three.x) -// assertFloatApproxEq(two.y, three.y) -// assertFloatApproxEq(two.z, three.z) - -// // Unlock Y -// const YUnlocked = [false, true, false] as [boolean, boolean, boolean] -// setComponent(testEntity, RigidBodyComponent, { enabledRotations: YUnlocked }) -// assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, YUnlocked) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const four = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxEq(three.x, four.x) -// assertFloatApproxNotEq(three.y, four.y) -// assertFloatApproxEq(three.z, four.z) - -// // Unlock Z -// const ZUnlocked = [false, false, true] as [boolean, boolean, boolean] -// setComponent(testEntity, RigidBodyComponent, { enabledRotations: ZUnlocked }) -// assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, ZUnlocked) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const five = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxEq(four.x, five.x) -// assertFloatApproxEq(four.y, five.y) -// assertFloatApproxNotEq(four.z, five.z) - -// // Unlock All -// const AllUnlocked = [true, true, true] as [boolean, boolean, boolean] -// setComponent(testEntity, RigidBodyComponent, { enabledRotations: AllUnlocked }) -// assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllUnlocked) -// body.applyTorqueImpulse(TorqueImpulse, false) -// physicsSystemExecute() -// const six = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() -// assertFloatApproxNotEq(five.x, six.x) -// assertFloatApproxNotEq(five.y, six.y) -// assertFloatApproxNotEq(five.z, six.z) -// }) -// }) // << reactor - -// describe('getTagComponentForRigidBody', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should return the expected tag components', () => { -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// assert.equal( -// getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), -// RigidBodyDynamicTagComponent -// ) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) -// assert.equal( -// getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), -// RigidBodyFixedTagComponent -// ) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) -// assert.equal( -// getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), -// RigidBodyKinematicTagComponent -// ) -// }) -// }) // getTagComponentForRigidBody -// }) +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { act, render } from '@testing-library/react' +import assert from 'assert' + +import { RigidBodyType } from '@dimforge/rapier3d-compat' +import { + SystemDefinitions, + UUIDComponent, + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + hasComponent, + removeComponent, + removeEntity, + serializeComponent, + setComponent +} from '@etherealengine/ecs' +import React from 'react' +import { Vector3 } from 'three' +import { PhysicsSystem, TransformComponent } from '../../SpatialModule' +import { Vector3_Zero } from '../../common/constants/MathConstants' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { Physics, PhysicsWorld } from '../classes/Physics' +import { + assertFloatApproxEq, + assertFloatApproxNotEq, + assertVecAllApproxNotEq, + assertVecApproxEq +} from '../classes/Physics.test' +import { BodyTypes } from '../types/PhysicsTypes' +import { ColliderComponent } from './ColliderComponent' +import { + RigidBodyComponent, + RigidBodyDynamicTagComponent, + RigidBodyFixedTagComponent, + RigidBodyKinematicTagComponent, + getTagComponentForRigidBody +} from './RigidBodyComponent' + +const RigidBodyComponentDefaults = { + type: BodyTypes.Fixed, + ccd: false, + allowRolling: true, + enabledRotations: [true, true, true] as [boolean, boolean, boolean], + canSleep: true, + gravityScale: 1, + previousPosition: 3, + previousRotation: 4, + position: 3, + rotation: 4, + targetKinematicPosition: 3, + targetKinematicRotation: 4, + linearVelocity: 3, + angularVelocity: 3, + targetKinematicLerpMultiplier: 0 +} + +export function assertArrayEqual(A: Array, B: Array, err = 'Arrays are not equal') { + assert.equal(A.length, B.length, err + ': Their lenght is not the same') + for (let id = 0; id < A.length && id < B.length; id++) { + assert.deepEqual(A[id], B[id], err + `: Their item[${id}] is not the same`) + } +} + +export function assertArrayNotEqual(A: Array, B: Array, err = 'Arrays are equal') { + for (let id = 0; id < A.length && id < B.length; id++) { + assert.notDeepEqual(A[id], B[id], err) + } +} + +export function assertRigidBodyComponentEqual(data, expected = RigidBodyComponentDefaults) { + assert.equal(data.type, expected.type) + assert.equal(data.ccd, expected.ccd) + assert.equal(data.allowRolling, expected.allowRolling) + assert.equal(data.enabledRotations.length, expected.enabledRotations.length) + assert.equal(data.enabledRotations[0], expected.enabledRotations[0]) + assert.equal(data.enabledRotations[1], expected.enabledRotations[1]) + assert.equal(data.enabledRotations[2], expected.enabledRotations[2]) + assert.equal(data.canSleep, expected.canSleep) + assert.equal(data.gravityScale, expected.gravityScale) + /** + // @todo Not serialized by the component + assertVecApproxEq(data.previousPosition, expected.previousPosition, 3) + assertVecApproxEq(data.previousRotation, expected.previousRotation, 4) + assertVecApproxEq(data.position, expected.position, 3) + assertVecApproxEq(data.rotation, expected.rotation, 4) + assertVecApproxEq(data.targetKinematicPosition, expected.targetKinematicPosition, 3) + assertVecApproxEq(data.targetKinematicRotation, expected.targetKinematicRotation, 4) + assertVecApproxEq(data.linearVelocity, expected.linearVelocity, 3) + assertVecApproxEq(data.angularVelocity, expected.angularVelocity, 3) + assert.equal(data.targetKinematicLerpMultiplier, expected.targetKinematicLerpMultiplier) + */ +} + +describe('RigidBodyComponent', () => { + describe('IDs', () => { + it('should initialize the RigidBodyComponent.name field with the expected value', () => { + assert.equal(RigidBodyComponent.name, 'RigidBodyComponent') + }) + it('should initialize the RigidBodyComponent.jsonID field with the expected value', () => { + assert.equal(RigidBodyComponent.jsonID, 'EE_rigidbody') + }) + }) + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + setComponent(testEntity, RigidBodyComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, RigidBodyComponent) + assertRigidBodyComponentEqual(data, RigidBodyComponentDefaults) + }) + }) // << onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + setComponent(testEntity, RigidBodyComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized RigidBodyComponent', () => { + const Expected = { + type: BodyTypes.Dynamic, + ccd: true, + allowRolling: false, + canSleep: false, + gravityScale: 2, + enabledRotations: [false, false, false] as [boolean, boolean, boolean] + } + const before = getComponent(testEntity, RigidBodyComponent) + assertRigidBodyComponentEqual(before, RigidBodyComponentDefaults) + + setComponent(testEntity, RigidBodyComponent, Expected) + const after = getComponent(testEntity, RigidBodyComponent) + assert.equal(after.type, Expected.type) + assert.equal(after.ccd, Expected.ccd) + assert.equal(after.allowRolling, Expected.allowRolling) + assert.equal(after.canSleep, Expected.canSleep) + assert.equal(after.gravityScale, Expected.gravityScale) + assert.equal(after.enabledRotations.length, Expected.enabledRotations.length) + assert.equal(after.enabledRotations[0], Expected.enabledRotations[0]) + assert.equal(after.enabledRotations[1], Expected.enabledRotations[1]) + assert.equal(after.enabledRotations[2], Expected.enabledRotations[2]) + }) + + it('should not change values of an initialized RigidBodyComponent when the data passed had incorrect types', () => { + const Incorrect = { + type: 1, + ccd: 'ccd', + allowRolling: 2, + canSleep: 3, + gravityScale: false, + enabledRotations: [4, 5, 6] + } + const before = getComponent(testEntity, RigidBodyComponent) + assertRigidBodyComponentEqual(before, RigidBodyComponentDefaults) + + // @ts-ignore Pass an incorrect type to setComponent + setComponent(testEntity, RigidBodyComponent, Incorrect) + const after = getComponent(testEntity, RigidBodyComponent) + assertRigidBodyComponentEqual(after, RigidBodyComponentDefaults) + }) + }) // << onSet + + describe('toJSON', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + setComponent(testEntity, RigidBodyComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should serialize the component's data correctly", () => { + const Expected = { + type: 'fixed', + ccd: false, + allowRolling: true, + enabledRotations: [true, true, true], + canSleep: true, + gravityScale: 1 + } + const json = serializeComponent(testEntity, RigidBodyComponent) + assert.deepEqual(json, Expected) + }) + }) // << toJSON + + describe('reactor', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let newPhysicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + Physics.destroyWorld(physicsWorld.id) + // if (newPhysicsWorld) Physics.destroyWorld(newPhysicsWorld.id) + removeEntity(testEntity) + return destroyEngine() + }) + + const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute + + it('should create a RigidBody for the entity in the new physicsWorld when the world is changed', async () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const before = physicsWorld.Rigidbodies.get(testEntity)!.handle + assert.ok(physicsWorld!.bodies.contains(before)) + + const newPhysicsEntity = createEntity() + setComponent(newPhysicsEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(newPhysicsEntity, SceneComponent) + setComponent(newPhysicsEntity, TransformComponent) + setComponent(newPhysicsEntity, EntityTreeComponent) + newPhysicsWorld = Physics.createWorld(getComponent(newPhysicsEntity, UUIDComponent)) + newPhysicsWorld!.timestep = 1 / 60 + + // Change the world + setComponent(testEntity, EntityTreeComponent, { parentEntity: newPhysicsEntity }) + + // Force react lifecycle to update Physics.useWorld + const { rerender, unmount } = render(<>) + await act(() => rerender(<>)) + + // Check the changes + RigidBodyComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately + const after = newPhysicsWorld.Rigidbodies.get(testEntity)!.handle + assert.ok(newPhysicsWorld!.bodies.contains(after)) + }) + + it('should set the correct RigidBody type on the API data when component.type changes', () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + const one = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() + assert.equal(one, RigidBodyType.Dynamic) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + const two = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() + assert.equal(two, RigidBodyType.Fixed) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) + const three = physicsWorld.Rigidbodies.get(testEntity)!.bodyType() + assert.equal(three, RigidBodyType.KinematicPositionBased) + }) + + it('should set and remove a RigidBodyDynamicTagComponent on the entity when the component.type changes to dynamic', () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const tag = RigidBodyDynamicTagComponent + removeComponent(testEntity, RigidBodyComponent) + assert.equal(hasComponent(testEntity, tag), false) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + assert.equal(hasComponent(testEntity, tag), true) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + assert.equal(hasComponent(testEntity, tag), false) + }) + + it('should set and remove a RigidBodyFixedTagComponent on the entity when the component.type changes to fixed', () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const tag = RigidBodyFixedTagComponent + removeComponent(testEntity, RigidBodyComponent) + assert.equal(hasComponent(testEntity, tag), false) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + assert.equal(hasComponent(testEntity, tag), true) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + assert.equal(hasComponent(testEntity, tag), false) + }) + + it('should set and remove a RigidBodyKinematicTagComponent on the entity when the component.type changes to kinematic', () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const tag = RigidBodyKinematicTagComponent + removeComponent(testEntity, RigidBodyComponent) + assert.equal(hasComponent(testEntity, tag), false) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) + assert.equal(hasComponent(testEntity, tag), true) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + assert.equal(hasComponent(testEntity, tag), false) + }) + + it('should enable CCD for the RigidBody on the API data when component.ccd changes', () => { + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const Expected = !RigidBodyComponentDefaults.ccd + const beforeBody = physicsWorld.Rigidbodies.get(testEntity)! + assert.ok(beforeBody) + const beforeAPI = beforeBody.isCcdEnabled() + assert.equal(beforeAPI, RigidBodyComponentDefaults.ccd) + const beforeECS = getComponent(testEntity, RigidBodyComponent).ccd + assert.equal(beforeECS, RigidBodyComponentDefaults.ccd) + + setComponent(testEntity, RigidBodyComponent, { ccd: Expected }) + const afterBody = physicsWorld.Rigidbodies.get(testEntity)! + assert.ok(afterBody) + const afterAPI = afterBody.isCcdEnabled() + assert.equal(afterAPI, Expected) + const afterECS = getComponent(testEntity, RigidBodyComponent).ccd + assert.equal(afterECS, Expected) + }) + + it('should lock/unlock rotations for the RigidBody on the API data when component.allowRolling changes', () => { + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + + assert.ok(RigidBodyComponent.reactorMap.get(testEntity)!.isRunning) + const TorqueImpulse = new Vector3(10, 20, 30) + const body = physicsWorld.Rigidbodies.get(testEntity)! + + // Defaults + const one = getComponent(testEntity, RigidBodyComponent).angularVelocity + const before = { x: one.x, y: one.y, z: one.z } + assertVecApproxEq(before, Vector3_Zero, 3) + const Expected = !RigidBodyComponentDefaults.allowRolling + assert.notEqual(getComponent(testEntity, RigidBodyComponent).allowRolling, Expected) // Should still be the default + + // Locked + setComponent(testEntity, RigidBodyComponent, { allowRolling: Expected }) + assert.equal(getComponent(testEntity, RigidBodyComponent).allowRolling, Expected) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const two = getComponent(testEntity, RigidBodyComponent).angularVelocity + const after = { x: two.x, y: two.y, z: two.z } + assertVecApproxEq(before, after, 3) + + // Unlocked + setComponent(testEntity, RigidBodyComponent, { allowRolling: !Expected }) + assert.equal(getComponent(testEntity, RigidBodyComponent).allowRolling, !Expected) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const three = getComponent(testEntity, RigidBodyComponent).angularVelocity + const unlocked = { x: three.x, y: three.y, z: three.z } + assertVecAllApproxNotEq(before, unlocked, 3) + }) + + it('should enable/disable rotations for each axis for the RigidBody on the API data when component.enabledRotations changes', () => { + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + + const reactor = RigidBodyComponent.reactorMap.get(testEntity)! + assert.ok(reactor.isRunning) + const TorqueImpulse = new Vector3(10, 20, 30) + const body = physicsWorld.Rigidbodies.get(testEntity)! + + // Defaults + const one = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxEq(one.x, Vector3_Zero.x) + assertFloatApproxEq(one.y, Vector3_Zero.y) + assertFloatApproxEq(one.z, Vector3_Zero.z) + + // Locked + const AllLocked = [false, false, false] as [boolean, boolean, boolean] + assertArrayNotEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllLocked) // Should still be the default + setComponent(testEntity, RigidBodyComponent, { enabledRotations: AllLocked }) + assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllLocked) + reactor.run() + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const two = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxEq(one.x, two.x) + assertFloatApproxEq(one.y, two.y) + assertFloatApproxEq(one.z, two.z) + + // Unlock X + const XUnlocked = [true, false, false] as [boolean, boolean, boolean] + setComponent(testEntity, RigidBodyComponent, { enabledRotations: XUnlocked }) + assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, XUnlocked) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const three = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxNotEq(two.x, three.x) + assertFloatApproxEq(two.y, three.y) + assertFloatApproxEq(two.z, three.z) + + // Unlock Y + const YUnlocked = [false, true, false] as [boolean, boolean, boolean] + setComponent(testEntity, RigidBodyComponent, { enabledRotations: YUnlocked }) + assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, YUnlocked) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const four = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxEq(three.x, four.x) + assertFloatApproxNotEq(three.y, four.y) + assertFloatApproxEq(three.z, four.z) + + // Unlock Z + const ZUnlocked = [false, false, true] as [boolean, boolean, boolean] + setComponent(testEntity, RigidBodyComponent, { enabledRotations: ZUnlocked }) + assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, ZUnlocked) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const five = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxEq(four.x, five.x) + assertFloatApproxEq(four.y, five.y) + assertFloatApproxNotEq(four.z, five.z) + + // Unlock All + const AllUnlocked = [true, true, true] as [boolean, boolean, boolean] + setComponent(testEntity, RigidBodyComponent, { enabledRotations: AllUnlocked }) + assertArrayEqual(getComponent(testEntity, RigidBodyComponent).enabledRotations, AllUnlocked) + body.applyTorqueImpulse(TorqueImpulse, false) + physicsSystemExecute() + const six = getComponent(testEntity, RigidBodyComponent).angularVelocity.clone() + assertFloatApproxNotEq(five.x, six.x) + assertFloatApproxNotEq(five.y, six.y) + assertFloatApproxNotEq(five.z, six.z) + }) + }) // << reactor + + describe('getTagComponentForRigidBody', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should return the expected tag components', () => { + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + assert.equal( + getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), + RigidBodyDynamicTagComponent + ) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Fixed }) + assert.equal( + getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), + RigidBodyFixedTagComponent + ) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Kinematic }) + assert.equal( + getTagComponentForRigidBody(getComponent(testEntity, RigidBodyComponent).type), + RigidBodyKinematicTagComponent + ) + }) + }) // getTagComponentForRigidBody +}) diff --git a/packages/spatial/src/physics/components/TriggerComponent.test.ts b/packages/spatial/src/physics/components/TriggerComponent.test.ts index 8cd88afc08..cb45e3ebb7 100644 --- a/packages/spatial/src/physics/components/TriggerComponent.test.ts +++ b/packages/spatial/src/physics/components/TriggerComponent.test.ts @@ -1,226 +1,226 @@ -// /* -// CPAL-1.0 License - -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. - -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. - -// The Original Code is Ethereal Engine. - -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. - -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ - -// import { -// EntityUUID, -// UUIDComponent, -// UndefinedEntity, -// createEngine, -// createEntity, -// destroyEngine, -// getComponent, -// removeComponent, -// removeEntity, -// serializeComponent, -// setComponent -// } from '@etherealengine/ecs' -// import assert from 'assert' -// import { Vector3 } from 'three' -// import { TransformComponent } from '../../SpatialModule' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent } from '../../transform/components/EntityTree' -// import { Physics, PhysicsWorld } from '../classes/Physics' -// import { CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' -// import { Shapes } from '../types/PhysicsTypes' -// import { ColliderComponent } from './ColliderComponent' -// import { ColliderComponentDefaults, assertColliderComponentEquals } from './ColliderComponent.test' -// import { RigidBodyComponent } from './RigidBodyComponent' -// import { TriggerComponent } from './TriggerComponent' - -// const TriggerComponentDefaults = { -// triggers: [] as Array<{ -// onEnter: null | string -// onExit: null | string -// target: null | EntityUUID -// }> -// } - -// function assertArrayEqual(A: Array, B: Array, err = 'Arrays are not equal') { -// assert.equal(A.length, B.length, err) -// for (let id = 0; id < A.length && id < B.length; id++) { -// assert.deepEqual(A[id], B[id], err) -// } -// } - -// function assertArrayNotEqual(A: Array, B: Array, err = 'Arrays are equal') { -// for (let id = 0; id < A.length && id < B.length; id++) { -// assert.notDeepEqual(A[id], B[id], err) -// } -// } - -// function assertTriggerComponentEqual(data, expected) { -// assertArrayEqual(data.triggers, expected.triggers) -// } - -// function assertTriggerComponentNotEqual(data, expected) { -// assertArrayNotEqual(data.triggers, expected.triggers) -// } - -// describe('TriggerComponent', () => { -// describe('IDs', () => { -// it('should initialize the TriggerComponent.name field with the expected value', () => { -// assert.equal(TriggerComponent.name, 'TriggerComponent') -// }) -// it('should initialize the TriggerComponent.jsonID field with the expected value', () => { -// assert.equal(TriggerComponent.jsonID, 'EE_trigger') -// }) -// }) - -// describe('onInit', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// testEntity = createEntity() -// setComponent(testEntity, TriggerComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should initialize the component with the expected default values', () => { -// const data = getComponent(testEntity, TriggerComponent) -// assertTriggerComponentEqual(data, TriggerComponentDefaults) -// }) -// }) // << onInit - -// describe('onSet', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// testEntity = createEntity() -// setComponent(testEntity, TriggerComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it('should change the values of an initialized TriggerComponent', () => { -// const Expected = { -// triggers: [ -// { -// onEnter: 'onEnter.Expected', -// onExit: 'onExit.Expected', -// target: 'target' as EntityUUID -// } -// ] -// } -// const before = getComponent(testEntity, TriggerComponent) -// assertTriggerComponentEqual(before, TriggerComponentDefaults) -// setComponent(testEntity, TriggerComponent, Expected) - -// const data = getComponent(testEntity, TriggerComponent) -// assertTriggerComponentEqual(data, Expected) -// }) - -// it('should not change values of an initialized TriggerComponent when the data passed had incorrect types', () => { -// const Incorrect = { triggers: 'triggers' } -// const before = getComponent(testEntity, TriggerComponent) -// assertTriggerComponentEqual(before, TriggerComponentDefaults) - -// // @ts-ignore -// setComponent(testEntity, TriggerComponent, Incorrect) -// const data = getComponent(testEntity, TriggerComponent) -// assertTriggerComponentEqual(data, TriggerComponentDefaults) -// }) -// }) // << onSet - -// describe('toJSON', () => { -// let testEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// testEntity = createEntity() -// setComponent(testEntity, TriggerComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should serialize the component's data correctly", () => { -// const json = serializeComponent(testEntity, TriggerComponent) -// assert.deepEqual(json, TriggerComponentDefaults) -// }) -// }) // << toJson - -// describe('reactor', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// physicsWorld!.timestep = 1 / 60 - -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// setComponent(testEntity, ColliderComponent) -// setComponent(testEntity, TriggerComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// it("should call Physics.setTrigger on the entity's collider when a new ColliderComponent is set", () => { -// assertColliderComponentEquals(getComponent(testEntity, ColliderComponent), ColliderComponentDefaults) -// removeComponent(testEntity, ColliderComponent) -// const ColliderComponentData = { -// shape: Shapes.Sphere, -// mass: 3, -// massCenter: new Vector3(1, 2, 3), -// friction: 1.0, -// restitution: 0.1, -// collisionLayer: CollisionGroups.Default, -// collisionMask: DefaultCollisionMask -// } -// setComponent(testEntity, ColliderComponent, ColliderComponentData) -// assertColliderComponentEquals(getComponent(testEntity, ColliderComponent), ColliderComponentData) -// const reactor = ColliderComponent.reactorMap.get(testEntity)! -// assert.ok(reactor.isRunning) -// const collider = physicsWorld.Colliders.get(testEntity)! -// assert.ok(collider) -// assert.ok(collider.isSensor()) -// }) -// }) // << reactor -// }) +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { + EntityUUID, + UUIDComponent, + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + removeComponent, + removeEntity, + serializeComponent, + setComponent +} from '@etherealengine/ecs' +import assert from 'assert' +import { Vector3 } from 'three' +import { TransformComponent } from '../../SpatialModule' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { Physics, PhysicsWorld } from '../classes/Physics' +import { CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' +import { Shapes } from '../types/PhysicsTypes' +import { ColliderComponent } from './ColliderComponent' +import { ColliderComponentDefaults, assertColliderComponentEquals } from './ColliderComponent.test' +import { RigidBodyComponent } from './RigidBodyComponent' +import { TriggerComponent } from './TriggerComponent' + +const TriggerComponentDefaults = { + triggers: [] as Array<{ + onEnter: null | string + onExit: null | string + target: null | EntityUUID + }> +} + +function assertArrayEqual(A: Array, B: Array, err = 'Arrays are not equal') { + assert.equal(A.length, B.length, err) + for (let id = 0; id < A.length && id < B.length; id++) { + assert.deepEqual(A[id], B[id], err) + } +} + +function assertArrayNotEqual(A: Array, B: Array, err = 'Arrays are equal') { + for (let id = 0; id < A.length && id < B.length; id++) { + assert.notDeepEqual(A[id], B[id], err) + } +} + +function assertTriggerComponentEqual(data, expected) { + assertArrayEqual(data.triggers, expected.triggers) +} + +function assertTriggerComponentNotEqual(data, expected) { + assertArrayNotEqual(data.triggers, expected.triggers) +} + +describe('TriggerComponent', () => { + describe('IDs', () => { + it('should initialize the TriggerComponent.name field with the expected value', () => { + assert.equal(TriggerComponent.name, 'TriggerComponent') + }) + it('should initialize the TriggerComponent.jsonID field with the expected value', () => { + assert.equal(TriggerComponent.jsonID, 'EE_trigger') + }) + }) + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, TriggerComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, TriggerComponent) + assertTriggerComponentEqual(data, TriggerComponentDefaults) + }) + }) // << onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, TriggerComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized TriggerComponent', () => { + const Expected = { + triggers: [ + { + onEnter: 'onEnter.Expected', + onExit: 'onExit.Expected', + target: 'target' as EntityUUID + } + ] + } + const before = getComponent(testEntity, TriggerComponent) + assertTriggerComponentEqual(before, TriggerComponentDefaults) + setComponent(testEntity, TriggerComponent, Expected) + + const data = getComponent(testEntity, TriggerComponent) + assertTriggerComponentEqual(data, Expected) + }) + + it('should not change values of an initialized TriggerComponent when the data passed had incorrect types', () => { + const Incorrect = { triggers: 'triggers' } + const before = getComponent(testEntity, TriggerComponent) + assertTriggerComponentEqual(before, TriggerComponentDefaults) + + // @ts-ignore + setComponent(testEntity, TriggerComponent, Incorrect) + const data = getComponent(testEntity, TriggerComponent) + assertTriggerComponentEqual(data, TriggerComponentDefaults) + }) + }) // << onSet + + describe('toJSON', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + testEntity = createEntity() + setComponent(testEntity, TriggerComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should serialize the component's data correctly", () => { + const json = serializeComponent(testEntity, TriggerComponent) + assert.deepEqual(json, TriggerComponentDefaults) + }) + }) // << toJson + + describe('reactor', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + physicsWorld!.timestep = 1 / 60 + + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + setComponent(testEntity, ColliderComponent) + setComponent(testEntity, TriggerComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should call Physics.setTrigger on the entity's collider when a new ColliderComponent is set", () => { + assertColliderComponentEquals(getComponent(testEntity, ColliderComponent), ColliderComponentDefaults) + removeComponent(testEntity, ColliderComponent) + const ColliderComponentData = { + shape: Shapes.Sphere, + mass: 3, + massCenter: new Vector3(1, 2, 3), + friction: 1.0, + restitution: 0.1, + collisionLayer: CollisionGroups.Default, + collisionMask: DefaultCollisionMask + } + setComponent(testEntity, ColliderComponent, ColliderComponentData) + assertColliderComponentEquals(getComponent(testEntity, ColliderComponent), ColliderComponentData) + const reactor = ColliderComponent.reactorMap.get(testEntity)! + assert.ok(reactor.isRunning) + const collider = physicsWorld.Colliders.get(testEntity)! + assert.ok(collider) + assert.ok(collider.isSensor()) + }) + }) // << reactor +}) diff --git a/packages/spatial/src/physics/systems/PhysicsSystem.test.ts b/packages/spatial/src/physics/systems/PhysicsSystem.test.ts index 428438a6c6..f5c696ba24 100644 --- a/packages/spatial/src/physics/systems/PhysicsSystem.test.ts +++ b/packages/spatial/src/physics/systems/PhysicsSystem.test.ts @@ -1,440 +1,440 @@ -// /* -// CPAL-1.0 License - -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. - -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. - -// The Original Code is Ethereal Engine. - -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. - -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ - -// import { destroyEngine } from '@etherealengine/ecs/src/Engine' - -// import { -// Entity, -// SystemDefinitions, -// SystemUUID, -// UUIDComponent, -// UndefinedEntity, -// createEntity, -// getComponent, -// getMutableComponent, -// hasComponent, -// removeEntity, -// setComponent -// } from '@etherealengine/ecs' -// import { createEngine } from '@etherealengine/ecs/src/Engine' -// import assert from 'assert' -// import { Quaternion, Vector3 } from 'three' -// import { TransformComponent } from '../../SpatialModule' -// import { Vector3_Zero } from '../../common/constants/MathConstants' -// import { smootheLerpAlpha } from '../../common/functions/MathLerpFunctions' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent } from '../../transform/components/EntityTree' -// import { Physics, PhysicsWorld } from '../classes/Physics' -// import { assertVecAllApproxNotEq, assertVecAnyApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' -// import { ColliderComponent } from '../components/ColliderComponent' -// import { CollisionComponent } from '../components/CollisionComponent' -// import { RigidBodyComponent } from '../components/RigidBodyComponent' -// import { BodyTypes } from '../types/PhysicsTypes' -// import { PhysicsSystem } from './PhysicsSystem' - -// // Epsilon Constants for Interpolation -// const LerpEpsilon = 0.000001 -// /** @note three.js Quat.slerp fails tests at 6 significant figures, but passes at 5 */ -// const SLerpEpsilon = 0.00001 - -// const Quaternion_Zero = new Quaternion(0, 0, 0, 1).normalize() - -// describe('smoothKinematicBody', () => { -// /** @description Pair of `deltaTime` and `substep` values that will be used during an interpolation test */ -// type Step = { dt: number; substep: number } -// /** @description Creates a Step object. @note Just a clarity/readability alias */ -// function createStep(dt: number, substep: number): Step { -// return { dt, substep } -// } - -// const DeltaTime = 1 / 60 -// const Start = { -// position: new Vector3(1, 2, 3), -// rotation: new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() -// } -// const Final = { -// position: new Vector3(4, 5, 6), -// rotation: new Quaternion(0.0, 0.2, 0.8, 0.0).normalize() -// } - -// /** @description List of steps that will be tested against for both the linear and smoooth interpolation tests */ -// const Step = { -// Tenth: createStep(DeltaTime, 0.1), -// Quarter: createStep(DeltaTime, 0.25), -// Half: createStep(DeltaTime, 0.5), -// One: createStep(DeltaTime, 1), -// Two: createStep(DeltaTime, 2) -// } - -// /** @description {@link Step} list, in array form */ -// const Steps = [Step.Tenth, Step.Quarter, Step.Half, Step.One, Step.Two] - -// /** @description List of non-zero values that {@link RigidbodyComponent.targetKinematicLerpMultiplier} will be set to during the gradual smoothing tests */ -// const KinematicMultiplierCases = [0.5, 0.25, 0.1, 0.01, 0.001, 0.0001, 2, 3, 4, 5] - -// /** -// * @section Initialize/Terminate the engine, entities and physics -// */ -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) - -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// // Set the Start..Final values for interpolation -// const body = getComponent(testEntity, RigidBodyComponent) -// body.previousPosition.set(Start.position.x, Start.position.y, Start.position.z) -// body.previousRotation.set(Start.rotation.x, Start.rotation.y, Start.rotation.z, Start.rotation.w) -// body.targetKinematicPosition.set(Final.position.x, Final.position.y, Final.position.z) -// body.targetKinematicRotation.set(Final.rotation.x, Final.rotation.y, Final.rotation.z, Final.rotation.w) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// describe('when RigidbodyComponent.targetKinematicLerpMultiplier is set to 0 ...', () => { -// /** @description Calculates the Deterministic Lerp value for the `@param entity`, as expected by the tests, based on the given {@link Step.substep} value */ -// function computeLerp(entity: Entity, step: Step) { -// const body = getComponent(entity, RigidBodyComponent) -// const result = { -// position: body.previousPosition.clone().lerp(body.targetKinematicPosition.clone(), step.substep).clone(), -// rotation: body.previousRotation.clone().slerp(body.targetKinematicRotation.clone(), step.substep).clone() -// } -// return result -// } -// /** @description Set the {@link RigidBodyComponent.targetKinematicLerpMultiplier} to 0 for all of the linear interpolation tests */ -// beforeEach(() => { -// getMutableComponent(testEntity, RigidBodyComponent).targetKinematicLerpMultiplier.set(0) -// }) - -// it('... should apply deterministic linear interpolation to the position of the KinematicBody of the given entity', () => { -// // Check data before -// const body = getComponent(testEntity, RigidBodyComponent) -// const before = body.position.clone() -// assertVecApproxEq(before, Vector3_Zero, 3, LerpEpsilon) - -// // Run and Check resulting data -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Quarter.dt, Step.Quarter.substep) -// const after = body.position.clone() -// assertVecAllApproxNotEq(before, after, 3, LerpEpsilon) -// assertVecApproxEq(after, computeLerp(testEntity, Step.Quarter).position, 3, LerpEpsilon) -// // Check the other Step cases -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Tenth.dt, Step.Tenth.substep) -// assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Tenth).position, 3, LerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Half.dt, Step.Half.substep) -// assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Half).position, 3, LerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.One.dt, Step.One.substep) -// assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.One).position, 3, LerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Two.dt, Step.Two.substep) -// assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Two).position, 3, LerpEpsilon) -// // Check substep precision Step cases -// const TestCount = 1_000_000 -// for (let divider = 1; divider <= TestCount; divider += 1_000) { -// const step = createStep(DeltaTime, 1 / divider) -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) -// assertVecApproxEq(body.position.clone(), computeLerp(testEntity, step).position, 3, LerpEpsilon) -// } -// }) - -// it('... should apply deterministic spherical linear interpolation to the rotation of the KinematicBody of the given entity', () => { -// // Check data before -// const body = getComponent(testEntity, RigidBodyComponent) -// const before = body.rotation.clone() -// assertVecApproxEq(before, new Quaternion(0, 0, 0, 1), 3, SLerpEpsilon) - -// // Run and Check resulting data -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Quarter.dt, Step.Quarter.substep) -// const after = body.rotation.clone() -// assertVecAllApproxNotEq(before, after, 4, SLerpEpsilon) -// assertVecApproxEq(after, computeLerp(testEntity, Step.Quarter).rotation, 4, SLerpEpsilon) -// // Check the other Step cases -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Tenth.dt, Step.Tenth.substep) -// assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Tenth).rotation, 4, SLerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Half.dt, Step.Half.substep) -// assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Half).rotation, 4, SLerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.One.dt, Step.One.substep) -// assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.One).rotation, 4, SLerpEpsilon) -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Two.dt, Step.Two.substep) -// assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Two).rotation, 4, SLerpEpsilon) -// // Check substep precision Step cases -// const TestCount = 1_000_000 -// for (let divider = 1; divider <= TestCount; divider += 1_000) { -// const step = createStep(DeltaTime, 1 / divider) -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) -// assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, step).rotation, 4, SLerpEpsilon) -// } -// }) -// }) - -// describe('when RigidbodyComponent.targetKinematicLerpMultiplier is set to a value other than 0 ...', () => { -// type LerpData = { -// position: { start: Vector3; final: Vector3 } -// rotation: { start: Quaternion; final: Quaternion } -// } - -// /** -// * @description Sets the entity's {@link RigidBodyComponent.targetKinematicLerpMultiplier} property to `@param mult` -// * @returns The `@param mult` itself */ -// function setMultiplier(entity: Entity, mult: number): number { -// getMutableComponent(entity, RigidBodyComponent).targetKinematicLerpMultiplier.set(mult) -// return mult -// } -// /** -// * @description Sets the entity's {@link RigidBodyComponent.targetKinematicLerpMultiplier} property to `@param mult` and calculates its smooth lerp alpha -// * @returns The exponentially smootheed Lerp Alpha value to use as `dt` in {@link smoothKinematicBody} */ -// function getAlphaWithMultiplier(entity: Entity, dt: number, mult: number): number { -// return smootheLerpAlpha(setMultiplier(entity, mult), dt) -// } - -// /** @description Computes the lerp of the (`@param start`,`@param final`) input Vectors without mutating their values */ -// function lerpNoRef(start: Vector3, final: Vector3, dt: number) { -// return start.clone().lerp(final.clone(), dt).clone() -// } -// /** @description Computes the fastSlerp of the (`@param start`,`@param final`) input Quaternions without mutating their values */ -// function fastSlerpNoRef(start: Quaternion, final: Quaternion, dt: number) { -// return start.clone().fastSlerp(final.clone(), dt).clone() -// } - -// /** @description Calculates the Exponential Lerp value for the `@param data`, as expected by the tests, based on the given `@param dt` alpha value */ -// function computeELerp(data: LerpData, alpha: number) { -// return { -// position: lerpNoRef(data.position.start, data.position.final, alpha), -// rotation: fastSlerpNoRef(data.rotation.start, data.rotation.final, alpha) -// } -// } - -// it('... should apply gradual smoothing (aka exponential interpolation) to the position of the KinematicBody of the given entity', () => { -// // Check data before -// const body = getComponent(testEntity, RigidBodyComponent) -// const before = body.position.clone() -// assertVecApproxEq(before, Vector3_Zero, 3, LerpEpsilon) - -// // Run and Check resulting data -// // ... Infinite smoothing case -// const MultInfinite = 1 // Multiplier 1 shouldn't change the position (aka. infinite smoothing) -// setMultiplier(testEntity, MultInfinite) -// Physics.smoothKinematicBody(physicsWorld, testEntity, DeltaTime, /*substep*/ 1) -// assertVecApproxEq(before, body.position, 3, LerpEpsilon) - -// // ... Hardcoded case -// setMultiplier(testEntity, 0.12345) -// Physics.smoothKinematicBody(physicsWorld, testEntity, 1 / 60, 1) -// const ExpectedHardcoded = { x: 0.1370581001805662, y: 0.17132262522570774, z: 0.20558715027084928 } -// assertVecApproxEq(body.position.clone(), ExpectedHardcoded, 3) - -// // ... Check the other Step cases -// for (const multiplier of KinematicMultiplierCases) { -// for (const step of Steps) { -// getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case -// const alpha = getAlphaWithMultiplier(testEntity, step.dt, multiplier) -// const before = { -// position: { start: body.position.clone(), final: body.targetKinematicPosition.clone() }, -// rotation: { start: body.rotation.clone(), final: body.targetKinematicRotation.clone() } -// } -// Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) -// assertVecApproxEq(body.position, computeELerp(before, alpha).position, 3, LerpEpsilon) -// } -// } -// }) - -// it('... should apply gradual smoothing (aka exponential interpolation) to the rotation of the KinematicBody of the given entity', () => { -// // Check data before -// const body = getComponent(testEntity, RigidBodyComponent) -// const before = body.rotation.clone() -// assertVecApproxEq(before, Quaternion_Zero, 4, SLerpEpsilon) - -// // Run and Check resulting data -// // ... Infinite smoothing case -// const MultInfinite = 1 // Multiplier 1 shouldn't change the rotation (aka. infinite smoothing) -// setMultiplier(testEntity, MultInfinite) -// Physics.smoothKinematicBody(physicsWorld, testEntity, DeltaTime, /*substep*/ 1) -// assertVecApproxEq(before, body.rotation, 3, SLerpEpsilon) - -// // ... Hardcoded case -// setMultiplier(testEntity, 0.12345) -// Physics.smoothKinematicBody(physicsWorld, testEntity, 1 / 60, 1) -// const ExpectedHardcoded = new Quaternion(0, 0.013047535062645674, 0.052190140250582696, 0.9985524073985961) -// assertVecApproxEq(body.rotation.clone(), ExpectedHardcoded, 4) - -// // ... Check the other Step cases -// for (const multiplier of KinematicMultiplierCases) { -// for (const step of Steps) { -// getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case -// const alpha = getAlphaWithMultiplier(testEntity, step.dt, multiplier) -// const before = { -// position: { start: body.position.clone(), final: body.targetKinematicPosition.clone() }, -// rotation: { start: body.rotation.clone(), final: body.targetKinematicRotation.clone() } -// } as LerpData -// Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) -// assertVecApproxEq(body.rotation, computeELerp(before, alpha).rotation, 3, SLerpEpsilon) -// } -// } -// }) -// }) -// }) - -// describe('PhysicsSystem', () => { -// describe('IDs', () => { -// it("should define the PhysicsSystem's UUID with the expected value", () => { -// assert.equal(PhysicsSystem, 'ee.engine.PhysicsSystem' as SystemUUID) -// }) -// }) - -// describe('execute', () => { -// let testEntity = UndefinedEntity -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity - -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// physicsWorld.timestep = 1 / 60 - -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(testEntity, ColliderComponent) -// }) - -// afterEach(() => { -// removeEntity(testEntity) -// return destroyEngine() -// }) - -// const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute - -// it('should step the physics', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const beforeBody = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(beforeBody) -// const before = beforeBody.linvel() -// assertVecApproxEq(before, Vector3_Zero, 3) -// Physics.applyImpulse(physicsWorld, testEntity, testImpulse) -// physicsSystemExecute() -// const afterBody = physicsWorld.Rigidbodies.get(testEntity) -// assert.ok(afterBody) -// const after = afterBody.linvel() -// assertVecAllApproxNotEq(after, before, 3) -// }) - -// function cloneRigidBodyPoseData(entity: Entity) { -// const body = getComponent(testEntity, RigidBodyComponent) -// return { -// previousPosition: body.previousPosition.clone(), -// previousRotation: body.previousRotation.clone(), -// position: body.position.clone(), -// rotation: body.rotation.clone(), -// targetKinematicPosition: body.targetKinematicPosition.clone(), -// targetKinematicRotation: body.targetKinematicRotation.clone(), -// linearVelocity: body.linearVelocity.clone(), -// angularVelocity: body.angularVelocity.clone() -// } -// } - -// it('should update poses on the ECS', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const before = cloneRigidBodyPoseData(testEntity) -// const body = getComponent(testEntity, RigidBodyComponent) -// assertVecApproxEq(before.previousPosition, body.previousPosition.clone(), 3) -// assertVecApproxEq(before.previousRotation, body.previousRotation.clone(), 3) -// assertVecApproxEq(before.position, body.position.clone(), 3) -// assertVecApproxEq(before.rotation, body.rotation.clone(), 4) -// assertVecApproxEq(before.targetKinematicPosition, body.targetKinematicPosition.clone(), 3) -// assertVecApproxEq(before.targetKinematicRotation, body.targetKinematicRotation.clone(), 4) -// assertVecApproxEq(before.linearVelocity, body.linearVelocity.clone(), 3) -// assertVecApproxEq(before.angularVelocity, body.angularVelocity.clone(), 3) - -// Physics.applyImpulse(physicsWorld, testEntity, testImpulse) -// physicsSystemExecute() - -// const after = cloneRigidBodyPoseData(testEntity) -// assertVecAnyApproxNotEq(after.previousPosition, before.previousPosition, 3) -// assertVecAnyApproxNotEq(after.previousRotation, before.previousRotation, 3) -// assertVecAnyApproxNotEq(after.position, before.position, 3) -// assertVecAnyApproxNotEq(after.rotation, before.rotation, 4) -// assertVecAnyApproxNotEq(after.targetKinematicPosition, before.targetKinematicPosition, 3) -// assertVecAnyApproxNotEq(after.targetKinematicRotation, before.targetKinematicRotation, 4) -// assertVecAnyApproxNotEq(after.linearVelocity, before.linearVelocity, 3) -// assertVecAnyApproxNotEq(after.angularVelocity, before.angularVelocity, 3) -// }) - -// it('should update collisions on the ECS', () => { -// const testImpulse = new Vector3(1, 2, 3) -// const entity1 = createEntity() -// setComponent(entity1, TransformComponent) -// setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity1, ColliderComponent) -// const entity2 = createEntity() -// setComponent(entity2, TransformComponent) -// setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) -// setComponent(entity2, ColliderComponent) -// // Check before -// assert.ok(!hasComponent(entity1, CollisionComponent)) -// assert.ok(!hasComponent(entity2, CollisionComponent)) - -// // Run and Check after -// Physics.applyImpulse(physicsWorld, entity1, testImpulse) -// physicsSystemExecute() -// assert.ok(hasComponent(entity1, ColliderComponent)) -// assert.ok(hasComponent(entity2, ColliderComponent)) -// }) -// }) - -// /** -// // @note The reactor is currently just binding data onMount and onUnmount -// // describe('reactor', () => {}) -// */ -// }) +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { destroyEngine } from '@etherealengine/ecs/src/Engine' + +import { + Entity, + SystemDefinitions, + SystemUUID, + UUIDComponent, + UndefinedEntity, + createEntity, + getComponent, + getMutableComponent, + hasComponent, + removeEntity, + setComponent +} from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' +import assert from 'assert' +import { Quaternion, Vector3 } from 'three' +import { TransformComponent } from '../../SpatialModule' +import { Vector3_Zero } from '../../common/constants/MathConstants' +import { smootheLerpAlpha } from '../../common/functions/MathLerpFunctions' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { Physics, PhysicsWorld } from '../classes/Physics' +import { assertVecAllApproxNotEq, assertVecAnyApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' +import { ColliderComponent } from '../components/ColliderComponent' +import { CollisionComponent } from '../components/CollisionComponent' +import { RigidBodyComponent } from '../components/RigidBodyComponent' +import { BodyTypes } from '../types/PhysicsTypes' +import { PhysicsSystem } from './PhysicsSystem' + +// Epsilon Constants for Interpolation +const LerpEpsilon = 0.000001 +/** @note three.js Quat.slerp fails tests at 6 significant figures, but passes at 5 */ +const SLerpEpsilon = 0.00001 + +const Quaternion_Zero = new Quaternion(0, 0, 0, 1).normalize() + +describe('smoothKinematicBody', () => { + /** @description Pair of `deltaTime` and `substep` values that will be used during an interpolation test */ + type Step = { dt: number; substep: number } + /** @description Creates a Step object. @note Just a clarity/readability alias */ + function createStep(dt: number, substep: number): Step { + return { dt, substep } + } + + const DeltaTime = 1 / 60 + const Start = { + position: new Vector3(1, 2, 3), + rotation: new Quaternion(0.5, 0.3, 0.2, 0.0).normalize() + } + const Final = { + position: new Vector3(4, 5, 6), + rotation: new Quaternion(0.0, 0.2, 0.8, 0.0).normalize() + } + + /** @description List of steps that will be tested against for both the linear and smoooth interpolation tests */ + const Step = { + Tenth: createStep(DeltaTime, 0.1), + Quarter: createStep(DeltaTime, 0.25), + Half: createStep(DeltaTime, 0.5), + One: createStep(DeltaTime, 1), + Two: createStep(DeltaTime, 2) + } + + /** @description {@link Step} list, in array form */ + const Steps = [Step.Tenth, Step.Quarter, Step.Half, Step.One, Step.Two] + + /** @description List of non-zero values that {@link RigidbodyComponent.targetKinematicLerpMultiplier} will be set to during the gradual smoothing tests */ + const KinematicMultiplierCases = [0.5, 0.25, 0.1, 0.01, 0.001, 0.0001, 2, 3, 4, 5] + + /** + * @section Initialize/Terminate the engine, entities and physics + */ + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + // Set the Start..Final values for interpolation + const body = getComponent(testEntity, RigidBodyComponent) + body.previousPosition.set(Start.position.x, Start.position.y, Start.position.z) + body.previousRotation.set(Start.rotation.x, Start.rotation.y, Start.rotation.z, Start.rotation.w) + body.targetKinematicPosition.set(Final.position.x, Final.position.y, Final.position.z) + body.targetKinematicRotation.set(Final.rotation.x, Final.rotation.y, Final.rotation.z, Final.rotation.w) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + describe('when RigidbodyComponent.targetKinematicLerpMultiplier is set to 0 ...', () => { + /** @description Calculates the Deterministic Lerp value for the `@param entity`, as expected by the tests, based on the given {@link Step.substep} value */ + function computeLerp(entity: Entity, step: Step) { + const body = getComponent(entity, RigidBodyComponent) + const result = { + position: body.previousPosition.clone().lerp(body.targetKinematicPosition.clone(), step.substep).clone(), + rotation: body.previousRotation.clone().slerp(body.targetKinematicRotation.clone(), step.substep).clone() + } + return result + } + /** @description Set the {@link RigidBodyComponent.targetKinematicLerpMultiplier} to 0 for all of the linear interpolation tests */ + beforeEach(() => { + getMutableComponent(testEntity, RigidBodyComponent).targetKinematicLerpMultiplier.set(0) + }) + + it('... should apply deterministic linear interpolation to the position of the KinematicBody of the given entity', () => { + // Check data before + const body = getComponent(testEntity, RigidBodyComponent) + const before = body.position.clone() + assertVecApproxEq(before, Vector3_Zero, 3, LerpEpsilon) + + // Run and Check resulting data + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Quarter.dt, Step.Quarter.substep) + const after = body.position.clone() + assertVecAllApproxNotEq(before, after, 3, LerpEpsilon) + assertVecApproxEq(after, computeLerp(testEntity, Step.Quarter).position, 3, LerpEpsilon) + // Check the other Step cases + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Tenth.dt, Step.Tenth.substep) + assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Tenth).position, 3, LerpEpsilon) + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Half.dt, Step.Half.substep) + assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Half).position, 3, LerpEpsilon) + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.One.dt, Step.One.substep) + assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.One).position, 3, LerpEpsilon) + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Two.dt, Step.Two.substep) + assertVecApproxEq(body.position.clone(), computeLerp(testEntity, Step.Two).position, 3, LerpEpsilon) + // Check substep precision Step cases + const TestCount = 1_000_000 + for (let divider = 1; divider <= TestCount; divider += 1_000) { + const step = createStep(DeltaTime, 1 / divider) + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) + assertVecApproxEq(body.position.clone(), computeLerp(testEntity, step).position, 3, LerpEpsilon) + } + }) + + it('... should apply deterministic spherical linear interpolation to the rotation of the KinematicBody of the given entity', () => { + // Check data before + const body = getComponent(testEntity, RigidBodyComponent) + const before = body.rotation.clone() + assertVecApproxEq(before, new Quaternion(0, 0, 0, 1), 3, SLerpEpsilon) + + // Run and Check resulting data + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Quarter.dt, Step.Quarter.substep) + const after = body.rotation.clone() + assertVecAllApproxNotEq(before, after, 4, SLerpEpsilon) + assertVecApproxEq(after, computeLerp(testEntity, Step.Quarter).rotation, 4, SLerpEpsilon) + // Check the other Step cases + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Tenth.dt, Step.Tenth.substep) + assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Tenth).rotation, 4, SLerpEpsilon) + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Half.dt, Step.Half.substep) + assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Half).rotation, 4, SLerpEpsilon) + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.One.dt, Step.One.substep) + assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.One).rotation, 4, SLerpEpsilon) + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, Step.Two.dt, Step.Two.substep) + assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, Step.Two).rotation, 4, SLerpEpsilon) + // Check substep precision Step cases + const TestCount = 1_000_000 + for (let divider = 1; divider <= TestCount; divider += 1_000) { + const step = createStep(DeltaTime, 1 / divider) + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) + assertVecApproxEq(body.rotation.clone(), computeLerp(testEntity, step).rotation, 4, SLerpEpsilon) + } + }) + }) + + describe('when RigidbodyComponent.targetKinematicLerpMultiplier is set to a value other than 0 ...', () => { + type LerpData = { + position: { start: Vector3; final: Vector3 } + rotation: { start: Quaternion; final: Quaternion } + } + + /** + * @description Sets the entity's {@link RigidBodyComponent.targetKinematicLerpMultiplier} property to `@param mult` + * @returns The `@param mult` itself */ + function setMultiplier(entity: Entity, mult: number): number { + getMutableComponent(entity, RigidBodyComponent).targetKinematicLerpMultiplier.set(mult) + return mult + } + /** + * @description Sets the entity's {@link RigidBodyComponent.targetKinematicLerpMultiplier} property to `@param mult` and calculates its smooth lerp alpha + * @returns The exponentially smootheed Lerp Alpha value to use as `dt` in {@link smoothKinematicBody} */ + function getAlphaWithMultiplier(entity: Entity, dt: number, mult: number): number { + return smootheLerpAlpha(setMultiplier(entity, mult), dt) + } + + /** @description Computes the lerp of the (`@param start`,`@param final`) input Vectors without mutating their values */ + function lerpNoRef(start: Vector3, final: Vector3, dt: number) { + return start.clone().lerp(final.clone(), dt).clone() + } + /** @description Computes the fastSlerp of the (`@param start`,`@param final`) input Quaternions without mutating their values */ + function fastSlerpNoRef(start: Quaternion, final: Quaternion, dt: number) { + return start.clone().fastSlerp(final.clone(), dt).clone() + } + + /** @description Calculates the Exponential Lerp value for the `@param data`, as expected by the tests, based on the given `@param dt` alpha value */ + function computeELerp(data: LerpData, alpha: number) { + return { + position: lerpNoRef(data.position.start, data.position.final, alpha), + rotation: fastSlerpNoRef(data.rotation.start, data.rotation.final, alpha) + } + } + + it('... should apply gradual smoothing (aka exponential interpolation) to the position of the KinematicBody of the given entity', () => { + // Check data before + const body = getComponent(testEntity, RigidBodyComponent) + const before = body.position.clone() + assertVecApproxEq(before, Vector3_Zero, 3, LerpEpsilon) + + // Run and Check resulting data + // ... Infinite smoothing case + const MultInfinite = 1 // Multiplier 1 shouldn't change the position (aka. infinite smoothing) + setMultiplier(testEntity, MultInfinite) + Physics.smoothKinematicBody(physicsWorld, testEntity, DeltaTime, /*substep*/ 1) + assertVecApproxEq(before, body.position, 3, LerpEpsilon) + + // ... Hardcoded case + setMultiplier(testEntity, 0.12345) + Physics.smoothKinematicBody(physicsWorld, testEntity, 1 / 60, 1) + const ExpectedHardcoded = { x: 0.1370581001805662, y: 0.17132262522570774, z: 0.20558715027084928 } + assertVecApproxEq(body.position.clone(), ExpectedHardcoded, 3) + + // ... Check the other Step cases + for (const multiplier of KinematicMultiplierCases) { + for (const step of Steps) { + getComponent(testEntity, RigidBodyComponent).position.set(0, 0, 0) // reset for next case + const alpha = getAlphaWithMultiplier(testEntity, step.dt, multiplier) + const before = { + position: { start: body.position.clone(), final: body.targetKinematicPosition.clone() }, + rotation: { start: body.rotation.clone(), final: body.targetKinematicRotation.clone() } + } + Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) + assertVecApproxEq(body.position, computeELerp(before, alpha).position, 3, LerpEpsilon) + } + } + }) + + it('... should apply gradual smoothing (aka exponential interpolation) to the rotation of the KinematicBody of the given entity', () => { + // Check data before + const body = getComponent(testEntity, RigidBodyComponent) + const before = body.rotation.clone() + assertVecApproxEq(before, Quaternion_Zero, 4, SLerpEpsilon) + + // Run and Check resulting data + // ... Infinite smoothing case + const MultInfinite = 1 // Multiplier 1 shouldn't change the rotation (aka. infinite smoothing) + setMultiplier(testEntity, MultInfinite) + Physics.smoothKinematicBody(physicsWorld, testEntity, DeltaTime, /*substep*/ 1) + assertVecApproxEq(before, body.rotation, 3, SLerpEpsilon) + + // ... Hardcoded case + setMultiplier(testEntity, 0.12345) + Physics.smoothKinematicBody(physicsWorld, testEntity, 1 / 60, 1) + const ExpectedHardcoded = new Quaternion(0, 0.013047535062645674, 0.052190140250582696, 0.9985524073985961) + assertVecApproxEq(body.rotation.clone(), ExpectedHardcoded, 4) + + // ... Check the other Step cases + for (const multiplier of KinematicMultiplierCases) { + for (const step of Steps) { + getComponent(testEntity, RigidBodyComponent).rotation.set(0, 0, 0, 1) // reset for next case + const alpha = getAlphaWithMultiplier(testEntity, step.dt, multiplier) + const before = { + position: { start: body.position.clone(), final: body.targetKinematicPosition.clone() }, + rotation: { start: body.rotation.clone(), final: body.targetKinematicRotation.clone() } + } as LerpData + Physics.smoothKinematicBody(physicsWorld, testEntity, step.dt, step.substep) + assertVecApproxEq(body.rotation, computeELerp(before, alpha).rotation, 3, SLerpEpsilon) + } + } + }) + }) +}) + +describe('PhysicsSystem', () => { + describe('IDs', () => { + it("should define the PhysicsSystem's UUID with the expected value", () => { + assert.equal(PhysicsSystem, 'ee.engine.PhysicsSystem' as SystemUUID) + }) + }) + + describe('execute', () => { + let testEntity = UndefinedEntity + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + physicsWorld.timestep = 1 / 60 + + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(testEntity, ColliderComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + const physicsSystemExecute = SystemDefinitions.get(PhysicsSystem)!.execute + + it('should step the physics', () => { + const testImpulse = new Vector3(1, 2, 3) + const beforeBody = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(beforeBody) + const before = beforeBody.linvel() + assertVecApproxEq(before, Vector3_Zero, 3) + Physics.applyImpulse(physicsWorld, testEntity, testImpulse) + physicsSystemExecute() + const afterBody = physicsWorld.Rigidbodies.get(testEntity) + assert.ok(afterBody) + const after = afterBody.linvel() + assertVecAllApproxNotEq(after, before, 3) + }) + + function cloneRigidBodyPoseData(entity: Entity) { + const body = getComponent(testEntity, RigidBodyComponent) + return { + previousPosition: body.previousPosition.clone(), + previousRotation: body.previousRotation.clone(), + position: body.position.clone(), + rotation: body.rotation.clone(), + targetKinematicPosition: body.targetKinematicPosition.clone(), + targetKinematicRotation: body.targetKinematicRotation.clone(), + linearVelocity: body.linearVelocity.clone(), + angularVelocity: body.angularVelocity.clone() + } + } + + it('should update poses on the ECS', () => { + const testImpulse = new Vector3(1, 2, 3) + const before = cloneRigidBodyPoseData(testEntity) + const body = getComponent(testEntity, RigidBodyComponent) + assertVecApproxEq(before.previousPosition, body.previousPosition.clone(), 3) + assertVecApproxEq(before.previousRotation, body.previousRotation.clone(), 3) + assertVecApproxEq(before.position, body.position.clone(), 3) + assertVecApproxEq(before.rotation, body.rotation.clone(), 4) + assertVecApproxEq(before.targetKinematicPosition, body.targetKinematicPosition.clone(), 3) + assertVecApproxEq(before.targetKinematicRotation, body.targetKinematicRotation.clone(), 4) + assertVecApproxEq(before.linearVelocity, body.linearVelocity.clone(), 3) + assertVecApproxEq(before.angularVelocity, body.angularVelocity.clone(), 3) + + Physics.applyImpulse(physicsWorld, testEntity, testImpulse) + physicsSystemExecute() + + const after = cloneRigidBodyPoseData(testEntity) + assertVecAnyApproxNotEq(after.previousPosition, before.previousPosition, 3) + assertVecAnyApproxNotEq(after.previousRotation, before.previousRotation, 3) + assertVecAnyApproxNotEq(after.position, before.position, 3) + assertVecAnyApproxNotEq(after.rotation, before.rotation, 4) + assertVecAnyApproxNotEq(after.targetKinematicPosition, before.targetKinematicPosition, 3) + assertVecAnyApproxNotEq(after.targetKinematicRotation, before.targetKinematicRotation, 4) + assertVecAnyApproxNotEq(after.linearVelocity, before.linearVelocity, 3) + assertVecAnyApproxNotEq(after.angularVelocity, before.angularVelocity, 3) + }) + + it('should update collisions on the ECS', () => { + const testImpulse = new Vector3(1, 2, 3) + const entity1 = createEntity() + setComponent(entity1, TransformComponent) + setComponent(entity1, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity1, ColliderComponent) + const entity2 = createEntity() + setComponent(entity2, TransformComponent) + setComponent(entity2, RigidBodyComponent, { type: BodyTypes.Dynamic }) + setComponent(entity2, ColliderComponent) + // Check before + assert.ok(!hasComponent(entity1, CollisionComponent)) + assert.ok(!hasComponent(entity2, CollisionComponent)) + + // Run and Check after + Physics.applyImpulse(physicsWorld, entity1, testImpulse) + physicsSystemExecute() + assert.ok(hasComponent(entity1, ColliderComponent)) + assert.ok(hasComponent(entity2, ColliderComponent)) + }) + }) + + /** + // @note The reactor is currently just binding data onMount and onUnmount + // describe('reactor', () => {}) + */ +}) diff --git a/packages/spatial/src/physics/systems/TriggerSystem.test.ts b/packages/spatial/src/physics/systems/TriggerSystem.test.ts index 13d0b72ff0..de73dcb2e9 100644 --- a/packages/spatial/src/physics/systems/TriggerSystem.test.ts +++ b/packages/spatial/src/physics/systems/TriggerSystem.test.ts @@ -1,268 +1,268 @@ -// /* -// CPAL-1.0 License +/* +CPAL-1.0 License -// The contents of this file are subject to the Common Public Attribution License -// Version 1.0. (the "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -// The License is based on the Mozilla Public License Version 1.1, but Sections 14 -// and 15 have been added to cover use of software over a computer network and -// provide for limited attribution for the Original Developer. In addition, -// Exhibit A has been modified to be consistent with Exhibit B. +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -// specific language governing rights and limitations under the License. +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. -// The Original Code is Ethereal Engine. +The Original Code is Ethereal Engine. -// The Original Developer is the Initial Developer. The Initial Developer of the -// Original Code is the Ethereal Engine team. +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. -// All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -// Ethereal Engine. All Rights Reserved. -// */ +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ -// import assert from 'assert' +import assert from 'assert' -// import { -// EntityUUID, -// SystemDefinitions, -// SystemUUID, -// UUIDComponent, -// UndefinedEntity, -// createEngine, -// createEntity, -// destroyEngine, -// getComponent, -// hasComponent, -// removeComponent, -// removeEntity, -// setComponent -// } from '@etherealengine/ecs' -// import { TransformComponent } from '../../SpatialModule' -// import { setCallback } from '../../common/CallbackComponent' -// import { SceneComponent } from '../../renderer/components/SceneComponents' -// import { EntityTreeComponent } from '../../transform/components/EntityTree' -// import { Physics, PhysicsWorld } from '../classes/Physics' -// import { ColliderComponent } from '../components/ColliderComponent' -// import { CollisionComponent } from '../components/CollisionComponent' -// import { RigidBodyComponent } from '../components/RigidBodyComponent' -// import { TriggerComponent } from '../components/TriggerComponent' -// import { ColliderHitEvent, CollisionEvents } from '../types/PhysicsTypes' -// import { TriggerSystem, triggerEnter, triggerExit } from './TriggerSystem' +import { + EntityUUID, + SystemDefinitions, + SystemUUID, + UUIDComponent, + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + hasComponent, + removeComponent, + removeEntity, + setComponent +} from '@etherealengine/ecs' +import { TransformComponent } from '../../SpatialModule' +import { setCallback } from '../../common/CallbackComponent' +import { SceneComponent } from '../../renderer/components/SceneComponents' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { Physics, PhysicsWorld } from '../classes/Physics' +import { ColliderComponent } from '../components/ColliderComponent' +import { CollisionComponent } from '../components/CollisionComponent' +import { RigidBodyComponent } from '../components/RigidBodyComponent' +import { TriggerComponent } from '../components/TriggerComponent' +import { ColliderHitEvent, CollisionEvents } from '../types/PhysicsTypes' +import { TriggerSystem, triggerEnter, triggerExit } from './TriggerSystem' -// describe('TriggerSystem', () => { -// describe('IDs', () => { -// it("should define the TriggerSystem's UUID with the expected value", () => { -// assert.equal(TriggerSystem, 'ee.engine.TriggerSystem' as SystemUUID) -// }) -// }) +describe('TriggerSystem', () => { + describe('IDs', () => { + it("should define the TriggerSystem's UUID with the expected value", () => { + assert.equal(TriggerSystem, 'ee.engine.TriggerSystem' as SystemUUID) + }) + }) -// const InvalidEntityUUID = 'dummyID-123456' as EntityUUID + const InvalidEntityUUID = 'dummyID-123456' as EntityUUID -// /** @todo Refactor: Simplify by using sinon.spy functions */ -// const EnterStartValue = 42 // Start testOnEnter at 42 -// let enterVal = EnterStartValue -// const TestOnEnterName = 'test.onEnter' -// function testOnEnter(ent1, ent2) { -// ++enterVal -// } + /** @todo Refactor: Simplify by using sinon.spy functions */ + const EnterStartValue = 42 // Start testOnEnter at 42 + let enterVal = EnterStartValue + const TestOnEnterName = 'test.onEnter' + function testOnEnter(ent1, ent2) { + ++enterVal + } -// /** @todo Refactor: Simplify by using sinon.spy functions */ -// const ExitStartValue = 10_042 // Start testOnExit at 10_042 -// let exitVal = ExitStartValue -// const TestOnExitName = 'test.onExit' -// function testOnExit(ent1, ent2) { -// ++exitVal -// } + /** @todo Refactor: Simplify by using sinon.spy functions */ + const ExitStartValue = 10_042 // Start testOnExit at 10_042 + let exitVal = ExitStartValue + const TestOnExitName = 'test.onExit' + function testOnExit(ent1, ent2) { + ++exitVal + } -// let triggerEntity = UndefinedEntity -// let targetEntity = UndefinedEntity -// let testEntity = UndefinedEntity -// let targetEntityUUID = '' as EntityUUID -// let physicsWorld: PhysicsWorld -// let physicsWorldEntity = UndefinedEntity + let triggerEntity = UndefinedEntity + let targetEntity = UndefinedEntity + let testEntity = UndefinedEntity + let targetEntityUUID = '' as EntityUUID + let physicsWorld: PhysicsWorld + let physicsWorldEntity = UndefinedEntity -// beforeEach(async () => { -// createEngine() -// await Physics.load() -// physicsWorldEntity = createEntity() -// setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setComponent(physicsWorldEntity, SceneComponent) -// setComponent(physicsWorldEntity, TransformComponent) -// setComponent(physicsWorldEntity, EntityTreeComponent) -// physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) -// physicsWorld.timestep = 1 / 60 + beforeEach(async () => { + createEngine() + await Physics.load() + physicsWorldEntity = createEntity() + setComponent(physicsWorldEntity, UUIDComponent, UUIDComponent.generateUUID()) + setComponent(physicsWorldEntity, SceneComponent) + setComponent(physicsWorldEntity, TransformComponent) + setComponent(physicsWorldEntity, EntityTreeComponent) + physicsWorld = Physics.createWorld(getComponent(physicsWorldEntity, UUIDComponent)) + physicsWorld.timestep = 1 / 60 -// // Create the entity -// testEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(testEntity, TransformComponent) -// setComponent(testEntity, RigidBodyComponent) -// setComponent(testEntity, ColliderComponent) + // Create the entity + testEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(testEntity, TransformComponent) + setComponent(testEntity, RigidBodyComponent) + setComponent(testEntity, ColliderComponent) -// targetEntity = createEntity() -// setComponent(targetEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setCallback(targetEntity, TestOnEnterName, testOnEnter) -// setCallback(targetEntity, TestOnExitName, testOnExit) -// targetEntityUUID = getComponent(targetEntity, UUIDComponent) + targetEntity = createEntity() + setComponent(targetEntity, UUIDComponent, UUIDComponent.generateUUID()) + setCallback(targetEntity, TestOnEnterName, testOnEnter) + setCallback(targetEntity, TestOnExitName, testOnExit) + targetEntityUUID = getComponent(targetEntity, UUIDComponent) -// triggerEntity = createEntity() -// setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) -// setComponent(triggerEntity, TransformComponent) -// setComponent(triggerEntity, RigidBodyComponent) -// setComponent(triggerEntity, ColliderComponent) -// setComponent(triggerEntity, TriggerComponent, { -// triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: targetEntityUUID }] -// }) -// }) + triggerEntity = createEntity() + setComponent(testEntity, EntityTreeComponent, { parentEntity: physicsWorldEntity }) + setComponent(triggerEntity, TransformComponent) + setComponent(triggerEntity, RigidBodyComponent) + setComponent(triggerEntity, ColliderComponent) + setComponent(triggerEntity, TriggerComponent, { + triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: targetEntityUUID }] + }) + }) -// afterEach(() => { -// removeEntity(testEntity) -// removeEntity(triggerEntity) -// removeEntity(targetEntity) -// return destroyEngine() -// }) + afterEach(() => { + removeEntity(testEntity) + removeEntity(triggerEntity) + removeEntity(targetEntity) + return destroyEngine() + }) -// describe('triggerEnter', () => { -// const Hit = {} as ColliderHitEvent // @todo The hitEvent argument is currently ignored in the function body -// describe('for all entity.triggerComponent.triggers ...', () => { -// it('... should only run if trigger.target defines the UUID of a valid entity', () => { -// setComponent(triggerEntity, TriggerComponent, { -// triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: InvalidEntityUUID }] -// }) -// assert.equal(enterVal, EnterStartValue) -// triggerEnter(triggerEntity, targetEntity, Hit) -// assert.equal(enterVal, EnterStartValue) -// }) + describe('triggerEnter', () => { + const Hit = {} as ColliderHitEvent // @todo The hitEvent argument is currently ignored in the function body + describe('for all entity.triggerComponent.triggers ...', () => { + it('... should only run if trigger.target defines the UUID of a valid entity', () => { + setComponent(triggerEntity, TriggerComponent, { + triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: InvalidEntityUUID }] + }) + assert.equal(enterVal, EnterStartValue) + triggerEnter(triggerEntity, targetEntity, Hit) + assert.equal(enterVal, EnterStartValue) + }) -// it('... should only run if trigger.onEnter callback has a value and is part of the target.CallbackComponent.callbacks map', () => { -// const noEnterEntity = createEntity() -// setComponent(noEnterEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setCallback(noEnterEntity, TestOnExitName, testOnExit) -// const noEnterEntityUUID = getComponent(noEnterEntity, UUIDComponent) -// setComponent(triggerEntity, TriggerComponent, { -// triggers: [{ onEnter: null, onExit: TestOnExitName, target: noEnterEntityUUID }] -// }) -// assert.equal(enterVal, EnterStartValue) -// triggerEnter(triggerEntity, targetEntity, Hit) -// assert.equal(enterVal, EnterStartValue) -// }) + it('... should only run if trigger.onEnter callback has a value and is part of the target.CallbackComponent.callbacks map', () => { + const noEnterEntity = createEntity() + setComponent(noEnterEntity, UUIDComponent, UUIDComponent.generateUUID()) + setCallback(noEnterEntity, TestOnExitName, testOnExit) + const noEnterEntityUUID = getComponent(noEnterEntity, UUIDComponent) + setComponent(triggerEntity, TriggerComponent, { + triggers: [{ onEnter: null, onExit: TestOnExitName, target: noEnterEntityUUID }] + }) + assert.equal(enterVal, EnterStartValue) + triggerEnter(triggerEntity, targetEntity, Hit) + assert.equal(enterVal, EnterStartValue) + }) -// it('... should run the target.CallbackComponent.callbacks[trigger.onEnter] function', () => { -// assert.equal(enterVal, EnterStartValue) -// triggerEnter(triggerEntity, targetEntity, Hit) -// assert.notEqual(enterVal, EnterStartValue) -// }) -// }) -// }) + it('... should run the target.CallbackComponent.callbacks[trigger.onEnter] function', () => { + assert.equal(enterVal, EnterStartValue) + triggerEnter(triggerEntity, targetEntity, Hit) + assert.notEqual(enterVal, EnterStartValue) + }) + }) + }) -// describe('triggerExit', () => { -// const Hit = {} as ColliderHitEvent // @todo The hitEvent argument is currently ignored in the function body -// describe('for all entity.triggerComponent.triggers ...', () => { -// it('... should only run if trigger.target defines the UUID of a valid entity', () => { -// setComponent(triggerEntity, TriggerComponent, { -// triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: InvalidEntityUUID }] -// }) -// assert.equal(exitVal, ExitStartValue) -// triggerExit(triggerEntity, targetEntity, Hit) -// assert.equal(exitVal, ExitStartValue) -// }) + describe('triggerExit', () => { + const Hit = {} as ColliderHitEvent // @todo The hitEvent argument is currently ignored in the function body + describe('for all entity.triggerComponent.triggers ...', () => { + it('... should only run if trigger.target defines the UUID of a valid entity', () => { + setComponent(triggerEntity, TriggerComponent, { + triggers: [{ onEnter: TestOnEnterName, onExit: TestOnExitName, target: InvalidEntityUUID }] + }) + assert.equal(exitVal, ExitStartValue) + triggerExit(triggerEntity, targetEntity, Hit) + assert.equal(exitVal, ExitStartValue) + }) -// it('... should only run if trigger.onExit callback has a value and is part of the target.CallbackComponent.callbacks map', () => { -// const noExitEntity = createEntity() -// setComponent(noExitEntity, UUIDComponent, UUIDComponent.generateUUID()) -// setCallback(noExitEntity, TestOnExitName, testOnExit) -// const noExitEntityUUID = getComponent(noExitEntity, UUIDComponent) -// setComponent(triggerEntity, TriggerComponent, { -// triggers: [{ onEnter: TestOnEnterName, onExit: null, target: noExitEntityUUID }] -// }) -// assert.equal(exitVal, ExitStartValue) -// triggerExit(triggerEntity, targetEntity, Hit) -// assert.equal(exitVal, ExitStartValue) -// }) + it('... should only run if trigger.onExit callback has a value and is part of the target.CallbackComponent.callbacks map', () => { + const noExitEntity = createEntity() + setComponent(noExitEntity, UUIDComponent, UUIDComponent.generateUUID()) + setCallback(noExitEntity, TestOnExitName, testOnExit) + const noExitEntityUUID = getComponent(noExitEntity, UUIDComponent) + setComponent(triggerEntity, TriggerComponent, { + triggers: [{ onEnter: TestOnEnterName, onExit: null, target: noExitEntityUUID }] + }) + assert.equal(exitVal, ExitStartValue) + triggerExit(triggerEntity, targetEntity, Hit) + assert.equal(exitVal, ExitStartValue) + }) -// it('... should run the target.CallbackComponent.callbacks[trigger.onExit] function', () => { -// assert.equal(exitVal, ExitStartValue) -// triggerExit(triggerEntity, targetEntity, Hit) -// assert.notEqual(exitVal, ExitStartValue) -// }) -// }) -// }) + it('... should run the target.CallbackComponent.callbacks[trigger.onExit] function', () => { + assert.equal(exitVal, ExitStartValue) + triggerExit(triggerEntity, targetEntity, Hit) + assert.notEqual(exitVal, ExitStartValue) + }) + }) + }) -// describe('execute', () => { -// const triggerSystemExecute = SystemDefinitions.get(TriggerSystem)!.execute + describe('execute', () => { + const triggerSystemExecute = SystemDefinitions.get(TriggerSystem)!.execute -// it('should only run for entities that have both a TriggerComponent and a CollisionComponent (aka. collisionQuery)', () => { -// const triggerTestStartHit = { -// type: CollisionEvents.TRIGGER_START, -// bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, -// bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, -// shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, -// shapeOther: physicsWorld.Colliders.get(testEntity)!, -// maxForceDirection: null, -// totalForce: null -// } as ColliderHitEvent + it('should only run for entities that have both a TriggerComponent and a CollisionComponent (aka. collisionQuery)', () => { + const triggerTestStartHit = { + type: CollisionEvents.TRIGGER_START, + bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, + bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, + shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, + shapeOther: physicsWorld.Colliders.get(testEntity)!, + maxForceDirection: null, + totalForce: null + } as ColliderHitEvent -// removeComponent(triggerEntity, TriggerComponent) -// setComponent(triggerEntity, CollisionComponent) -// const collision = getComponent(triggerEntity, CollisionComponent) -// collision?.set(testEntity, triggerTestStartHit) + removeComponent(triggerEntity, TriggerComponent) + setComponent(triggerEntity, CollisionComponent) + const collision = getComponent(triggerEntity, CollisionComponent) + collision?.set(testEntity, triggerTestStartHit) -// const beforeEnter = EnterStartValue + 1 // +1 because the system runs once before this test -// const beforeExit = ExitStartValue + 1 -// assert.equal(enterVal, beforeEnter) -// assert.equal(exitVal, beforeExit) -// triggerSystemExecute() -// assert.equal(enterVal, beforeEnter) -// assert.equal(exitVal, beforeExit) -// }) + const beforeEnter = EnterStartValue + 1 // +1 because the system runs once before this test + const beforeExit = ExitStartValue + 1 + assert.equal(enterVal, beforeEnter) + assert.equal(exitVal, beforeExit) + triggerSystemExecute() + assert.equal(enterVal, beforeEnter) + assert.equal(exitVal, beforeExit) + }) -// it('should run `triggerEnter` for all entities that match the collisionQuery and have a CollisionComponent', () => { -// const triggerTestStartHit = { -// type: CollisionEvents.TRIGGER_START, -// bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, -// bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, -// shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, -// shapeOther: physicsWorld.Colliders.get(testEntity)!, -// maxForceDirection: null, -// totalForce: null -// } as ColliderHitEvent + it('should run `triggerEnter` for all entities that match the collisionQuery and have a CollisionComponent', () => { + const triggerTestStartHit = { + type: CollisionEvents.TRIGGER_START, + bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, + bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, + shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, + shapeOther: physicsWorld.Colliders.get(testEntity)!, + maxForceDirection: null, + totalForce: null + } as ColliderHitEvent -// const beforeEnter = EnterStartValue + 1 // +1 because the system runs once before this test -// assert.equal(enterVal, beforeEnter) -// // Set a start collision and run the system -// assert.ok(!hasComponent(triggerEntity, CollisionComponent)) -// setComponent(triggerEntity, CollisionComponent) -// const collision = getComponent(triggerEntity, CollisionComponent) -// collision?.set(testEntity, triggerTestStartHit) -// triggerSystemExecute() -// // Check after -// assert.notEqual(enterVal, beforeEnter) -// }) + const beforeEnter = EnterStartValue + 1 // +1 because the system runs once before this test + assert.equal(enterVal, beforeEnter) + // Set a start collision and run the system + assert.ok(!hasComponent(triggerEntity, CollisionComponent)) + setComponent(triggerEntity, CollisionComponent) + const collision = getComponent(triggerEntity, CollisionComponent) + collision?.set(testEntity, triggerTestStartHit) + triggerSystemExecute() + // Check after + assert.notEqual(enterVal, beforeEnter) + }) -// it('should run `triggerExit` for all entities that match the collisionQuery and have a CollisionComponent', () => { -// const triggerTestEndHit = { -// type: CollisionEvents.TRIGGER_END, -// bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, -// bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, -// shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, -// shapeOther: physicsWorld.Colliders.get(testEntity)!, -// maxForceDirection: null, -// totalForce: null -// } as ColliderHitEvent + it('should run `triggerExit` for all entities that match the collisionQuery and have a CollisionComponent', () => { + const triggerTestEndHit = { + type: CollisionEvents.TRIGGER_END, + bodySelf: physicsWorld.Rigidbodies.get(triggerEntity)!, + bodyOther: physicsWorld.Rigidbodies.get(testEntity)!, + shapeSelf: physicsWorld.Colliders.get(triggerEntity)!, + shapeOther: physicsWorld.Colliders.get(testEntity)!, + maxForceDirection: null, + totalForce: null + } as ColliderHitEvent -// const beforeExit = ExitStartValue + 1 // +1 because the system runs once before this test -// assert.equal(exitVal, beforeExit) -// // Set an end collision and run the system -// assert.ok(!hasComponent(triggerEntity, CollisionComponent)) -// setComponent(triggerEntity, CollisionComponent) -// const collision = getComponent(triggerEntity, CollisionComponent) -// collision?.set(testEntity, triggerTestEndHit) -// triggerSystemExecute() -// // Check after -// assert.notEqual(exitVal, beforeExit) -// }) -// }) -// }) + const beforeExit = ExitStartValue + 1 // +1 because the system runs once before this test + assert.equal(exitVal, beforeExit) + // Set an end collision and run the system + assert.ok(!hasComponent(triggerEntity, CollisionComponent)) + setComponent(triggerEntity, CollisionComponent) + const collision = getComponent(triggerEntity, CollisionComponent) + collision?.set(testEntity, triggerTestEndHit) + triggerSystemExecute() + // Check after + assert.notEqual(exitVal, beforeExit) + }) + }) +}) From 737d20b8f1f49b9bcdc51d237620df7330555c9e Mon Sep 17 00:00:00 2001 From: Josh Field Date: Tue, 6 Aug 2024 08:33:20 +1000 Subject: [PATCH 02/20] fix collider reactivity (#10877) * fix collider reactivity * fix tests * optimize --- .../spatial/src/physics/components/ColliderComponent.tsx | 7 ++++--- packages/spatial/src/transform/components/EntityTree.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/spatial/src/physics/components/ColliderComponent.tsx b/packages/spatial/src/physics/components/ColliderComponent.tsx index b83efb5a3d..f6c3604a40 100644 --- a/packages/spatial/src/physics/components/ColliderComponent.tsx +++ b/packages/spatial/src/physics/components/ColliderComponent.tsx @@ -23,12 +23,12 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { useEffect, useLayoutEffect } from 'react' import { Vector3 } from 'three' import { defineComponent, useComponent, useEntityContext, useOptionalComponent } from '@etherealengine/ecs' import { useState } from '@etherealengine/hyperflux' +import { useLayoutEffect } from 'react' import { useAncestorWithComponent } from '../../transform/components/EntityTree' import { TransformComponent } from '../../transform/components/TransformComponent' import { Physics } from '../classes/Physics' @@ -88,10 +88,11 @@ export const ColliderComponent = defineComponent({ const triggerComponent = useOptionalComponent(entity, TriggerComponent) const hasCollider = useState(false) - useEffect(() => { + useLayoutEffect(() => { if (!rigidbodyComponent || !physicsWorld) return const colliderDesc = Physics.createColliderDesc(physicsWorld, entity, rigidbodyEntity) + if (!colliderDesc) return Physics.attachCollider(physicsWorld, colliderDesc, rigidbodyEntity, entity) @@ -132,7 +133,7 @@ export const ColliderComponent = defineComponent({ Physics.setCollisionMask(physicsWorld, entity, component.collisionMask.value) }, [physicsWorld, component.collisionMask]) - useEffect(() => { + useLayoutEffect(() => { if (!physicsWorld || !triggerComponent?.value || !hasCollider.value) return Physics.setTrigger(physicsWorld, entity, true) diff --git a/packages/spatial/src/transform/components/EntityTree.tsx b/packages/spatial/src/transform/components/EntityTree.tsx index c07a819c0b..41feecb691 100644 --- a/packages/spatial/src/transform/components/EntityTree.tsx +++ b/packages/spatial/src/transform/components/EntityTree.tsx @@ -384,7 +384,7 @@ export function useTreeQuery(entity: Entity) { * @returns */ export function useAncestorWithComponent(entity: Entity, component: ComponentType) { - const result = useHookstate(UndefinedEntity) + const result = useHookstate(() => getAncestorWithComponent(entity, component)) useImmediateEffect(() => { let unmounted = false From efda5644e3e3dd80d0d8397efd64a4ec60ed2194 Mon Sep 17 00:00:00 2001 From: Josh Field Date: Tue, 6 Aug 2024 09:38:00 +1000 Subject: [PATCH 03/20] fix duplicate new scene (#10880) --- packages/editor/src/components/toolbar/Toolbar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/editor/src/components/toolbar/Toolbar.tsx b/packages/editor/src/components/toolbar/Toolbar.tsx index 62c8229608..316da9c6c9 100644 --- a/packages/editor/src/components/toolbar/Toolbar.tsx +++ b/packages/editor/src/components/toolbar/Toolbar.tsx @@ -80,8 +80,6 @@ const onClickNewScene = async () => { if (!confirm) return } - onNewScene() - const newSceneUIAddons = getState(EditorState).uiAddons.newScene if (Object.keys(newSceneUIAddons).length > 0) { PopoverState.showPopupover() From 981d0e30dac162c3d229591f91be8c280e273068 Mon Sep 17 00:00:00 2001 From: Josh Field Date: Tue, 6 Aug 2024 10:24:43 +1000 Subject: [PATCH 04/20] rigidbody initialization quick fix (#10883) --- packages/spatial/src/physics/components/ColliderComponent.tsx | 4 ++-- packages/spatial/src/physics/components/RigidBodyComponent.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/spatial/src/physics/components/ColliderComponent.tsx b/packages/spatial/src/physics/components/ColliderComponent.tsx index f6c3604a40..ad84c9889b 100644 --- a/packages/spatial/src/physics/components/ColliderComponent.tsx +++ b/packages/spatial/src/physics/components/ColliderComponent.tsx @@ -89,7 +89,7 @@ export const ColliderComponent = defineComponent({ const hasCollider = useState(false) useLayoutEffect(() => { - if (!rigidbodyComponent || !physicsWorld) return + if (!rigidbodyComponent?.initialized?.value || !physicsWorld) return const colliderDesc = Physics.createColliderDesc(physicsWorld, entity, rigidbodyEntity) @@ -102,7 +102,7 @@ export const ColliderComponent = defineComponent({ Physics.removeCollider(physicsWorld, entity) hasCollider.set(false) } - }, [physicsWorld, component.shape, rigidbodyEntity, !!rigidbodyComponent, transform.scale]) + }, [physicsWorld, component.shape, !!rigidbodyComponent?.initialized?.value, transform.scale]) useLayoutEffect(() => { if (!physicsWorld) return diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.ts b/packages/spatial/src/physics/components/RigidBodyComponent.ts index e868092bc9..2c0706dd38 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.ts +++ b/packages/spatial/src/physics/components/RigidBodyComponent.ts @@ -67,6 +67,8 @@ export const RigidBodyComponent = defineComponent({ canSleep: true, gravityScale: 1, // internal + /** @deprecated @todo make the physics api properly reactive to remove this property */ + initialized: false, previousPosition: proxifyVector3(this.previousPosition, entity), previousRotation: proxifyQuaternion(this.previousRotation, entity), position: proxifyVector3(this.position, entity), @@ -118,8 +120,10 @@ export const RigidBodyComponent = defineComponent({ useEffect(() => { if (!physicsWorld) return Physics.createRigidBody(physicsWorld, entity) + component.initialized.set(true) return () => { Physics.removeRigidbody(physicsWorld, entity) + component.initialized.set(false) } }, [physicsWorld]) From 1b3f72699b04f7eae272f86d933555b3ca6d5d18 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 5 Aug 2024 17:27:43 -0700 Subject: [PATCH 05/20] Upload drag drop files MT fix (#10881) * Upload drag drop files MT fix * Array length check --- .../editor/src/functions/assetFunctions.ts | 2 +- .../editor/panels/Files/container/index.tsx | 48 +++++++------------ 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/editor/src/functions/assetFunctions.ts b/packages/editor/src/functions/assetFunctions.ts index fb0c7f040d..4f979bbe9f 100644 --- a/packages/editor/src/functions/assetFunctions.ts +++ b/packages/editor/src/functions/assetFunctions.ts @@ -35,7 +35,7 @@ import { modelResourcesPath } from '@etherealengine/engine/src/assets/functions/ import { pathJoin } from '@etherealengine/common/src/utils/miscUtils' -const handleUploadFiles = (projectName: string, directoryPath: string, files: FileList) => { +export const handleUploadFiles = (projectName: string, directoryPath: string, files: FileList | File[]) => { return Promise.all( Array.from(files).map((file) => { const fileDirectory = file.webkitRelativePath || file.name diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index 2f1078afb6..441eab9ae0 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -25,7 +25,6 @@ Ethereal Engine. All Rights Reserved. import { FileThumbnailJobState } from '@etherealengine/client-core/src/common/services/FileThumbnailJobState' import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' -import { uploadToFeathersService } from '@etherealengine/client-core/src/util/upload' import config from '@etherealengine/common/src/config' import { FileBrowserContentType, @@ -33,12 +32,10 @@ import { UserID, archiverPath, fileBrowserPath, - fileBrowserUploadPath, projectPath, staticResourcePath } from '@etherealengine/common/src/schema.type.module' import { CommonKnownContentTypes } from '@etherealengine/common/src/utils/CommonKnownContentTypes' -import { processFileName } from '@etherealengine/common/src/utils/processFileName' import { Engine } from '@etherealengine/ecs' import { AssetSelectionChangePropsType } from '@etherealengine/editor/src/components/assets/AssetsPreviewPanel' import { @@ -51,7 +48,11 @@ import ImageCompressionPanel from '@etherealengine/editor/src/components/assets/ import ModelCompressionPanel from '@etherealengine/editor/src/components/assets/ModelCompressionPanel' import { DndWrapper } from '@etherealengine/editor/src/components/dnd/DndWrapper' import { SupportedFileTypes } from '@etherealengine/editor/src/constants/AssetTypes' -import { downloadBlobAsZip, inputFileWithAddToScene } from '@etherealengine/editor/src/functions/assetFunctions' +import { + downloadBlobAsZip, + handleUploadFiles, + inputFileWithAddToScene +} from '@etherealengine/editor/src/functions/assetFunctions' import { bytesToSize, unique } from '@etherealengine/editor/src/functions/utils' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { ClickPlacementState } from '@etherealengine/editor/src/systems/ClickPlacementSystem' @@ -346,10 +347,8 @@ const FileBrowserContentPanel: React.FC = (props) await moveContent(data.fullName, newName, data.path, destinationPath, false) } } else { - const destinationPathCleaned = removeLeadingTrailingSlash(destinationPath) - const folder = destinationPathCleaned //destinationPathCleaned.substring(0, destinationPathCleaned.lastIndexOf('/') + 1) - const projectName = folder.split('/')[1] - const relativePath = folder.replace('projects/' + projectName + '/', '') + const path = selectedDirectory.get(NO_PROXY).slice(1) + const toUpload = [] as File[] await Promise.all( data.files.map(async (file) => { @@ -358,38 +357,23 @@ const FileBrowserContentPanel: React.FC = (props) // creating directory await fileService.create(`${destinationPath}${file.name}`) } else { - try { - const name = processFileName(file.name) - await uploadToFeathersService(fileBrowserUploadPath, [file], { - args: [ - { - project: projectName, - path: relativePath + '/' + name, - contentType: file.type - } - ] - }).promise - } catch (err) { - NotificationService.dispatchNotify(err.message, { variant: 'error' }) - } + toUpload.push(file) } }) ) + + if (toUpload.length) { + try { + await handleUploadFiles(projectName, path, toUpload) + } catch (err) { + NotificationService.dispatchNotify(err.message, { variant: 'error' }) + } + } } await refreshDirectory() } - function removeLeadingTrailingSlash(str) { - if (str.startsWith('/')) { - str = str.substring(1) - } - if (str.endsWith('/')) { - str = str.substring(0, str.length - 1) - } - return str - } - const onBackDirectory = () => { const pattern = /([^/]+)/g const result = selectedDirectory.value.match(pattern) From 609b1369100fc3be840002423bf0db8e85917caf Mon Sep 17 00:00:00 2001 From: Daniel Belmes <3631206+DanielBelmes@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:29:14 -0700 Subject: [PATCH 06/20] LerpTransform from rigid body uses rigid body instead of scene entity (#10882) --- .../src/physics/systems/PhysicsPreTransformSystem.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/spatial/src/physics/systems/PhysicsPreTransformSystem.ts b/packages/spatial/src/physics/systems/PhysicsPreTransformSystem.ts index 00e139c75f..98f379f3dd 100644 --- a/packages/spatial/src/physics/systems/PhysicsPreTransformSystem.ts +++ b/packages/spatial/src/physics/systems/PhysicsPreTransformSystem.ts @@ -30,7 +30,6 @@ import { ECSState } from '@etherealengine/ecs/src/ECSState' import { getState } from '@etherealengine/hyperflux' import { Vector3_One, Vector3_Zero } from '../../common/constants/MathConstants' -import { SceneComponent } from '../../renderer/components/SceneComponents' import { EntityTreeComponent, getAncestorWithComponent, iterateEntityNode } from '../../transform/components/EntityTree' import { TransformComponent } from '../../transform/components/TransformComponent' import { computeTransformMatrix, isDirty, TransformDirtyUpdateSystem } from '../../transform/systems/TransformSystem' @@ -79,13 +78,13 @@ export const lerpTransformFromRigidbody = (entity: Entity, alpha: number) => { const transform = getComponent(entity, TransformComponent) - const sceneEntity = getAncestorWithComponent(entity, SceneComponent) - const sceneTransform = getComponent(sceneEntity, TransformComponent) - parentMatrixInverse.copy(sceneTransform.matrixWorld).invert() + const rigidBodyEntity = getAncestorWithComponent(entity, RigidBodyComponent) + const rigidBodyTransform = getComponent(rigidBodyEntity, TransformComponent) + parentMatrixInverse.copy(rigidBodyTransform.matrixWorld).invert() localMatrix.compose(position, rotation, Vector3_One).premultiply(parentMatrixInverse) localMatrix.decompose(position, rotation, scale) transform.matrix.compose(position, rotation, transform.scale) - transform.matrixWorld.multiplyMatrices(sceneTransform.matrixWorld, transform.matrix) + transform.matrixWorld.multiplyMatrices(rigidBodyTransform.matrixWorld, transform.matrix) /** set all children dirty deeply, but set this entity to clean */ iterateEntityNode(entity, setDirty) From 3c491062228ad2e538baa636adf312d2e5338f73 Mon Sep 17 00:00:00 2001 From: lucas3900 <66710631+lucas3900@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:47:49 -0400 Subject: [PATCH 07/20] remove old workflows (#10886) --- .github/workflows/dev-deploy.yml | 91 ------------------------------- .github/workflows/prod-deploy.yml | 91 ------------------------------- 2 files changed, 182 deletions(-) delete mode 100755 .github/workflows/dev-deploy.yml delete mode 100755 .github/workflows/prod-deploy.yml diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml deleted file mode 100755 index 41c5901310..0000000000 --- a/.github/workflows/dev-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: dev-deploy - -on: - push: - branches: [dev] -jobs: - secrets-gate-run: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-run.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-run - run: | - if [ ${{ secrets.DEPLOYMENTS_ENABLED }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - secrets-gate-webhook: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-webhook.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-webhook - run: | - if [ ${{ secrets.SEND_FINISHED_WEBHOOK }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - dev-deploy: - needs: - - secrets-gate-run - if: ${{ needs.secrets-gate-run.outputs.ok == 'enabled' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Setup Helm - run: scripts/setup_helm_builder.sh - - name: Setup AWS - run: scripts/setup_aws_builder.sh $EKS_AWS_ACCESS_KEY_ID $EKS_AWS_ACCESS_KEY_SECRET $AWS_REGION $CLUSTER_NAME - env: - EKS_AWS_ACCESS_KEY_ID: ${{ secrets.EKS_AWS_ACCESS_KEY_ID }} - EKS_AWS_ACCESS_KEY_SECRET: ${{ secrets.EKS_AWS_ACCESS_KEY_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} - - name: Space debug - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: move package.json - run: mv package.json package.jsonmoved - - name: npm-install 'cli', @aws-sdk/client-ecr(-public), and @kubernetes/client-node - run: npm install cli @aws-sdk/client-ecr @aws-sdk/client-ecr-public @kubernetes/client-node - - name: restore package.json - run: mv package.jsonmoved package.json - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v2 - - name: Build and Push Docker Image - run: bash scripts/build_docker_builder.sh dev $GITHUB_SHA $AWS_REGION $PRIVATE_REPO - env: - STORAGE_AWS_ACCESS_KEY_ID: ${{ secrets.STORAGE_AWS_ACCESS_KEY_ID }} - STORAGE_AWS_ACCESS_KEY_SECRET: ${{ secrets.STORAGE_AWS_ACCESS_KEY_SECRET }} - REPO_NAME: ${{ secrets.DEV_REPO_NAME }} - AWS_REGION: ${{ secrets.AWS_REGION }} - REPO_URL: ${{ secrets.DEV_REPO_URL }} - REPO_PROVIDER: ${{ secrets.REPO_PROVIDER }} - PRIVATE_REPO: ${{ secrets.PRIVATE_REPO }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Deploy to EKS - run: bash scripts/deploy_builder.sh dev $GITHUB_SHA - - name: Job succeeded - if: ${{ needs.secrets-gate-webhook.outputs.ok == 'enabled' }} - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - env: - JOB_STATUS: ${{ job.status }} - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} - HOOK_OS_NAME: ${{ runner.os }} - WORKFLOW_NAME: ${{ github.workflow }} - run: | - git clone https://github.com/DiscordHooks/github-actions-discord-webhook.git webhook - bash webhook/send.sh $JOB_STATUS $WEBHOOK_URL - shell: bash diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml deleted file mode 100755 index 1539bc63f5..0000000000 --- a/.github/workflows/prod-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: prod-deploy -on: - push: - branches: - [main] -jobs: - secrets-gate-run: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-run.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-run - run: | - if [ ${{ secrets.DEPLOYMENTS_ENABLED }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - secrets-gate-webhook: - runs-on: ubuntu-latest - outputs: - ok: ${{ steps.check-secrets-webhook.outputs.ok }} - steps: - - name: check for secrets needed to run workflows - id: check-secrets-webhook - run: | - if [ ${{ secrets.SEND_FINISHED_WEBHOOK }} == 'true' ]; then - echo "ok=enabled" >> $GITHUB_OUTPUT - fi - prod-deploy: - needs: - - secrets-gate-run - if: ${{ needs.secrets-gate-run.outputs.ok == 'enabled' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Setup Helm - run: scripts/setup_helm_builder.sh - - name: Setup AWS - run: scripts/setup_aws_builder.sh $EKS_AWS_ACCESS_KEY_ID $EKS_AWS_ACCESS_KEY_SECRET $AWS_REGION $CLUSTER_NAME - env: - EKS_AWS_ACCESS_KEY_ID: ${{ secrets.EKS_AWS_ACCESS_KEY_ID }} - EKS_AWS_ACCESS_KEY_SECRET: ${{ secrets.EKS_AWS_ACCESS_KEY_SECRET }} - AWS_REGION: ${{ secrets.AWS_REGION }} - CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }} - - name: Space debug - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - - name: move package.json - run: mv package.json package.jsonmoved - - name: npm-install 'cli', @aws-sdk/client-ecr(-public), and @kubernetes/client-node - run: npm install cli @aws-sdk/client-ecr @aws-sdk/client-ecr-public @kubernetes/client-node - - name: restore package.json - run: mv package.jsonmoved package.json - - name: Expose GitHub Runtime - uses: crazy-max/ghaction-github-runtime@v2 - - name: Build and Push Docker Image - run: bash scripts/build_docker_builder.sh prod $GITHUB_SHA $AWS_REGION $PRIVATE_REPO - env: - STORAGE_AWS_ACCESS_KEY_ID: ${{ secrets.STORAGE_AWS_ACCESS_KEY_ID }} - STORAGE_AWS_ACCESS_KEY_SECRET: ${{ secrets.STORAGE_AWS_ACCESS_KEY_SECRET }} - REPO_NAME: ${{ secrets.PROD_REPO_NAME }} - AWS_REGION: ${{ secrets.AWS_REGION }} - REPO_URL: ${{ secrets.PROD_REPO_URL }} - REPO_PROVIDER: ${{ secrets.REPO_PROVIDER }} - PRIVATE_REPO: ${{ secrets.PRIVATE_REPO }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Deploy to EKS - run: bash scripts/deploy_builder.sh prod $GITHUB_SHA - - name: Job succeeded - if: ${{ needs.secrets-gate-webhook.outputs.ok == 'enabled' }} - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - env: - JOB_STATUS: ${{ job.status }} - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} - HOOK_OS_NAME: ${{ runner.os }} - WORKFLOW_NAME: ${{ github.workflow }} - run: | - git clone https://github.com/DiscordHooks/github-actions-discord-webhook.git webhook - bash webhook/send.sh $JOB_STATUS $WEBHOOK_URL - shell: bash From 3e509e0e46f6f7931fb76b1cb23c5a4b883e71f7 Mon Sep 17 00:00:00 2001 From: Josh Field Date: Tue, 6 Aug 2024 15:15:45 +1000 Subject: [PATCH 08/20] fix rigidbody crash server (#10891) --- packages/spatial/src/physics/components/RigidBodyComponent.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.ts b/packages/spatial/src/physics/components/RigidBodyComponent.ts index 2c0706dd38..9384e3bcdc 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.ts +++ b/packages/spatial/src/physics/components/RigidBodyComponent.ts @@ -28,6 +28,7 @@ import { Types } from 'bitecs' import { useEntityContext } from '@etherealengine/ecs' import { defineComponent, + hasComponent, removeComponent, setComponent, useComponent @@ -123,6 +124,7 @@ export const RigidBodyComponent = defineComponent({ component.initialized.set(true) return () => { Physics.removeRigidbody(physicsWorld, entity) + if (!hasComponent(entity, RigidBodyComponent)) return component.initialized.set(false) } }, [physicsWorld]) From c0b4c2541658d30b8e0e73ded53e8926613362fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Galv=C3=A1n?= <33644205+jose-galvan@users.noreply.github.com> Date: Tue, 6 Aug 2024 01:08:23 -0600 Subject: [PATCH 09/20] IR-3561: Magic Link template support for mobile and desktop (#10889) * feat: refactor magic link email template to support desktop and mobile chore: layout template to reuse styles in other templates * Fixed logo link --------- Co-authored-by: Hanzla Mateen --- .env.local.default | 2 +- .../email-templates/account/layout.pug | 80 +++++++++++++++++++ .../account/magiclink-email.pug | 27 +++---- 3 files changed, 92 insertions(+), 17 deletions(-) create mode 100755 packages/server-core/email-templates/account/layout.pug diff --git a/.env.local.default b/.env.local.default index 4d9454f2cd..d187727b1a 100644 --- a/.env.local.default +++ b/.env.local.default @@ -17,7 +17,7 @@ KEY=certs/key.pem # Client variables --------------- APP_TITLE="IR Engine" -APP_LOGO=https://etherealengine-static.s3-us-east-1.amazonaws.com/logo.png +APP_LOGO=https://preview.ir.world/static/ir.svg APP_URL=https://localhost:3000 APP_HOST=localhost:3000 APP_PORT=3000 diff --git a/packages/server-core/email-templates/account/layout.pug b/packages/server-core/email-templates/account/layout.pug new file mode 100755 index 0000000000..36fd7cfdd6 --- /dev/null +++ b/packages/server-core/email-templates/account/layout.pug @@ -0,0 +1,80 @@ +//- layout.pug +doctype html +html + head + style. + body{ + font-family: Roboto, Arial; + height: 100%; + width: auto; + background: #fff; + margin: 0; + padding-top: 2.5rem; + } + .container { + width: 75%; + max-width: 75%; + height: auto; + margin: 0 auto; + background: #383650; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + padding: 2rem; + padding-bottom: 4rem; + border-radius: 10px; + } + .main { + margin: 0 auto; + max-width: 90%; + } + .header { + padding-bottom: 2rem; + } + .logo{ + display: block; + margin: 0 auto; + heigh: auto; + max-width: 3em; + max-height: 4em; + } + .title-container{ + background: #5045AB; + padding: 2rem; + } + .message-container{ + background: #fff; + padding: 2rem; + } + h1 { + color: #fff; + font-size: 1.8rem; + text-wrap: wrap; + text-overflow: ellipsis; + } + h2 { + color: #918EAD; + } + p { + color: #8f9299; + font-weight: bold; + } + a { + word-wrap: break-word; + } + .link-btn { + width: 96%; + display: inline-block; + background: #5045AB; + text-align: center; + padding: 0.6rem; + border-radius: 5px; + text-decoration: none; + color: #fff !important; + font-weight: bold; + font-size: 1.2rem; + } +body + div(class='container') + .header + img(src=logo, class='logo') + .main + block content \ No newline at end of file diff --git a/packages/server-core/email-templates/account/magiclink-email.pug b/packages/server-core/email-templates/account/magiclink-email.pug index 7c11b93029..b248fc665b 100755 --- a/packages/server-core/email-templates/account/magiclink-email.pug +++ b/packages/server-core/email-templates/account/magiclink-email.pug @@ -1,16 +1,11 @@ -doctype html -body(style='margin: 0;') - div(style='margin: 0 auto; width:75%;background: #fff;') - div(style='margin-top:40px;height: auto;background: #383650;box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;padding: 4rem; border-radius: 10px;') - .hd(style='padding-bottom:30px') - img(src=logo, style='display: block; margin: 0 auto; max-width: 6em;') - .main - div(style='background: #5045AB;padding: 3rem 4rem 1.5rem 4rem;') - h1(style='font-family: Roboto, Arial; color: #fff;font-size: 1.8rem;') You've Got The Magic Link! - div(style='background: #fff;padding: 2rem;') - h2(style='font-family: Roboto, Arial;color: #918EAD;') Hi 👋! - p(style='font-family: Roboto, Arial;color: #8f9299;font-weight: bold;') You asked us to send you a magic link for quickly signing into your account. - a(href=hashLink, style='font-family: Roboto, Arial;width: 96%;display: inline-block;background: #5045AB;text-align: center;padding: 0.6rem;border-radius: 5px;text-decoration: none;color: #fff;font-weight: bold;font-size: 1.2rem;font-family: Roboto, Arial;') Sign In Now! - div - p(style='color: #8f9299;font-weight: bold;font-family: Roboto, Arial;') Or copy and paste this link into your browser: - a(style='word-wrap: break-word;', href=hashLink) #{hashLink} \ No newline at end of file +extends layout.pug +block content + .title-container + h1 You've Got The Magic Link! + .message-container + h2 Hi 👋! + p You asked us to send you a magic link for quickly signing into your account. + a(href=hashLink class='link-btn') Sign In Now! + div + p Or copy and paste this link into your browser: + a(href=hashLink) #{hashLink} \ No newline at end of file From 50aa2842f96a3f940dce5d590da5af1dc95932ff Mon Sep 17 00:00:00 2001 From: Kyle Baran Date: Tue, 6 Aug 2024 09:53:18 -0700 Subject: [PATCH 10/20] Fixed bugs in instanceserver connections (#10887) * Fixed bugs in instanceserver connections There were still some bugs surrounding a client connecting to an instanceserver if the instance record for that server no longer existed. updateInstance needed to return false in that situation. This still led to a situation where the client would be stuck trying to authenticate forever. Added a step prior to authentication where the client listens for a message with { instanceReady: true }, which is sent by setupSocketFunctions. If the server isn't ready within 10 seconds, then it returns { instanceReady: false }, and the client will do a new instanceserver provision and hopefully connect properly this time. * make eslint happy --------- Co-authored-by: Josh Field --- .../transports/SocketWebRTCClientFunctions.ts | 113 +++++++++++++----- .../src/avatar/functions/receiveJoinWorld.ts | 4 + .../instanceserver/src/SocketFunctions.ts | 18 ++- packages/instanceserver/src/channels.ts | 10 +- 4 files changed, 113 insertions(+), 32 deletions(-) diff --git a/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts b/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts index 1bb3f11853..67300cb262 100755 --- a/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts +++ b/packages/client-core/src/transports/SocketWebRTCClientFunctions.ts @@ -54,7 +54,7 @@ import { getSearchParamFromURL } from '@etherealengine/common/src/utils/getSearc import { Engine } from '@etherealengine/ecs/src/Engine' import { defineSystem, destroySystem } from '@etherealengine/ecs/src/SystemFunctions' import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups' -import { AuthTask } from '@etherealengine/engine/src/avatar/functions/receiveJoinWorld' +import { AuthTask, ReadyTask } from '@etherealengine/engine/src/avatar/functions/receiveJoinWorld' import { Identifiable, PeerID, State, dispatchAction, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Action, @@ -98,6 +98,8 @@ import { stopFaceTracking, stopLipsyncTracking } from '../media/webcam/WebcamInput' +import { ChannelState } from '../social/services/ChannelService' +import { LocationState } from '../social/services/LocationService' import { AuthState } from '../user/services/AuthService' import { MediaStreamState, MediaStreamService as _MediaStreamService } from './MediaStreams' import { clearPeerMediaChannels } from './PeerMediaChannelState' @@ -221,7 +223,7 @@ export const connectToInstance = ( if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() }, 3000) - const onConnect = () => { + const onConnect = async () => { if (aborted || !primus) return connecting = false primus.off('incoming::open', onConnect) @@ -230,31 +232,49 @@ export const connectToInstance = ( clearTimeout(connectionFailTimeout) const topic = locationID ? NetworkTopics.world : NetworkTopics.media - authenticatePrimus(primus, instanceID, topic) - - /** Server closed the connection. */ - const onDisconnect = () => { - if (aborted) return - if (primus) { - primus.off('incoming::end', onDisconnect) - primus.off('end', onDisconnect) + const instanceserverReady = await checkInstanceserverReady(primus, instanceID, topic) + if (instanceserverReady) { + await authenticatePrimus(primus, instanceID, topic) + + /** Server closed the connection. */ + const onDisconnect = () => { + if (aborted) return + if (primus) { + primus.off('incoming::end', onDisconnect) + primus.off('end', onDisconnect) + } + const network = getState(NetworkState).networks[instanceID] as SocketWebRTCClientNetwork + if (!network) return logger.error('Disconnected from unconnected instance ' + instanceID) + + logger.info('Disconnected from network %o', { topic: network.topic, id: network.id }) + /** + * If we are disconnected (server closes our socket) rather than leave the network, + * we just need to destroy and recreate the transport + */ + closeNetwork(network) + /** If we still have the instance provisioned, we should try again */ + if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() + } + // incoming::end is emitted when the server closes the connection + primus.on('incoming::end', onDisconnect) + // end is emitted when the client closes the connection + primus.on('end', onDisconnect) + } else { + if (locationID) { + const currentLocation = getMutableState(LocationState).currentLocation.location + const currentLocationId = currentLocation.id.value + currentLocation.id.set(undefined as unknown as LocationID) + currentLocation.id.set(currentLocationId) + } else { + const channelState = getMutableState(ChannelState) + const targetChannelId = channelState.targetChannelId.value + channelState.targetChannelId.set(undefined as unknown as ChannelID) + channelState.targetChannelId.set(targetChannelId) } - const network = getState(NetworkState).networks[instanceID] as SocketWebRTCClientNetwork - if (!network) return logger.error('Disconnected from unconnected instance ' + instanceID) - - logger.info('Disonnected from network %o', { topic: network.topic, id: network.id }) - /** - * If we are disconnected (server closes our socket) rather than leave the network, - * we just need to destroy and recreate the transport - */ - closeNetwork(network) - /** If we still have the instance provisioned, we should try again */ - if (instanceStillProvisioned(instanceID, locationID, channelID)) _connect() + primus.removeAllListeners() + primus.end() + console.log('PRIMUS GONE') } - // incoming::end is emitted when the server closes the connection - primus.on('incoming::end', onDisconnect) - // end is emitted when the client closes the connection - primus.on('end', onDisconnect) } primus!.on('incoming::open', onConnect) } @@ -283,6 +303,45 @@ export const getChannelIdFromTransport = (network: SocketWebRTCClientNetwork) => return isWorldConnection ? null : currentChannelInstanceConnection?.channelId } +export async function checkInstanceserverReady(primus: Primus, instanceID: InstanceID, topic: Topic) { + logger.info('Checking that instanceserver is ready') + const { instanceReady } = await new Promise((resolve) => { + const onStatus = (response: ReadyTask) => { + // eslint-disable-next-line no-prototype-builtins + if (response.hasOwnProperty('instanceReady')) { + clearInterval(interval) + resolve(response) + primus.off('data', onStatus) + primus.removeListener('incoming::end', onDisconnect) + } + } + + primus.on('data', onStatus) + + let disconnected = false + const interval = setInterval(() => { + if (disconnected) { + clearInterval(interval) + resolve({ instanceReady: false }) + primus.removeAllListeners() + primus.end() + return + } + }, 100) + + const onDisconnect = () => { + disconnected = true + } + primus.addListener('incoming::end', onDisconnect) + }) + + if (!instanceReady) { + unprovisionInstance(topic, instanceID) + } + + return instanceReady +} + export async function authenticatePrimus(primus: Primus, instanceID: InstanceID, topic: Topic) { logger.info('Authenticating instance ' + instanceID) @@ -325,10 +384,10 @@ export async function authenticatePrimus(primus: Primus, instanceID: InstanceID, /** We failed to connect to be authenticated, we do not want to try again */ // TODO: do we want to unprovision here? unprovisionInstance(topic, instanceID) - return logger.error(new Error('Unable to connect with credentials' + error)) + return logger.error(new Error('Unable to connect with credentials ' + error)) } - connectToNetwork(primus, instanceID, topic, hostPeerID!, routerRtpCapabilities!, cachedActions!) + await connectToNetwork(primus, instanceID, topic, hostPeerID!, routerRtpCapabilities!, cachedActions!) } export const connectToNetwork = async ( diff --git a/packages/engine/src/avatar/functions/receiveJoinWorld.ts b/packages/engine/src/avatar/functions/receiveJoinWorld.ts index 9c1af3c67e..68ce4f87e6 100644 --- a/packages/engine/src/avatar/functions/receiveJoinWorld.ts +++ b/packages/engine/src/avatar/functions/receiveJoinWorld.ts @@ -51,6 +51,10 @@ export type AuthTask = { error?: AuthError } +export type ReadyTask = { + instanceReady: boolean +} + export type JoinWorldRequestData = { inviteCode?: InviteCode } diff --git a/packages/instanceserver/src/SocketFunctions.ts b/packages/instanceserver/src/SocketFunctions.ts index 7b8489fee4..9ade3e2b18 100644 --- a/packages/instanceserver/src/SocketFunctions.ts +++ b/packages/instanceserver/src/SocketFunctions.ts @@ -41,6 +41,8 @@ import { getServerNetwork } from './SocketWebRTCServerFunctions' const logger = multiLogger.child({ component: 'instanceserver:spark' }) +const NON_READY_INTERVALS = 100 //100 tenths of a second, i.e. 10 seconds + export const setupSocketFunctions = async (app: Application, spark: any) => { let authTask: AuthTask | undefined @@ -50,15 +52,27 @@ export const setupSocketFunctions = async (app: Application, spark: any) => { * * Authorize user and make sure everything is valid before allowing them to join the world **/ - await new Promise((resolve) => { + const ready = await new Promise((resolve) => { + let counter = 0 const interval = setInterval(() => { + counter++ if (getState(InstanceServerState).ready) { clearInterval(interval) - resolve() + resolve(true) + } + if (counter > NON_READY_INTERVALS) { + clearInterval(interval) + resolve(false) } }, 100) }) + if (!ready) { + app.primus.write({ instanceReady: false }) + return + } + + app.primus.write({ instanceReady: true }) const network = getServerNetwork(app) const onAuthenticationRequest = async (data) => { diff --git a/packages/instanceserver/src/channels.ts b/packages/instanceserver/src/channels.ts index 38f141ed79..ec99989535 100755 --- a/packages/instanceserver/src/channels.ts +++ b/packages/instanceserver/src/channels.ts @@ -409,9 +409,13 @@ const updateInstance = async ({ if (isNeedingNewServer && !instanceStarted) { instanceStarted = true const initialized = await initializeInstance({ app, status, headers, userId }) - if (initialized) await loadEngine({ app, sceneId, headers }) - else instanceStarted = false - return true + if (initialized) { + await loadEngine({ app, sceneId, headers }) + return true + } else { + instanceStarted = false + return false + } } else { try { if (!getState(InstanceServerState).ready) From 98c0b1f8660ef60b24aa5134a524e9f9f8442d58 Mon Sep 17 00:00:00 2001 From: David Gordon <94419856+dinomut1@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:20:18 -0700 Subject: [PATCH 11/20] ignore hash when checking cache validity (#10900) --- .../loaders/gltf/extensions/CachedImageLoadExtension.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts b/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts index 706cbe7c98..683ed2f24a 100644 --- a/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts +++ b/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts @@ -35,7 +35,9 @@ class CachedImageLoadExtension extends ImporterExtension implements GLTFLoaderPl loadTexture(textureIndex) { const options = this.parser.options - if (!options.url?.endsWith('.gltf')) { + const baseURL = new URL(options.url) + + if (!baseURL.pathname.endsWith('.gltf')) { return this.parser.loadTexture(textureIndex) } const json = this.parser.json From 1195e4788fe9c4d85713397a66d1ce214a090416 Mon Sep 17 00:00:00 2001 From: Aditya Mitra <55396651+aditya-mitra@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:57:36 +0530 Subject: [PATCH 12/20] apply proper offsets to select (#10799) * apply proper offsets to select * add x offset calculation * fix offset in select --- packages/common/src/utils/offsets.ts | 44 +++++++++++++++++++ .../panels/Properties/container/index.tsx | 32 ++------------ .../src/primitives/tailwind/Select/index.tsx | 13 +++++- 3 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 packages/common/src/utils/offsets.ts diff --git a/packages/common/src/utils/offsets.ts b/packages/common/src/utils/offsets.ts new file mode 100644 index 0000000000..984f49135d --- /dev/null +++ b/packages/common/src/utils/offsets.ts @@ -0,0 +1,44 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import { Bounds, getBounds, getViewportBounds } from '@etherealengine/xrui' + +export const calculateAndApplyYOffset = (element: HTMLElement | null, additionalOffset = 0) => { + if (!element) { + return + } + const popupBounds = getBounds(element) + const viewportBounds = getViewportBounds(new Bounds()) + + const overflowBottom = + (popupBounds?.top ?? 0) + (popupBounds?.height ?? 0) - (viewportBounds.top + viewportBounds.height) + let offsetY = 0 + + if (overflowBottom > 0) { + offsetY = -(popupBounds?.height ?? 0) + additionalOffset + } + + element.style.transform = `translateY(${offsetY}px)` +} diff --git a/packages/ui/src/components/editor/panels/Properties/container/index.tsx b/packages/ui/src/components/editor/panels/Properties/container/index.tsx index e6184a67e3..124a8a207c 100644 --- a/packages/ui/src/components/editor/panels/Properties/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Properties/container/index.tsx @@ -30,13 +30,13 @@ import { UUIDComponent } from '@etherealengine/ecs' import { Component, ComponentJSONIDMap, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { NO_PROXY, getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' +import { calculateAndApplyYOffset } from '@etherealengine/common/src/utils/offsets' import { EntityUUID } from '@etherealengine/ecs' import { ComponentEditorsState } from '@etherealengine/editor/src/services/ComponentEditors' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { SelectionState } from '@etherealengine/editor/src/services/SelectionServices' import { GLTFNodeState } from '@etherealengine/engine/src/gltf/GLTFDocumentState' import { MaterialSelectionState } from '@etherealengine/engine/src/scene/materials/MaterialLibraryState' -import { Bounds, getBounds, getViewportBounds } from '@etherealengine/xrui/core/dom-utils' import { HiOutlinePlusCircle } from 'react-icons/hi' import Button from '../../../../../primitives/tailwind/Button' import { Popup } from '../../../../tailwind/Popup' @@ -55,29 +55,6 @@ const EntityComponentEditor = (props: { entity; component; multiEdit }) => { return } -const calculateAndApplyOffset = (popupRef: React.RefObject) => { - if (popupRef.current) { - const popupBounds = getBounds(popupRef.current) - const viewportBounds = getViewportBounds(new Bounds()) - - const overflowTop = viewportBounds.top - (popupBounds?.top ?? 0) - const overflowBottom = - (popupBounds?.top ?? 0) + (popupBounds?.height ?? 0) - (viewportBounds.top + viewportBounds.height) - - let offsetY = 0 - - if (overflowTop > 0) { - // popup is overflowing at the top, move it down - offsetY = overflowTop - } else if (overflowBottom > 0) { - // popup is overflowing at the bottom, move it up - offsetY = -overflowBottom - } - - popupRef.current.style.transform = `translateY(${offsetY}px)` - } -} - const EntityEditor = (props: { entityUUID: EntityUUID; multiEdit: boolean }) => { const { t } = useTranslation() const { entityUUID, multiEdit } = props @@ -96,15 +73,14 @@ const EntityEditor = (props: { entityUUID: EntityUUID; multiEdit: boolean }) => useEffect(() => { const handleResize = () => { - calculateAndApplyOffset(popupRef) + calculateAndApplyYOffset(popupRef.current) } window.addEventListener('resize', handleResize) - return () => { window.removeEventListener('resize', handleResize) } - }, [popupRef]) + }, []) const [isAddComponentMenuOpen, setIsAddComponentMenuOpen] = useState(false) @@ -128,7 +104,7 @@ const EntityEditor = (props: { entityUUID: EntityUUID; multiEdit: boolean }) => {t('editor:properties.lbl-addComponent')} } - onOpen={() => calculateAndApplyOffset(popupRef)} + onOpen={() => calculateAndApplyYOffset(popupRef.current)} >
setIsAddComponentMenuOpen(false)} /> diff --git a/packages/ui/src/primitives/tailwind/Select/index.tsx b/packages/ui/src/primitives/tailwind/Select/index.tsx index 6a17f995e1..2dc53d8288 100644 --- a/packages/ui/src/primitives/tailwind/Select/index.tsx +++ b/packages/ui/src/primitives/tailwind/Select/index.tsx @@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next' import { MdOutlineKeyboardArrowDown } from 'react-icons/md' import { twMerge } from 'tailwind-merge' +import { calculateAndApplyYOffset } from '@etherealengine/common/src/utils/offsets' import { useClickOutside } from '@etherealengine/common/src/utils/useClickOutside' import { useHookstate } from '@etherealengine/hyperflux' @@ -77,13 +78,22 @@ const Select = ({ inputContainerClassName }: SelectProps) => { const ref = useRef(null) + const menuRef = useRef(null) const { t } = useTranslation() const showOptions = useHookstate(false) - const filteredOptions = useHookstate(JSON.parse(JSON.stringify(options))) + const filteredOptions = useHookstate(JSON.parse(JSON.stringify(options)) as SelectOptionsType[]) const selectLabel = useHookstate('') useClickOutside(ref, () => showOptions.set(false)) + useEffect(() => { + const handleResize = () => { + calculateAndApplyYOffset(menuRef.current, -50) + } + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + useEffect(() => { const labelName = options.find((option) => option.value === currentValue)?.label if (labelName) selectLabel.set(labelName || '') @@ -157,6 +167,7 @@ const Select = ({ className={`absolute z-30 mt-2 w-full rounded border border-theme-primary bg-theme-surface-main ${ showOptions.value ? 'visible' : 'hidden' }`} + ref={menuRef} >
    li]:px-4 [&>li]:py-2', menuClassname)}> {filteredOptions.value.map((option) => ( From 4cbb8be01920292d9687719226ad978a3f9c33c6 Mon Sep 17 00:00:00 2001 From: Aditya Mitra <55396651+aditya-mitra@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:11:59 +0530 Subject: [PATCH 13/20] match tooltip with figma design (#10690) * match tooltip with figma design * fix check-errors * fix check errors * match title background with figma * fix check errors * fix check errors --------- Co-authored-by: Appaji --- .../client-core/src/admin/adminRoutes.tsx | 2 +- .../admin/components/project/ProjectTable.tsx | 6 +- .../admin/components/server/ServerTable.tsx | 2 +- .../components/settings/tabs/features.tsx | 2 +- .../components/settings/tabs/project.tsx | 3 +- .../components/user/AccountIdentifiers.tsx | 18 ++--- .../dialogs/ImportSettingsPanelDialog.tsx | 8 +-- .../components/editor/input/Group/index.tsx | 18 +---- .../editor/input/Numeric/Stepper/index.tsx | 10 +-- .../src/components/editor/layout/Tooltip.tsx | 41 ----------- .../editor/panels/Assets/container/index.tsx | 6 +- .../editor/panels/Files/browserGrid/index.tsx | 2 +- .../editor/panels/Files/container/index.tsx | 69 +++++++++++++++++-- .../editor/panels/Properties/index.tsx | 7 +- .../panels/Properties/material/index.tsx | 8 +-- .../panels/Viewport/tools/GizmoTool.tsx | 8 +-- .../editor/panels/Viewport/tools/GridTool.tsx | 4 +- .../panels/Viewport/tools/PlayModeTool.tsx | 8 +-- .../panels/Viewport/tools/RenderTool.tsx | 2 +- .../Viewport/tools/SceneHelpersTool.tsx | 24 +++---- .../Viewport/tools/TransformPivotTool.tsx | 4 +- .../Viewport/tools/TransformSnapTool.tsx | 8 +-- .../Viewport/tools/TransformSpaceTool.tsx | 9 ++- .../tailwind/Tooltip/index.stories.tsx | 2 +- .../src/primitives/tailwind/Tooltip/index.tsx | 33 ++++++--- 25 files changed, 156 insertions(+), 148 deletions(-) delete mode 100755 packages/ui/src/components/editor/layout/Tooltip.tsx diff --git a/packages/client-core/src/admin/adminRoutes.tsx b/packages/client-core/src/admin/adminRoutes.tsx index 5112926c2f..fb4f1f2e5a 100644 --- a/packages/client-core/src/admin/adminRoutes.tsx +++ b/packages/client-core/src/admin/adminRoutes.tsx @@ -71,7 +71,7 @@ const AdminTopBar = () => { )} - + diff --git a/packages/client-core/src/admin/components/project/ProjectTable.tsx b/packages/client-core/src/admin/components/project/ProjectTable.tsx index 42c99da25b..a664aeff37 100644 --- a/packages/client-core/src/admin/components/project/ProjectTable.tsx +++ b/packages/client-core/src/admin/components/project/ProjectTable.tsx @@ -221,12 +221,12 @@ export default function ProjectTable(props: { search: string }) { {row.name} {!!row.needsRebuild && ( - + )} {!!row.hasLocalChanges && ( - + )} @@ -243,7 +243,7 @@ export default function ProjectTable(props: { search: string }) { visibility: handleVisibilityChange(row)} />, commitSHA: ( - + <>{row.commitSHA?.slice(0, 8)} {' '} diff --git a/packages/client-core/src/admin/components/server/ServerTable.tsx b/packages/client-core/src/admin/components/server/ServerTable.tsx index 7663191f09..a98544ec45 100644 --- a/packages/client-core/src/admin/components/server/ServerTable.tsx +++ b/packages/client-core/src/admin/components/server/ServerTable.tsx @@ -66,7 +66,7 @@ function ServerStatus({ serverPodInfo }: { serverPodInfo: ServerPodInfoType }) { )}
    {serverPodInfo.containers.map((container) => ( - +
    ))} diff --git a/packages/client-core/src/admin/components/settings/tabs/features.tsx b/packages/client-core/src/admin/components/settings/tabs/features.tsx index 7bfe316da7..bdca91c495 100644 --- a/packages/client-core/src/admin/components/settings/tabs/features.tsx +++ b/packages/client-core/src/admin/components/settings/tabs/features.tsx @@ -121,7 +121,7 @@ const FeatureItem = ({ feature }: { feature: FeatureFlagSettingType }) => { /> {feature.userId && ( {appleIp ? ( - + ) : null} {discordIp ? ( - + ) : null} {googleIp ? ( - + ) : null} {facebookIp ? ( - + ) : null} {twitterIp ? ( - + ) : null} {linkedinIp ? ( - + ) : null} {githubIp ? ( - + ) : null} {smsIp ? ( - + ) : null} {emailIp ? ( - + ) : null} diff --git a/packages/editor/src/components/dialogs/ImportSettingsPanelDialog.tsx b/packages/editor/src/components/dialogs/ImportSettingsPanelDialog.tsx index 3af31d0272..77523e0ec9 100644 --- a/packages/editor/src/components/dialogs/ImportSettingsPanelDialog.tsx +++ b/packages/editor/src/components/dialogs/ImportSettingsPanelDialog.tsx @@ -78,7 +78,7 @@ const ImageCompressionBox = ({ compressProperties }: { compressProperties: State onChange={compressProperties.flipY.set} label={t('editor:properties.model.transform.flipY')} /> - +
    @@ -88,7 +88,7 @@ const ImageCompressionBox = ({ compressProperties }: { compressProperties: State onChange={compressProperties.srgb.set} label={t('editor:properties.model.transform.srgb')} /> - +
    @@ -98,7 +98,7 @@ const ImageCompressionBox = ({ compressProperties }: { compressProperties: State onChange={compressProperties.mipmaps.set} label={t('editor:properties.model.transform.mipmaps')} /> - +
@@ -108,7 +108,7 @@ const ImageCompressionBox = ({ compressProperties }: { compressProperties: State onChange={compressProperties.normalMap.set} label={t('editor:properties.model.transform.normalMap')} /> - + diff --git a/packages/ui/src/components/editor/input/Group/index.tsx b/packages/ui/src/components/editor/input/Group/index.tsx index a3dabb78df..33814b4b7d 100644 --- a/packages/ui/src/components/editor/input/Group/index.tsx +++ b/packages/ui/src/components/editor/input/Group/index.tsx @@ -30,7 +30,6 @@ import { MdOutlineHelpOutline } from 'react-icons/md' import { twMerge } from 'tailwind-merge' import Label from '../../../../primitives/tailwind/Label' import Tooltip from '../../../../primitives/tailwind/Tooltip' -import { InfoTooltip } from '../../layout/Tooltip' /** * Used to provide styles for InputGroupContainer div. @@ -98,21 +97,6 @@ export const InputGroupInfoIcon = ({ onClick = () => {} }) => ( /> ) -interface InputGroupInfoProp { - info: string | JSX.Element -} - -/** - * Used to render InfoTooltip component. - */ -export function InputGroupInfo({ info }: InputGroupInfoProp) { - return ( - - - - ) -} - export interface InputGroupProps { name?: string label: string @@ -142,7 +126,7 @@ export function InputGroup({
{info && ( - + )} diff --git a/packages/ui/src/components/editor/input/Numeric/Stepper/index.tsx b/packages/ui/src/components/editor/input/Numeric/Stepper/index.tsx index e9cb56064d..1aae8099e0 100644 --- a/packages/ui/src/components/editor/input/Numeric/Stepper/index.tsx +++ b/packages/ui/src/components/editor/input/Numeric/Stepper/index.tsx @@ -29,7 +29,7 @@ import { FaChevronLeft, FaChevronRight } from 'react-icons/fa' import { t } from 'i18next' import { twMerge } from 'tailwind-merge' import NumericInput, { NumericInputProp } from '..' -import { InfoTooltip } from '../../../layout/Tooltip' +import Tooltip from '../../../../../primitives/tailwind/Tooltip' export function NumericStepperInput({ style, @@ -54,23 +54,23 @@ export function NumericStepperInput({ return (
- + - + - + - +
) } diff --git a/packages/ui/src/components/editor/layout/Tooltip.tsx b/packages/ui/src/components/editor/layout/Tooltip.tsx deleted file mode 100755 index bd511855c8..0000000000 --- a/packages/ui/src/components/editor/layout/Tooltip.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* -CPAL-1.0 License - -The contents of this file are subject to the Common Public Attribution License -Version 1.0. (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at -https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. -The License is based on the Mozilla Public License Version 1.1, but Sections 14 -and 15 have been added to cover use of software over a computer network and -provide for limited attribution for the Original Developer. In addition, -Exhibit A has been modified to be consistent with Exhibit B. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the -specific language governing rights and limitations under the License. - -The Original Code is Ethereal Engine. - -The Original Developer is the Initial Developer. The Initial Developer of the -Original Code is the Ethereal Engine team. - -All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 -Ethereal Engine. All Rights Reserved. -*/ - -import React from 'react' -import Tooltip, { TooltipProps } from '../../../primitives/tailwind/Tooltip' - -export function InfoTooltip({ title, info, ...props }: TooltipProps & { info?: string }) { - const tooltipTitle = info ? ( -

- {title} -


- {info} -

- ) : ( - title - ) - - return -} diff --git a/packages/ui/src/components/editor/panels/Assets/container/index.tsx b/packages/ui/src/components/editor/panels/Assets/container/index.tsx index 8d7f69d3ce..f52482c543 100644 --- a/packages/ui/src/components/editor/panels/Assets/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Assets/container/index.tsx @@ -183,7 +183,7 @@ const ResourceFile = (props: { - + {name} @@ -592,13 +592,13 @@ const AssetPanel = () => {
- +
- +
diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx index 50da0ecdf8..70c60946f2 100644 --- a/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/index.tsx @@ -187,7 +187,7 @@ export const FileGridItem: React.FC = (props) => { />
- +
{props.item.fullName}
diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index 441eab9ae0..3008bb79bc 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -658,27 +658,84 @@ const FileBrowserContentPanel: React.FC = (props) ) } + const ViewModeSettings = () => { + const viewModeSettings = useHookstate(getMutableState(FilesViewModeSettings)) + return ( + +