diff --git a/packages/spatial/src/physics/classes/Physics.test.tsx b/packages/spatial/src/physics/classes/Physics.test.tsx index 8fd2154c72..a443f53748 100644 --- a/packages/spatial/src/physics/classes/Physics.test.tsx +++ b/packages/spatial/src/physics/classes/Physics.test.tsx @@ -122,6 +122,18 @@ export function assertVecAllApproxNotEq(A, B, elems: number, epsilon = Epsilon) if (elems > 3) assertFloatApproxNotEq(A.w, B.w, epsilon) } +export function assertMatrixApproxEq(A, B, epsilon = Epsilon) { + for (let id = 0; id < 16; ++id) { + assertFloatApproxEq(A.elements[id], B.elements[id], epsilon) + } +} + +export function assertMatrixAllApproxNotEq(A, B, epsilon = Epsilon) { + for (let id = 0; id < 16; ++id) { + assertFloatApproxNotEq(A.elements[id], B.elements[id], epsilon) + } +} + export const boxDynamicConfig = { shapeType: ShapeType.Cuboid, bodyType: RigidBodyType.Fixed, diff --git a/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx b/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx index 710cbf0a6f..6bee56c53b 100644 --- a/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx +++ b/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx @@ -1,36 +1,40 @@ -// /* -// 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 { Entity, EntityUUID, UUIDComponent, + UndefinedEntity, createEntity, destroyEngine, getComponent, getMutableComponent, + hasComponent, + removeEntity, + serializeComponent, setComponent } from '@etherealengine/ecs' import { createEngine } from '@etherealengine/ecs/src/Engine' @@ -41,79 +45,388 @@ import React from 'react' import { Fog, FogExp2, MathUtils, ShaderChunk } from 'three' import { mockSpatialEngine } from '../../../tests/util/mockSpatialEngine' import { EngineState } from '../../EngineState' +import { destroySpatialEngine, initializeSpatialEngine } from '../../initializeEngine' +import { assertFloatApproxEq, assertFloatApproxNotEq } from '../../physics/classes/Physics.test' import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { FogShaders as FogShadersList } from '../FogSystem' import { RendererComponent } from '../WebGLRendererSystem' import { FogSettingsComponent, FogType } from './FogSettingsComponent' import { FogShaders } from './FogShaders' import { FogComponent } from './SceneComponents' +const FogSettingsComponentDefaults = { + type: FogType.Disabled as FogType, + color: '#FFFFFF', + density: 0.005, + near: 1, + far: 1000, + timeScale: 1, + height: 0.05 +} + +function assertFogSettingsComponentEq(A, B): void { + assert.equal(A.type, B.type) + assert.equal(A.color, B.color) + assertFloatApproxEq(A.density, B.density) + assert.equal(A.near, B.near) + assert.equal(A.far, B.far) + assert.equal(A.timeScale, B.timeScale) + assertFloatApproxEq(A.height, B.height) +} + +function assertFogSettingsComponentJSONEq(A, B): void { + assert.equal(A.type, B.type) + assert.equal(A.color, B.color) + assertFloatApproxEq(A.density, B.density) + assert.equal(A.near, B.near) + assert.equal(A.far, B.far) + assert.equal(A.timeScale, B.timeScale) + assertFloatApproxEq(A.height, B.height) +} + describe('FogSettingsComponent', () => { - let rootEntity: Entity - let entity: Entity + describe('IDs', () => { + it('should initialize the FogSettingsComponent.name field with the expected value', () => { + assert.equal(FogSettingsComponent.name, 'FogSettingsComponent') + }) - beforeEach(() => { - createEngine() + it('should initialize the FogSettingsComponent.jsonID field with the expected value', () => { + assert.equal(FogSettingsComponent.jsonID, 'EE_fog') + }) + }) //:: IDs - mockSpatialEngine() + describe('onInit', () => { + let testEntity = UndefinedEntity - rootEntity = getState(EngineState).viewerEntity + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, FogSettingsComponent) + }) - entity = createEntity() - setComponent(entity, UUIDComponent, MathUtils.generateUUID() as EntityUUID) - setComponent(entity, FogSettingsComponent) - setComponent(entity, EntityTreeComponent) + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) - setComponent(rootEntity, RendererComponent, { scenes: [entity] }) - }) + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, FogSettingsComponent) + assertFogSettingsComponentEq(data, FogSettingsComponentDefaults) + }) + }) //:: onInit - afterEach(() => { - return destroyEngine() - }) + describe('toJSON', () => { + beforeEach(async () => { + createEngine() + }) + + afterEach(() => { + return destroyEngine() + }) + + it("should serialize the component's default data as expected", () => { + const testEntity = createEntity() + setComponent(testEntity, FogSettingsComponent) + + const result = serializeComponent(testEntity, FogSettingsComponent) + const Expected = { + type: 'disabled', + color: '#FFFFFF', + density: 0.005, + near: 1, + far: 1000, + timeScale: 1, + height: 0.05 + } + assertFogSettingsComponentJSONEq(result, Expected) + }) + + it("should serialize the component's non-default data as expected", () => { + const Data = { + type: FogType.Exponential as FogType, + color: '#123456', + density: 1.234, + near: 42, + far: 10_000, + timeScale: 2, + height: 0.3 + } + const testEntity = createEntity() + setComponent(testEntity, FogSettingsComponent, Data) + + const result = serializeComponent(testEntity, FogSettingsComponent) + const Expected = { + ...Data, + type: Data.type as string + } + assertFogSettingsComponentJSONEq(result, Expected) + }) + }) //:: toJSON + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, FogSettingsComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized FogSettingsComponent', () => { + assertFogSettingsComponentEq(getComponent(testEntity, FogSettingsComponent), FogSettingsComponentDefaults) + const Data = { + type: FogType.Exponential as FogType, + color: '#123456', + density: 1.234, + near: 42, + far: 10_000, + timeScale: 2, + height: 0.3 + } + setComponent(testEntity, FogSettingsComponent, Data) + const result = getComponent(testEntity, FogSettingsComponent) + assert.notEqual(result.type, FogSettingsComponentDefaults.type) + assert.equal(result.type, Data.type) + + assert.notEqual(result.color, FogSettingsComponentDefaults.color) + assert.equal(result.color, Data.color) + + assertFloatApproxNotEq(result.density, FogSettingsComponentDefaults.density) + assertFloatApproxEq(result.density, Data.density) + + assert.notEqual(result.near, FogSettingsComponentDefaults.near) + assert.equal(result.near, Data.near) + + assert.notEqual(result.far, FogSettingsComponentDefaults.far) + assert.equal(result.far, Data.far) + + assert.notEqual(result.timeScale, FogSettingsComponentDefaults.timeScale) + assert.equal(result.timeScale, Data.timeScale) + + assertFloatApproxNotEq(result.height, FogSettingsComponentDefaults.height) + assertFloatApproxEq(result.height, Data.height) + }) + + it('should not change values of an initialized FogSettingsComponent when the data passed had incorrect types', () => { + assertFogSettingsComponentEq(getComponent(testEntity, FogSettingsComponent), FogSettingsComponentDefaults) + const Incorrect = { + type: 1, + color: 2, + density: 'dense', + near: '1234', + far: 'far', + timeScale: 'timeScale', + height: 'height' + } + // @ts-ignore Coerce the data with incorrect types into the setComponent call + setComponent(testEntity, FogSettingsComponent, Incorrect) + + const result = getComponent(testEntity, FogSettingsComponent) + assertFogSettingsComponentEq(result, FogSettingsComponentDefaults) + }) + }) //:: onSet - it('Create Fog Setting Component', async () => { - const fogSettingsComponent = getMutableComponent(entity, FogSettingsComponent) - assert(fogSettingsComponent.value, 'fog setting component exists') - - fogSettingsComponent.type.set(FogType.Height) - fogSettingsComponent.color.set('#ff0000') - fogSettingsComponent.far.set(2000) - fogSettingsComponent.near.set(2) - fogSettingsComponent.density.set(0.02) - const { rerender, unmount } = render(<>) - - await act(() => { - rerender(<>) - }) - const fogComponent = getComponent(entity, FogComponent) - assert(fogComponent, 'created fog component') - assert(fogComponent.color.r == 1 && fogComponent.color.g == 0 && fogComponent.color.b == 0, 'fog set color') - const fog = fogComponent as Fog - assert(fog.far == 2000, 'fog set far') - assert(fog.near == 2, 'fog set near') - const fogExp2 = fogComponent as FogExp2 - assert(fogExp2.density == 0.02, 'fog set density') - - assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.heightFog) - assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.heightFog) - assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.heightFog) - assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.heightFog) - - fogSettingsComponent.type.set(FogType.Linear) - await act(() => { - rerender(<>) - }) - assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.default) - assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.default) - assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.default) - assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.default) - - fogSettingsComponent.type.set(FogType.Brownian) - await act(() => { - rerender(<>) - }) - assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.brownianMotionFog) - assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.brownianMotionFog) - assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.brownianMotionFog) - assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.brownianMotionFog) + describe('reactor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + initializeSpatialEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + destroySpatialEngine() + return destroyEngine() + }) + + it('should trigger when fog.type changes', () => { + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { type: FogType.Linear }) + assert.equal(hasComponent(testEntity, FogComponent), true) + }) + + it('should trigger when fog.color changes', () => { + const ExpectedString = '#000000' + const ExpectedColor = { r: 0, g: 0, b: 0, isColor: true } + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Linear }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assert.equal(getComponent(testEntity, FogSettingsComponent).color, FogSettingsComponentDefaults.color) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { color: ExpectedString }) + assert.equal(getComponent(testEntity, FogSettingsComponent).color, ExpectedString) + assert.deepEqual(getComponent(testEntity, FogComponent).color, ExpectedColor) + }) + + it('should trigger when fog.density changes', () => { + const Expected = 0.42 // (default: 0.005) + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Exponential }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).density, FogSettingsComponentDefaults.density) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { density: Expected }) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).density, Expected) + const result = getComponent(testEntity, FogComponent) as FogExp2 + assertFloatApproxEq(result.density, Expected) + }) + + it('should trigger when fog.near changes', () => { + const Expected = 42 // (default: 1) + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Exponential }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).density, FogSettingsComponentDefaults.density) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { near: Expected }) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).near, Expected) + const result = getComponent(testEntity, FogComponent) as Fog + assertFloatApproxEq(result.near, Expected) + }) + + it('should trigger when fog.far changes', () => { + const Expected = 42 // (default: 1) + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Exponential }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).density, FogSettingsComponentDefaults.density) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { far: Expected }) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).far, Expected) + const result = getComponent(testEntity, FogComponent) as Fog + assertFloatApproxEq(result.far, Expected) + }) + + it('should trigger when fog.height changes', () => { + const Expected = 0.42 // (default: 0.05) + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Height }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).height, FogSettingsComponentDefaults.height) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { height: Expected }) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).height, Expected) + for (const shader of FogShadersList) { + assertFloatApproxEq(shader.uniforms.heightFactor.value, Expected) + } + }) + + it('should trigger when fog.timeScale changes', () => { + const Expected = 42 // (default: 1) + setComponent(testEntity, FogSettingsComponent) + assert.equal(getComponent(testEntity, FogSettingsComponent).type, FogType.Disabled) + assert.equal(hasComponent(testEntity, FogComponent), false) + // Sanity check the initial data + setComponent(testEntity, FogSettingsComponent, { type: FogType.Brownian }) + assert.equal(hasComponent(testEntity, FogComponent), true) + assertFloatApproxEq( + getComponent(testEntity, FogSettingsComponent).timeScale, + FogSettingsComponentDefaults.timeScale + ) + // Trigger the reactor and Check the result + setComponent(testEntity, FogSettingsComponent, { timeScale: Expected }) + assertFloatApproxEq(getComponent(testEntity, FogSettingsComponent).timeScale, Expected) + for (const shader of FogShadersList) { + assertFloatApproxEq(shader.uniforms.fogTimeScale.value, Expected) + } + }) + }) //:: reactor + + describe('General Purpose', () => { + let rootEntity: Entity + let entity: Entity + + beforeEach(() => { + createEngine() + + mockSpatialEngine() + + rootEntity = getState(EngineState).viewerEntity + + entity = createEntity() + setComponent(entity, UUIDComponent, MathUtils.generateUUID() as EntityUUID) + setComponent(entity, FogSettingsComponent) + setComponent(entity, EntityTreeComponent) + + setComponent(rootEntity, RendererComponent, { scenes: [entity] }) + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should initialize/create a FogSettingsComponent, and all its data, as expected', async () => { + const fogSettingsComponent = getMutableComponent(entity, FogSettingsComponent) + assert(fogSettingsComponent.value, 'fog setting component exists') + + fogSettingsComponent.type.set(FogType.Height) + fogSettingsComponent.color.set('#ff0000') + fogSettingsComponent.far.set(2000) + fogSettingsComponent.near.set(2) + fogSettingsComponent.density.set(0.02) + const { rerender, unmount } = render(<>) + + await act(() => { + rerender(<>) + }) + const fogComponent = getComponent(entity, FogComponent) + assert(fogComponent, 'created fog component') + assert(fogComponent.color.r == 1 && fogComponent.color.g == 0 && fogComponent.color.b == 0, 'fog set color') + const fog = fogComponent as Fog + assert(fog.far == 2000, 'fog set far') + assert(fog.near == 2, 'fog set near') + const fogExp2 = fogComponent as FogExp2 + assert(fogExp2.density == 0.02, 'fog set density') + + assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.heightFog) + assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.heightFog) + assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.heightFog) + assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.heightFog) + + fogSettingsComponent.type.set(FogType.Linear) + await act(() => { + rerender(<>) + }) + assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.default) + assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.default) + assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.default) + assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.default) + + fogSettingsComponent.type.set(FogType.Brownian) + await act(() => { + rerender(<>) + }) + assert(ShaderChunk.fog_fragment == FogShaders.fog_fragment.brownianMotionFog) + assert(ShaderChunk.fog_pars_fragment == FogShaders.fog_pars_fragment.brownianMotionFog) + assert(ShaderChunk.fog_vertex == FogShaders.fog_vertex.brownianMotionFog) + assert(ShaderChunk.fog_pars_vertex == FogShaders.fog_pars_vertex.brownianMotionFog) + + unmount() + }) }) }) diff --git a/packages/spatial/src/renderer/components/FogShaders.test.ts b/packages/spatial/src/renderer/components/FogShaders.test.ts new file mode 100644 index 0000000000..f4baee2962 --- /dev/null +++ b/packages/spatial/src/renderer/components/FogShaders.test.ts @@ -0,0 +1,125 @@ +/* +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 { ShaderChunk } from 'three' +import { FogShaders, initBrownianMotionFogShader, initHeightFogShader, removeFogShader } from './FogShaders' + +describe('FogShaders', () => { + describe('removeFogShader', () => { + it('should set ShaderChunk.fog_pars_fragment to the default fog_pars_fragment shader', () => { + assert.equal(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.default) + initHeightFogShader() + assert.notEqual(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.default) + removeFogShader() + assert.equal(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.default) + }) + + it('should set ShaderChunk.fog_pars_vertex to the default fog_pars_vertex shader', () => { + assert.equal(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.default) + initHeightFogShader() + assert.notEqual(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.default) + removeFogShader() + assert.equal(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.default) + }) + + it('should set ShaderChunk.fog_fragment to the default fog_fragment shader', () => { + assert.equal(ShaderChunk.fog_fragment, FogShaders.fog_fragment.default) + initHeightFogShader() + assert.notEqual(ShaderChunk.fog_fragment, FogShaders.fog_fragment.default) + removeFogShader() + assert.equal(ShaderChunk.fog_fragment, FogShaders.fog_fragment.default) + }) + + it('should set ShaderChunk.fog_vertex to the default fog_vertex shader', () => { + assert.equal(ShaderChunk.fog_vertex, FogShaders.fog_vertex.default) + initHeightFogShader() + assert.notEqual(ShaderChunk.fog_vertex, FogShaders.fog_vertex.default) + removeFogShader() + assert.equal(ShaderChunk.fog_vertex, FogShaders.fog_vertex.default) + }) + }) + + describe('initBrownianMotionFogShader', () => { + afterEach(() => { + removeFogShader() + }) + + it('should initialize ShaderChunk.fog_pars_vertex with the brownianMotionFog pars vertex shader', () => { + assert.notEqual(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.brownianMotionFog) + initBrownianMotionFogShader() + assert.equal(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.brownianMotionFog) + }) + + it('should initialize ShaderChunk.fog_vertex with the brownianMotionFog vertex shader', () => { + assert.notEqual(ShaderChunk.fog_vertex, FogShaders.fog_vertex.brownianMotionFog) + initBrownianMotionFogShader() + assert.equal(ShaderChunk.fog_vertex, FogShaders.fog_vertex.brownianMotionFog) + }) + + it('should initialize ShaderChunk.fog_pars_fragment with the brownianMotionFog pars fragment shader', () => { + assert.notEqual(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.brownianMotionFog) + initBrownianMotionFogShader() + assert.equal(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.brownianMotionFog) + }) + + it('should initialize ShaderChunk.fog_fragment with the brownianMotionFog fragment shader', () => { + assert.notEqual(ShaderChunk.fog_fragment, FogShaders.fog_fragment.brownianMotionFog) + initBrownianMotionFogShader() + assert.equal(ShaderChunk.fog_fragment, FogShaders.fog_fragment.brownianMotionFog) + }) + }) + + describe('initHeightFogShader', () => { + afterEach(() => { + removeFogShader() + }) + + it('should initialize ShaderChunk.fog_pars_vertex with the heightFog pars vertex shader', () => { + assert.notEqual(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.heightFog) + initHeightFogShader() + assert.equal(ShaderChunk.fog_pars_vertex, FogShaders.fog_pars_vertex.heightFog) + }) + + it('should initialize ShaderChunk.fog_vertex with the heightFog vertex shader', () => { + assert.notEqual(ShaderChunk.fog_vertex, FogShaders.fog_vertex.heightFog) + initHeightFogShader() + assert.equal(ShaderChunk.fog_vertex, FogShaders.fog_vertex.heightFog) + }) + + it('should initialize ShaderChunk.fog_pars_fragment with the heightFog pars fragment shader', () => { + assert.notEqual(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.heightFog) + initHeightFogShader() + assert.equal(ShaderChunk.fog_pars_fragment, FogShaders.fog_pars_fragment.heightFog) + }) + + it('should initialize ShaderChunk.fog_fragment with the heightFog fragment shader', () => { + assert.notEqual(ShaderChunk.fog_fragment, FogShaders.fog_fragment.heightFog) + initHeightFogShader() + assert.equal(ShaderChunk.fog_fragment, FogShaders.fog_fragment.heightFog) + }) + }) +}) diff --git a/packages/spatial/src/renderer/components/GroupComponent.test.tsx b/packages/spatial/src/renderer/components/GroupComponent.test.tsx new file mode 100644 index 0000000000..2c185e35c6 --- /dev/null +++ b/packages/spatial/src/renderer/components/GroupComponent.test.tsx @@ -0,0 +1,423 @@ +/* +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 { + EntityContext, + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + hasComponent, + removeComponent, + removeEntity, + setComponent +} from '@etherealengine/ecs' +import { ReactorRoot, startReactor } from '@etherealengine/hyperflux' +import assert from 'assert' +import React from 'react' +import sinon from 'sinon' +import { BoxGeometry, Layers, Matrix4, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three' +import { + assertMatrixAllApproxNotEq, + assertMatrixApproxEq, + assertVecAllApproxNotEq, + assertVecApproxEq +} from '../../physics/classes/Physics.test' +import { assertArrayEqual } from '../../physics/components/RigidBodyComponent.test' +import { TransformComponent } from '../RendererModule' +import { + GroupComponent, + GroupQueryReactor, + GroupReactor, + addObjectToGroup, + removeGroupComponent, + removeObjectFromGroup +} from './GroupComponent' +import { Layer } from './ObjectLayerComponent' +import { VisibleComponent } from './VisibleComponent' + +const GroupComponentDefaults = [] as Object3D[] + +function assertGroupComponentEq(A, B) { + assertArrayEqual(A, B) +} + +describe('GroupComponent', () => { + describe('IDs', () => { + it('should initialize the GroupComponent.name field with the expected value', () => { + assert.equal(GroupComponent.name, 'GroupComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, GroupComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, GroupComponent) + assertGroupComponentEq(data, GroupComponentDefaults) + }) + }) //:: onInit + + describe('onRemove', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, GroupComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should call the T.removeFromParent function for every object in the group that has a parent', () => { + const spy1 = sinon.spy() + const spy2 = sinon.spy() + const meshParent = new Mesh(new BoxGeometry()) + const mesh1 = new Mesh(new BoxGeometry()) + const mesh2 = new Mesh(new SphereGeometry()) + mesh1.removeFromParent = spy1 + mesh2.removeFromParent = spy2 + mesh1.name = 'Mesh1' + mesh2.name = 'Mesh2' + mesh1.parent = meshParent + mesh2.parent = meshParent + addObjectToGroup(testEntity, mesh1) + addObjectToGroup(testEntity, mesh2) + assert.equal(spy1.called, false) + assert.equal(spy2.called, false) + // Run and Check the result + removeComponent(testEntity, GroupComponent) + assert.equal(spy1.called, true) + assert.equal(spy2.called, true) + }) + }) //:: onRemove + + describe('removeGroupComponent', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, GroupComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should call the T.removeFromParent function for every object in the group', () => { + const spy1 = sinon.spy() + const spy2 = sinon.spy() + const mesh1 = new Mesh(new BoxGeometry()) + const mesh2 = new Mesh(new SphereGeometry()) + mesh1.removeFromParent = spy1 + mesh2.removeFromParent = spy2 + mesh1.name = 'Mesh1' + mesh2.name = 'Mesh2' + addObjectToGroup(testEntity, mesh1) + addObjectToGroup(testEntity, mesh2) + assert.equal(spy1.called, false) + assert.equal(spy2.called, false) + // Run and Check the result + removeGroupComponent(testEntity) + assert.equal(spy1.called, true) + assert.equal(spy2.called, true) + }) + + it('should remove the GroupComponent from the entity', () => { + assert.equal(hasComponent(testEntity, GroupComponent), true) + // Run and Check the result + removeGroupComponent(testEntity) + assert.equal(hasComponent(testEntity, GroupComponent), false) + }) + }) //:: removeGroupComponent + + describe('removeObjectFromGroup', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, GroupComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should call the T.removeFromParent function of the object if the object has a parent', () => { + const spy = sinon.spy() + const meshParent = new Mesh(new BoxGeometry()) + const mesh = new Mesh(new BoxGeometry()) + mesh.removeFromParent = spy + mesh.name = 'Mesh1' + mesh.parent = meshParent + addObjectToGroup(testEntity, mesh) + assert.equal(spy.called, false) + // Run and Check the result + removeObjectFromGroup(testEntity, mesh) + assert.equal(spy.called, true) + }) + + it('should remove the `@param object` from the GroupComponent list if the entity has a GroupComponent that contains the `@param object`', () => { + const mesh1 = new Mesh(new BoxGeometry()) + const mesh2 = new Mesh(new SphereGeometry()) + addObjectToGroup(testEntity, mesh1) + addObjectToGroup(testEntity, mesh2) + assert.equal(getComponent(testEntity, GroupComponent).includes(mesh2), true) + // Run and Check the result + removeObjectFromGroup(testEntity, mesh2) + assert.equal(getComponent(testEntity, GroupComponent).includes(mesh2), false) + }) + + it('should remove the GroupComponent from the entity if the group has no objects left after removing the `@param object`', () => { + const mesh = new Mesh(new BoxGeometry()) + addObjectToGroup(testEntity, mesh) + assert.equal(getComponent(testEntity, GroupComponent).includes(mesh), true) + // Run and Check the result + removeObjectFromGroup(testEntity, mesh) + assert.equal(hasComponent(testEntity, GroupComponent), false) + }) + }) //:: removeObjectFromGroup + + describe('addObjectToGroup', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should add the object to the GroupComponent list of objects', () => { + setComponent(testEntity, GroupComponent) + const mesh = new Mesh(new BoxGeometry()) + assert.equal(getComponent(testEntity, GroupComponent).includes(mesh), false) + addObjectToGroup(testEntity, mesh) + assert.equal(getComponent(testEntity, GroupComponent).includes(mesh), true) + }) + + it("should add a GroupComponent to the entity if it doesn't already have one", () => { + assert.equal(hasComponent(testEntity, GroupComponent), false) + const mesh = new Mesh(new BoxGeometry()) + addObjectToGroup(testEntity, mesh) + assert.equal(hasComponent(testEntity, GroupComponent), true) + }) + + it("should add a TransformComponent to the entity if it doesn't already have one", () => { + assert.equal(hasComponent(testEntity, TransformComponent), false) + const mesh = new Mesh(new BoxGeometry()) + addObjectToGroup(testEntity, mesh) + assert.equal(hasComponent(testEntity, TransformComponent), true) + }) + + it('should set the entity property of the `@param object` to `@param entity`', () => { + const mesh = new Mesh(new BoxGeometry()) + assert.notEqual(mesh.entity, testEntity) + addObjectToGroup(testEntity, mesh) + assert.equal(mesh.entity, testEntity) + }) + + it("should set the position value of the object to the value of the entity's TransformComponent.position", () => { + const Expected = new Vector3(40, 41, 42) + const mesh = new Mesh(new BoxGeometry()) + setComponent(testEntity, TransformComponent, { position: Expected }) + assertVecAllApproxNotEq(mesh.position, getComponent(testEntity, TransformComponent).position, 3) + addObjectToGroup(testEntity, mesh) + assertVecApproxEq(mesh.position, getComponent(testEntity, TransformComponent).position, 3) + }) + + it("should set the quaterion value of the object to the value of the entity's TransformComponent.rotation", () => { + const Expected = new Quaternion(40, 41, 42, 43).normalize() + const mesh = new Mesh(new BoxGeometry()) + setComponent(testEntity, TransformComponent, { rotation: Expected }) + assertVecAllApproxNotEq(mesh.quaternion, getComponent(testEntity, TransformComponent).rotation, 4) + addObjectToGroup(testEntity, mesh) + assertVecApproxEq(mesh.quaternion, getComponent(testEntity, TransformComponent).rotation, 4) + }) + + it("should set the scale value of the object to the value of the entity's TransformComponent.scale", () => { + const Expected = new Vector3(40, 41, 42) + const mesh = new Mesh(new BoxGeometry()) + setComponent(testEntity, TransformComponent, { scale: Expected }) + assertVecAllApproxNotEq(mesh.scale, getComponent(testEntity, TransformComponent).scale, 3) + addObjectToGroup(testEntity, mesh) + assertVecApproxEq(mesh.scale, getComponent(testEntity, TransformComponent).scale, 3) + }) + + it('should set the matrixAutoUpdate value of the object to false', () => { + const mesh = new Mesh(new BoxGeometry()) + assert.equal(mesh.matrixAutoUpdate, true) + addObjectToGroup(testEntity, mesh) + assert.equal(mesh.matrixAutoUpdate, false) + }) + + it('should set the matrixWorldAutoUpdate value of the object to false', () => { + const mesh = new Mesh(new BoxGeometry()) + assert.equal(mesh.matrixWorldAutoUpdate, true) + addObjectToGroup(testEntity, mesh) + assert.equal(mesh.matrixWorldAutoUpdate, false) + }) + + it('should set the frustumCulled value of the object to false', () => { + const mesh = new Mesh(new BoxGeometry()) + assert.equal(mesh.frustumCulled, true) + addObjectToGroup(testEntity, mesh) + assert.equal(mesh.frustumCulled, false) + }) + + it('should set the layers value of the object to a new Layer whichs ID is the `@param entity`', () => { + const mesh = new Mesh(new BoxGeometry()) + assert.equal(mesh.layers instanceof Layers, true) + addObjectToGroup(testEntity, mesh) + assert.equal(mesh.layers instanceof Layer, true) + // @ts-ignore Typescript doesn't understand the ObjectLayer type override done by addObjectToGroup + assert.equal(mesh.layers.entity, testEntity) + }) + + it("should set the matrix value of the object to the value of the entity's TransformComponent.matrix", () => { + const Expected = new Matrix4() + for (let id = 0; id < 16; ++id) Expected.elements[id] = id * 0.001 + const position = new Vector3() + const rotation = new Quaternion() + const scale = new Vector3() + Expected.decompose(position, rotation, scale) + const mesh = new Mesh(new BoxGeometry()) + setComponent(testEntity, TransformComponent, { position: position, rotation: rotation, scale: scale }) + assertMatrixAllApproxNotEq(mesh.matrix, Expected) + addObjectToGroup(testEntity, mesh) + assertMatrixApproxEq(mesh.matrix, getComponent(testEntity, TransformComponent).matrix) + }) + + it("should set the matrixWorld value of the object to the value of the entity's TransformComponent.matrixWorld", () => { + const Expected = new Matrix4() + for (let id = 0; id < 16; ++id) Expected.elements[id] = id * 0.001 + const position = new Vector3() + const rotation = new Quaternion() + const scale = new Vector3() + Expected.decompose(position, rotation, scale) + const mesh = new Mesh(new BoxGeometry()) + setComponent(testEntity, TransformComponent, { position: position, rotation: rotation, scale: scale }) + assertMatrixAllApproxNotEq(mesh.matrixWorld, Expected) + addObjectToGroup(testEntity, mesh) + assertMatrixApproxEq(mesh.matrixWorld, getComponent(testEntity, TransformComponent).matrixWorld) + }) + }) //:: addObjectToGroup + + describe('GroupReactor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should run the `@param GroupChildReactor` once for every object contained in the GroupComponent of the entity', () => { + const mesh1 = new Mesh(new BoxGeometry()) + const mesh2 = new Mesh(new BoxGeometry()) + addObjectToGroup(testEntity, mesh1) + addObjectToGroup(testEntity, mesh2) + setComponent(testEntity, GroupComponent) + const thingSpy = sinon.spy() + function Thing() { + thingSpy() + return null + } + const root = startReactor(() => { + return React.createElement( + EntityContext.Provider, + { value: testEntity }, + + ) + }) as ReactorRoot + root.run() + assert.equal(thingSpy.callCount, 2) + removeObjectFromGroup(testEntity, mesh1) + removeObjectFromGroup(testEntity, mesh2) + root.run() + assert.equal(thingSpy.callCount, 2) + }) + }) //:: GroupReactor + + describe('GroupQueryReactor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should run the `@param GroupChildReactor` once for every entity that contains both a GroupComponent and the given list of `@param Components`', () => { + const mesh1 = new Mesh(new BoxGeometry()) + const mesh2 = new Mesh(new BoxGeometry()) + addObjectToGroup(testEntity, mesh1) + addObjectToGroup(testEntity, mesh2) + setComponent(testEntity, GroupComponent) + const thingSpy = sinon.spy() + function Thing() { + thingSpy() + return null + } + const ComponentList = [VisibleComponent] + for (const component of ComponentList) setComponent(testEntity, component) + const root = startReactor(() => { + return + }) + root.run() + assert.equal(thingSpy.callCount, 2) + }) + }) //:: GroupQueryReactor +}) diff --git a/packages/spatial/src/renderer/components/HighlightComponent.test.tsx b/packages/spatial/src/renderer/components/HighlightComponent.test.tsx new file mode 100644 index 0000000000..d1a0120bb9 --- /dev/null +++ b/packages/spatial/src/renderer/components/HighlightComponent.test.tsx @@ -0,0 +1,274 @@ +/* +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 { + Engine, + Entity, + EntityUUID, + SystemDefinitions, + UUIDComponent, + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + getMutableComponent, + getOptionalComponent, + hasComponent, + removeComponent, + removeEntity, + setComponent +} from '@etherealengine/ecs' +import { getMutableState, getState } from '@etherealengine/hyperflux' +import { act, render } from '@testing-library/react' +import assert from 'assert' +import React from 'react' +import { BoxGeometry, MathUtils, Mesh } from 'three' +import { mockSpatialEngine } from '../../../tests/util/mockSpatialEngine' +import { EngineState } from '../../EngineState' +import { destroySpatialEngine } from '../../initializeEngine' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { TransformComponent } from '../RendererModule' +import { RendererState } from '../RendererState' +import { RendererComponent, WebGLRendererSystem } from '../WebGLRendererSystem' +import { GroupComponent, addObjectToGroup } from './GroupComponent' +import { HighlightComponent, HighlightSystem } from './HighlightComponent' +import { MeshComponent } from './MeshComponent' +import { PostProcessingComponent } from './PostProcessingComponent' +import { SceneComponent } from './SceneComponents' +import { VisibleComponent } from './VisibleComponent' + +describe('HighlightComponent', () => { + describe('IDs', () => { + it('should initialize the HighlightComponent.name field with the expected value', () => { + assert.equal(HighlightComponent.name, 'HighlightComponent') + }) + }) //:: IDs +}) + +describe('HighlightSystem', () => { + describe('IDs', () => { + it('should initialize the HighlightSystem.uuid field with the expected value', () => { + assert.equal(SystemDefinitions.get(HighlightSystem)!.uuid, 'HighlightSystem') + }) + }) //:: IDs + + describe('insert', () => { + it('should be set to run before the WebGLRendererSystem', () => { + const insert = SystemDefinitions.get(HighlightSystem)!.insert + assert.notEqual(insert, undefined) + assert.equal(insert?.before, WebGLRendererSystem) + assert.equal(insert?.with, undefined) + assert.equal(insert?.after, undefined) + }) + }) + + describe('execute', () => { + beforeEach(async () => { + createEngine() + }) + + afterEach(() => { + destroySpatialEngine() + return destroyEngine() + }) + + function createOutlineEntity(name: string): { id: Entity; name: string } { + const result = createEntity() + setComponent(result, HighlightComponent) + setComponent(result, VisibleComponent) + setComponent(result, MeshComponent, new Mesh(new BoxGeometry())) + getMutableComponent(result, MeshComponent).name.set(name) + setComponent(result, GroupComponent) + setComponent(result, EntityTreeComponent) + return { + id: result, + name: name + } + } + + it('should set the list of objects of every entity that has a MeshComponent, a GroupComponent and a VisibleComponent to the rendererComponent.effectComposer?.OutlineEffect?.selection list', () => { + mockSpatialEngine() + const entity1 = createOutlineEntity('entity1') + const entity2 = createOutlineEntity('entity2') + const entity3 = createOutlineEntity('entity3') + const Expected = [entity1.name, entity2.name, entity3.name] + // Get the system definition + const highlightSystemExecute = SystemDefinitions.get(HighlightSystem)!.execute + // Run and Check the result + highlightSystemExecute() + const result = getComponent(Engine.instance.viewerEntity, RendererComponent).effectComposer?.OutlineEffect + .selection + const list = [...result!.values()] + list.forEach((value, _) => assert.equal(Expected.includes(value.name), true)) + }) + + it('should not do anything if the Engine.instance.viewerEntity does not have a RendererComponent', () => { + const entity1 = createOutlineEntity('entity1') + const entity2 = createOutlineEntity('entity2') + const entity3 = createOutlineEntity('entity3') + const Expected = [entity1.name, entity2.name, entity3.name] + // Sanity check before running + assert.equal(hasComponent(getState(EngineState).viewerEntity, RendererComponent), false) + // Get the system definition + const highlightSystemExecute = SystemDefinitions.get(HighlightSystem)!.execute + // Run and Check the result + highlightSystemExecute() + const result = getOptionalComponent(getState(EngineState).viewerEntity, RendererComponent)?.effectComposer + ?.OutlineEffect.selection + assert.equal(result, undefined) + }) + + it("should not add any child of the query [HighlightComponent, VisibleComponent] that doesn't have a MeshComponent to the rendererComponent.effectComposer?.OutlineEffect?.selection list", () => { + mockSpatialEngine() + + const queryEntity = createEntity() + const mesh = new Mesh(new BoxGeometry()) + setComponent(queryEntity, HighlightComponent) + setComponent(queryEntity, VisibleComponent) + setComponent(queryEntity, TransformComponent) + setComponent(queryEntity, MeshComponent, mesh) + addObjectToGroup(queryEntity, mesh) + const notQueryEntity1 = createOutlineEntity('notQueryEntity1') + const notQueryEntity2 = createOutlineEntity('notQueryEntity2') + setComponent(notQueryEntity1.id, EntityTreeComponent, { parentEntity: queryEntity }) + setComponent(notQueryEntity2.id, EntityTreeComponent, { parentEntity: queryEntity }) + removeComponent(notQueryEntity1.id, MeshComponent) + removeComponent(notQueryEntity2.id, MeshComponent) + + // Get the system definition + const highlightSystemExecute = SystemDefinitions.get(HighlightSystem)!.execute + // Run and Check the result + highlightSystemExecute() + const result = getOptionalComponent(getState(EngineState).viewerEntity, RendererComponent)?.effectComposer + ?.OutlineEffect.selection + for (const obj of result!) { + assert.notEqual(obj.entity, notQueryEntity1) + assert.notEqual(obj.entity, notQueryEntity2) + } + }) + + it("should not add any child of the query [HighlightComponent, VisibleComponent] that doesn't have a GroupComponent to the rendererComponent.effectComposer?.OutlineEffect?.selection list", () => { + mockSpatialEngine() + + const queryEntity = createEntity() + const mesh = new Mesh(new BoxGeometry()) + setComponent(queryEntity, HighlightComponent) + setComponent(queryEntity, VisibleComponent) + setComponent(queryEntity, TransformComponent) + setComponent(queryEntity, MeshComponent, mesh) + addObjectToGroup(queryEntity, mesh) + const notQueryEntity1 = createOutlineEntity('notQueryEntity1') + const notQueryEntity2 = createOutlineEntity('notQueryEntity2') + setComponent(notQueryEntity1.id, EntityTreeComponent, { parentEntity: queryEntity }) + setComponent(notQueryEntity2.id, EntityTreeComponent, { parentEntity: queryEntity }) + removeComponent(notQueryEntity1.id, GroupComponent) + removeComponent(notQueryEntity2.id, GroupComponent) + + // Get the system definition + const highlightSystemExecute = SystemDefinitions.get(HighlightSystem)!.execute + // Run and Check the result + highlightSystemExecute() + const result = getOptionalComponent(getState(EngineState).viewerEntity, RendererComponent)?.effectComposer + ?.OutlineEffect.selection + for (const obj of result!) { + assert.notEqual(obj.entity, notQueryEntity1) + assert.notEqual(obj.entity, notQueryEntity2) + } + }) + + it("should not add any child of the query [HighlightComponent, VisibleComponent] that doesn't have a VisibleComponent to the rendererComponent.effectComposer?.OutlineEffect?.selection list", () => { + mockSpatialEngine() + + const queryEntity = createEntity() + const mesh = new Mesh(new BoxGeometry()) + setComponent(queryEntity, HighlightComponent) + setComponent(queryEntity, VisibleComponent) + setComponent(queryEntity, TransformComponent) + setComponent(queryEntity, MeshComponent, mesh) + addObjectToGroup(queryEntity, mesh) + const notQueryEntity1 = createOutlineEntity('notQueryEntity1') + const notQueryEntity2 = createOutlineEntity('notQueryEntity2') + setComponent(notQueryEntity1.id, EntityTreeComponent, { parentEntity: queryEntity }) + setComponent(notQueryEntity2.id, EntityTreeComponent, { parentEntity: queryEntity }) + removeComponent(notQueryEntity1.id, VisibleComponent) + removeComponent(notQueryEntity2.id, VisibleComponent) + + // Get the system definition + const highlightSystemExecute = SystemDefinitions.get(HighlightSystem)!.execute + // Run and Check the result + highlightSystemExecute() + const result = getOptionalComponent(getState(EngineState).viewerEntity, RendererComponent)?.effectComposer + ?.OutlineEffect.selection + for (const obj of result!) { + assert.notEqual(obj.entity, notQueryEntity1) + assert.notEqual(obj.entity, notQueryEntity2) + } + }) + }) + + describe('General Purpose', () => { + let rootEntity = UndefinedEntity + let testEntity = UndefinedEntity + + beforeEach(() => { + createEngine() + mockSpatialEngine() + rootEntity = getState(EngineState).viewerEntity + + testEntity = createEntity() + setComponent(testEntity, UUIDComponent, MathUtils.generateUUID() as EntityUUID) + getMutableState(RendererState).usePostProcessing.set(true) + setComponent(testEntity, SceneComponent) + setComponent(testEntity, PostProcessingComponent, { enabled: true }) + setComponent(testEntity, EntityTreeComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + removeEntity(rootEntity) + return destroyEngine() + }) + + it('should add the OutlineEffect to the RendererComponent.effectComposer.EffectPass.effects list', async () => { + const effectKey = 'OutlineEffect' + + // force nested reactors to run + const { rerender, unmount } = render(<>) + await act(() => rerender(<>)) + + // Check that the effect composer is setup + const effectComposer = getComponent(rootEntity, RendererComponent).effectComposer + assert.notEqual(Boolean(effectComposer), false, 'the effect composer is not setup correctly') + + // Check that the effect pass has the the effect set + // @ts-ignore Allow access to the `effects` private field + const effects = effectComposer.EffectPass.effects + assert.equal(Boolean(effects.find((it) => it.name == effectKey)), true) + + unmount() + }) + }) +}) diff --git a/packages/spatial/src/renderer/components/InfiniteGridHelper.test.ts b/packages/spatial/src/renderer/components/InfiniteGridHelper.test.ts new file mode 100644 index 0000000000..35813cf6f8 --- /dev/null +++ b/packages/spatial/src/renderer/components/InfiniteGridHelper.test.ts @@ -0,0 +1,267 @@ +/* +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 { + createEngine, + createEntity, + defineQuery, + destroyEngine, + getComponent, + getMutableComponent, + hasComponent, + removeComponent, + removeEntity, + setComponent, + UndefinedEntity +} from '@etherealengine/ecs' +import { getMutableState, getState } from '@etherealengine/hyperflux' +import assert from 'assert' +import { Color, ShaderMaterial } from 'three' +import { NameComponent } from '../../common/NameComponent' +import { assertFloatApproxEq, assertFloatApproxNotEq } from '../../physics/classes/Physics.test' +import { EntityTreeComponent } from '../../transform/components/EntityTree' +import { RendererState } from '../RendererState' +import { createInfiniteGridHelper, InfiniteGridComponent } from './InfiniteGridHelper' +import { LineSegmentComponent } from './LineSegmentComponent' +import { MeshComponent } from './MeshComponent' +import { VisibleComponent } from './VisibleComponent' + +type InfiniteGridComponentData = { + size: number + color: Color + distance: number +} + +const InfiniteGridComponentDefaults = { + size: 1, + color: new Color(0x535353), + distance: 200 +} as InfiniteGridComponentData + +function assertInfiniteGridComponentEq(A: InfiniteGridComponentData, B: InfiniteGridComponentData) { + assert.equal(A.size, B.size) + assertFloatApproxEq(A.color.r, B.color.r) + assertFloatApproxEq(A.color.g, B.color.g) + assertFloatApproxEq(A.color.b, B.color.b) + assert.equal(A.distance, B.distance) +} + +function assertInfiniteGridComponentNotEq(A: InfiniteGridComponentData, B: InfiniteGridComponentData) { + assert.notEqual(A.size, B.size) + assertFloatApproxNotEq(A.color.r, B.color.r) + assertFloatApproxNotEq(A.color.g, B.color.g) + assertFloatApproxNotEq(A.color.b, B.color.b) + assert.notEqual(A.distance, B.distance) +} + +describe('InfiniteGridComponent', () => { + describe('IDs', () => { + it('should initialize the InfiniteGridComponent.name field with the expected value', () => { + assert.equal(InfiniteGridComponent.name, 'InfiniteGridComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, InfiniteGridComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, InfiniteGridComponent) + assertInfiniteGridComponentEq(data, InfiniteGridComponentDefaults) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, InfiniteGridComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized InfiniteGridComponent', () => { + const Expected = { + size: 42, + color: new Color(0x123456), + distance: 10_000 + } + const data = getComponent(testEntity, InfiniteGridComponent) + assertInfiniteGridComponentEq(data, InfiniteGridComponentDefaults) + setComponent(testEntity, InfiniteGridComponent, Expected) + assertInfiniteGridComponentNotEq(data, InfiniteGridComponentDefaults) + assertInfiniteGridComponentEq(data, Expected) + }) + + it('should not change values of an initialized InfiniteGridComponent when the data passed had incorrect types', () => { + const Incorrect = { + size: 'somesize', + color: 42, + distance: 'somedistance' + } + const data = getComponent(testEntity, InfiniteGridComponent) + assertInfiniteGridComponentEq(data, InfiniteGridComponentDefaults) + // @ts-ignore Coerce the data with incorrect types into the setComponent call + setComponent(testEntity, InfiniteGridComponent, Incorrect) + assertInfiniteGridComponentEq(data, InfiniteGridComponentDefaults) + }) + }) //:: onSet + + describe('reactor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, InfiniteGridComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should trigger when engineRendererSettings.gridHeight changes', () => { + const Expected = 42 + const gridHeightBefore = getState(RendererState).gridHeight + getMutableState(RendererState).gridHeight.set(Expected) + const gridHeightAfter = getState(RendererState).gridHeight + assert.notEqual(gridHeightBefore, gridHeightAfter) + // Run and Check the result + InfiniteGridComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately + assert.equal(getComponent(testEntity, MeshComponent).position.y, Expected) + }) + + it('should trigger when component.color changes', () => { + const Expected = new Color(0xffffff) + assert.notDeepEqual(getComponent(testEntity, InfiniteGridComponent).color, Expected) + getMutableComponent(testEntity, InfiniteGridComponent).color.set(Expected) + assert.deepEqual(getComponent(testEntity, InfiniteGridComponent).color, Expected) + // Run and Check the result + InfiniteGridComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately + const result = getComponent(testEntity, MeshComponent).material as ShaderMaterial + assert.equal(result.uniforms.uColor.value, Expected) + }) + + it('should trigger when component.size changes', () => { + const Expected = 42 + assert.notEqual(getComponent(testEntity, InfiniteGridComponent).size, Expected) + getMutableComponent(testEntity, InfiniteGridComponent).size.set(Expected) + assert.equal(getComponent(testEntity, InfiniteGridComponent).size, Expected) + // Run and Check the result + InfiniteGridComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately + const result = getComponent(testEntity, MeshComponent).material as ShaderMaterial + assert.equal(result.uniforms.uSize1.value, Expected) + assert.equal(result.uniforms.uSize2.value, Expected * 10) + }) + + describe('when distance changes ...', () => { + it("... should change the uniforms.uDistance value for the Mesh's ShaderMaterial", () => { + const Expected = 42 + assert.notEqual(getComponent(testEntity, InfiniteGridComponent).distance, Expected) + getMutableComponent(testEntity, InfiniteGridComponent).distance.set(Expected) + assert.equal(getComponent(testEntity, InfiniteGridComponent).distance, Expected) + // Run and Check the result + InfiniteGridComponent.reactorMap.get(testEntity)!.run() // Reactor is already running. But force-run it so changes are applied immediately + const result = getComponent(testEntity, MeshComponent).material as ShaderMaterial + assert.equal(result.uniforms.uDistance.value, Expected) + }) + + const LineColors = ['red', 'green', 'blue'] // duplicate of the lineColors array inside the component.distance useEffect + const LineQuery = defineQuery([LineSegmentComponent]) + it('... should create, for every line color, an entity that has a LineSegmentComponent with name `infinite-grid-helper-line-${i}`', () => { + const Names = ['infinite-grid-helper-line-0', 'infinite-grid-helper-line-1', 'infinite-grid-helper-line-2'] + assert.equal(LineQuery().length, LineColors.length) + for (const entity of LineQuery()) { + assert.equal(Names.includes(getComponent(entity, LineSegmentComponent).name), true) + } + }) + + it('... should create, for every line color, an entity that has an EntityTreeComponent whose parent should be the entityContext', () => { + assert.equal(LineQuery().length, LineColors.length) + for (const entity of LineQuery()) { + assert.equal(getComponent(entity, EntityTreeComponent).parentEntity, testEntity) + } + }) + + it('... should remove all lineEntities of the grid when the InfiniteGridComponent is removed from the entity', () => { + assert.equal(LineQuery().length, LineColors.length) + removeComponent(testEntity, InfiniteGridComponent) + assert.equal(LineQuery().length, 0) + }) + }) + }) //:: reactor +}) + +describe('createInfiniteGridHelper', () => { + beforeEach(async () => { + createEngine() + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should return a valid entity', () => { + const result = createInfiniteGridHelper() + assert.notEqual(result, UndefinedEntity) + }) + + it('should assign an EntityTreeComponent to the returned entity', () => { + const result = createInfiniteGridHelper() + assert.equal(hasComponent(result, EntityTreeComponent), true) + }) + + it('should assign an InfiniteGridComponent to the returned entity', () => { + const result = createInfiniteGridHelper() + assert.equal(hasComponent(result, InfiniteGridComponent), true) + }) + + it("should assign a NameComponent to the returned entity, with a value of 'Infinite Grid Helper'", () => { + const result = createInfiniteGridHelper() + assert.equal(hasComponent(result, NameComponent), true) + assert.equal(getComponent(result, NameComponent), 'Infinite Grid Helper') + }) + + it('should assign a VisibleComponent to the returned entity, with a value of `true`', () => { + const result = createInfiniteGridHelper() + assert.equal(hasComponent(result, VisibleComponent), true) + }) +}) diff --git a/packages/spatial/src/renderer/components/InfiniteGridHelper.ts b/packages/spatial/src/renderer/components/InfiniteGridHelper.ts index 4d17477348..faae3fea32 100644 --- a/packages/spatial/src/renderer/components/InfiniteGridHelper.ts +++ b/packages/spatial/src/renderer/components/InfiniteGridHelper.ts @@ -128,7 +128,7 @@ void main() { ` export const InfiniteGridComponent = defineComponent({ - name: 'Infinite Grid', + name: 'InfiniteGridComponent', onInit(entity) { return { size: 1, diff --git a/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx b/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx index e44b197320..babdfc659b 100644 --- a/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx +++ b/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx @@ -27,203 +27,413 @@ import { act, render } from '@testing-library/react' import assert from 'assert' import React, { useEffect } from 'react' import sinon from 'sinon' -import { BoxGeometry, LineBasicMaterial, LineSegments, MeshBasicMaterial, SphereGeometry } from 'three' - -import { getComponent, getMutableComponent, hasComponent, removeComponent, setComponent } from '@etherealengine/ecs' +import { + BoxGeometry, + BufferGeometry, + Color, + ColorRepresentation, + LineBasicMaterial, + LineSegments, + Material, + MeshBasicMaterial, + SphereGeometry +} from 'three' + +import { + Entity, + getComponent, + getMutableComponent, + hasComponent, + removeComponent, + setComponent, + UndefinedEntity +} from '@etherealengine/ecs' import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getState } from '@etherealengine/hyperflux' import { createEngine } from '@etherealengine/ecs/src/Engine' +import { NameComponent } from '../../common/NameComponent' import { ResourceState } from '../../resources/ResourceState' import { ObjectLayerMasks, ObjectLayers } from '../constants/ObjectLayers' import { GroupComponent } from './GroupComponent' import { LineSegmentComponent } from './LineSegmentComponent' import { ObjectLayerComponents, ObjectLayerMaskComponent } from './ObjectLayerComponent' +import { VisibleComponent } from './VisibleComponent' + +type LineSegmentComponentData = { + name: string + geometry: BufferGeometry + material: Material & { color: Color } + color: undefined | ColorRepresentation + layerMask: typeof ObjectLayers.NodeHelper + entity: undefined | Entity +} + +const LineSegmentComponentDefaults = { + name: 'line-segment', + geometry: null!, + material: new LineBasicMaterial(), + color: undefined, + layerMask: ObjectLayers.NodeHelper, + entity: undefined +} as LineSegmentComponentData + +function assertLineSegmentComponentEq(A: LineSegmentComponentData, B: LineSegmentComponentData) { + if (A === null && B === null) return + assert.equal(A.name, B.name) + if (A.geometry === null && B.geometry === null) assert(true) + else if (A.geometry === null) assert(false, 'Geometry of A is not equal to B. B has geometry, but A.geometry is null') + else if (B.geometry === null) assert(false, 'Geometry of B is not equal to A. A has geometry, but B.geometry is null') + else assert.deepEqual(A.geometry, B.geometry) + assert.deepEqual(A.material, B.material) + assert.deepEqual(A.color, B.color) + assert.equal(A.layerMask, B.layerMask) + assert.equal(A.entity, B.entity) +} describe('LineSegmentComponent', () => { - beforeEach(async () => { - createEngine() - }) - - afterEach(() => { - return destroyEngine() - }) - - it('Creates LineSegmentComponent', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) - - const Reactor = () => { - useEffect(() => { - setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) - }, []) - - return <> - } - - const { rerender, unmount } = render() - - const resourceState = getState(ResourceState) - - act(async () => { - assert(hasComponent(entity, LineSegmentComponent)) - assert(resourceState.resources[geometry.uuid]) - assert(resourceState.resources[material.uuid]) - removeEntity(entity) - unmount() - }).then(() => { - assert(!hasComponent(entity, LineSegmentComponent)) - assert(!resourceState.resources[geometry.uuid]) - assert(!resourceState.resources[material.uuid]) - done() - }) - }) - - it('Updates LineSegmentComponent correctly', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) - - const spy = sinon.spy() - geometry.dispose = spy - material.dispose = spy - - const geoResourceID = geometry.uuid - const matResourceID = material.uuid - - const geometry2 = new SphereGeometry(0.5) - const material2 = new LineBasicMaterial() - - geometry2.dispose = spy - material2.dispose = spy - - const Reactor = () => { - useEffect(() => { - setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) - }, []) - - return <> - } - - const { rerender, unmount } = render() - - const resourceState = getState(ResourceState) - act(async () => { - assert(hasComponent(entity, LineSegmentComponent)) - assert(resourceState.resources[geoResourceID]) - assert( - resourceState.resources[geoResourceID].asset && - (resourceState.resources[geoResourceID].asset as BoxGeometry).type === 'BoxGeometry' - ) - assert(resourceState.resources[matResourceID]) - assert( - resourceState.resources[matResourceID].asset && - (resourceState.resources[matResourceID].asset as MeshBasicMaterial).type === 'MeshBasicMaterial' - ) - const lineSegmentComponent = getMutableComponent(entity, LineSegmentComponent) - lineSegmentComponent.geometry.set(geometry2) - lineSegmentComponent.material.set(material2) - rerender() - }).then(() => { - sinon.assert.calledTwice(spy) - assert( - resourceState.resources[geoResourceID].asset && - (resourceState.resources[geoResourceID].asset as SphereGeometry).type === 'SphereGeometry' - ) - assert( - resourceState.resources[matResourceID].asset && - (resourceState.resources[matResourceID].asset as LineBasicMaterial).type === 'LineBasicMaterial' - ) - removeEntity(entity) - assert(!hasComponent(entity, LineSegmentComponent)) - assert(!resourceState.resources[geoResourceID]) - assert(!resourceState.resources[matResourceID]) - assert(spy.callCount === 4) - unmount() - done() - }) - }) - - it('Removes resources when LineSegmentComponent is unmounted', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) - - const spy = sinon.spy() - geometry.dispose = spy - material.dispose = spy - - const Reactor = () => { - useEffect(() => { - setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) - return () => { - removeComponent(entity, LineSegmentComponent) - } - }, []) - - return <> - } - - const { rerender, unmount } = render() - - const resourceState = getState(ResourceState) - - act(async () => { - assert(hasComponent(entity, LineSegmentComponent)) - assert(resourceState.resources[geometry.uuid]) - assert(resourceState.resources[material.uuid]) - unmount() - }).then(() => { - assert(!hasComponent(entity, LineSegmentComponent)) - assert(!resourceState.resources[geometry.uuid]) - assert(!resourceState.resources[material.uuid]) - sinon.assert.calledTwice(spy) - removeEntity(entity) - done() - }) - }) - - it('Sets LineSegment layer mask correctly', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) - - const layerMask = ObjectLayerMasks.NodeHelper - const layer = ObjectLayers.NodeHelper - - const Reactor = () => { - useEffect(() => { - setComponent(entity, LineSegmentComponent, { - geometry: geometry, - material: material, - layerMask: layerMask - }) - return () => { - removeComponent(entity, LineSegmentComponent) - } - }, []) - - return <> - } - - const { rerender, unmount } = render() - - act(async () => { - rerender() - }).then(() => { - assert(hasComponent(entity, LineSegmentComponent)) - assert(hasComponent(entity, GroupComponent)) - assert(hasComponent(entity, ObjectLayerMaskComponent)) - assert(hasComponent(entity, ObjectLayerComponents[layer])) - const group = getComponent(entity, GroupComponent) - const lineSegments = group[0] as LineSegments - assert(lineSegments.isLineSegments) - assert(lineSegments.layers.mask === layerMask) - unmount() - removeEntity(entity) - done() - }) - }) + describe('IDs', () => { + it('should initialize the LineSegmentComponent.name field with the expected value', () => { + assert.equal(LineSegmentComponent.name, 'LineSegmentComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected values', () => { + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + const Expected = LineSegmentComponentDefaults + Expected.geometry = geometry + Expected.material = material + setComponent(testEntity, LineSegmentComponent, { geometry: geometry, material: material }) + const data = getComponent(testEntity, LineSegmentComponent) + assertLineSegmentComponentEq(data, Expected) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should throw an error if the data assigned does not provide a valid `LineSegmentComponent.geometry` object', () => { + const material = new MeshBasicMaterial({ color: 0xffff00 }) + assert.throws(() => setComponent(testEntity, LineSegmentComponent, { material: material })) + }) + + it('should change the values of an initialized LineSegmentComponent', () => { + const geometry1 = new BoxGeometry(1, 1, 1) + const material1 = new MeshBasicMaterial({ color: 0x111111 }) + const Expected = LineSegmentComponentDefaults + Expected.geometry = geometry1 + Expected.material = material1 + setComponent(testEntity, LineSegmentComponent, { geometry: geometry1, material: material1 }) + const data = getComponent(testEntity, LineSegmentComponent) + assertLineSegmentComponentEq(data, Expected) + + const geometry2 = new BoxGeometry(2, 2, 2) + const material2 = new MeshBasicMaterial({ color: 0x222222 }) + setComponent(testEntity, LineSegmentComponent, { geometry: geometry2, material: material2 }) + Expected.geometry = geometry2 + Expected.material = material2 + const result = getComponent(testEntity, LineSegmentComponent) + assertLineSegmentComponentEq(result, Expected) + }) + }) //:: onSet + + describe('reactor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should call addObjectToGroup(lineSegment) with the entity when it mounts', () => { + assert.equal(hasComponent(testEntity, GroupComponent), false) + setComponent(testEntity, LineSegmentComponent, { + geometry: new BoxGeometry(1, 1, 1), + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(hasComponent(testEntity, GroupComponent), true) + }) + + it('should set a VisibleComponent to the entity when it mounts', () => { + assert.equal(hasComponent(testEntity, VisibleComponent), false) + setComponent(testEntity, LineSegmentComponent, { + geometry: new BoxGeometry(1, 1, 1), + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(hasComponent(testEntity, VisibleComponent), true) + }) + + it('should call removeObjectFromGroup(lineSegment) with the entity when it unmounts', () => { + assert.equal(hasComponent(testEntity, GroupComponent), false) + setComponent(testEntity, LineSegmentComponent, { + geometry: new BoxGeometry(1, 1, 1), + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(hasComponent(testEntity, GroupComponent), true) + removeComponent(testEntity, GroupComponent) + assert.equal(hasComponent(testEntity, GroupComponent), false) + }) + + it('should trigger when component.name changes', () => { + const Expected = 'TestLineName' + assert.equal(hasComponent(testEntity, NameComponent), false) + const geometry = new BoxGeometry(1, 1, 1) + setComponent(testEntity, LineSegmentComponent, { + geometry: geometry, + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(hasComponent(testEntity, NameComponent), true) + setComponent(testEntity, LineSegmentComponent, { + name: Expected, + geometry: geometry, + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + const result = getComponent(testEntity, NameComponent) + assert.equal(result, Expected) + }) + + it('should trigger when component.layerMask changes', () => { + const Expected = 42 + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + const geometry = new BoxGeometry(1, 1, 1) + setComponent(testEntity, LineSegmentComponent, { + geometry: geometry, + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + assert.notEqual(getComponent(testEntity, ObjectLayerMaskComponent), Expected) + setComponent(testEntity, LineSegmentComponent, { + layerMask: Expected, + geometry: geometry, + material: new MeshBasicMaterial({ color: 0x111111 }) + }) + assert.equal(getComponent(testEntity, ObjectLayerMaskComponent), Expected) + }) + + it('should set the LineSegment layerMask correctly', (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const layerMask = ObjectLayerMasks.NodeHelper + const layer = ObjectLayers.NodeHelper + + const Reactor = () => { + useEffect(() => { + setComponent(entity, LineSegmentComponent, { + geometry: geometry, + material: material, + layerMask: layerMask + }) + return () => { + removeComponent(entity, LineSegmentComponent) + } + }, []) + + return <> + } + + const { rerender, unmount } = render() + + act(async () => { + rerender() + }).then(() => { + assert(hasComponent(entity, LineSegmentComponent)) + assert(hasComponent(entity, GroupComponent)) + assert(hasComponent(entity, ObjectLayerMaskComponent)) + assert(hasComponent(entity, ObjectLayerComponents[layer])) + const group = getComponent(entity, GroupComponent) + const lineSegments = group[0] as LineSegments + assert(lineSegments.isLineSegments) + assert(lineSegments.layers.mask === layerMask) + unmount() + removeEntity(entity) + done() + }) + }) + + it('should trigger when component.color changes', () => { + const Expected = new Color('#123456') + assert.equal(hasComponent(testEntity, NameComponent), false) + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0x111111 }) + setComponent(testEntity, LineSegmentComponent, { + geometry: geometry, + material: material + }) + assert.notDeepEqual(getComponent(testEntity, LineSegmentComponent).material.color, Expected) + setComponent(testEntity, LineSegmentComponent, { + color: Expected, + geometry: geometry, + material: material + }) + const result = getComponent(testEntity, LineSegmentComponent).material.color + assert.deepEqual(result, Expected) + }) + + it('should create a LineSegmentComponent correctly', (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const Reactor = () => { + useEffect(() => { + setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) + }, []) + + return <> + } + + const { rerender, unmount } = render() + + const resourceState = getState(ResourceState) + + act(async () => { + assert(hasComponent(entity, LineSegmentComponent)) + assert(resourceState.resources[geometry.uuid]) + assert(resourceState.resources[material.uuid]) + removeEntity(entity) + unmount() + }).then(() => { + assert(!hasComponent(entity, LineSegmentComponent)) + assert(!resourceState.resources[geometry.uuid]) + assert(!resourceState.resources[material.uuid]) + done() + }) + }) + + it('should update the LineSegmentComponent data correctly', (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const spy = sinon.spy() + geometry.dispose = spy + material.dispose = spy + + const geoResourceID = geometry.uuid + const matResourceID = material.uuid + + const geometry2 = new SphereGeometry(0.5) + const material2 = new LineBasicMaterial() + + geometry2.dispose = spy + material2.dispose = spy + + const Reactor = () => { + useEffect(() => { + setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) + }, []) + + return <> + } + + const { rerender, unmount } = render() + + const resourceState = getState(ResourceState) + act(async () => { + assert(hasComponent(entity, LineSegmentComponent)) + assert(resourceState.resources[geoResourceID]) + assert( + resourceState.resources[geoResourceID].asset && + (resourceState.resources[geoResourceID].asset as BoxGeometry).type === 'BoxGeometry' + ) + assert(resourceState.resources[matResourceID]) + assert( + resourceState.resources[matResourceID].asset && + (resourceState.resources[matResourceID].asset as MeshBasicMaterial).type === 'MeshBasicMaterial' + ) + const lineSegmentComponent = getMutableComponent(entity, LineSegmentComponent) + lineSegmentComponent.geometry.set(geometry2) + lineSegmentComponent.material.set(material2) + rerender() + }).then(() => { + sinon.assert.calledTwice(spy) + assert( + resourceState.resources[geoResourceID].asset && + (resourceState.resources[geoResourceID].asset as SphereGeometry).type === 'SphereGeometry' + ) + assert( + resourceState.resources[matResourceID].asset && + (resourceState.resources[matResourceID].asset as LineBasicMaterial).type === 'LineBasicMaterial' + ) + removeEntity(entity) + assert(!hasComponent(entity, LineSegmentComponent)) + assert(!resourceState.resources[geoResourceID]) + assert(!resourceState.resources[matResourceID]) + assert(spy.callCount === 4) + unmount() + done() + }) + }) + + it('should remove the LineSegmentComponent resources when it is unmounted', (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const spy = sinon.spy() + geometry.dispose = spy + material.dispose = spy + + const Reactor = () => { + useEffect(() => { + setComponent(entity, LineSegmentComponent, { geometry: geometry, material: material }) + return () => { + removeComponent(entity, LineSegmentComponent) + } + }, []) + + return <> + } + + const { rerender, unmount } = render() + + const resourceState = getState(ResourceState) + + act(async () => { + assert(hasComponent(entity, LineSegmentComponent)) + assert(resourceState.resources[geometry.uuid]) + assert(resourceState.resources[material.uuid]) + unmount() + }).then(() => { + assert(!hasComponent(entity, LineSegmentComponent)) + assert(!resourceState.resources[geometry.uuid]) + assert(!resourceState.resources[material.uuid]) + sinon.assert.calledTwice(spy) + removeEntity(entity) + done() + }) + }) + }) //:: reactor }) diff --git a/packages/spatial/src/renderer/components/MeshComponent.test.tsx b/packages/spatial/src/renderer/components/MeshComponent.test.tsx index 010404dad4..f9f1312218 100644 --- a/packages/spatial/src/renderer/components/MeshComponent.test.tsx +++ b/packages/spatial/src/renderer/components/MeshComponent.test.tsx @@ -29,10 +29,17 @@ import React from 'react' import sinon from 'sinon' import { BoxGeometry, Color, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, SphereGeometry } from 'three' -import { getComponent, hasComponent, removeComponent, setComponent } from '@etherealengine/ecs' +import { + getComponent, + getMutableComponent, + hasComponent, + removeComponent, + setComponent, + UndefinedEntity +} from '@etherealengine/ecs' import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' -import { State, getState } from '@etherealengine/hyperflux' +import { getState, State } from '@etherealengine/hyperflux' import { createEngine } from '@etherealengine/ecs/src/Engine' import { Geometry } from '../../common/constants/Geometry' @@ -41,102 +48,259 @@ import { GroupComponent } from './GroupComponent' import { MeshComponent, useMeshComponent } from './MeshComponent' describe('MeshComponent', () => { - beforeEach(async () => { - createEngine() - }) + describe('IDs', () => { + it('should initialize the MeshComponent.name field with the expected value', () => { + assert.equal(MeshComponent.name, 'MeshComponent') + }) - afterEach(() => { - return destroyEngine() - }) + it('should initialize the MeshComponent.jsonID field with the expected value', () => { + assert.equal(MeshComponent.jsonID, 'EE_mesh') + }) + }) //:: IDs - it('MeshComponent is created correctly', () => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) + describe('onInit', () => { + let testEntity = UndefinedEntity - setComponent(entity, MeshComponent, new Mesh(geometry, material)) + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) - assert(hasComponent(entity, MeshComponent)) - const mesh = getComponent(entity, MeshComponent) - assert(mesh.geometry === geometry) - assert(mesh.material === material) + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) - removeComponent(entity, MeshComponent) + it('should initialize the component correctly', () => { + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) - assert(!hasComponent(entity, MeshComponent)) - }) + setComponent(testEntity, MeshComponent, new Mesh(geometry, material)) - it('useMeshComponent creates mesh correctly', () => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) + assert(hasComponent(testEntity, MeshComponent)) + const data = getComponent(testEntity, MeshComponent) + assert.equal(data.geometry === geometry, true) + assert.equal(data.material === material, true) - assert.doesNotThrow(() => { - const Reactor = () => { - const mesh = useMeshComponent(entity, geometry, material) - return <> - } + removeComponent(testEntity, MeshComponent) - const { rerender, unmount } = render() + assert(!hasComponent(testEntity, MeshComponent)) + }) + }) //:: onInit - assert(hasComponent(entity, MeshComponent)) - const mesh = getComponent(entity, MeshComponent) - assert(hasComponent(entity, GroupComponent) && getComponent(entity, GroupComponent).includes(mesh)) - assert(mesh.userData['ignoreOnExport']) - unmount() + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() }) - }) - it('useMeshComponent disposes resources correctly', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xffff00 }) + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) - const spy = sinon.spy() - geometry.dispose = spy - material.dispose = spy + it('should throw an error if the data assigned does not provide a valid `MeshComponent.geometry` object', () => { + assert.throws(() => setComponent(testEntity, MeshComponent)) + }) - assert.doesNotThrow(() => { - const Reactor = () => { - const mesh = useMeshComponent(entity, geometry, material) - return <> - } + it('should change the values of an initialized MeshComponent', () => { + const Initial = new Mesh(new SphereGeometry()) + const Expected = new Mesh(new BoxGeometry()) + setComponent(testEntity, MeshComponent, Initial) + const before = getComponent(testEntity, MeshComponent) + assert.equal(before.uuid, Initial.uuid) + // Run and Check the result + setComponent(testEntity, MeshComponent, Expected) + const result = getComponent(testEntity, MeshComponent) + assert.notEqual(result.uuid, Initial.uuid) + assert.equal(result.uuid, Expected.uuid) + }) + }) //:: onSet - const { rerender, unmount } = render() + describe('reactor', () => { + let testEntity = UndefinedEntity - assert(hasComponent(entity, MeshComponent)) - const meshUUID = getComponent(entity, MeshComponent).uuid - const resourceState = getState(ResourceState) - act(async () => { - assert(resourceState.resources[meshUUID]) - assert(resourceState.resources[geometry.uuid]) - assert(resourceState.resources[material.uuid]) + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should trigger when component changes', () => { + const Initial = new Mesh(new SphereGeometry()) + const Expected = new Mesh(new BoxGeometry()) + setComponent(testEntity, MeshComponent, Initial) + const before = getComponent(testEntity, MeshComponent) + assert.equal(before.uuid, Initial.uuid) + // Run and Check the result + getMutableComponent(testEntity, MeshComponent).set(Expected) + const result = getComponent(testEntity, MeshComponent) + assert.notEqual(result.uuid, Initial.uuid) + assert.equal(result.uuid, Expected.uuid) + }) + + it('should trigger when component.geometry changes', () => { + const Initial = new SphereGeometry() + const Expected = new BoxGeometry() + const mesh = new Mesh(Initial) + setComponent(testEntity, MeshComponent, mesh) + const before = getComponent(testEntity, MeshComponent).geometry + assert.equal(before.uuid, Initial.uuid) + // Run and Check the result + getMutableComponent(testEntity, MeshComponent).geometry.set(Expected) + const result = getComponent(testEntity, MeshComponent).geometry + assert.notEqual(result.uuid, Initial.uuid) + assert.equal(result.uuid, Expected.uuid) + }) + + it('should trigger when component.material changes', () => { + const Initial = new Material() + const Expected = new Material() + const mesh = new Mesh(new BoxGeometry()) + mesh.material = Initial + setComponent(testEntity, MeshComponent, mesh) + const before = getComponent(testEntity, MeshComponent).material as Material + assert.equal(before.uuid, Initial.uuid) + // Run and Check the result + getMutableComponent(testEntity, MeshComponent).material.set(Expected) + const result = getComponent(testEntity, MeshComponent).material as Material + assert.notEqual(result.uuid, Initial.uuid) + assert.equal(result.uuid, Expected.uuid) + }) + }) //:: reactor + + describe('useMeshComponent', () => { + beforeEach(async () => { + createEngine() + }) + + afterEach(() => { + return destroyEngine() + }) + + it('should create a mesh correctly', () => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + assert.doesNotThrow(() => { + const Reactor = () => { + const mesh = useMeshComponent(entity, geometry, material) + return <> + } + + const { rerender, unmount } = render() + + assert(hasComponent(entity, MeshComponent)) + const mesh = getComponent(entity, MeshComponent) + assert(hasComponent(entity, GroupComponent) && getComponent(entity, GroupComponent).includes(mesh)) + assert(mesh.userData['ignoreOnExport']) unmount() - }).then(() => { - assert(!hasComponent(entity, MeshComponent)) - assert(!resourceState.resources[meshUUID]) - assert(!resourceState.resources[geometry.uuid]) - assert(!resourceState.resources[material.uuid]) - sinon.assert.calledTwice(spy) - removeEntity(entity) - done() }) }) - }) - it('useMeshComponent updates geometry correctly', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const geometry2 = new SphereGeometry(0.5) - const material = new MeshBasicMaterial({ color: 0xffff00 }) + it('should dispose resources correctly', (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const spy = sinon.spy() + geometry.dispose = spy + material.dispose = spy - const geoUUID = geometry.uuid + assert.doesNotThrow(() => { + const Reactor = () => { + const mesh = useMeshComponent(entity, geometry, material) + return <> + } - const spy = sinon.spy() - geometry.dispose = spy + const { rerender, unmount } = render() - let meshState = undefined as undefined | State> - assert.doesNotThrow(() => { + assert(hasComponent(entity, MeshComponent)) + const meshUUID = getComponent(entity, MeshComponent).uuid + const resourceState = getState(ResourceState) + act(async () => { + assert(resourceState.resources[meshUUID]) + assert(resourceState.resources[geometry.uuid]) + assert(resourceState.resources[material.uuid]) + unmount() + }).then(() => { + assert(!hasComponent(entity, MeshComponent)) + assert(!resourceState.resources[meshUUID]) + assert(!resourceState.resources[geometry.uuid]) + assert(!resourceState.resources[material.uuid]) + sinon.assert.calledTwice(spy) + removeEntity(entity) + done() + }) + }) + }) + + it("should update the mesh's geometry correctly", (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const geometry2 = new SphereGeometry(0.5) + const material = new MeshBasicMaterial({ color: 0xffff00 }) + + const geoUUID = geometry.uuid + + const spy = sinon.spy() + geometry.dispose = spy + + let meshState = undefined as undefined | State> + assert.doesNotThrow(() => { + const Reactor = () => { + const mesh = useMeshComponent(entity, geometry, material) + meshState = mesh + return <> + } + + const { rerender, unmount } = render() + + assert(hasComponent(entity, MeshComponent)) + const resourceState = getState(ResourceState) + act(async () => { + assert(meshState) + assert(meshState.geometry.value) + assert(resourceState.resources[geoUUID].asset) + assert(resourceState.resources[geoUUID].references.length == 1) + assert((resourceState.resources[geoUUID].asset as BoxGeometry).type === 'BoxGeometry') + assert(meshState.geometry.type.value === 'BoxGeometry') + meshState.geometry.set(geometry2) + rerender() + }).then(() => { + sinon.assert.calledOnce(spy) + assert(meshState) + assert(meshState.geometry.value) + assert(resourceState.resources[geoUUID].asset) + assert(resourceState.resources[geoUUID].references.length == 1) + assert((resourceState.resources[geoUUID].asset as SphereGeometry).type === 'SphereGeometry') + assert(meshState.geometry.type.value === 'SphereGeometry') + unmount() + removeEntity(entity) + done() + }) + }) + }) + + it("should update the mesh's material correctly", (done) => { + const entity = createEntity() + const geometry = new BoxGeometry(1, 1, 1) + const material = new MeshBasicMaterial({ color: 0xdadada }) + const material2 = new LineBasicMaterial({ color: 0xffff00 }) + + const matUUID = material.uuid + + const spy = sinon.spy() + material.dispose = spy + + let meshState = undefined as undefined | State> const Reactor = () => { const mesh = useMeshComponent(entity, geometry, material) meshState = mesh @@ -147,90 +311,45 @@ describe('MeshComponent', () => { assert(hasComponent(entity, MeshComponent)) const resourceState = getState(ResourceState) - act(async () => { - assert(meshState) - assert(meshState.geometry.value) - assert(resourceState.resources[geoUUID].asset) - assert(resourceState.resources[geoUUID].references.length == 1) - assert((resourceState.resources[geoUUID].asset as BoxGeometry).type === 'BoxGeometry') - assert(meshState.geometry.type.value === 'BoxGeometry') - meshState.geometry.set(geometry2) - rerender() - }).then(() => { - sinon.assert.calledOnce(spy) - assert(meshState) - assert(meshState.geometry.value) - assert(resourceState.resources[geoUUID].asset) - assert(resourceState.resources[geoUUID].references.length == 1) - assert((resourceState.resources[geoUUID].asset as SphereGeometry).type === 'SphereGeometry') - assert(meshState.geometry.type.value === 'SphereGeometry') - unmount() - removeEntity(entity) - done() - }) - }) - }) - - it('useMeshComponent updates material correctly', (done) => { - const entity = createEntity() - const geometry = new BoxGeometry(1, 1, 1) - const material = new MeshBasicMaterial({ color: 0xdadada }) - const material2 = new LineBasicMaterial({ color: 0xffff00 }) - - const matUUID = material.uuid - - const spy = sinon.spy() - material.dispose = spy - - let meshState = undefined as undefined | State> - const Reactor = () => { - const mesh = useMeshComponent(entity, geometry, material) - meshState = mesh - return <> - } - - const { rerender, unmount } = render() - - assert(hasComponent(entity, MeshComponent)) - const resourceState = getState(ResourceState) - act(async () => { - rerender() - }).then(() => { - assert(meshState) - assert(meshState.material.value) - assert(resourceState.resources[matUUID].asset) - assert(resourceState.resources[matUUID].references.length == 1) - assert((resourceState.resources[matUUID].asset as MeshBasicMaterial).type === 'MeshBasicMaterial') - assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0xdadada) - assert(meshState.material.type.value === 'MeshBasicMaterial') - assert(meshState.material.color.value.getHex() === 0xdadada) - meshState.material.set(material2) act(async () => { rerender() }).then(() => { - sinon.assert.calledOnce(spy) assert(meshState) + assert(meshState.material.value) assert(resourceState.resources[matUUID].asset) assert(resourceState.resources[matUUID].references.length == 1) - assert((resourceState.resources[matUUID].asset as LineBasicMaterial).type === 'LineBasicMaterial') - assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0xffff00) - assert(meshState.material.type.value === 'LineBasicMaterial') - assert(meshState.material.color.value.getHex() === 0xffff00) - meshState.material.color.set(new Color(0x000000)) + assert((resourceState.resources[matUUID].asset as MeshBasicMaterial).type === 'MeshBasicMaterial') + assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0xdadada) + assert(meshState.material.type.value === 'MeshBasicMaterial') + assert(meshState.material.color.value.getHex() === 0xdadada) + meshState.material.set(material2) act(async () => { rerender() }).then(() => { - // Dispose wasn't called again because just a property was changed in the material, not the material itself sinon.assert.calledOnce(spy) assert(meshState) - assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0x000000) + assert(resourceState.resources[matUUID].asset) + assert(resourceState.resources[matUUID].references.length == 1) + assert((resourceState.resources[matUUID].asset as LineBasicMaterial).type === 'LineBasicMaterial') + assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0xffff00) assert(meshState.material.type.value === 'LineBasicMaterial') - assert(meshState.material.color.value.getHex() === 0x000000) - unmount() - removeEntity(entity) - done() + assert(meshState.material.color.value.getHex() === 0xffff00) + meshState.material.color.set(new Color(0x000000)) + act(async () => { + rerender() + }).then(() => { + // Dispose wasn't called again because just a property was changed in the material, not the material itself + sinon.assert.calledOnce(spy) + assert(meshState) + assert((resourceState.resources[matUUID].asset as LineBasicMaterial).color.getHex() === 0x000000) + assert(meshState.material.type.value === 'LineBasicMaterial') + assert(meshState.material.color.value.getHex() === 0x000000) + unmount() + removeEntity(entity) + done() + }) }) }) }) - }) + }) //:: useMeshComponent }) diff --git a/packages/spatial/src/renderer/components/MeshComponent.ts b/packages/spatial/src/renderer/components/MeshComponent.ts index af917d2cd4..caded86967 100644 --- a/packages/spatial/src/renderer/components/MeshComponent.ts +++ b/packages/spatial/src/renderer/components/MeshComponent.ts @@ -41,7 +41,7 @@ import { BoundingBoxComponent } from '../../transform/components/BoundingBoxComp import { addObjectToGroup, removeObjectFromGroup } from './GroupComponent' export const MeshComponent = defineComponent({ - name: 'Mesh Component', + name: 'MeshComponent', jsonID: 'EE_mesh', onInit: (entity) => null! as Mesh, diff --git a/packages/spatial/src/renderer/components/Object3DComponent.test.ts b/packages/spatial/src/renderer/components/Object3DComponent.test.ts new file mode 100644 index 0000000000..32bc14ebac --- /dev/null +++ b/packages/spatial/src/renderer/components/Object3DComponent.test.ts @@ -0,0 +1,124 @@ +/* +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 { + createEngine, + createEntity, + destroyEngine, + getComponent, + removeEntity, + setComponent, + UndefinedEntity +} from '@etherealengine/ecs' +import assert from 'assert' +import { BoxGeometry, Mesh, Object3D } from 'three' +import { NameComponent } from '../../common/NameComponent' +import { Object3DComponent } from './Object3DComponent' + +const Object3DComponentDefaults = null! as Object3D + +function assertObject3DComponentEq(A: Object3D, B: Object3D) { + assert.equal(Boolean(A), Boolean(B)) + assert.equal(A.isObject3D, B.isObject3D) + assert.equal(A.uuid, B.uuid) +} + +describe('Object3DComponent', () => { + describe('IDs', () => { + it('should initialize the Object3DComponent.name field with the expected value', () => { + assert.equal(Object3DComponent.name, 'Object3DComponent') + }) + + it('should initialize the Object3DComponent.jsonID field with the expected value', () => { + assert.equal(Object3DComponent.jsonID, 'EE_object3d') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const Expected = new Mesh(new BoxGeometry()) + setComponent(testEntity, Object3DComponent, Expected) + const data = getComponent(testEntity, Object3DComponent) + assertObject3DComponentEq(data, Expected) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should throw an error if the data assigned does not provide a valid `Object3D` object', () => { + assert.throws(() => setComponent(testEntity, Object3DComponent)) + }) + + it('should not throw an error if the data assigned provides a valid `Object3D` object', () => { + assert.doesNotThrow(() => setComponent(testEntity, Object3DComponent, new Mesh(new BoxGeometry()))) + }) + + it('should change the values of an initialized Object3DComponent', () => { + setComponent(testEntity, Object3DComponent, new Mesh(new BoxGeometry())) + const before = getComponent(testEntity, Object3DComponent).uuid + setComponent(testEntity, Object3DComponent, new Mesh(new BoxGeometry())) + const after = getComponent(testEntity, Object3DComponent).uuid + assert.notEqual(before, after) + }) + + it("should set the object3d.name to the value of the entity's NameComponent when the entity has one", () => { + const Expected = 'testEntity' + setComponent(testEntity, NameComponent, Expected) + setComponent(testEntity, Object3DComponent, new Mesh(new BoxGeometry())) + assert.equal(getComponent(testEntity, Object3DComponent).name, Expected) + }) + + it("should not set the object3d.name when the entity doesn't have a NameComponent", () => { + setComponent(testEntity, Object3DComponent, new Mesh(new BoxGeometry())) + const result = getComponent(testEntity, Object3DComponent).name + assert.equal(result, '') + }) + }) //:: onSet + + /** @todo This component should have a reactor that updates the name of the object when a NameComponent is set on the entity after its Object3DComponent is set? */ +}) diff --git a/packages/spatial/src/renderer/components/Object3DComponent.ts b/packages/spatial/src/renderer/components/Object3DComponent.ts index d351ccc9d6..e4c79718c2 100644 --- a/packages/spatial/src/renderer/components/Object3DComponent.ts +++ b/packages/spatial/src/renderer/components/Object3DComponent.ts @@ -30,7 +30,7 @@ import { defineComponent, getComponent, hasComponent } from '@etherealengine/ecs import { NameComponent } from '../../common/NameComponent' export const Object3DComponent = defineComponent({ - name: 'Object3D Component', + name: 'Object3DComponent', jsonID: 'EE_object3d', onInit: (entity) => null! as Object3D, diff --git a/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx b/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx index 8acf7cfecc..421f7674e6 100644 --- a/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx +++ b/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx @@ -26,15 +26,24 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' import { BoxGeometry, Mesh, MeshBasicMaterial } from 'three' -import { getComponent, hasComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { + getComponent, + hasComponent, + removeComponent, + serializeComponent, + setComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { destroyEngine } from '@etherealengine/ecs/src/Engine' -import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' +import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' +import { UndefinedEntity } from '@etherealengine/ecs' import { createEngine } from '@etherealengine/ecs/src/Engine' import { addObjectToGroup } from './GroupComponent' -import { Layer, ObjectLayerComponents, ObjectLayerMaskComponent } from './ObjectLayerComponent' +import { Layer, ObjectLayerComponents, ObjectLayerMaskComponent, ObjectLayerMaskDefault } from './ObjectLayerComponent' -describe('ObjectLayerComponent', () => { +const maxBitWidth = 32 + +describe('ObjectLayerComponent : todo.Organize', () => { beforeEach(async () => { createEngine() }) @@ -196,3 +205,710 @@ describe('ObjectLayerComponent', () => { assert(layer2.test(layer3)) }) }) + +type ObjectLayerMaskComponentData = any + +function assertObjectLayerMaskComponentEq(A: ObjectLayerMaskComponentData, B: ObjectLayerMaskComponentData) { + assert.equal(Boolean(A), Boolean(B)) + assert.equal(A.isObjectLayerMask, B.isObjectLayerMask) +} + +describe('ObjectLayerMaskComponent', () => { + describe('IDs', () => { + it('should initialize the ObjectLayerMaskComponent.name field with the expected value', () => { + assert.equal(ObjectLayerMaskComponent.name, 'ObjectLayerMaskComponent') + }) + }) //:: IDs + + describe('schema', () => { + it('should initialize the schema with the expected values', () => { + assert.notEqual(ObjectLayerMaskComponent.schema, undefined) + const KeysSchema = Object.keys(ObjectLayerMaskComponent.schema) + assert.equal(KeysSchema.length, 1) + assert.equal(KeysSchema.includes('mask'), true) + assert.notEqual(ObjectLayerMaskComponent.schema.mask, undefined) + assert.equal(ObjectLayerMaskComponent.schema.mask, 'i32') + }) + }) + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, ObjectLayerMaskComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, ObjectLayerMaskComponent) + assertObjectLayerMaskComponentEq(data, ObjectLayerMaskDefault) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the value of the component to `@param mask`', () => { + const Expected = 42 + setComponent(testEntity, ObjectLayerMaskComponent, Expected) + const result = getComponent(testEntity, ObjectLayerMaskComponent) + assert.equal(result, Expected) + }) + + it('should set the mask value for the entity to `@param mask`', () => { + const Expected = 42 + setComponent(testEntity, ObjectLayerMaskComponent, Expected) + const result = ObjectLayerMaskComponent.mask[testEntity] + assert.equal(result, Expected) + }) + + it('should set an ObjectLayerComponent for every bit of the `@param mask` that is set', () => { + const ActiveBits = [8, 4, 2] + const Mask = (1 << 8) | (1 << 4) | (1 << 2) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), false) + setComponent(testEntity, ObjectLayerMaskComponent, Mask) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), true) + }) + + it('should remove any ObjectLayerComponent for the bits of `@param mask` that are not set', () => { + const InitialBits = [12, 21] + const Initial = (1 << 12) | (1 << 21) + const ActiveBits = [8, 4, 2] + const Mask = (1 << 8) | (1 << 4) | (1 << 2) + for (const id in InitialBits) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[InitialBits[id]]), false) + setComponent(testEntity, ObjectLayerMaskComponent, Initial) + for (const id in InitialBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[InitialBits[id]]), true) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), false) + setComponent(testEntity, ObjectLayerMaskComponent, Mask) + for (const id in InitialBits) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[InitialBits[id]]), false) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), true) + }) + }) //:: onSet + + describe('onRemove', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, ObjectLayerMaskComponent, 42) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should remove all ObjectLayerComponent for the entity', () => { + removeComponent(testEntity, ObjectLayerMaskComponent) + for (let id = 0; id < maxBitWidth; ++id) { + assert.equal(hasComponent(testEntity, ObjectLayerComponents[id]), false) + } + }) + + it("should set component's value to 0", () => { + assert.equal(getComponent(testEntity, ObjectLayerMaskComponent), 42) + removeComponent(testEntity, ObjectLayerMaskComponent) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + }) + }) //:: onRemove + + describe('toJSON', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, ObjectLayerMaskComponent, 42) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should return the serialized data correctly', () => { + const result = serializeComponent(testEntity, ObjectLayerMaskComponent) + assert.equal(typeof result, 'number') + assert.equal(result, 42) + }) + }) //:: toJSON + + describe('setLayer', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should add/set an ObjectLayerMaskComponent to the entity, with the value of the `@param layer` converted into a mask', () => { + const Layer = 10 + ObjectLayerMaskComponent.setLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), true) + assert.equal(getComponent(testEntity, ObjectLayerMaskComponent), 1 << Layer) + }) + }) //:: setLayer + + describe('enableLayer', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should add the ObjectLayerComponent with ID `@param layer` to the entity', () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), false) + ObjectLayerMaskComponent.enableLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), true) + }) + + it("should add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.enableLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + + it('should not do anything if the entity does not exist', () => { + const Layer = 10 + assert.equal(hasComponent(UndefinedEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.enableLayer(UndefinedEntity, Layer) + assert.equal(hasComponent(UndefinedEntity, ObjectLayerMaskComponent), false) + }) + }) //:: enableLayer + + describe('enableLayers', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const Layers = [10, 4] + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.enableLayers(testEntity, ...Layers) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + + it('should add an ObjectLayerComponent to the entity for every ID contained in the `@param layers` list', () => { + const Layers = [10, 4] + for (const layer in Layers) assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layers[layer]]), false) + ObjectLayerMaskComponent.enableLayers(testEntity, ...Layers) + for (const layer in Layers) assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layers[layer]]), true) + }) + }) //:: enableLayers + + describe('disableLayer', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.disableLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + + it('should remove the ObjectLayerComponent with ID `@param layer` from the entity', () => { + const Layer = 10 + setComponent(testEntity, ObjectLayerComponents[Layer]) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), true) + ObjectLayerMaskComponent.disableLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), false) + }) + + it('should not do anything if the entity does not exist', () => { + const Layer = 10 + assert.equal(hasComponent(UndefinedEntity, ObjectLayerComponents[Layer]), false) + ObjectLayerMaskComponent.disableLayer(UndefinedEntity, Layer) + assert.equal(hasComponent(UndefinedEntity, ObjectLayerComponents[Layer]), false) + }) + }) //:: disableLayer + + describe('disableLayers', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.disableLayers(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + + it('should remove every ObjectLayerComponent from the entity for every ID contained in the `@param layers` list', () => { + const Layers = [10, 4] + ObjectLayerMaskComponent.enableLayers(testEntity, ...Layers) + for (const layer in Layers) assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layers[layer]]), true) + ObjectLayerMaskComponent.disableLayers(testEntity, ...Layers) + for (const layer in Layers) assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layers[layer]]), false) + }) + }) //:: disableLayers + + describe('toggleLayer', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + ObjectLayerMaskComponent.toggleLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + + it("should add the ObjectLayerComponent with ID `@param layer` to the entity if it doesn't have one", () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), false) + ObjectLayerMaskComponent.toggleLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), true) + }) + + it('should remove the ObjectLayerComponent with ID `@param layer` from the entity if it already has it', () => { + const Layer = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), false) + ObjectLayerMaskComponent.setLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), true) + ObjectLayerMaskComponent.toggleLayer(testEntity, Layer) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Layer]), false) + }) + }) //:: toggleLayer + + describe('setMask', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the value of the component to `@param mask`', () => { + const Expected = 42 + ObjectLayerMaskComponent.setMask(testEntity, Expected) + const result = getComponent(testEntity, ObjectLayerMaskComponent) + assert.equal(result, Expected) + }) + + it('should set the mask value for the entity to `@param mask`', () => { + const Expected = 42 + ObjectLayerMaskComponent.setMask(testEntity, Expected) + const result = ObjectLayerMaskComponent.mask[testEntity] + assert.equal(result, Expected) + }) + + it('should set an ObjectLayerComponent for every bit of the `@param mask` that is set', () => { + const ActiveBits = [8, 4, 2] + const Mask = (1 << 8) | (1 << 4) | (1 << 2) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), false) + ObjectLayerMaskComponent.setMask(testEntity, Mask) + for (const id in ActiveBits) assert.equal(hasComponent(testEntity, ObjectLayerComponents[ActiveBits[id]]), true) + }) + }) //:: setMask +}) + +describe('ObjectLayerComponents', () => { + describe('IDs', () => { + ;[...Array(maxBitWidth)].forEach((_, index, __) => { + it(`should initialize the ObjectLayerComponents[${index}].name field with the expected value`, () => { + assert.equal(ObjectLayerComponents[index].name, `ObjectLayer${index}`) + }) + }) + }) //:: IDs + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should activate the bit for the respective layer ID in ObjectLayerMaskComponent.mask[entity]', () => { + const Layer = 10 + setComponent(testEntity, ObjectLayerComponents[Layer]) + const hasLayer = Boolean(ObjectLayerMaskComponent.mask[testEntity] & (1 << Layer)) // true when mask contains the Layer bit + assert.equal(hasLayer, true) + }) + }) //:: onSet + + describe('onRemove', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should deactivate the bit for the respective layer ID in ObjectLayerMaskComponent.mask[entity]', () => { + const Layer = 10 + setComponent(testEntity, ObjectLayerComponents[Layer]) + const before = Boolean(ObjectLayerMaskComponent.mask[testEntity] & (1 << Layer)) // true when mask contains the Layer bit + assert.equal(before, true) + removeComponent(testEntity, ObjectLayerComponents[Layer]) + const hasLayer = Boolean(ObjectLayerMaskComponent.mask[testEntity] & (1 << Layer)) // true when mask contains the Layer bit + assert.equal(hasLayer, false) + }) + }) //:: onRemove +}) + +describe('Layer', () => { + describe('constructor', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add an ObjectLayerMaskComponent component to the entity if it doesn't have one", () => { + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + const result = new Layer(testEntity) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + }) //:: constructor + + describe('get mask', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should get the mask contained in the ObjectLayerMaskComponent.mask array, for the entity described in the class's data", () => { + const layer = new Layer(testEntity) + assert.equal(layer.mask, ObjectLayerMaskComponent.mask[testEntity]) + }) + }) //:: get mask + + describe('set mask', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the mask contained in the ObjectLayerMaskComponent.mask array, for the entity described in the class's data, to the value given in the assignment statement", () => { + const Expected = 42 + const layer = new Layer(testEntity) + assert.notEqual(ObjectLayerMaskComponent.mask[testEntity], Expected) + assert.equal(ObjectLayerMaskComponent.mask[testEntity], layer.mask) + layer.mask = Expected + assert.equal(ObjectLayerMaskComponent.mask[testEntity], Expected) + }) + }) //:: set mask + + describe('set', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should set the given `@param channel` layer into the ObjectLayerMaskComponent of the entity described by the class's data", () => { + const Expected = 10 + const layer = new Layer(testEntity) + layer.set(Expected) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[Expected]), true) + assert.equal(getComponent(testEntity, ObjectLayerMaskComponent), 1 << Expected) + }) + }) //:: set + + describe('enable', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add the ObjectLayerComponent with ID `@param layer` to the entity and add an ObjectLayerMaskComponent to the entity if it doesn't have one", () => { + const ID = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), false) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), false) + const layer = new Layer(testEntity) + layer.enable(ID) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), true) + assert.equal(hasComponent(testEntity, ObjectLayerMaskComponent), true) + }) + }) //:: enable + + describe('enableAll', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should activate all ObjectLayers for the entity described in the class's data", () => { + for (const component of ObjectLayerComponents) assert.equal(hasComponent(testEntity, component), false) + const layer = new Layer(testEntity) + layer.enableAll() + for (const component of ObjectLayerComponents) assert.equal(hasComponent(testEntity, component), true) + }) + }) //:: enableAll + + describe('toggle', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add the ObjectLayerComponent with ID `@param layer` to the entity if it doesn't have one", () => { + const ID = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), false) + const layer = new Layer(testEntity) + layer.toggle(ID) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), true) + }) + + it('should remove the ObjectLayerComponent with ID `@param layer` to the entity if it already has it', () => { + const ID = 10 + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), false) + const layer = new Layer(testEntity) + layer.set(ID) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), true) + layer.toggle(ID) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), false) + }) + }) //:: toggle + + describe('disable', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should remove the ObjectLayerComponent with ID `@param layer` from the entity described in the class's data", () => { + const ID = 10 + const layer = new Layer(testEntity) + setComponent(testEntity, ObjectLayerComponents[ID]) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), true) + layer.disable(ID) + assert.equal(hasComponent(testEntity, ObjectLayerComponents[ID]), false) + }) + }) //:: disable + + describe('disableAll', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should remove all ObjectLayers from the entity described in the class's data", () => { + for (const component of ObjectLayerComponents) assert.equal(hasComponent(testEntity, component), false) + const layer = new Layer(testEntity) + for (const component of ObjectLayerComponents) setComponent(testEntity, component) + layer.disableAll() + for (const component of ObjectLayerComponents) assert.equal(hasComponent(testEntity, component), false) + }) + }) //:: disableAll + + describe('test', () => { + let oneEntity = UndefinedEntity + let twoEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + oneEntity = createEntity() + twoEntity = createEntity() + }) + + afterEach(() => { + removeEntity(oneEntity) + removeEntity(twoEntity) + return destroyEngine() + }) + + it("should return true when the entity's mask contains all layers of the `@param layers` mask", () => { + const Mask = (1 << 8) | (1 << 4) | (1 << 2) + const layerA = new Layer(oneEntity) + const layerB = new Layer(twoEntity) + layerA.set(Mask) + const before = layerA.test(layerB) + assert.equal(before, false) + layerB.set(Mask) + const result = layerA.test(layerB) + assert.equal(result, true) + }) + + it("should return false when the entity's mask does not contain all layers of the `@param layers` mask", () => { + const Mask1 = (1 << 8) | (1 << 4) | (1 << 2) + const Mask2 = (1 << 8) | (1 << 4) + const layerA = new Layer(oneEntity) + const layerB = new Layer(twoEntity) + layerA.set(Mask1) + const before = layerA.test(layerB) + assert.equal(before, false) + layerB.set(Mask2) + const result = layerA.test(layerB) + assert.equal(result, false) + }) + }) //:: test + + describe('isEnabled', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should return false when the entity's mask does not contain the `@param channel` layer", () => { + const ID = 10 + const Other = 4 + const layer = new Layer(testEntity) + assert.equal(layer.isEnabled(ID), false) + layer.enable(Other) + assert.equal(layer.isEnabled(ID), false) + }) + + it("should return true when the entity's mask contains the `@param channel` layer", () => { + const ID = 10 + const layer = new Layer(testEntity) + assert.equal(layer.isEnabled(ID), false) + layer.enable(ID) + assert.equal(layer.isEnabled(ID), true) + }) + }) //:: isEnabled +}) //:: Layer diff --git a/packages/spatial/src/renderer/components/ObjectLayerComponent.ts b/packages/spatial/src/renderer/components/ObjectLayerComponent.ts index fe46af7636..c0e3316420 100644 --- a/packages/spatial/src/renderer/components/ObjectLayerComponent.ts +++ b/packages/spatial/src/renderer/components/ObjectLayerComponent.ts @@ -51,20 +51,28 @@ export const ObjectLayerComponents = Array.from({ length: maxBitWidth }, (_, i) }) }) +export const ObjectLayerMaskDefault = 1 << 0 // enable layer 0 + export const ObjectLayerMaskComponent = defineComponent({ name: 'ObjectLayerMaskComponent', schema: { mask: Types.i32 }, onInit(entity) { - return 1 << 0 // enable layer 0 + return ObjectLayerMaskDefault // enable layer 0 }, /** + * @description * Takes a layer mask as a parameter, not a layer (eg. layer mask with value 256 enables layer 8) - * Incorrect usage setComponent(entity, ObjectLayerMaskComponent, ObjectLayers.NodeHelper) - * Correct usage setComponent(entity, ObjectLayerMaskComponent, ObjectLayerMasks.NodeHelper) + * ```ts + * // Incorrect usage + * setComponent(entity, ObjectLayerMaskComponent, ObjectLayers.NodeHelper) + * + * // Correct usage + * setComponent(entity, ObjectLayerMaskComponent, ObjectLayerMasks.NodeHelper) + * ``` */ - onSet(entity, component, mask = 1 << 0) { + onSet(entity, component, mask = ObjectLayerMaskDefault) { for (let i = 0; i < maxBitWidth; i++) { const isSet = (mask & ((1 << i) | 0)) !== 0 if (isSet) { diff --git a/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx b/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx index 6e41fb4d0f..875105dbbd 100644 --- a/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx +++ b/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx @@ -26,7 +26,15 @@ import assert from 'assert' import { MathUtils } from 'three' -import { Entity, EntityUUID, UUIDComponent, getComponent, getMutableComponent, setComponent } from '@etherealengine/ecs' +import { + EntityUUID, + UUIDComponent, + UndefinedEntity, + getComponent, + getMutableComponent, + serializeComponent, + setComponent +} from '@etherealengine/ecs' import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { noiseAddToEffectRegistry } from '@etherealengine/engine/src/postprocessing/NoiseEffect' @@ -35,97 +43,261 @@ import { RendererComponent } from '@etherealengine/spatial/src/renderer/WebGLRen import { SceneComponent } from '@etherealengine/spatial/src/renderer/components/SceneComponents' import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { act, render } from '@testing-library/react' +import { Effect } from 'postprocessing' import React from 'react' import { mockSpatialEngine } from '../../../tests/util/mockSpatialEngine' import { EngineState } from '../../EngineState' +import { destroySpatialEngine, initializeSpatialEngine } from '../../initializeEngine' import { RendererState } from '../RendererState' import { PostProcessingComponent } from './PostProcessingComponent' +type PostProcessingComponentData = { + enabled: boolean + effects: Record +} + +const PostProcessingComponentDefaults = { + enabled: false, + effects: {} +} as PostProcessingComponentData + +const TestShader = 'void main() { gl_FragColor = vec4(1.0,0.0,1.0,1.0); }' + +function assertPostProcessingComponentEq(A: PostProcessingComponentData, B: PostProcessingComponentData) { + assert.equal(A.enabled, B.enabled) + assert.equal(Object.keys(A.effects).length, Object.keys(B.effects).length) + + for (const id in A.effects) { + assert.equal(Object.keys(B.effects).includes(id), true) + const a = A.effects[id] + const b = B.effects[id] + assert.equal(a.name, b.name) + assert.equal(a.getFragmentShader(), b.getFragmentShader()) + } +} + describe('PostProcessingComponent', () => { - let rootEntity: Entity - let entity: Entity + describe('IDs', () => { + it('should initialize the PostProcessingComponent.name field with the expected value', () => { + assert.equal(PostProcessingComponent.name, 'PostProcessingComponent') + }) - beforeEach(() => { - createEngine() + it('should initialize the PostProcessingComponent.jsonID field with the expected value', () => { + assert.equal(PostProcessingComponent.jsonID, 'EE_postprocessing') + }) + }) //:: IDs - mockSpatialEngine() + describe('onInit', () => { + let testEntity = UndefinedEntity - rootEntity = getState(EngineState).viewerEntity + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, PostProcessingComponent) + }) - entity = createEntity() - setComponent(entity, UUIDComponent, MathUtils.generateUUID() as EntityUUID) - getMutableState(RendererState).usePostProcessing.set(true) - setComponent(entity, SceneComponent) - setComponent(entity, PostProcessingComponent, { enabled: true }) - setComponent(entity, EntityTreeComponent) + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) - //set data to test - setComponent(rootEntity, RendererComponent, { scenes: [entity] }) - }) + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, PostProcessingComponent) + assertPostProcessingComponentEq(data, PostProcessingComponentDefaults) + }) + }) //:: onInit - afterEach(() => { - return destroyEngine() - }) + describe('onSet', () => { + let testEntity = UndefinedEntity - it('Create default post processing component', () => { - const postProcessingComponent = getComponent(entity, PostProcessingComponent) - assert(postProcessingComponent, 'post processing component exists') - }) + beforeEach(async () => { + createEngine() + initializeSpatialEngine() + testEntity = createEntity() + setComponent(testEntity, PostProcessingComponent) + }) - it('Test Effect Composure amd Highlight Effect', async () => { - const effectKey = 'OutlineEffect' + afterEach(() => { + removeEntity(testEntity) + destroySpatialEngine() + return destroyEngine() + }) - //force nested reactors to run - const { rerender, unmount } = render(<>) - await act(() => rerender(<>)) + it('should change the values of an initialized PostProcessingComponent', () => { + const Expected = { + enabled: true, + effects: { + effect1: new Effect('test.effect1', TestShader), + effect2: new Effect('test.effect2', TestShader) + } + } as PostProcessingComponentData + // Sanity check the data + assertPostProcessingComponentEq( + getComponent(testEntity, PostProcessingComponent), + PostProcessingComponentDefaults + ) + // Run and Check the result + setComponent(testEntity, PostProcessingComponent, Expected) + const result = getComponent(testEntity, PostProcessingComponent) + assertPostProcessingComponentEq(result, Expected) + }) - const effectComposer = getComponent(rootEntity, RendererComponent).effectComposer - console.log(getComponent(rootEntity, RendererComponent)) + it('should not change values of an initialized PostProcessingComponent when the data passed had incorrect types', () => { + const Incorrect = { + effects: 42, + enabled: 46 & 2 + } + // Sanity check the data + assertPostProcessingComponentEq( + getComponent(testEntity, PostProcessingComponent), + PostProcessingComponentDefaults + ) + // Run and Check the result + // @ts-ignore Coerce the incorrect data type into the component + setComponent(testEntity, PostProcessingComponent, Incorrect) + assertPostProcessingComponentEq( + getComponent(testEntity, PostProcessingComponent), + PostProcessingComponentDefaults + ) + }) + }) //:: onSet - //test that the effect composer is setup - assert(effectComposer, 'effect composer is setup') + describe('toJSON', () => { + let testEntity = UndefinedEntity - //test that the effect pass has the the effect set - const effects = (effectComposer?.EffectPass as any).effects - assert(effects.find((el) => el.name == effectKey)) + beforeEach(async () => { + createEngine() + initializeSpatialEngine() + testEntity = createEntity() + setComponent(testEntity, PostProcessingComponent) + }) - unmount() - }) + afterEach(() => { + removeEntity(testEntity) + destroySpatialEngine() + return destroyEngine() + }) + + it("should serialize the component's data as expected", () => { + const Data = { + enabled: true, + effects: { + effect1: new Effect('test.effect1', TestShader), + effect2: new Effect('test.effect2', TestShader) + } + } as PostProcessingComponentData + + const Expected1 = { + enabled: false, + effects: {} + } + const Expected2 = { + enabled: Data.enabled, + effects: { + effect1: { + name: 'test.effect1', + renderer: null, + attributes: 0, + fragmentShader: 'void main() { gl_FragColor = vec4(1.0,0.0,1.0,1.0); }', + vertexShader: null, + defines: {}, + uniforms: {}, + extensions: null, + blendMode: { + _blendFunction: 23, + opacity: { value: 1 }, + _listeners: { change: [null] } + }, //:: blendMode + _inputColorSpace: 'srgb-linear', + _outputColorSpace: '' + }, //:: effect1 + effect2: { + name: 'test.effect2', + renderer: null, + attributes: 0, + fragmentShader: 'void main() { gl_FragColor = vec4(1.0,0.0,1.0,1.0); }', + vertexShader: null, + defines: {}, + uniforms: {}, + extensions: null, + blendMode: { + _blendFunction: 23, + opacity: { value: 1 }, + _listeners: { change: [null] } + }, //:: blendMode + _inputColorSpace: 'srgb-linear', + _outputColorSpace: '' + } //:: effect2 + } //:: effects + } + const result1 = serializeComponent(testEntity, PostProcessingComponent) + assert.deepEqual(result1, Expected1) + setComponent(testEntity, PostProcessingComponent, Data) + const result2 = serializeComponent(testEntity, PostProcessingComponent) + assert.deepEqual(result2, Expected2) + }) + }) //:: toJSON + + /** + // @todo Write after the reactor has been replaced with spatial queries or distance checks + describe('reactor', () => {}) //:: reactor + */ + + describe('General Purpose', () => { + let rootEntity = UndefinedEntity + let testEntity = UndefinedEntity + + beforeEach(() => { + createEngine() + + mockSpatialEngine() + + rootEntity = getState(EngineState).viewerEntity + + testEntity = createEntity() + setComponent(testEntity, UUIDComponent, MathUtils.generateUUID() as EntityUUID) + getMutableState(RendererState).usePostProcessing.set(true) + setComponent(testEntity, SceneComponent) + setComponent(testEntity, PostProcessingComponent, { enabled: true }) + setComponent(testEntity, EntityTreeComponent) + + //set data to test + setComponent(rootEntity, RendererComponent, { scenes: [testEntity] }) + }) + + afterEach(() => { + removeEntity(testEntity) + removeEntity(rootEntity) + return destroyEngine() + }) - it('Test Effect Add and Remove', async () => { - const effectKey = 'NoiseEffect' - noiseAddToEffectRegistry() + it('should add and remove effects correctly', async () => { + const effectKey = 'NoiseEffect' + noiseAddToEffectRegistry() - const { rerender, unmount } = render(<>) + const { rerender, unmount } = render(<>) - await act(() => rerender(<>)) + await act(() => rerender(<>)) - const postProcessingComponent = getMutableComponent(entity, PostProcessingComponent) - postProcessingComponent.effects[effectKey].isActive.set(true) + const postProcessingComponent = getMutableComponent(testEntity, PostProcessingComponent) + postProcessingComponent.effects[effectKey].isActive.set(true) - setComponent(rootEntity, RendererComponent) - await act(() => rerender(<>)) + setComponent(rootEntity, RendererComponent) + await act(() => rerender(<>)) - // @ts-ignore - let effects = getComponent(rootEntity, RendererComponent).effectComposer.EffectPass.effects - console.log( - getComponent(rootEntity, RendererComponent).effects, - effects.map((el) => el.name) - ) - assert( - effects.find((el) => el.name == effectKey), - ' Effect turned on' - ) + // @ts-ignore Allow access to the EffectPass.effects private field + const before = getComponent(rootEntity, RendererComponent).effectComposer.EffectPass.effects + assert.equal(Boolean(before.find((el) => el.name == effectKey)), true, effectKey + ' should be turned on') - postProcessingComponent.effects[effectKey].isActive.set(false) + postProcessingComponent.effects[effectKey].isActive.set(false) - await act(() => rerender(<>)) + await act(() => rerender(<>)) - // @ts-ignore - effects = getComponent(rootEntity, RendererComponent).effectComposer.EffectPass.effects - assert(!effects.find((el) => el.name == effectKey), ' Effect turned off') + // @ts-ignore Allow access to the EffectPass.effects private field + const after = getComponent(rootEntity, RendererComponent).effectComposer.EffectPass.effects + assert.equal(Boolean(after.find((el) => el.name == effectKey)), false, effectKey + ' should be turned off') - removeEntity(entity) - unmount() + unmount() + }) }) }) diff --git a/packages/spatial/src/renderer/components/SceneComponent.test.tsx b/packages/spatial/src/renderer/components/SceneComponent.test.tsx new file mode 100644 index 0000000000..024637f1b2 --- /dev/null +++ b/packages/spatial/src/renderer/components/SceneComponent.test.tsx @@ -0,0 +1,273 @@ +/* +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 { + UndefinedEntity, + createEngine, + createEntity, + destroyEngine, + getComponent, + removeEntity, + setComponent +} from '@etherealengine/ecs' +import { ImmutableObject } from '@etherealengine/hyperflux' +import assert from 'assert' +import { Color, CubeTexture, FogBase, Texture } from 'three' +import { BackgroundComponent, EnvironmentMapComponent, FogComponent, SceneComponent } from './SceneComponents' + +describe('SceneComponent', () => { + describe('IDs', () => { + it('should initialize the SceneComponent.name field with the expected value', () => { + assert.equal(SceneComponent.name, 'SceneComponent') + }) + }) //:: IDs +}) + +type BackgroundComponentData = ImmutableObject | ImmutableObject | ImmutableObject +const BackgroundComponentDefaults = null! as BackgroundComponentData + +function assertBackgroundComponentEq(A: BackgroundComponentData, B: BackgroundComponentData) { + assert.equal(Boolean(A), Boolean(B)) + const a = (A !== null && A.toJSON) || A + const b = (B !== null && B.toJSON) || B + assert.deepEqual(a, b) +} + +function assertBackgroundComponentNotEq(A: BackgroundComponentData, B: BackgroundComponentData) { + const a = (A !== null && A.toJSON) || A + const b = (B !== null && B.toJSON) || B + assert.notDeepEqual(a, b) +} + +describe('BackgroundComponent', () => { + describe('IDs', () => { + it('should initialize the BackgroundComponent.name field with the expected value', () => { + assert.equal(BackgroundComponent.name, 'BackgroundComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, BackgroundComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, BackgroundComponent) + assertBackgroundComponentEq(data, BackgroundComponentDefaults) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, BackgroundComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized BackgroundComponent', () => { + const before = getComponent(testEntity, BackgroundComponent) + setComponent(testEntity, BackgroundComponent, new Color('#123456')) + const after = getComponent(testEntity, BackgroundComponent) + assertBackgroundComponentNotEq(before, after) + }) + + it('should not change values of an initialized BackgroundComponent when the data passed had incorrect types', () => { + const before = getComponent(testEntity, BackgroundComponent) + // @ts-ignore Coerce an incorrect type into the component's data + setComponent(testEntity, BackgroundComponent, '#123456') + const after = getComponent(testEntity, BackgroundComponent) + assertBackgroundComponentEq(before, after) + }) + }) //:: onSet +}) //:: BackgroundComponent + +const EnvironmentMapComponentDefaults = null! as Texture + +function assertEnvironmentMapComponentEq(A: Texture, B: Texture) { + assert.deepEqual(A, B) +} + +function assertEnvironmentMapComponentNotEq(A: Texture, B: Texture) { + assert.notDeepEqual(A, B) +} + +describe('EnvironmentMapComponent', () => { + describe('IDs', () => { + it('should initialize the EnvironmentMapComponent.name field with the expected value', () => { + assert.equal(EnvironmentMapComponent.name, 'EnvironmentMapComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, EnvironmentMapComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, EnvironmentMapComponent) + assertEnvironmentMapComponentEq(data, EnvironmentMapComponentDefaults) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, EnvironmentMapComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized EnvironmentMapComponent', () => { + const before = getComponent(testEntity, EnvironmentMapComponent) + setComponent(testEntity, EnvironmentMapComponent, new Texture({} as OffscreenCanvas, 303)) + const after = getComponent(testEntity, EnvironmentMapComponent) + assertEnvironmentMapComponentNotEq(before, after) + }) + + it('should not change values of an initialized EnvironmentMapComponent when the data passed had incorrect types', () => { + const before = getComponent(testEntity, EnvironmentMapComponent) + // @ts-ignore Coerce an incorrect type into the component's data + setComponent(testEntity, EnvironmentMapComponent, '#123456') + const after = getComponent(testEntity, EnvironmentMapComponent) + assertEnvironmentMapComponentEq(before, after) + }) + }) //:: onSet +}) //:: EnvironmentMapComponent + +type FogData = FogBase +const FogComponentDefaults = null! as FogData + +function assertFogComponentEq(A: FogData, B: FogData) { + assert.equal(Boolean(A), Boolean(B)) + if (!A && !B) return + const a = { + name: (A !== null && A.name) || A, + color: (A !== null && A.color) || A, + json: (A !== null && A.toJSON) || A + } + const b = { + name: (B !== null && B.name) || B, + color: (B !== null && B.color) || B, + json: (B !== null && B.toJSON) || B + } + assert.equal(a.name, b.name) + assert.deepEqual(a.color, b.color) + assert.deepEqual(a.json, b.json) +} + +function assertFogComponentNotEq(A: FogData, B: FogData) { + const a = (A !== null && A.toJSON) || A + const b = (B !== null && B.toJSON) || B + assert.notDeepEqual(a, b) +} + +describe('FogComponent', () => { + describe('IDs', () => { + it('should initialize the FogComponent.name field with the expected value', () => { + assert.equal(FogComponent.name, 'FogComponent') + }) + }) //:: IDs + + describe('onInit', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, FogComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should initialize the component with the expected default values', () => { + const data = getComponent(testEntity, FogComponent) + assertFogComponentEq(data, FogComponentDefaults) + }) + }) //:: onInit + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + setComponent(testEntity, FogComponent) + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should change the values of an initialized FogComponent', () => { + const before = getComponent(testEntity, FogComponent) + setComponent(testEntity, FogComponent, { name: 'testFog' } as FogData) + const after = getComponent(testEntity, FogComponent) + assertFogComponentNotEq(before, after) + }) + + it('should not change values of an initialized FogComponent when the data passed had incorrect types', () => { + const before = getComponent(testEntity, FogComponent) + // @ts-ignore Coerce an incorrect type into the component's data + setComponent(testEntity, FogComponent, '#123456') + const after = getComponent(testEntity, FogComponent) + assertFogComponentEq(before, after) + }) + }) //:: onSet +}) //:: FogComponent diff --git a/packages/spatial/src/renderer/components/VisibleComponent.test.ts b/packages/spatial/src/renderer/components/VisibleComponent.test.ts new file mode 100644 index 0000000000..d44e74b4e1 --- /dev/null +++ b/packages/spatial/src/renderer/components/VisibleComponent.test.ts @@ -0,0 +1,121 @@ +/* +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 { + createEngine, + createEntity, + destroyEngine, + getComponent, + hasComponent, + removeEntity, + serializeComponent, + setComponent, + UndefinedEntity +} from '@etherealengine/ecs' +import assert from 'assert' +import { setVisibleComponent, VisibleComponent } from './VisibleComponent' + +const VisibleComponentDefault = true + +describe('VisibleComponent', () => { + describe('IDs', () => { + it('should initialize the VisibleComponent.name field with the expected value', () => { + assert.equal(VisibleComponent.name, 'VisibleComponent') + }) + + it('should initialize the VisibleComponent.jsonID field with the expected value', () => { + assert.equal(VisibleComponent.jsonID, 'EE_visible') + }) + }) //:: IDs + + describe('onSet', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should set the value of the VisibleComponent correctly', () => { + assert.notEqual(hasComponent(testEntity, VisibleComponent), VisibleComponentDefault) + setComponent(testEntity, VisibleComponent) + assert.equal(getComponent(testEntity, VisibleComponent), VisibleComponentDefault) + }) + }) //:: onSet + + describe('toJSON', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it('should serialize the component data as expected', () => { + setComponent(testEntity, VisibleComponent) + const result = serializeComponent(testEntity, VisibleComponent) + assert.equal(typeof result, 'boolean') + assert.equal(result, true) + }) + }) //:: toJSON +}) //:: VisibleComponent + +describe('setVisibleComponent', () => { + let testEntity = UndefinedEntity + + beforeEach(async () => { + createEngine() + testEntity = createEntity() + }) + + afterEach(() => { + removeEntity(testEntity) + return destroyEngine() + }) + + it("should add a VisibleComponent to the entity when it doesn't have one and `@param visible` is set to true", () => { + assert.equal(hasComponent(testEntity, VisibleComponent), false) + setVisibleComponent(testEntity, true) + assert.equal(hasComponent(testEntity, VisibleComponent), true) + }) + + it('should remove the VisibleComponent from the entity when it has one and `@param visible` is set to false', () => { + assert.equal(hasComponent(testEntity, VisibleComponent), false) + setVisibleComponent(testEntity, true) + assert.equal(hasComponent(testEntity, VisibleComponent), true) + setVisibleComponent(testEntity, false) + assert.equal(hasComponent(testEntity, VisibleComponent), false) + }) +}) //:: setVisibleComponent diff --git a/packages/spatial/src/transform/components/EntityTree.tsx b/packages/spatial/src/transform/components/EntityTree.tsx index 41feecb691..e71b52d2d3 100644 --- a/packages/spatial/src/transform/components/EntityTree.tsx +++ b/packages/spatial/src/transform/components/EntityTree.tsx @@ -168,12 +168,15 @@ export function removeEntityNodeRecursively(entity: Entity) { } /** - * Traverse child nodes of the given node. Traversal will start from the passed node - * note - does not support removing the current node during traversal - * @param entity Node to be traverse - * @param cb Callback function which will be called for every traverse - * @param index index of the curren node in it's parent - * @param tree Entity Tree + * @description + * Recursively call the `@param cb` function on `@param entity` and all of its children. + * The `@param cb` function will also be called for `@param entity` + * The tree will be traversed using the respective {@link EntityTreeComponent} of each entity found in the tree. + * @note + * Does not support removing the current `@param entity` node during traversal + * @param entity Entity Node where traversal will start + * @param cb Callback function called for every entity of the tree + * @param index Index of the current node (relative to its parent) */ export function traverseEntityNode(entity: Entity, cb: (entity: Entity, index: number) => void, index = 0): void { const entityTreeNode = getComponent(entity, EntityTreeComponent)