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)