diff --git a/package.json b/package.json index 734d633..22dca61 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "Assets and tests for Ethereal Engine core development", "main": "", "scripts": { - "quick-test":"mocha --config .mocharc.js --spec 'e2e/dev/*.test.ts'", + "quick-test": "mocha --config .mocharc.js --spec 'e2e/dev/*.test.ts'", "check-errors": "tsc --noemit", - "test": "mocha --config .mocharc.js --spec 'tests/**/*.test.ts'", + "test": "exit 0", "test-e2e": "mocha --config .mocharc.js --spec 'e2e/**/*.test.ts'", "benchmark": "mocha --config .mocharc.js --timeout 3700000 --spec 'e2e/benchmark/basic.test.ts'", "precommit": "no-master-commits -b main", diff --git a/src/benchmarksRoute.tsx b/src/benchmarksRoute.tsx index 589d664..db24441 100644 --- a/src/benchmarksRoute.tsx +++ b/src/benchmarksRoute.tsx @@ -6,38 +6,48 @@ import AvatarIKBenchmarkEntry from './benchmarks/avatarIKBenchmark' import HeapBenchmarkEntry from './benchmarks/heapBenchmark' import ParticlesBenchmarkEntry from './benchmarks/particlesBenchmark' import PhysicsBenchmarkEntry from './benchmarks/physicsBenchmark' -import Routes, { RouteData } from './sceneRoute' +import Routes, { RouteCategories } from './sceneRoute' -export const benchmarks: RouteData[] = [ +export const benchmarks: RouteCategories = [ { - name: 'Avatar Benchmark', - description: '', - entry: AvatarBenchmarkEntry + category: 'Avatar', + routes: [ + { + name: 'Basic Benchmark', + description: '', + entry: AvatarBenchmarkEntry + }, + { + name: 'IK Benchmark', + description: '', + entry: AvatarIKBenchmarkEntry + } + ] }, { - name: 'Avatar IK Benchmark', - description: '', - entry: AvatarIKBenchmarkEntry - }, - { - name: 'Particles Benchmark', - description: '', - entry: ParticlesBenchmarkEntry - }, - { - name: 'Physics Benchmark', - description: '', - entry: PhysicsBenchmarkEntry - }, - { - name: 'Heap Benchmark', - description: '', - entry: HeapBenchmarkEntry + category: 'Core', + routes: [ + { + name: 'Particles Benchmark', + description: '', + entry: ParticlesBenchmarkEntry + }, + { + name: 'Physics Benchmark', + description: '', + entry: PhysicsBenchmarkEntry + }, + { + name: 'Heap Benchmark', + description: '', + entry: HeapBenchmarkEntry + } + ] } ] const BenchmarkRoutes = () => { - return + return } export default BenchmarkRoutes diff --git a/src/engine/benchmarks/BenchmarkNodeEditors.tsx b/src/engine/benchmarks/BenchmarkNodeEditors.tsx index 36d1387..e49b982 100644 --- a/src/engine/benchmarks/BenchmarkNodeEditors.tsx +++ b/src/engine/benchmarks/BenchmarkNodeEditors.tsx @@ -1,5 +1,5 @@ -import NodeEditor from '@etherealengine/editor/src/components/properties/NodeEditor' import { EditorComponentType } from '@etherealengine/editor/src/components/properties/Util' +import NodeEditor from '@etherealengine/ui/src/components/editor/properties/nodeEditor' import React from 'react' export const ProfilingComponentNodeEditor: EditorComponentType = (props) => { diff --git a/src/examples/PostProcessing.tsx b/src/examples/PostProcessing.tsx index 7bbc78d..b39d5f9 100644 --- a/src/examples/PostProcessing.tsx +++ b/src/examples/PostProcessing.tsx @@ -6,13 +6,13 @@ import { LocationIcons } from '@etherealengine/client-core/src/components/Locati import { useQuery } from '@etherealengine/ecs' import { getComponent, updateComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' -import { PostProcessingSettingsEditor } from '@etherealengine/editor/src/components/properties/PostProcessingSettingsEditor' import { EditorControlFunctions } from '@etherealengine/editor/src/functions/EditorControlFunctions' import { SelectionState } from '@etherealengine/editor/src/services/SelectionServices' import { UUIDComponent } from '@etherealengine/ecs' import { useSearchParams } from 'react-router-dom' import { Template } from './utils/template' import { PostProcessingComponent } from '@etherealengine/spatial/src/renderer/components/PostProcessingComponent' +import PostProcessingSettingsEditor from '@etherealengine/ui/src/components/editor/properties/postProcessing' export default function PostProcessing() { const entity = useHookstate(null) diff --git a/src/examples/VisualScript.tsx b/src/examples/VisualScript.tsx index 4739fd2..2a5087e 100644 --- a/src/examples/VisualScript.tsx +++ b/src/examples/VisualScript.tsx @@ -2,7 +2,7 @@ import React from 'react' import { setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' -import { ActiveVisualScript } from '@etherealengine/editor/src/components/visualScript/VisualFlow' +import { ActiveVisualScript } from '@etherealengine/ui/src/components/editor/panels/VisualScript/container' import { VisualScriptComponent } from '@etherealengine/engine' import { GraphJSON } from '@etherealengine/visual-script' import AutoSizer from 'react-virtualized-auto-sizer' diff --git a/src/examples/XRMeshes.tsx b/src/examples/XRMeshes.tsx index 5f7a812..8305efc 100644 --- a/src/examples/XRMeshes.tsx +++ b/src/examples/XRMeshes.tsx @@ -1,15 +1,29 @@ import React, { useEffect } from 'react' -import { BufferGeometry, Mesh, MeshBasicMaterial, MeshNormalMaterial } from 'three' +import { BufferGeometry, Mesh, MeshBasicMaterial, MeshNormalMaterial, Vector3 } from 'three' -import { setComponent, useComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' +import { + getComponent, + removeComponent, + setComponent, + useComponent, + useOptionalComponent +} from '@etherealengine/ecs/src/ComponentFunctions' +import { createEntity, removeEntity, useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' import { addObjectToGroup, removeObjectFromGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { QueryReactor } from '@etherealengine/ecs/src/QueryFunctions' +import { TransformComponent } from '@etherealengine/spatial' +import { ColliderComponent } from '@etherealengine/spatial/src/physics/components/ColliderComponent' +import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' +import { CollisionGroups, DefaultCollisionMask } from '@etherealengine/spatial/src/physics/enums/CollisionGroups' +import { BodyTypes, Shapes } from '@etherealengine/spatial/src/physics/types/PhysicsTypes' +import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' +import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' import { XRDetectedMeshComponent } from '@etherealengine/spatial/src/xr/XRDetectedMeshComponent' import { XRDetectedPlaneComponent } from '@etherealengine/spatial/src/xr/XRDetectedPlaneComponent' import { Template } from './utils/template' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' const wireframeMaterial = new MeshBasicMaterial({ wireframe: true }) const normalMaterial = new MeshNormalMaterial({ opacity: 0.5, transparent: true }) @@ -28,11 +42,41 @@ export const DetectedPlanes = () => { NameComponent, 'Plane ' + (xrPlane.plane.value.semanticLabel ?? xrPlane.plane.orientation.value) ) + + const geometry = xrPlane.geometry.value as BufferGeometry + const box = geometry.boundingBox! + const height = box.max.x - box.min.x + const width = box.max.z - box.min.z + + /** Create a child entity such that we can have a distinct scale for the collider */ + + const colliderEntity = createEntity() + setComponent(colliderEntity, NameComponent, 'Plane ' + entity + ' Collider') + setComponent(colliderEntity, EntityTreeComponent, { + parentEntity: entity, + }) + setComponent(colliderEntity, TransformComponent, { + scale: new Vector3(height, 0.01, width) + }) + + setComponent(colliderEntity, RigidBodyComponent, { + type: BodyTypes.Fixed + }) + setComponent(colliderEntity, ColliderComponent, { + shape: Shapes.Box, + collisionLayer: CollisionGroups.Ground, + collisionMask: DefaultCollisionMask + }) return () => { + removeEntity(colliderEntity) removeObjectFromGroup(entity, transparentMesh) } }, [xrPlane.geometry]) + useEffect(() => { + if (!xrPlane.value || !xrPlane.geometry.value) return + }, [xrPlane?.geometry]) + return null } @@ -46,6 +90,15 @@ export const DetectedMeshes = () => { const outlineMesh = new Mesh(xrmesh.geometry.value as BufferGeometry, wireframeMaterial) addObjectToGroup(entity, outlineMesh) setComponent(entity, NameComponent, 'Plane ' + (xrmesh.mesh.value.semanticLabel ?? entity)) + + setComponent(entity, RigidBodyComponent, { + type: BodyTypes.Fixed + }) + setComponent(entity, ColliderComponent, { + shape: Shapes.Mesh, + collisionLayer: CollisionGroups.Ground, + collisionMask: DefaultCollisionMask + }) return () => { removeObjectFromGroup(entity, outlineMesh) } diff --git a/src/examples/immersiveAR.tsx b/src/examples/immersiveAR.tsx new file mode 100644 index 0000000..313c093 --- /dev/null +++ b/src/examples/immersiveAR.tsx @@ -0,0 +1,51 @@ +import { MediaIconsBox } from '@etherealengine/client-core/src/components/MediaIconsBox' +import { useLoadEngineWithScene, useNetwork } from '@etherealengine/client-core/src/components/World/EngineHooks' +import { QueryReactor, createEntity, removeEntity, setComponent } from '@etherealengine/ecs' +import { GroundPlaneComponent } from '@etherealengine/engine/src/scene/components/GroundPlaneComponent' +import { getMutableState, getState, useImmediateEffect, useMutableState } from '@etherealengine/hyperflux' +import { TransformComponent } from '@etherealengine/spatial' +import { EngineState } from '@etherealengine/spatial/src/EngineState' +import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' +import { XRDetectedMeshComponent } from '@etherealengine/spatial/src/xr/XRDetectedMeshComponent' +import { XRDetectedPlaneComponent } from '@etherealengine/spatial/src/xr/XRDetectedPlaneComponent' +import { EmulatorDevtools } from 'ee-bot/devtool/EmulatorDevtools' +import 'ee-bot/src/functions/BotHookSystem' +import React from 'react' +import { useRouteScene } from '../sceneRoute' +import { DetectedMeshes, DetectedPlanes } from './XRMeshes' + +export default function ImmersiveAR() { + useRouteScene('default-project', 'public/scenes/apartment.gltf') + useNetwork({ online: false }) + useLoadEngineWithScene() + const viewerEntity = useMutableState(EngineState).viewerEntity.value + + useImmediateEffect(() => { + if (!viewerEntity) return + getMutableState(RendererState).gridVisibility.set(true) + getMutableState(RendererState).physicsDebug.set(true) + + /** Add ground plane to ensure the avatar never falls out of the world */ + const localFloorEntity = getState(EngineState).localFloorEntity + const groundPlaneEntity = createEntity() + setComponent(groundPlaneEntity, EntityTreeComponent, { parentEntity: localFloorEntity }) + setComponent(groundPlaneEntity, TransformComponent) + setComponent(groundPlaneEntity, GroundPlaneComponent, { visible: false }) + + return () => { + removeEntity(groundPlaneEntity) + } + }, [viewerEntity]) + + return ( + <> + + + +
+ +
+ + ) +} diff --git a/src/examples/immersiveVR.tsx b/src/examples/immersiveVR.tsx new file mode 100644 index 0000000..60d545f --- /dev/null +++ b/src/examples/immersiveVR.tsx @@ -0,0 +1,37 @@ +import { MediaIconsBox } from '@etherealengine/client-core/src/components/MediaIconsBox' +import { useLoadEngineWithScene, useNetwork } from '@etherealengine/client-core/src/components/World/EngineHooks' +import { QueryReactor } from '@etherealengine/ecs' +import { getMutableState, useImmediateEffect, useMutableState } from '@etherealengine/hyperflux' +import { EngineState } from '@etherealengine/spatial/src/EngineState' +import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' +import { XRDetectedMeshComponent } from '@etherealengine/spatial/src/xr/XRDetectedMeshComponent' +import { XRDetectedPlaneComponent } from '@etherealengine/spatial/src/xr/XRDetectedPlaneComponent' +import { EmulatorDevtools } from 'ee-bot/devtool/EmulatorDevtools' +import 'ee-bot/src/functions/BotHookSystem' +import React from 'react' +import { useRouteScene } from '../sceneRoute' +import { DetectedMeshes, DetectedPlanes } from './XRMeshes' + +export default function ImmersiveVR() { + useRouteScene('default-project', 'public/scenes/default.gltf') + useNetwork({ online: false }) + useLoadEngineWithScene() + const viewerEntity = useMutableState(EngineState).viewerEntity.value + + useImmediateEffect(() => { + if (!viewerEntity) return + getMutableState(RendererState).gridVisibility.set(true) + getMutableState(RendererState).physicsDebug.set(true) + }, [viewerEntity]) + + return ( + <> + + + +
+ +
+ + ) +} diff --git a/src/examples/multipleScenes.tsx b/src/examples/multipleScenes.tsx new file mode 100644 index 0000000..b913d6e --- /dev/null +++ b/src/examples/multipleScenes.tsx @@ -0,0 +1,264 @@ +import { useLoadEngineWithScene, useNetwork } from '@etherealengine/client-core/src/components/World/EngineHooks' +import { + Engine, + Entity, + EntityUUID, + UUIDComponent, + UndefinedEntity, + createEntity, + defineComponent, + defineQuery, + defineSystem, + getComponent, + getMutableComponent, + getOptionalComponent, + removeEntity, + setComponent, + useOptionalComponent +} from '@etherealengine/ecs' +import { GLTFComponent } from '@etherealengine/engine/src/gltf/GLTFComponent' +import { GLTFAssetState, GLTFSourceState } from '@etherealengine/engine/src/gltf/GLTFState' +import { PrimitiveGeometryComponent } from '@etherealengine/engine/src/scene/components/PrimitiveGeometryComponent' +import { GeometryTypeEnum } from '@etherealengine/engine/src/scene/constants/GeometryTypeEnum' +import { getMutableState, useHookstate, useImmediateEffect } from '@etherealengine/hyperflux' +import { DirectionalLightComponent, PhysicsPreTransformSystem, TransformComponent } from '@etherealengine/spatial' +import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' +import { CameraOrbitComponent } from '@etherealengine/spatial/src/camera/components/CameraOrbitComponent' +import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' +import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' +import { ColliderComponent } from '@etherealengine/spatial/src/physics/components/ColliderComponent' +import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' +import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' +import { RendererComponent } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem' +import { SceneComponent } from '@etherealengine/spatial/src/renderer/components/SceneComponents' +import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' +import { + MaterialInstanceComponent, + MaterialStateComponent +} from '@etherealengine/spatial/src/renderer/materials/MaterialComponent' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' +import { computeTransformMatrix } from '@etherealengine/spatial/src/transform/systems/TransformSystem' +import { GLTF } from '@gltf-transform/core' +import React, { useEffect } from 'react' +import { Cache, Color, Euler, MathUtils, Matrix4, MeshLambertMaterial, Quaternion, Vector3 } from 'three' +import { Transform } from './utils/transform' + +const TestSuiteBallTagComponent = defineComponent({ name: 'TestSuiteBallTagComponent' }) +let physicsEntityCount = 0 +export const createPhysicsEntity = (sceneEntity: Entity) => { + const entity = createEntity() + + const i = physicsEntityCount++ + + const position = new Vector3(Math.random() * 10 - 5, Math.random() * 2 + 2, Math.random() * 10 - 5) + setComponent(entity, UUIDComponent, ('Ball-' + i) as EntityUUID) + setComponent(entity, EntityTreeComponent, { parentEntity: sceneEntity }) + setComponent(entity, TransformComponent, { position, scale: new Vector3(2, 2, 2) }) + setComponent(entity, VisibleComponent, true) + setComponent(entity, RigidBodyComponent, { type: 'dynamic' }) + setComponent(entity, TestSuiteBallTagComponent) + + const colliderEntity = createEntity() + setComponent(colliderEntity, VisibleComponent, true) + setComponent(colliderEntity, UUIDComponent, ('Ball-' + i + '-collider') as EntityUUID) + setComponent(colliderEntity, EntityTreeComponent, { parentEntity: entity }) + setComponent(colliderEntity, TransformComponent, { scale: new Vector3(0.25, 0.25, 0.25) }) + setComponent(colliderEntity, ColliderComponent, { + shape: 'sphere', + mass: MathUtils.randFloat(0.5, 1.5), + friction: MathUtils.randFloat(0.1, 1.0), + restitution: MathUtils.randFloat(0.1, 1.0) + }) + setComponent(colliderEntity, PrimitiveGeometryComponent, { + geometryType: GeometryTypeEnum.SphereGeometry + }) + setComponent(colliderEntity, InputComponent) + + return entity +} + +// create scene with a rigidbody loaded offset from the origin +const createSceneGLTF = (id: string): GLTF.IGLTF => ({ + asset: { + version: '2.0', + generator: 'iR Engine' + }, + scenes: [{ nodes: [0] }], + scene: 0, + nodes: [ + { + matrix: [5, 0, 0, 0, 0, 0.1, 0, 0, 0, 0, 5, 0, 0, 1, 0, 1], + name: 'Rigidbody', + extensions: { + EE_uuid: 'rigidbody-' + id, + EE_visible: true, + EE_rigidbody: { + type: 'fixed' + }, + EE_collider: { + shape: 'box' + }, + EE_primitive_geometry: { + geometryType: 0, + geometryParams: { + width: 1, + height: 1, + depth: 1 + } + } + } + } + ], + extensionsUsed: ['EE_uuid', 'EE_visible', 'EE_rigidbody', 'EE_collider', 'EE_primitive_geometry'] +}) + +const SceneReactor = (props: { + coord: Vector3 + transform: { position: Vector3; rotation: Quaternion; scale: Vector3 } +}) => { + const { coord, transform } = props + + const gltfEntityState = useHookstate(UndefinedEntity) + const gltfComponent = useOptionalComponent(gltfEntityState.value, GLTFComponent) + + useEffect(() => { + const sceneID = `scene-${coord.x}-${coord.z}` + const gltf = createSceneGLTF(sceneID) + + const sceneURL = `/${sceneID}.gltf` + + Cache.add(sceneURL, gltf) + + const gltfEntity = GLTFSourceState.load(sceneURL, sceneURL as EntityUUID) + getMutableComponent(Engine.instance.viewerEntity, RendererComponent).scenes.merge([gltfEntity]) + setComponent(gltfEntity, SceneComponent) + getMutableState(GLTFAssetState)[sceneURL].set(gltfEntity) + + gltfEntityState.set(gltfEntity) + + return () => { + GLTFSourceState.unload(gltfEntity) + getMutableState(GLTFAssetState)[sceneURL].set(gltfEntity) + } + }, []) + + useEffect(() => { + const gltfEntity = gltfEntityState.value + + // reset transform + setComponent(gltfEntity, TransformComponent, { + position: coord + .clone() + .sub(new Vector3(0.5, 0, 0.5)) + .multiplyScalar(gridSpacing), + rotation: new Quaternion(), + scale: new Vector3(0.5, 0.5, 0.5) + }) + + // apply transform state + const transformComponent = getComponent(gltfEntity, TransformComponent) + const mat4 = new Matrix4() + transformComponent.matrix.multiply(mat4.compose(transform.position, transform.rotation, transform.scale)) + transformComponent.matrix.decompose( + transformComponent.position, + transformComponent.rotation, + transformComponent.scale + ) + computeTransformMatrix(gltfEntity) + }, [transform.position, transform.rotation, transform.scale]) + + useEffect(() => { + if (gltfComponent?.progress?.value !== 100) return + const entities = [] as Entity[] + for (let i = 0; i < 10; i++) { + entities.push(createPhysicsEntity(gltfEntityState.value)) + } + return () => { + for (const entity of entities) { + removeEntity(entity) + } + } + }, [gltfComponent?.progress?.value]) + + return null +} + +const testSuiteBallTagQuery = defineQuery([TestSuiteBallTagComponent]) + +const execute = () => { + for (const entity of testSuiteBallTagQuery()) { + const rigidbody = getComponent(entity, RigidBodyComponent) + const transform = getComponent(entity, TransformComponent) + if (rigidbody.position.y < -10) { + transform.position.set(Math.random() * 10 - 5, Math.random() * 2 + 2, Math.random() * 10 - 5) + } + + const colliderEntity = getComponent(entity, EntityTreeComponent).children[0] + + const isPointerOver = getComponent(colliderEntity, InputComponent).inputSources.length > 0 + const materialInstance = getOptionalComponent(colliderEntity, MaterialInstanceComponent) + if (!materialInstance) continue + const materialEntity = UUIDComponent.getEntityByUUID(materialInstance.uuid[0]) + const material = getComponent(materialEntity, MaterialStateComponent).material as MeshLambertMaterial + material.color.set(isPointerOver ? 'red' : 'white') + } +} + +export const BallResetSystem = defineSystem({ + uuid: 'ee-development-test-suite.multiplescenes.ball-reset-system', + insert: { before: PhysicsPreTransformSystem }, + execute +}) + +const gridCount = 3 +const gridSpacing = 10 + +export default function MultipleScenesEntry() { + useNetwork({ online: false }) + useLoadEngineWithScene() + + useImmediateEffect(() => { + const lightEntity = createEntity() + setComponent(lightEntity, TransformComponent, { rotation: new Quaternion().setFromEuler(new Euler(-4, -0.5, 0)) }) + setComponent(lightEntity, NameComponent, 'directional light') + setComponent(lightEntity, VisibleComponent) + setComponent(lightEntity, DirectionalLightComponent, { intensity: 1, color: new Color(0xffffff) }) + getMutableComponent(Engine.instance.viewerEntity, RendererComponent).scenes.merge([lightEntity]) + + getMutableState(RendererState).gridVisibility.set(true) + getMutableState(RendererState).physicsDebug.set(true) + const entity = Engine.instance.viewerEntity + setComponent(entity, CameraOrbitComponent) + setComponent(entity, InputComponent) + getComponent(entity, CameraComponent).position.set(0, 3, 4) + }, []) + + const coordsState = useHookstate([]) + + useEffect(() => { + const coords = [] as Vector3[] + for (let i = -gridCount * 0.5; i < gridCount * 0.5; i++) { + for (let j = -gridCount * 0.5; j < gridCount * 0.5; j++) { + coords.push(new Vector3(i, 0, j)) + } + } + coordsState.set(coords) + }, []) + + const transformState = useHookstate({ + position: new Vector3(), + rotation: new Quaternion(), + scale: new Vector3(1, 1, 1) + }) + + return ( + <> + {coordsState.value.map((coord) => ( + + ))} +
+ +
+ + ) +} diff --git a/src/examples/utils/transform.tsx b/src/examples/utils/transform.tsx new file mode 100644 index 0000000..745cc8e --- /dev/null +++ b/src/examples/utils/transform.tsx @@ -0,0 +1,46 @@ +import { State } from '@etherealengine/hyperflux' +import EulerInput from '@etherealengine/ui/src/components/editor/input/Euler' +import InputGroup from '@etherealengine/ui/src/components/editor/input/Group' +import Vector3Input from '@etherealengine/ui/src/components/editor/input/Vector3' +import PropertyGroup from '@etherealengine/ui/src/components/editor/properties/group' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Euler, Quaternion, Vector3 } from 'three' + +export const Transform = (props: { + title?: string + transformState: State<{ position: Vector3; rotation: Quaternion; scale: Vector3 }> +}) => { + const { transformState } = props + const { t } = useTranslation() + + const { position, rotation, scale } = transformState.value + + const onChangePosition = (value: Vector3) => transformState.position.set(new Vector3().copy(value)) + const onChangeRotation = (value: Euler) => transformState.rotation.set(new Quaternion().setFromEuler(value)) + const onChangeScale = (value: Vector3) => transformState.scale.set(new Vector3().copy(value)) + + return ( + + + + + + + + + + + + ) +} diff --git a/src/examplesRoute.tsx b/src/examplesRoute.tsx index 6276740..aad7708 100644 --- a/src/examplesRoute.tsx +++ b/src/examplesRoute.tsx @@ -5,44 +5,77 @@ import AvatarMocapEntry from './examples/avatarMocap' import AvatarTestEntry from './examples/avatarTest' import ComponentExamplesRoute, { subComponentExamples } from './examples/componentExamples/componentExamples' import GLTFViewer from './examples/gltf' -import Routes, { RouteData } from './sceneRoute' import InstanceConnection from './examples/InstanceConnection' +import ImmersiveAR from './examples/immersiveAR' +import ImmersiveVR from './examples/immersiveVR' +import MultipleScenesEntry from './examples/multipleScenes' +import Routes, { RouteCategories } from './sceneRoute' -export const examples: RouteData[] = [ +export const examples: RouteCategories = [ { - name: 'Components Example', - description: 'Component examples', - entry: ComponentExamplesRoute, - sub: subComponentExamples.map((sub) => ({ + category: 'WebXR', + routes: [ + { + name: 'Immersive AR', + description: 'Immersive AR example', + entry: ImmersiveAR, + spawnAvatar: true + }, + { + name: 'Immersive VR', + description: 'Immersive VR example', + entry: ImmersiveVR, + spawnAvatar: true + } + ] + }, + { + category: 'Components', + routes: subComponentExamples.map((sub) => ({ name: sub.name, description: sub.description, - props: { Reactor: sub.Reactor } + entry: () => })) }, { - name: 'Avatar Mocap', - description: 'Avatar mocap example', - entry: AvatarMocapEntry - }, - { - name: 'Avatar Test', - description: 'Load many avatars', - entry: AvatarTestEntry - }, - { - name: 'GLTF Viewer', - description: 'Drag and drop GLTF files', - entry: GLTFViewer + category: 'Avatar', + routes: [ + { + name: 'Mocap', + description: 'Avatar mocap example', + entry: AvatarMocapEntry + }, + { + name: 'Test', + description: 'Load many avatars', + entry: AvatarTestEntry + } + ] }, { - name: 'Instance Connection', - description: 'Test instance server connection', - entry: InstanceConnection + category: 'Scene', + routes: [ + { + name: 'GLTF Viewer', + description: 'Drag and drop GLTF files', + entry: GLTFViewer + }, + { + name: 'Multiple', + description: 'multiple scenes example', + entry: MultipleScenesEntry + }, + { + name: 'Instance Connection', + description: 'Test instance server connection', + entry: InstanceConnection + } + ] } ] const ExampleRoutes = () => { - return + return } export default ExampleRoutes diff --git a/src/sceneRoute.tsx b/src/sceneRoute.tsx index 4cfa25d..08477f8 100644 --- a/src/sceneRoute.tsx +++ b/src/sceneRoute.tsx @@ -1,8 +1,9 @@ // @ts-ignore import styles from './sceneRoute.css?inline' -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect } from 'react' +import { SearchParamState } from '@etherealengine/client-core/src/common/services/RouterService' import { useLoadEngineWithScene, useNetwork } from '@etherealengine/client-core/src/components/World/EngineHooks' import { useLoadScene } from '@etherealengine/client-core/src/components/World/LoadLocationScene' import { useEngineCanvas } from '@etherealengine/client-core/src/hooks/useEngineCanvas' @@ -11,27 +12,31 @@ import { staticResourcePath } from '@etherealengine/common/src/schema.type.modul import { Entity, getComponent, setComponent } from '@etherealengine/ecs' import '@etherealengine/engine/src/EngineModule' import { GLTFAssetState } from '@etherealengine/engine/src/gltf/GLTFState' -import { useHookstate, useImmediateEffect, useMutableState } from '@etherealengine/hyperflux' +import { + getMutableState, + none, + useHookstate, + useImmediateEffect, + useMutableState, + useReactiveRef +} from '@etherealengine/hyperflux' import { EngineState } from '@etherealengine/spatial/src/EngineState' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' import { CameraOrbitComponent } from '@etherealengine/spatial/src/camera/components/CameraOrbitComponent' import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' +import Button from '@etherealengine/ui/src/primitives/tailwind/Button' +import { HiChevronDown, HiChevronLeft, HiChevronRight, HiChevronUp } from 'react-icons/hi2' -type Metadata = { +export type RouteData = { name: string description: string -} - -type SubRoute = Metadata & { - props: {} -} - -export type RouteData = Metadata & { entry: (...args: any[]) => any - sub?: SubRoute[] + spawnAvatar?: boolean } +export type RouteCategories = Array<{ category: string; routes: RouteData[] }> + export const buttonStyle = { width: 'auto', height: '100%', @@ -56,6 +61,7 @@ const Header = (props: { header: string }) => { } export const useRouteScene = (projectName = 'ee-development-test-suite', sceneName = 'public/scenes/Examples.gltf') => { + const viewerEntity = useMutableState(EngineState).viewerEntity.value useLoadScene({ projectName: projectName, sceneName: sceneName }) useNetwork({ online: false }) useLoadEngineWithScene() @@ -64,7 +70,6 @@ export const useRouteScene = (projectName = 'ee-development-test-suite', sceneNa const gltfState = useMutableState(GLTFAssetState) const sceneEntity = useHookstate(undefined) - const viewerEntity = useMutableState(EngineState).viewerEntity.value useEffect(() => { if (!assetQuery.data[0]) return @@ -78,106 +83,112 @@ export const useRouteScene = (projectName = 'ee-development-test-suite', sceneNa if (!viewerEntity) return setComponent(viewerEntity, CameraOrbitComponent) setComponent(viewerEntity, InputComponent) - getComponent(viewerEntity, CameraComponent).position.set(0, 0, 4) + getComponent(viewerEntity, CameraComponent).position.set(0, 3, 4) }, [viewerEntity]) return sceneEntity } -const routeKey = 'route' -const subRouteKey = 'subroute' +const getPathForRoute = (category: string, name: string) => { + return (category.toLowerCase() + '_' + name.toLocaleLowerCase()).replace(' ', '_') +} -const Routes = (props: { routes: RouteData[]; header: string }) => { - const { routes, header } = props - const [currentRoute, setCurrentRoute] = useState(null as null | number) - const [currentSubRoute, setCurrentSubRoute] = useState(0) +const Routes = (props: { routeCategories: RouteCategories; header: string }) => { + const { routeCategories, header } = props + const currentRoute = useMutableState(SearchParamState).example.value + const categoriesShown = useHookstate({} as Record) + const hidden = useHookstate(false) - const ref = useRef(null as null | HTMLDivElement) + const [ref, setRef] = useReactiveRef() useEngineCanvas(ref) - const onClick = (routeIndex: number) => { - setCurrentRoute(routeIndex) - setCurrentSubRoute(0) - } + const viewerEntity = useHookstate(getMutableState(EngineState).viewerEntity).value - const onSubClick = (subIndex: number) => { - setCurrentSubRoute(subIndex) + const onClick = (category: string, route: string) => { + SearchParamState.set('example', getPathForRoute(category, route)) } - useEffect(() => { - const queryString = window.location.search - const urlParams = new URLSearchParams(queryString) - const routeIndexStr = urlParams.get(routeKey) as any - if (routeIndexStr) { - const routeIndex = Number(routeIndexStr) - setCurrentRoute(routeIndex) - const subIndexStr = urlParams.get(subRouteKey) as any - if (subIndexStr) { - const subIndex = Number(subIndexStr) - setCurrentSubRoute(subIndex) - } - } - }, []) + const selectedRoute = routeCategories.flatMap((route) => + route.routes.filter((r) => getPathForRoute(route.category, r.name) === currentRoute) + )[0] useEffect(() => { - if (currentRoute === null) return - const url = new URL(window.location.href) - url.searchParams.set(routeKey, currentRoute.toString()) - url.searchParams.set(subRouteKey, currentSubRoute.toString()) - window.history.pushState(null, '', url.toString()) - }, [currentRoute, currentSubRoute]) - - const selectedRoute = currentRoute !== null ? routes[currentRoute] : null - const selectedSub = selectedRoute && selectedRoute.sub && selectedRoute.sub[currentSubRoute] + if (selectedRoute?.spawnAvatar) SearchParamState.set('spectate', none) + else SearchParamState.set('spectate', '') + }, [selectedRoute]) + const Entry = selectedRoute && selectedRoute.entry - const subProps = selectedSub ? selectedSub.props : {} return ( <>
-
+
- {index === currentRoute && routes[currentRoute]?.sub && ( -
- {routes[currentRoute].sub?.map((sub, subIndex) => { - const subTitle = sub.name - const subDesc = sub.description - return ( + {categoryShown.value && + category.routes.map((route, index) => { + const title = route.name + const desc = route.description + const path = getPathForRoute(category.category, title) + return ( +
onSubClick(subIndex)} + className={path === currentRoute ? 'SelectedItemContainer' : 'RouteItemContainer'} + onClick={() => onClick(category.category, title)} > -
{subTitle}
-
{subDesc}
+
{title}
+
{desc}
- ) - })} -
- )} + + ) + })} ) })}
-
+
+ {viewerEntity && Entry && }
- {Entry && } ) } diff --git a/tests/avatars/AvatarIntegrationTests.test.ts b/tests/avatars/AvatarIntegrationTests.test.ts deleted file mode 100644 index 413fa15..0000000 --- a/tests/avatars/AvatarIntegrationTests.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' -import { loadDRACODecoderNode } from '@etherealengine/engine/src/assets/loaders/gltf/NodeDracoLoader' -import { overrideFileLoaderLoad } from '@etherealengine/engine/tests/util/loadGLTFAssetNode' -import { getMutableState } from '@etherealengine/hyperflux' -import { createMockNetwork } from '@etherealengine/network/tests/createMockNetwork' -import { EngineState } from '@etherealengine/spatial/src/EngineState' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' -import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' -import appRootPath from 'app-root-path' -import fs from 'fs' -import path from 'path' - -import { NetworkState } from '@etherealengine/network' -import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' -import packageJson from '../../package.json' - -import '@etherealengine/engine/src/EngineModule' - -overrideFileLoaderLoad() - -// for easier debug -console.warn = () => {} - -const avatarPath = `/packages/projects/projects/${packageJson.name}/avatars/` -const animGLB = '/packages/projects/default-project/assets/Animations.glb' - -const getAllFiles = (dirPath, arrayOfFiles) => { - const avatarPathAbsolute = path.join(appRootPath.path, dirPath) - const files = fs.readdirSync(avatarPathAbsolute) - arrayOfFiles = arrayOfFiles || [] - files.forEach(function (file) { - if (fs.statSync(avatarPathAbsolute + '/' + file).isDirectory()) { - arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles) - } else { - arrayOfFiles.push(path.join(dirPath, '/', file)) - } - }) - return arrayOfFiles -} - -const fetchAvatarList = () => { - const assetPaths = getAllFiles(avatarPath, []) - const avatarList = assetPaths.filter((url) => url.endsWith('glb')) - return avatarList -} - -describe.skip('avatarFunctions Integration', async () => { - // before(async () => { - // await loadDRACODecoderNode() - // }) - - // beforeEach(async () => { - // createEngine() - // createMockNetwork() - // Engine.instance.userID = NetworkState.worldNetwork.hostId - // getMutableState(EngineState).publicPath.set('') - // await Physics.load() - // getMutableState(PhysicsState).physicsWorld.set(Physics.createWorld()) - // }) - - // afterEach(() => { - // return destroyEngine() - // }) - - // describe('loadAvatarForEntity', () => { - // const assetPaths = fetchAvatarList() - // let i = 1 - // for (const modelURL of assetPaths) { - // it('should bone match, and rig avatar ' + modelURL.replace(avatarPath, ''), async function () { - // const userId = `userId-${i}` as UserID - // dispatchAction( - // AvatarNetworkAction.spawn({ - // $from: userId, - // position: new Vector3(), - // rotation: new Quaternion(), - // networkId: i++ as NetworkId, - // entityUUID: userId as string as EntityUUID - // }) - // ) - - // applyIncomingActions() - - // await act(() => receiveActions(EntityNetworkState)) - - // const entity = UUIDComponent.entitiesByUUID[userId as any as EntityUUID] - - // spawnAvatarReceptor(userId as string as EntityUUID) - - // const avatar = getComponent(entity, AvatarComponent) - // // make sure this is set later on - // avatar.avatarHeight = 0 - // avatar.avatarHalfHeight = 0 - - // // run the logic - // const model = (await loadAvatarModelAsset(modelURL)) as any - // setupAvatarForUser(entity, model) - - // // do assertions - // const avatarComponent = getComponent(entity, AvatarComponent) - - // assert(avatarComponent.model!.children.length) - // assert(avatarComponent.avatarHeight > 0) - // assert(avatarComponent.avatarHalfHeight > 0) - - // const { rig } = getComponent(entity, AvatarRigComponent) - // assert(rig) - // assert(rig.hips.node) - - // // TODO: this currently isn't working, the update method doesnt show up in the VRM object - // // assert.equal(hasComponent(entity, UpdatableComponent), asset.split('.').pop() === 'vrm') - // }) - // } - // }) -})