diff --git a/packages/editor/src/classes/TransformGizmoControlComponent.ts b/packages/editor/src/classes/TransformGizmoControlComponent.ts index d60ab802e7..43051cd450 100755 --- a/packages/editor/src/classes/TransformGizmoControlComponent.ts +++ b/packages/editor/src/classes/TransformGizmoControlComponent.ts @@ -121,12 +121,7 @@ export const TransformGizmoControlComponent = defineComponent({ if (typeof json.showY === 'number') component.showY.set(json.showY) if (typeof json.showZ === 'number') component.showZ.set(json.showZ) }, - onRemove: (entity, component) => { - component.controlledEntities.set([]) - component.visualEntity.set(UndefinedEntity) - component.planeEntity.set(UndefinedEntity) - component.pivotEntity.set(UndefinedEntity) - }, + reactor: function (props) { const gizmoControlEntity = useEntityContext() const gizmoControlComponent = useComponent(gizmoControlEntity, TransformGizmoControlComponent) diff --git a/packages/editor/src/classes/TransformGizmoControlledComponent.ts b/packages/editor/src/classes/TransformGizmoControlledComponent.ts index 7c4091699a..8ab9ccdf48 100755 --- a/packages/editor/src/classes/TransformGizmoControlledComponent.ts +++ b/packages/editor/src/classes/TransformGizmoControlledComponent.ts @@ -56,9 +56,6 @@ export const TransformGizmoControlledComponent = defineComponent({ controller: UndefinedEntity } }, - onRemove: (entity, component) => { - component.controller.set(UndefinedEntity) - }, reactor: function (props) { const entity = useEntityContext() diff --git a/packages/editor/src/classes/TransformGizmoVisualComponent.ts b/packages/editor/src/classes/TransformGizmoVisualComponent.ts index 6a55343314..8255d3e7fe 100755 --- a/packages/editor/src/classes/TransformGizmoVisualComponent.ts +++ b/packages/editor/src/classes/TransformGizmoVisualComponent.ts @@ -108,16 +108,11 @@ export const TransformGizmoVisualComponent = defineComponent({ } return visual }, + onSet(entity, component, json) { if (!json) return }, - onRemove: (entity, component) => { - for (const mode in TransformMode) { - removeEntity(component.gizmo[mode]) - removeEntity(component.picker[mode]) - removeEntity(component.helper[mode]) - } - }, + reactor: function (props) { const gizmoVisualEntity = useEntityContext() const visualComponent = useComponent(gizmoVisualEntity, TransformGizmoVisualComponent) @@ -179,6 +174,10 @@ export const TransformGizmoVisualComponent = defineComponent({ cleanupGizmo(pickerObject[mode]) removeObjectFromGroup(helper[mode], helperObject[mode]) cleanupGizmo(helperObject[mode]) + + removeEntity(gizmo[mode]) + removeEntity(picker[mode]) + removeEntity(helper[mode]) } } }, []) diff --git a/packages/engine/src/avatar/components/AvatarAnimationComponent.ts b/packages/engine/src/avatar/components/AvatarAnimationComponent.ts index 39d6d3bde5..9c39b2f4ab 100755 --- a/packages/engine/src/avatar/components/AvatarAnimationComponent.ts +++ b/packages/engine/src/avatar/components/AvatarAnimationComponent.ts @@ -30,14 +30,12 @@ import { AnimationAction, Group, Matrix4, SkeletonHelper, Vector3 } from 'three' import { defineComponent, getComponent, - removeComponent, setComponent, useComponent, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Entity } from '@etherealengine/ecs/src/Entity' import { createEntity, entityExists, removeEntity, useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' -import { getMutableState, matches, none, useHookstate } from '@etherealengine/hyperflux' +import { getMutableState, matches, useHookstate } from '@etherealengine/hyperflux' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { setObjectLayers } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' @@ -102,7 +100,6 @@ export const AvatarRigComponent = defineComponent({ rawRig: null! as VRMHumanBones, /** contains ik solve data */ ikMatrices: {} as Record, - helperEntity: null as Entity | null, /** The VRM model */ vrm: null! as VRM, avatarURL: null as string | null @@ -117,11 +114,6 @@ export const AvatarRigComponent = defineComponent({ if (matches.string.test(json.avatarURL)) component.avatarURL.set(json.avatarURL) }, - onRemove: (entity, component) => { - // ensure synchronously removed - if (component.helperEntity.value) removeComponent(component.helperEntity.value, ComputedTransformComponent) - }, - reactor: function () { const entity = useEntityContext() const debugEnabled = useHookstate(getMutableState(RendererState).avatarDebug) @@ -144,7 +136,6 @@ export const AvatarRigComponent = defineComponent({ const helperEntity = createEntity() setVisibleComponent(helperEntity, true) addObjectToGroup(helperEntity, helper) - rigComponent.helperEntity.set(helperEntity) setComponent(helperEntity, NameComponent, helper.name) setObjectLayers(helper, ObjectLayers.AvatarHelper) @@ -158,7 +149,6 @@ export const AvatarRigComponent = defineComponent({ return () => { removeEntity(helperEntity) - rigComponent.helperEntity.set(none) } }, [visible, debugEnabled, pending, rigComponent.normalizedRig]) diff --git a/packages/engine/src/gltf/GLTFState.test.tsx b/packages/engine/src/gltf/GLTFState.test.tsx index eddbadc537..5719814a0a 100644 --- a/packages/engine/src/gltf/GLTFState.test.tsx +++ b/packages/engine/src/gltf/GLTFState.test.tsx @@ -373,8 +373,14 @@ describe('GLTFState', () => { assert.equal(getComponent(nodeEntity!, VisibleComponent), true) assert(getComponent(nodeEntity!, HemisphereLightComponent)) - assert.equal(getComponent(nodeEntity!, HemisphereLightComponent).skyColor.getHex(), new Color('green').getHex()) - assert.equal(getComponent(nodeEntity!, HemisphereLightComponent).groundColor.getHex(), new Color('purple').getHex()) + assert.equal( + new Color(getComponent(nodeEntity!, HemisphereLightComponent).skyColor).getHex(), + new Color('green').getHex() + ) + assert.equal( + new Color(getComponent(nodeEntity!, HemisphereLightComponent).groundColor).getHex(), + new Color('purple').getHex() + ) assert.equal(getComponent(nodeEntity!, HemisphereLightComponent).intensity, 0.5) }) diff --git a/packages/engine/src/scene/components/HyperspaceTagComponent.ts b/packages/engine/src/scene/components/HyperspaceTagComponent.ts index 6b080de8f3..2872048eb4 100644 --- a/packages/engine/src/scene/components/HyperspaceTagComponent.ts +++ b/packages/engine/src/scene/components/HyperspaceTagComponent.ts @@ -56,7 +56,7 @@ import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' import { createEntity, removeEntity, useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { useExecute } from '@etherealengine/ecs/src/SystemFunctions' -import { getMutableState, getState } from '@etherealengine/hyperflux' +import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' import { ObjectDirection } from '@etherealengine/spatial/src/common/constants/MathConstants' import { createTransitionState } from '@etherealengine/spatial/src/common/functions/createTransitionState' @@ -172,26 +172,23 @@ export const HyperspaceTagComponent = defineComponent({ return { // all internals sceneVisible: true, - transition: createTransitionState(0.5, 'OUT'), - hyperspaceEffectEntity: UndefinedEntity, - ambientLightEntity: UndefinedEntity + transition: createTransitionState(0.5, 'OUT') } }, - onRemove(entity, component) { - removeEntity(component.ambientLightEntity.value) - destroyEntityTree(component.hyperspaceEffectEntity.value) - }, - reactor: () => { const entity = useEntityContext() const [galaxyTexture] = useTexture( `${config.client.fileServer}/projects/default-project/assets/galaxyTexture.jpg`, entity ) + const hyperspaceEffectEntityState = useHookstate(createEntity) + const ambientLightEntityState = useHookstate(createEntity) useEffect(() => { - const hyperspaceEffectEntity = createEntity() + const hyperspaceEffectEntity = hyperspaceEffectEntityState.value + const ambientLightEntity = ambientLightEntityState.value + const hyperspaceEffect = new PortalEffect(hyperspaceEffectEntity) addObjectToGroup(hyperspaceEffectEntity, hyperspaceEffect) setObjectLayers(hyperspaceEffect, ObjectLayers.Portal) @@ -200,7 +197,6 @@ export const HyperspaceTagComponent = defineComponent({ setComponent(hyperspaceEffectEntity, EntityTreeComponent, { parentEntity: entity }) setComponent(hyperspaceEffectEntity, VisibleComponent) - const ambientLightEntity = createEntity() const light = new AmbientLight('#aaa') light.layers.enable(ObjectLayers.Portal) addObjectToGroup(ambientLightEntity, light) @@ -222,14 +218,16 @@ export const HyperspaceTagComponent = defineComponent({ new Vector3(0, 0, 1).applyQuaternion(cameraTransform.rotation).setY(0).normalize() ) - getMutableComponent(entity, HyperspaceTagComponent).hyperspaceEffectEntity.set(hyperspaceEffectEntity) - getMutableComponent(entity, HyperspaceTagComponent).ambientLightEntity.set(ambientLightEntity) + return () => { + removeEntity(ambientLightEntity) + destroyEntityTree(hyperspaceEffectEntity) + } }, []) useEffect(() => { if (!galaxyTexture) return - const hyperspaceEffectEntity = getComponent(entity, HyperspaceTagComponent).hyperspaceEffectEntity + const hyperspaceEffectEntity = hyperspaceEffectEntityState.value const hyperspaceEffect = getComponent(hyperspaceEffectEntity, GroupComponent)[0] as any as PortalEffect hyperspaceEffect.texture = galaxyTexture }, [galaxyTexture]) @@ -238,8 +236,9 @@ export const HyperspaceTagComponent = defineComponent({ () => { if (!hasComponent(entity, HyperspaceTagComponent)) return - const { transition, hyperspaceEffectEntity } = getComponent(entity, HyperspaceTagComponent) + const hyperspaceEffectEntity = hyperspaceEffectEntityState.value if (!hyperspaceEffectEntity) return + const { transition } = getComponent(entity, HyperspaceTagComponent) const hyperspaceEffect = getComponent(hyperspaceEffectEntity, GroupComponent)[0] as any as PortalEffect const cameraTransform = getComponent(Engine.instance.cameraEntity, TransformComponent) diff --git a/packages/engine/src/scene/components/MediaComponent.ts b/packages/engine/src/scene/components/MediaComponent.ts index 99e1a27195..0eb965cbe3 100644 --- a/packages/engine/src/scene/components/MediaComponent.ts +++ b/packages/engine/src/scene/components/MediaComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import type Hls from 'hls.js' -import { startTransition, useEffect } from 'react' +import { startTransition, useEffect, useLayoutEffect } from 'react' import { DoubleSide, MeshBasicMaterial, PlaneGeometry, Vector3 } from 'three' import { isClient } from '@etherealengine/common/src/utils/getEnvironment' @@ -103,19 +103,26 @@ export const MediaElementComponent = defineComponent({ component.element.set(json.element as HTMLMediaElement) }, - onRemove: (entity, component) => { - const element = component.element.get({ noproxy: true }) as HTMLMediaElement - component.hls.value?.destroy() - component.hls.set(none) - const audioNodeGroup = AudioNodeGroups.get(element) - if (audioNodeGroup && audioNodeGroup.panner) removePannerNode(audioNodeGroup) - AudioNodeGroups.delete(element) - element.pause() - element.removeAttribute('src') - element.load() - element.remove() - component.element.set(none) - component.abortController.value.abort() + reactor: () => { + const entity = useEntityContext() + const mediaElementComponent = useComponent(entity, MediaElementComponent) + + useLayoutEffect(() => { + return () => { + const element = mediaElementComponent.element.get({ noproxy: true }) as HTMLMediaElement + mediaElementComponent.hls.value?.destroy() + mediaElementComponent.hls.set(none) + const audioNodeGroup = AudioNodeGroups.get(element) + if (audioNodeGroup && audioNodeGroup.panner) removePannerNode(audioNodeGroup) + AudioNodeGroups.delete(element) + element.pause() + element.removeAttribute('src') + element.load() + element.remove() + mediaElementComponent.element.set(none) + mediaElementComponent.abortController.value.abort() + } + }, []) }, errors: ['MEDIA_ERROR', 'HLS_ERROR'] @@ -156,10 +163,6 @@ export const MediaComponent = defineComponent({ } }, - onRemove: (entity, component) => { - removeComponent(entity, MediaElementComponent) - }, - toJSON: (entity, component) => { return { controls: component.controls.value, @@ -288,6 +291,10 @@ export function MediaReactor() { document.body.removeEventListener('touchend', handleAutoplay) renderer.domElement.removeEventListener('pointerup', handleAutoplay) renderer.domElement.removeEventListener('touchend', handleAutoplay) + + removeComponent(entity, BoundingBoxComponent) + removeComponent(entity, InputComponent) + removeComponent(entity, MediaElementComponent) } }, []) diff --git a/packages/engine/src/scene/components/SourceComponent.ts b/packages/engine/src/scene/components/SourceComponent.ts index 1046ae22fb..01ab52ee40 100644 --- a/packages/engine/src/scene/components/SourceComponent.ts +++ b/packages/engine/src/scene/components/SourceComponent.ts @@ -23,9 +23,10 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { useEntityContext } from '@etherealengine/ecs' +import { defineComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' -import { hookstate, none } from '@etherealengine/hyperflux' +import { hookstate, none, useImmediateEffect } from '@etherealengine/hyperflux' const entitiesBySource = {} as Record @@ -38,26 +39,32 @@ export const SourceComponent = defineComponent({ if (typeof src !== 'string') throw new Error('SourceComponent expects a non-empty string') component.set(src) - - const exists = SourceComponent.entitiesBySource[src] - const entitiesBySourceState = SourceComponent.entitiesBySourceState[src] - if (exists) { - if (exists.includes(entity)) return - entitiesBySourceState.merge([entity]) - } else { - entitiesBySourceState.set([entity]) - } }, - onRemove: (entity, component) => { - const src = component.value + reactor: () => { + const entity = useEntityContext() + const sourceComponent = useComponent(entity, SourceComponent) + + useImmediateEffect(() => { + const source = sourceComponent.value + const entitiesBySourceState = SourceComponent.entitiesBySourceState[source] + if (!entitiesBySourceState.value) { + entitiesBySourceState.set([entity]) + } else { + entitiesBySourceState.merge([entity]) + } + + return () => { + const entities = SourceComponent.entitiesBySource[source].filter((currentEntity) => currentEntity !== entity) + if (entities.length === 0) { + SourceComponent.entitiesBySourceState[source].set(none) + } else { + SourceComponent.entitiesBySourceState[source].set(entities) + } + } + }, [sourceComponent]) - const entities = SourceComponent.entitiesBySource[src].filter((currentEntity) => currentEntity !== entity) - if (entities.length === 0) { - SourceComponent.entitiesBySourceState[src].set(none) - } else { - SourceComponent.entitiesBySourceState[src].set(entities) - } + return null }, entitiesBySourceState: hookstate(entitiesBySource), diff --git a/packages/spatial/src/camera/components/CameraComponent.ts b/packages/spatial/src/camera/components/CameraComponent.ts index 7e9de51290..fec2172d39 100644 --- a/packages/spatial/src/camera/components/CameraComponent.ts +++ b/packages/spatial/src/camera/components/CameraComponent.ts @@ -25,13 +25,16 @@ Ethereal Engine. All Rights Reserved. import { ArrayCamera, PerspectiveCamera } from 'three' -import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { defineComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { useEntityContext } from '@etherealengine/ecs' +import { useImmediateEffect } from '@etherealengine/hyperflux' import { addObjectToGroup, removeObjectFromGroup } from '../../renderer/components/GroupComponent' export const CameraComponent = defineComponent({ name: 'CameraComponent', jsonID: 'EE_camera', + onInit: (entity) => { const camera = new ArrayCamera() camera.fov = 60 @@ -41,17 +44,15 @@ export const CameraComponent = defineComponent({ camera.cameras = [new PerspectiveCamera().copy(camera, false)] return camera }, + onSet: (entity, component, json) => { - addObjectToGroup(entity, component.value as ArrayCamera) if (!json) return if (typeof json.fov === 'number') component.fov.set(json.fov) - if (typeof json.aspect === 'number') component.fov.set(json.aspect) - if (typeof json.near === 'number') component.fov.set(json.near) - if (typeof json.far === 'number') component.fov.set(json.far) - }, - onRemove: (entity, component) => { - removeObjectFromGroup(entity, component.value as ArrayCamera) + if (typeof json.aspect === 'number') component.aspect.set(json.aspect) + if (typeof json.near === 'number') component.near.set(json.near) + if (typeof json.far === 'number') component.far.set(json.far) }, + toJSON: (entity, component) => { return { fov: component.fov.value, @@ -59,5 +60,19 @@ export const CameraComponent = defineComponent({ near: component.near.value, far: component.far.value } + }, + + reactor: () => { + const entity = useEntityContext() + const cameraComponent = useComponent(entity, CameraComponent) + + useImmediateEffect(() => { + const camera = cameraComponent.value as ArrayCamera + addObjectToGroup(entity, camera) + return () => { + removeObjectFromGroup(entity, camera) + } + }, []) + return null } }) diff --git a/packages/spatial/src/common/NameComponent.ts b/packages/spatial/src/common/NameComponent.ts index f25fa2416c..b9d869f0a8 100755 --- a/packages/spatial/src/common/NameComponent.ts +++ b/packages/spatial/src/common/NameComponent.ts @@ -23,9 +23,10 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { useEntityContext } from '@etherealengine/ecs' +import { defineComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' -import { hookstate, none } from '@etherealengine/hyperflux' +import { useImmediateEffect } from '@etherealengine/hyperflux' const entitiesByName = {} as Record @@ -36,30 +37,28 @@ export const NameComponent = defineComponent({ onSet: (entity, component, name?: string) => { if (typeof name !== 'string') throw new Error('NameComponent expects a non-empty string') - // remove the entity from the previous name state - if (component.value && entitiesByName[component.value]) { - const index = entitiesByName[component.value].indexOf(entity) - NameComponent.entitiesByNameState[component.value][index].set(none) - if (!entitiesByName[component.value].length) NameComponent.entitiesByNameState[component.value].set(none) - } - // set the new name component.set(name) - // add the entity to the new name state - const exists = NameComponent.entitiesByName[name] - const entitiesByNameState = NameComponent.entitiesByNameState - if (exists) { - if (!exists.includes(entity)) entitiesByNameState.merge({ [name]: [...exists, entity] }) - } else entitiesByNameState.merge({ [name]: [entity] }) }, - onRemove: (entity, component) => { - const name = component.value - const namedEntities = NameComponent.entitiesByNameState[name] - const isSingleton = namedEntities.length === 1 - isSingleton && namedEntities.set(none) - !isSingleton && namedEntities.set(namedEntities.value.filter((namedEntity) => namedEntity !== entity)) + reactor: () => { + const entity = useEntityContext() + const nameComponent = useComponent(entity, NameComponent) + + useImmediateEffect(() => { + const name = nameComponent.value + if (!entitiesByName[name]) { + entitiesByName[name] = [] + } + + entitiesByName[name].push(entity) + return () => { + const index = entitiesByName[name].indexOf(entity) + entitiesByName[name].splice(index, 1) + } + }, [nameComponent.value]) + + return null }, - entitiesByNameState: hookstate(entitiesByName), entitiesByName: entitiesByName as Readonly }) diff --git a/packages/spatial/src/renderer/components/GroupComponent.tsx b/packages/spatial/src/renderer/components/GroupComponent.tsx index 0d4e404e28..8ac85bb60b 100644 --- a/packages/spatial/src/renderer/components/GroupComponent.tsx +++ b/packages/spatial/src/renderer/components/GroupComponent.tsx @@ -51,14 +51,6 @@ export const GroupComponent = defineComponent({ onInit: (entity: Entity) => { return [] as Object3D[] - }, - - onRemove: (entity, component) => { - for (const obj of component.value) { - if (obj.parent) { - obj.removeFromParent() - } - } } }) diff --git a/packages/spatial/src/renderer/components/Object3DComponent.ts b/packages/spatial/src/renderer/components/Object3DComponent.ts index d351ccc9d6..cc02f0c1ec 100644 --- a/packages/spatial/src/renderer/components/Object3DComponent.ts +++ b/packages/spatial/src/renderer/components/Object3DComponent.ts @@ -25,8 +25,9 @@ Ethereal Engine. All Rights Reserved. import { Object3D } from 'three' -import { defineComponent, getComponent, hasComponent } from '@etherealengine/ecs' +import { defineComponent, useComponent, useEntityContext, useOptionalComponent } from '@etherealengine/ecs' +import { NO_PROXY, useImmediateEffect } from '@etherealengine/hyperflux' import { NameComponent } from '../../common/NameComponent' export const Object3DComponent = defineComponent({ @@ -36,7 +37,20 @@ export const Object3DComponent = defineComponent({ onInit: (entity) => null! as Object3D, onSet: (entity, component, object3d: Object3D) => { if (!object3d || !object3d.isObject3D) throw new Error('Object3DComponent: Invalid object3d') - if (hasComponent(entity, NameComponent)) object3d.name = getComponent(entity, NameComponent) component.set(object3d) + }, + + reactor: () => { + const entity = useEntityContext() + const object3DComponent = useComponent(entity, Object3DComponent) + const nameComponent = useOptionalComponent(entity, NameComponent) + + useImmediateEffect(() => { + if (!nameComponent) return + const object = object3DComponent.get(NO_PROXY) as Object3D + object.name = nameComponent.value + }, [nameComponent?.value]) + + return null } }) diff --git a/packages/spatial/src/renderer/components/lights/AmbientLightComponent.ts b/packages/spatial/src/renderer/components/lights/AmbientLightComponent.ts index 0555b04228..71264d5431 100644 --- a/packages/spatial/src/renderer/components/lights/AmbientLightComponent.ts +++ b/packages/spatial/src/renderer/components/lights/AmbientLightComponent.ts @@ -24,12 +24,13 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { AmbientLight, Color } from 'three' +import { AmbientLight, ColorRepresentation } from 'three' import { defineComponent, setComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { matches } from '@etherealengine/hyperflux' +import { matchesColor } from '../../../common/functions/MatchesUtils' import { useDisposable } from '../../../resources/resourceHooks' import { addObjectToGroup, removeObjectFromGroup } from '../GroupComponent' import { LightTagComponent } from './LightTagComponent' @@ -40,16 +41,14 @@ export const AmbientLightComponent = defineComponent({ onInit: (entity) => { return { - // todo, maybe we want to reference light.color instead of creating a new Color? - color: new Color(), + color: 0xffffff as ColorRepresentation, intensity: 1 } }, onSet: (entity, component, json) => { if (!json) return - if (matches.object.test(json.color) && json.color.isColor) component.color.set(json.color) - if (matches.string.test(json.color)) component.color.value.set(json.color) + if (matchesColor.test(json.color)) component.color.set(json.color) if (matches.number.test(json.intensity)) component.intensity.set(json.intensity) }, diff --git a/packages/spatial/src/renderer/components/lights/DirectionalLightComponent.ts b/packages/spatial/src/renderer/components/lights/DirectionalLightComponent.ts index 86e1fb0f39..6a31e12dda 100644 --- a/packages/spatial/src/renderer/components/lights/DirectionalLightComponent.ts +++ b/packages/spatial/src/renderer/components/lights/DirectionalLightComponent.ts @@ -34,7 +34,7 @@ import { useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' -import { matches, useMutableState } from '@etherealengine/hyperflux' +import { matches, useImmediateEffect, useMutableState } from '@etherealengine/hyperflux' import { mergeBufferGeometries } from '../../../common/classes/BufferGeometryUtils' import { useDisposable } from '../../../resources/resourceHooks' @@ -147,7 +147,7 @@ export const DirectionalLightComponent = defineComponent({ const [light] = useDisposable(DirectionalLight, entity) const lightHelper = useOptionalComponent(entity, LineSegmentComponent) - useEffect(() => { + useImmediateEffect(() => { setComponent(entity, LightTagComponent) directionalLightComponent.light.set(light) addObjectToGroup(entity, light) diff --git a/packages/spatial/src/renderer/components/lights/HemisphereLightComponent.ts b/packages/spatial/src/renderer/components/lights/HemisphereLightComponent.ts index 1ed2932159..8bd3d8b458 100644 --- a/packages/spatial/src/renderer/components/lights/HemisphereLightComponent.ts +++ b/packages/spatial/src/renderer/components/lights/HemisphereLightComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { Color, HemisphereLight } from 'three' +import { ColorRepresentation, HemisphereLight } from 'three' import { defineComponent, @@ -37,6 +37,7 @@ import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { matches, useMutableState } from '@etherealengine/hyperflux' import { LightHelperComponent } from '../../../common/debug/LightHelperComponent' +import { matchesColor } from '../../../common/functions/MatchesUtils' import { useDisposable } from '../../../resources/resourceHooks' import { RendererState } from '../../RendererState' import { addObjectToGroup, removeObjectFromGroup } from '../GroupComponent' @@ -48,20 +49,16 @@ export const HemisphereLightComponent = defineComponent({ onInit: (entity) => { return { - skyColor: new Color(), - groundColor: new Color(), + skyColor: 0xffffff as ColorRepresentation, + groundColor: 0xffffff as ColorRepresentation, intensity: 1 } }, onSet: (entity, component, json) => { if (!json) return - if (matches.object.test(json.skyColor) && json.skyColor.isColor) component.skyColor.set(json.skyColor) - if (matches.string.test(json.skyColor) || matches.number.test(json.skyColor)) - component.skyColor.value.set(json.skyColor) - if (matches.object.test(json.groundColor) && json.groundColor.isColor) component.groundColor.set(json.groundColor) - if (matches.string.test(json.groundColor) || matches.number.test(json.groundColor)) - component.groundColor.value.set(json.groundColor) + if (matchesColor.test(json.skyColor)) component.skyColor.set(json.skyColor) + if (matchesColor.test(json.groundColor)) component.groundColor.set(json.groundColor) if (matches.number.test(json.intensity)) component.intensity.set(json.intensity) }, diff --git a/packages/spatial/src/renderer/components/lights/PointLightComponent.ts b/packages/spatial/src/renderer/components/lights/PointLightComponent.ts index d7c62095b9..db46218d23 100644 --- a/packages/spatial/src/renderer/components/lights/PointLightComponent.ts +++ b/packages/spatial/src/renderer/components/lights/PointLightComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { Color, PointLight } from 'three' +import { ColorRepresentation, PointLight } from 'three' import { defineComponent, @@ -38,6 +38,7 @@ import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { matches, useMutableState } from '@etherealengine/hyperflux' import { LightHelperComponent } from '../../../common/debug/LightHelperComponent' +import { matchesColor } from '../../../common/functions/MatchesUtils' import { useDisposable } from '../../../resources/resourceHooks' import { isMobileXRHeadset } from '../../../xr/XRState' import { RendererState } from '../../RendererState' @@ -50,7 +51,7 @@ export const PointLightComponent = defineComponent({ onInit: (entity) => { return { - color: new Color(), + color: 0xffffff as ColorRepresentation, intensity: 1, range: 0, decay: 2, @@ -63,8 +64,7 @@ export const PointLightComponent = defineComponent({ onSet: (entity, component, json) => { if (!json) return - if (matches.object.test(json.color) && json.color.isColor) component.color.set(json.color) - if (matches.string.test(json.color) || matches.number.test(json.color)) component.color.value.set(json.color) + if (matchesColor.test(json.color)) component.color.set(json.color) if (matches.number.test(json.intensity)) component.intensity.set(json.intensity) if (matches.number.test(json.range)) component.range.set(json.range) if (matches.number.test(json.decay)) component.decay.set(json.decay) diff --git a/packages/spatial/src/renderer/components/lights/SpotLightComponent.ts b/packages/spatial/src/renderer/components/lights/SpotLightComponent.ts index 3ff54d0f9f..ab391e25eb 100644 --- a/packages/spatial/src/renderer/components/lights/SpotLightComponent.ts +++ b/packages/spatial/src/renderer/components/lights/SpotLightComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { Color, SpotLight } from 'three' +import { ColorRepresentation, SpotLight } from 'three' import { defineComponent, @@ -37,10 +37,11 @@ import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { matches, useMutableState } from '@etherealengine/hyperflux' import { LightHelperComponent } from '../../../common/debug/LightHelperComponent' +import { matchesColor } from '../../../common/functions/MatchesUtils' import { useDisposable } from '../../../resources/resourceHooks' import { isMobileXRHeadset } from '../../../xr/XRState' -import { useUpdateLight } from '../../functions/useUpdateLight' import { RendererState } from '../../RendererState' +import { useUpdateLight } from '../../functions/useUpdateLight' import { addObjectToGroup, removeObjectFromGroup } from '../GroupComponent' import { LightTagComponent } from './LightTagComponent' @@ -57,7 +58,7 @@ export const SpotLightComponent = defineComponent({ onInit: (entity) => { return { - color: new Color(), + color: 0xffffff as ColorRepresentation, intensity: 10, range: 0, decay: 2, @@ -71,8 +72,7 @@ export const SpotLightComponent = defineComponent({ onSet: (entity, component, json) => { if (!json) return - if (matches.object.test(json.color) && json.color.isColor) component.color.set(json.color) - if (matches.string.test(json.color) || matches.number.test(json.color)) component.color.value.set(json.color) + if (matchesColor.test(json.color)) component.color.set(json.color) if (matches.number.test(json.intensity)) component.intensity.set(json.intensity) if (matches.number.test(json.range)) component.range.set(json.range) if (matches.number.test(json.decay)) component.decay.set(json.decay) diff --git a/packages/spatial/src/transform/components/ComputedTransformComponent.ts b/packages/spatial/src/transform/components/ComputedTransformComponent.ts index ecf06aca6a..2c44760d91 100644 --- a/packages/spatial/src/transform/components/ComputedTransformComponent.ts +++ b/packages/spatial/src/transform/components/ComputedTransformComponent.ts @@ -28,6 +28,7 @@ import { matches } from 'ts-matches' import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' +import { useImmediateEffect } from '@etherealengine/hyperflux' import { TransformComponent } from './TransformComponent' export const ComputedTransformComponent = defineComponent({ @@ -46,7 +47,12 @@ export const ComputedTransformComponent = defineComponent({ matches.arrayOf(matches.number).test(json.referenceEntities) && component.referenceEntities.set(json.referenceEntities) if (typeof json.computeFunction === 'function') component.merge({ computeFunction: json.computeFunction }) + }, - TransformComponent.transformsNeedSorting = true + reactor: () => { + useImmediateEffect(() => { + TransformComponent.transformsNeedSorting = true + }, []) + return null } }) diff --git a/packages/spatial/src/transform/components/DistanceComponents.ts b/packages/spatial/src/transform/components/DistanceComponents.ts index 8c08310a3f..d2a1cad955 100644 --- a/packages/spatial/src/transform/components/DistanceComponents.ts +++ b/packages/spatial/src/transform/components/DistanceComponents.ts @@ -25,8 +25,10 @@ Ethereal Engine. All Rights Reserved. import { Types } from 'bitecs' +import { useEntityContext } from '@etherealengine/ecs' import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' +import { useLayoutEffect } from 'react' export const DistanceComponentSchema = { squaredDistance: Types.f32 } @@ -44,9 +46,14 @@ export const FrustumCullCameraComponent = defineComponent({ name: 'FrustumCullCameraComponent', schema: FrustumCullCameraSchema, - onRemove(entity, component) { - // reset upon removing the component - FrustumCullCameraComponent.isCulled[entity] = 0 + reactor: () => { + const entity = useEntityContext() + useLayoutEffect(() => { + return () => { + // reset upon removing the component + FrustumCullCameraComponent.isCulled[entity] = 0 + } + }, []) } }) diff --git a/packages/spatial/src/transform/components/TransformComponent.ts b/packages/spatial/src/transform/components/TransformComponent.ts index 63225e8048..a6b858ff29 100755 --- a/packages/spatial/src/transform/components/TransformComponent.ts +++ b/packages/spatial/src/transform/components/TransformComponent.ts @@ -26,13 +26,20 @@ Ethereal Engine. All Rights Reserved. import { Types } from 'bitecs' import { Euler, Matrix4, Quaternion, Vector3 } from 'three' -import { defineComponent, getComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { + defineComponent, + getComponent, + getOptionalComponent, + useComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' import { EntityTreeComponent, getAncestorWithComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' +import { useEntityContext } from '@etherealengine/ecs' +import { useImmediateEffect } from '@etherealengine/hyperflux' import { isZero } from '../../common/functions/MathFunctions' import { proxifyQuaternionWithDirty, proxifyVector3WithDirty } from '../../common/proxies/createThreejsProxy' import { SceneComponent } from '../../renderer/components/SceneComponents' @@ -90,17 +97,6 @@ export const TransformComponent = defineComponent({ if (json?.position) component.position.value.copy(json.position) if (rotation) component.rotation.value.copy(rotation) if (json?.scale && !isZero(json.scale)) component.scale.value.copy(json.scale) - - const transform = getComponent(entity, TransformComponent) - composeMatrix(entity) - const entityTree = getOptionalComponent(entity, EntityTreeComponent) - const parentEntity = entityTree?.parentEntity - if (parentEntity) { - const parentTransform = getOptionalComponent(parentEntity, TransformComponent) - if (parentTransform) transform.matrixWorld.multiplyMatrices(parentTransform.matrixWorld, transform.matrix) - } else { - transform.matrixWorld.copy(transform.matrix) - } }, toJSON: (entity, component) => { @@ -111,8 +107,28 @@ export const TransformComponent = defineComponent({ } }, - onRemove: (entity) => { - delete TransformComponent.dirtyTransforms[entity] + reactor: () => { + const entity = useEntityContext() + const transformComponent = useComponent(entity, TransformComponent) + + useImmediateEffect(() => { + const transform = transformComponent.value as TransformComponentType + composeMatrix(entity) + const entityTree = getOptionalComponent(entity, EntityTreeComponent) + const parentEntity = entityTree?.parentEntity + if (parentEntity) { + const parentTransform = getOptionalComponent(parentEntity, TransformComponent) + if (parentTransform) transform.matrixWorld.multiplyMatrices(parentTransform.matrixWorld, transform.matrix) + } else { + transform.matrixWorld.copy(transform.matrix) + } + + return () => { + delete TransformComponent.dirtyTransforms[entity] + } + }, []) + + return null }, getWorldPosition: (entity: Entity, vec3: Vector3) => { diff --git a/packages/spatial/src/xr/XRComponents.ts b/packages/spatial/src/xr/XRComponents.ts index 97dc763307..c74738c16f 100644 --- a/packages/spatial/src/xr/XRComponents.ts +++ b/packages/spatial/src/xr/XRComponents.ts @@ -27,9 +27,14 @@ import type { VRMHumanBoneName } from '@pixiv/three-vrm' import { useEffect } from 'react' import { Engine, UndefinedEntity } from '@etherealengine/ecs' -import { defineComponent, setComponent, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { + defineComponent, + setComponent, + useComponent, + useOptionalComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' -import { getState, matches } from '@etherealengine/hyperflux' +import { NO_PROXY, getState, matches, useImmediateEffect } from '@etherealengine/hyperflux' import { EntityTreeComponent } from '../transform/components/EntityTree' import { TransformComponent } from '../transform/components/TransformComponent' @@ -285,12 +290,21 @@ export const XRAnchorComponent = defineComponent({ anchor: XRAnchor } ) => { - component.anchor.value?.delete() component.anchor.set(data.anchor) }, - onRemove: (entity, component) => { - component.anchor.value.delete() + reactor: () => { + const entity = useEntityContext() + const xrAnchorComponent = useComponent(entity, XRAnchorComponent) + + useImmediateEffect(() => { + const anchor = xrAnchorComponent.anchor.get(NO_PROXY) + return () => { + anchor?.delete() + } + }, [xrAnchorComponent.anchor]) + + return null } }) @@ -307,18 +321,28 @@ export const XRSpaceComponent = defineComponent({ onSet: (entity, component, args: { space: XRSpace; baseSpace: XRSpace }) => { component.space.set(args.space) component.baseSpace.set(args.baseSpace) + }, - let parentEntity = UndefinedEntity - switch (args.baseSpace) { - case ReferenceSpace.localFloor: - parentEntity = Engine.instance.localFloorEntity - break - case ReferenceSpace.viewer: - parentEntity = Engine.instance.cameraEntity - break - } + reactor: () => { + const entity = useEntityContext() + const xrSpaceComponent = useComponent(entity, XRSpaceComponent) + + useImmediateEffect(() => { + const baseSpace = xrSpaceComponent.baseSpace.value + let parentEntity = UndefinedEntity + switch (baseSpace) { + case ReferenceSpace.localFloor: + parentEntity = Engine.instance.localFloorEntity + break + case ReferenceSpace.viewer: + parentEntity = Engine.instance.cameraEntity + break + } + + setComponent(entity, EntityTreeComponent, { parentEntity }) + setComponent(entity, TransformComponent) + }, []) - setComponent(entity, EntityTreeComponent, { parentEntity }) - setComponent(entity, TransformComponent) + return null } }) diff --git a/packages/spatial/src/xrui/components/PointerComponent.ts b/packages/spatial/src/xrui/components/PointerComponent.ts index 56dbf9f9df..d161c667bc 100644 --- a/packages/spatial/src/xrui/components/PointerComponent.ts +++ b/packages/spatial/src/xrui/components/PointerComponent.ts @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { useEffect } from 'react' +import { useEffect, useLayoutEffect } from 'react' import { BufferGeometry, Float32BufferAttribute, @@ -73,14 +73,9 @@ export const PointerComponent = defineComponent({ onSet: (entity, component, json) => { if (!json) return - if (matches.object.test(json.inputSource)) component.inputSource.set(json.inputSource) }, - onRemove: (entity, component) => { - PointerComponent.pointers.delete(component.inputSource.value as XRInputSource) - }, - reactor: () => { const entity = useEntityContext() const pointerComponentState = useComponent(entity, PointerComponent) @@ -98,6 +93,13 @@ export const PointerComponent = defineComponent({ } }) + useLayoutEffect(() => { + const inputSource = pointerComponentState.inputSource.value as XRInputSource + return () => { + PointerComponent.pointers.delete(inputSource) + } + }, []) + useEffect(() => { const inputSource = pointerComponentState.inputSource.value const pointer = createPointer(inputSource as XRInputSource) diff --git a/packages/spatial/src/xrui/components/XRUIComponent.ts b/packages/spatial/src/xrui/components/XRUIComponent.ts index 1895877889..7e9ebb523c 100644 --- a/packages/spatial/src/xrui/components/XRUIComponent.ts +++ b/packages/spatial/src/xrui/components/XRUIComponent.ts @@ -23,8 +23,10 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { useEntityContext } from '@etherealengine/ecs' +import { defineComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import type { WebContainer3D } from '@etherealengine/xrui' +import { useLayoutEffect } from 'react' export const XRUIComponent = defineComponent({ name: 'XRUIComponent', @@ -39,7 +41,17 @@ export const XRUIComponent = defineComponent({ } }, - onRemove: (entity, component) => { - component.value.destroy() + reactor: () => { + const entity = useEntityContext() + const xruiComponent = useComponent(entity, XRUIComponent) + + useLayoutEffect(() => { + const xrui = xruiComponent.value + return () => { + xrui.destroy() + } + }, []) + + return null } }) diff --git a/packages/ui/src/primitives/tailwind/Color/index.tsx b/packages/ui/src/primitives/tailwind/Color/index.tsx index 1ea12dfb09..abeb3c2c31 100644 --- a/packages/ui/src/primitives/tailwind/Color/index.tsx +++ b/packages/ui/src/primitives/tailwind/Color/index.tsx @@ -25,13 +25,13 @@ Ethereal Engine. All Rights Reserved. import { ColorResult } from '@uiw/color-convert' import SketchPicker from '@uiw/react-color-sketch' import React from 'react' -import { Color } from 'three' +import { Color, ColorRepresentation } from 'three' import { twMerge } from 'tailwind-merge' import Text from '../Text' interface ColorInputProp { - value: Color + value: ColorRepresentation onChange: (color: Color) => void onRelease?: (color: Color) => void disabled?: boolean @@ -50,7 +50,8 @@ export function ColorInput({ textClassName, sketchPickerClassName }: ColorInputProp) { - const hexColor = typeof value.getHexString === 'function' ? '#' + value.getHexString() : '#000' + const color = new Color(value) + const hexColor = typeof color.getHexString === 'function' ? '#' + color.getHexString() : '#000' const handleChange = (result: ColorResult) => { const color = new Color(result.hex) @@ -78,7 +79,7 @@ export function ColorInput({ onChange={handleChange} disableAlpha={true} onPointerLeave={() => { - onRelease && onRelease(value) + onRelease && onRelease(color) }} />