diff --git a/.vscode/launch.json b/.vscode/launch.json index 14ffd0301f6..9b3f11d2ca6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -84,6 +84,12 @@ "request": "launch", "type": "node-terminal", }, + { + "command": "cd packages/editor && npm run test", + "name": "npm run test - editor", + "request": "launch", + "type": "node-terminal", + }, { "command": "cd packages/spatial && npm run test", "name": "npm run test - spatial", diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index a2642353d68..f629e666a78 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -1034,6 +1034,8 @@ "copyURL": "Copy URL", "openInNewTab": "Open URL in New Tab", "deleteAsset": "Delete Asset", + "prefab": "Prefab", + "prefab-search" : "Search Prefabs ...", "components": "Components", "components-search": "Search Components ...", "component-detail": { @@ -1119,7 +1121,8 @@ "scene-assets": { "no-category": "No category selected", "search-placeholder": "Search for an asset ...", - "preview": "Preview" + "preview" : "Preview", + "info-drag-drop": "Drag and Drop these items into the scene" } }, "hierarchy": { diff --git a/packages/client-core/src/common/services/FileThumbnailJobState.tsx b/packages/client-core/src/common/services/FileThumbnailJobState.tsx index 0aa7200a0a0..037124f0e2c 100644 --- a/packages/client-core/src/common/services/FileThumbnailJobState.tsx +++ b/packages/client-core/src/common/services/FileThumbnailJobState.tsx @@ -36,7 +36,7 @@ import { } from '@etherealengine/ecs' import { previewScreenshot } from '@etherealengine/editor/src/functions/takeScreenshot' import { useTexture } from '@etherealengine/engine/src/assets/functions/resourceLoaderHooks' -import { SceneState } from '@etherealengine/engine/src/scene/SceneState' +import { GLTFDocumentState } from '@etherealengine/engine/src/gltf/GLTFDocumentState' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' import { getModelSceneID } from '@etherealengine/engine/src/scene/functions/loaders/ModelFunctions' import { defineState, getMutableState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux' @@ -152,7 +152,7 @@ const ThumbnailJobReactor = (props: { src: string }) => { entity: UndefinedEntity }) const loadPromiseState = useHookstate(null as Promise | null) // for asset loading - const sceneState = useHookstate(getMutableState(SceneState).scenes) // for model rendering + const sceneState = useHookstate(getMutableState(GLTFDocumentState)) // for model rendering const [tex] = useTexture(state.fileType.value === 'texture' ? props.src : '') // for texture loading // Load and render image diff --git a/packages/common/src/utils/CommonKnownContentTypes.ts b/packages/common/src/utils/CommonKnownContentTypes.ts index 968571ef48c..ed6de532eee 100644 --- a/packages/common/src/utils/CommonKnownContentTypes.ts +++ b/packages/common/src/utils/CommonKnownContentTypes.ts @@ -30,6 +30,7 @@ Ethereal Engine. All Rights Reserved. */ export const CommonKnownContentTypes = { material: 'model/material', + prefab: 'model/prefab', lookdev: 'model/lookdev', xre: 'prefab/xre', gltf: 'model/gltf', diff --git a/packages/common/src/utils/guessContentType.tsx b/packages/common/src/utils/guessContentType.tsx index ed05fa52aa4..03c5216a9c7 100644 --- a/packages/common/src/utils/guessContentType.tsx +++ b/packages/common/src/utils/guessContentType.tsx @@ -36,6 +36,8 @@ export function guessContentType(url: string): string { //check for xre gltf extension if (/\.material\.gltf$/.test(contentPath)) { return CommonKnownContentTypes.material + } else if (/\.prefab\.gltf$/.test(contentPath)) { + return CommonKnownContentTypes.prefab } else if (/\.lookdev\.gltf$/.test(contentPath)) { return CommonKnownContentTypes.lookdev } diff --git a/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx b/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx index 6c15ebed99c..25c802a3a80 100644 --- a/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx +++ b/packages/editor/src/components/hierarchy/HierarchyPanelContainer.tsx @@ -42,7 +42,7 @@ import { } from '@etherealengine/spatial/src/transform/components/EntityTree' import MenuItem from '@mui/material/MenuItem' -import { PopoverPosition } from '@mui/material/Popover' +import Popover, { PopoverPosition } from '@mui/material/Popover' import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' import { Engine, EntityUUID, UUIDComponent } from '@etherealengine/ecs' @@ -58,8 +58,10 @@ import { EditorState } from '../../services/EditorServices' import { SelectionState } from '../../services/SelectionServices' import Search from '../Search/Search' import useUpload from '../assets/useUpload' +import { PopoverContext } from '../element/PopoverContext' import { PropertiesPanelButton } from '../inputs/Button' import { ContextMenu } from '../layout/ContextMenu' +import PrefabList from '../prefabs/PrefabList' import { HeirarchyTreeNodeType, heirarchyTreeWalker } from './HeirarchyTreeWalker' import { HierarchyTreeNode, HierarchyTreeNodeProps, RenameNodeData, getNodeElId } from './HierarchyTreeNode' import styles from './styles.module.scss' @@ -440,7 +442,8 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit {MemoTreeNode} ) - + const anchorElement = useHookstate(null) + const open = !!anchorElement.value return ( <>
@@ -460,11 +463,38 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit fontSize: '12px', lineHeight: '0.5' }} - onClick={() => EditorControlFunctions.createObjectFromSceneElement()} + //onClick={() => EditorControlFunctions.createObjectFromSceneElement()} + onClick={(event) => { + anchorElement.set(event.currentTarget) + }} > {t('editor:hierarchy.lbl-addEntity')}
+ { + anchorElement.set(null) + } + }} + > + anchorElement.set(null)} + anchorOrigin={{ + vertical: 'center', + horizontal: 'left' + }} + transformOrigin={{ + vertical: 'center', + horizontal: 'right' + }} + > + + + onRenameNode(contextSelectedItem!)}>{t('editor:hierarchy.lbl-rename')} { drop(item: SceneElementType, monitor) { const vec3 = new Vector3() getCursorSpawnPosition(monitor.getClientOffset() as Vector2, vec3) + EditorControlFunctions.createObjectFromSceneElement([ - { name: item!.componentJsonID }, + { name: (item as SceneElementType).componentJsonID }, { name: TransformComponent.jsonID, props: { position: vec3 } } ]) } diff --git a/packages/editor/src/components/prefabs/PrefabEditors.tsx b/packages/editor/src/components/prefabs/PrefabEditors.tsx new file mode 100644 index 00000000000..c3c6a9a2daa --- /dev/null +++ b/packages/editor/src/components/prefabs/PrefabEditors.tsx @@ -0,0 +1,35 @@ +/* +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 config from '@etherealengine/common/src/config' +import { defineState } from '@etherealengine/hyperflux' + +export const PrefabShelfCategories = defineState({ + name: 'ee.editor.PrefabShelfCategories', + initial: () => { + return { + //hardcode to test replace with parseStorageProviderURLs + 'Point Light Prefab': `${config.client.fileServer}/projects/default-project/assets/pointLight.prefab.gltf`, + 'Geometry Prefab': `${config.client.fileServer}/projects/default-project/assets/geo.prefab.gltf`, + 'Empty Prefab': 'empty' + + //will continue to add more prefabs + } as Record + } +}) diff --git a/packages/editor/src/components/prefabs/PrefabList.tsx b/packages/editor/src/components/prefabs/PrefabList.tsx new file mode 100644 index 00000000000..9ed26f088c4 --- /dev/null +++ b/packages/editor/src/components/prefabs/PrefabList.tsx @@ -0,0 +1,154 @@ +/* +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 { startCase } from 'lodash' +import React, { useRef } from 'react' +import { useTranslation } from 'react-i18next' + +import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' + +import PlaceHolderIcon from '@mui/icons-material/GroupAddOutlined' +import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material' + +import InputText from '@etherealengine/client-core/src/common/components/InputText' +import { ComponentJsonType } from '@etherealengine/engine/src/scene/types/SceneTypes' +import Typography from '@etherealengine/ui/src/primitives/mui/Typography' +import { ComponentEditorsState } from '../../functions/ComponentEditors' +import { EditorControlFunctions } from '../../functions/EditorControlFunctions' +import { addMediaNode } from '../../functions/addMediaNode' +import { usePopoverContextClose } from '../element/PopoverContext' +import { PrefabShelfCategories } from './PrefabEditors' + +const PrefabListItem = ({ item }: { item: string }) => { + const { t } = useTranslation() + const Icon = getState(ComponentEditorsState)[item]?.iconComponent ?? PlaceHolderIcon + const handleClosePopover = usePopoverContextClose() + + return ( + { + const PrefabNameShelfCategories = getState(PrefabShelfCategories) + const componentJsons: ComponentJsonType[] = [] + const url = PrefabNameShelfCategories[item] + // PrefabNameShelfCategories[item].forEach((component) => { + // componentJsons.push({ name: component.jsonID as string }) + // }) + //EditorControlFunctions.createObjectFromSceneElement(componentJsons) + + //add prefab gltfs in the scene via add media node + if (url === 'empty') { + EditorControlFunctions.createObjectFromSceneElement() + } else { + //inside add media use dereference for model component + addMediaNode(url) + } + + handleClosePopover() + }} + > + + + + + {startCase(item.replace('-', ' ').toLowerCase())} + + } + secondary={ + + {t(`editor:layout.assetGrid.component-detail.${item}`)} + + } + /> + + ) +} +const ScenePrefabListItem = ({ categoryItems }: { categoryItems: string[]; isCollapsed: boolean }) => { + return ( + <> + + {categoryItems.map((item) => ( + + ))} + + + ) +} + +const usePrefabShelfCategories = (search: string) => { + useHookstate(getMutableState(PrefabShelfCategories)).value + + if (!search) { + return Object.entries(getState(PrefabShelfCategories)) + } + + const searchRegExp = new RegExp(search, 'gi') + + return Object.entries(getState(PrefabShelfCategories)) + .map(([category, items]) => { + const filteredcategory = category.match(searchRegExp)?.length ? category : '' + return [filteredcategory, items] as [string, string] + }) + .filter(([_, items]) => !!items.length) +} + +export function PrefabList() { + const { t } = useTranslation() + const search = useHookstate({ local: '', query: '' }) + const searchTimeout = useRef | null>(null) + const shelves = usePrefabShelfCategories(search.query.value) + const shelveslist: string[] = [] + shelves.map(([category, items]) => { + shelveslist.push(category) + }) + + const onSearch = (text: string) => { + search.local.set(text) + if (searchTimeout.current) clearTimeout(searchTimeout.current) + searchTimeout.current = setTimeout(() => { + search.query.set(text) + }, 50) + } + + return ( + + + {t('editor:layout.assetGrid.prefab')} + + onSearch(e.target.value)} + /> + + } + > + + + ) +} + +export default PrefabList diff --git a/packages/editor/src/components/properties/ModelNodeEditor.tsx b/packages/editor/src/components/properties/ModelNodeEditor.tsx index a8f15aeffda..b35455e3024 100755 --- a/packages/editor/src/components/properties/ModelNodeEditor.tsx +++ b/packages/editor/src/components/properties/ModelNodeEditor.tsx @@ -41,7 +41,7 @@ import { recursiveHipsLookup } from '@etherealengine/engine/src/avatar/AvatarBon import { exportRelativeGLTF } from '../../functions/exportGLTF' import { EditorState } from '../../services/EditorServices' import BooleanInput from '../inputs/BooleanInput' -import { PropertiesPanelButton } from '../inputs/Button' +import { Button, PropertiesPanelButton } from '../inputs/Button' import InputGroup from '../inputs/InputGroup' import ModelInput from '../inputs/ModelInput' import SelectInput from '../inputs/SelectInput' @@ -139,6 +139,9 @@ export const ModelNodeEditor: EditorComponentType = (props) => { {errors?.LOADING_ERROR || (errors?.INVALID_SOURCE && ErrorPopUp({ message: t('editor:properties.model.error-url') }))} + { const SceneReactor = SystemDefinitions.get(SceneLoadingSystem)!.reactor! const sceneTag = + const GLTFSnapshotReactor = GLTFSnapshotState.reactor! + const gltfTag = + it('modifyProperty', async () => { applyIncomingActions() @@ -151,11 +155,15 @@ describe('EditorControlFunctions', () => { EditorControlFunctions.modifyProperty([child2_1Entity], FogSettingsComponent, prop) applyIncomingActions() - await act(() => rerender(sceneTag)) - + if (format === '.scene.json') { + await act(() => rerender(sceneTag)) + } else { + await act(() => rerender(gltfTag)) + } const child2_1Entity_2 = UUIDComponent.getEntityByUUID('child_2_1' as EntityUUID) const newComponent = getComponent(child2_1Entity_2, FogSettingsComponent) + const documentState = getState(GLTFDocumentState) assert.deepStrictEqual(newComponent, prop) unmount() diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index c165ca71bfb..abb18c7c994 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -234,6 +234,10 @@ const getAssetClass = (assetFileName: string): AssetClass => { console.log('Lookdev asset') return AssetClass.Lookdev } + if (/\.(prefab.gltf)$/.test(assetFileName)) { + console.log('prefab asset') + return AssetClass.Prefab + } return AssetClass.Model } else if (/\.(png|jpg|jpeg|tga|ktx2|dds)$/.test(assetFileName)) { return AssetClass.Image @@ -341,6 +345,10 @@ const assetLoadCallback = const material = asset as Material material.userData.type = assetType } + if (assetClass === AssetClass.Prefab) { + //load prefab gltf without parent model + const gltf = asset as GLTF + } if ([AssetClass.Image, AssetClass.Video].includes(assetClass)) { const texture = asset as Texture texture.wrapS = RepeatWrapping diff --git a/packages/engine/src/assets/enum/AssetClass.ts b/packages/engine/src/assets/enum/AssetClass.ts index 641e57d63d8..9c8ace87106 100755 --- a/packages/engine/src/assets/enum/AssetClass.ts +++ b/packages/engine/src/assets/enum/AssetClass.ts @@ -35,6 +35,7 @@ export enum AssetClass { Document = 'Document', Text = 'Text', Script = 'Script', + Prefab = 'Prefab', Unknown = 'unknown', Volumetric = 'Volumetric' } diff --git a/packages/engine/src/gltf/GLTFState.tsx b/packages/engine/src/gltf/GLTFState.tsx index 20399a1b795..23298370e32 100644 --- a/packages/engine/src/gltf/GLTFState.tsx +++ b/packages/engine/src/gltf/GLTFState.tsx @@ -36,7 +36,7 @@ import { createEntity, getComponent, getMutableComponent, - removeComponent, + hasComponent, removeEntity, setComponent, useComponent, @@ -57,13 +57,20 @@ import { import { TransformComponent } from '@etherealengine/spatial' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' import { useGet } from '@etherealengine/spatial/src/common/functions/FeathersHooks' +import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' +import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' +import { Object3DComponent } from '@etherealengine/spatial/src/renderer/components/Object3DComponent' import { SceneComponent } from '@etherealengine/spatial/src/renderer/components/SceneComponents' import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { GLTF } from '@gltf-transform/core' import React, { useEffect, useLayoutEffect } from 'react' -import { MathUtils, Matrix4, Quaternion, Vector3 } from 'three' +import { Group, MathUtils, Matrix4, Quaternion, Vector3 } from 'three' +import { ModelComponent } from '../scene/components/ModelComponent' import { SourceComponent } from '../scene/components/SourceComponent' +import { getGLTFSnapshot } from '../scene/functions/GLTFConversion' +import { proxifyParentChildRelationships } from '../scene/functions/loadGLTFModel' +import { getModelSceneID } from '../scene/functions/loaders/ModelFunctions' import { GLTFComponent } from './GLTFComponent' import { GLTFDocumentState, GLTFModifiedState, GLTFSnapshotAction } from './GLTFDocumentState' @@ -113,6 +120,10 @@ export const GLTFSourceState = defineState({ const sourceID = `${getComponent(entity, UUIDComponent)}-${source}` setComponent(entity, SourceComponent, sourceID) setComponent(entity, GLTFComponent, { src: source }) + const obj3d = new Group() + setComponent(entity, Object3DComponent, obj3d) + addObjectToGroup(entity, obj3d) + proxifyParentChildRelationships(obj3d) getMutableState(GLTFSourceState)[sourceID].set(entity) return entity }, @@ -124,6 +135,18 @@ export const GLTFSourceState = defineState({ } }) +function applySnapshot(source: string, data: GLTF.IGLTF) { + const state = getMutableState(GLTFSnapshotState)[source] + if (!state.value) { + state.set({ index: 0, snapshots: [data] }) + getMutableState(GLTFDocumentState)[source].set(data) + return + } + state.index.set(state.index.value + 1) + state.snapshots.merge([data]) + getMutableState(GLTFDocumentState)[source].set(data) +} + export const GLTFSnapshotState = defineState({ name: 'ee.engine.gltf.GLTFSnapshotState', initial: {} as Record< @@ -136,16 +159,7 @@ export const GLTFSnapshotState = defineState({ receptors: { onSnapshot: GLTFSnapshotAction.createSnapshot.receive((action) => { - const { data } = action - const state = getMutableState(GLTFSnapshotState)[action.source] - if (!state.value) { - state.set({ index: 0, snapshots: [data] }) - getMutableState(GLTFDocumentState)[action.source].set(data) - return - } - state.index.set(state.index.value + 1) - state.snapshots.merge([data]) - getMutableState(GLTFDocumentState)[action.source].set(data) + applySnapshot(action.source, action.data) }), onUndo: GLTFSnapshotAction.undo.receive((action) => { @@ -183,6 +197,7 @@ export const GLTFSnapshotState = defineState({ ))} + ) }, @@ -197,6 +212,93 @@ export const GLTFSnapshotState = defineState({ data: GLTF.IGLTF source: string } + }, + + injectSnapshot: (srcNode: EntityUUID, srcSnapshotID: string, dstNode: EntityUUID, dstSnapshotID: string) => { + const snapshot = getGLTFSnapshot(srcSnapshotID) + const parentSnapshot = GLTFSnapshotState.cloneCurrentSnapshot(dstSnapshotID) + //create new node list with the model entity removed + //remove model entity from scene nodes + const srcEntity = UUIDComponent.getEntityByUUID(srcNode) + const srcTransform = getComponent(srcEntity, TransformComponent) + const childEntities = getComponent(srcEntity, EntityTreeComponent).children + for (const child of childEntities) { + const transform = getComponent(child, TransformComponent) + //apply the model's transform to the children, such that it has the same world transform after the model is removed + //combine position + const position = new Vector3().copy(transform.position) + position.applyQuaternion(srcTransform.rotation) + position.add(srcTransform.position) + //combine rotation + const rotation = new Quaternion().copy(srcTransform.rotation) + rotation.multiply(transform.rotation) + //combine scale + const scale = new Vector3().copy(transform.scale) + scale.multiply(srcTransform.scale) + //set new transform on the node in the new snapshot + const childNode = snapshot.data.nodes?.find( + (node) => node.extensions?.[UUIDComponent.jsonID] === getComponent(child, UUIDComponent) + ) + if (!childNode) continue + childNode.matrix = new Matrix4().compose(position, rotation, scale).toArray() + } + const modelIndex = parentSnapshot.data.nodes?.findIndex( + (node) => node.extensions?.[UUIDComponent.jsonID] === srcNode + ) + parentSnapshot.data.scenes![0].nodes = parentSnapshot.data.scenes![0].nodes.filter((node) => node !== modelIndex) + const newNodes = parentSnapshot.data.nodes?.filter((node) => node.extensions?.[UUIDComponent.jsonID] !== srcNode) + //recalculate child indices + if (!newNodes) return + for (const node of newNodes) { + if (!node.children) continue + const newChildren: number[] = [] + for (const child of node.children) { + const childNode = parentSnapshot.data.nodes?.[child] + const childUUID = childNode?.extensions?.[UUIDComponent.jsonID] + if (!childUUID) continue + const childIndex = newNodes.findIndex((node) => node.extensions?.[UUIDComponent.jsonID] === childUUID) + if (childIndex === -1) continue + newChildren.push(childIndex) + } + node.children = newChildren + } + parentSnapshot.data.nodes = newNodes + + const rootIndices = snapshot.data.scenes?.[0].nodes! + const roots = rootIndices.map((index) => snapshot.data.nodes?.[index]) + parentSnapshot.data.nodes = [...parentSnapshot.data.nodes!, ...snapshot.data.nodes!] + const childIndices = roots.map((root) => parentSnapshot.data.nodes!.findIndex((node) => node === root)!) + const parentNode = parentSnapshot.data.nodes?.find((node) => node.extensions?.[UUIDComponent.jsonID] === dstNode) + //if the parent is not the root of the gltf document, add the child indices to the parent's children + if (parentNode) { + parentNode.children = [...(parentNode.children ?? []), ...childIndices] + } else { + //otherwise, add the child indices to the scene's nodes as roots + parentSnapshot.data.scenes![0].nodes.push(...childIndices) + } + + //recalculate child indices of newly added nodes + for (const node of parentSnapshot.data.nodes!) { + if (!node.children) continue + //only operate on nodes that are being injected + if (!snapshot.data.nodes!.includes(node)) continue + + const newChildren: number[] = [] + for (const child of node.children) { + const childNode = snapshot.data.nodes?.[child] + const childUUID = childNode?.extensions?.[UUIDComponent.jsonID] + if (!childUUID) continue + const newChildIndex = parentSnapshot.data.nodes!.findIndex( + (node) => node.extensions?.[UUIDComponent.jsonID] === childUUID + ) + if (newChildIndex === -1) continue + newChildren.push(newChildIndex) + } + node.children = newChildren + } + applySnapshot(dstSnapshotID, parentSnapshot.data) + getMutableState(GLTFSnapshotState)[srcSnapshotID].set(none) + getMutableState(GLTFDocumentState)[srcSnapshotID].set(none) } }) @@ -212,6 +314,18 @@ const GLTFSnapshotReactor = (props: { source: string }) => { return null } +const ModelGLTFReactor = () => { + const entity = useEntityContext() + + const source = getModelSceneID(entity) + const gltfDocumentState = useHookstate(getMutableState(GLTFDocumentState)[source]) + + const parentUUID = useComponent(entity, UUIDComponent).value + if (!gltfDocumentState.value) return null + + return +} + const ChildGLTFReactor = () => { const entity = useEntityContext() @@ -226,7 +340,7 @@ const ChildGLTFReactor = () => { } export const DocumentReactor = (props: { documentID: string; parentUUID: EntityUUID }) => { - const documentState = useHookstate(getMutableState(GLTFDocumentState)[props.documentID]) + const documentState = useMutableState(GLTFDocumentState)[props.documentID] as State if (!documentState.scenes.value) return null const nodes = documentState.scenes![documentState.scene.value!].nodes as State @@ -292,7 +406,25 @@ const NodeReactor = (props: { nodeIndex: number; childIndex: number; parentUUID: } } + if (!hasComponent(entity, Object3DComponent) && !hasComponent(entity, MeshComponent)) { + const obj3d = new Group() + obj3d.entity = entity + addObjectToGroup(entity, obj3d) + proxifyParentChildRelationships(obj3d) + setComponent(entity, Object3DComponent, obj3d) + } + return () => { + //check if entity is in some other document + const uuid = getComponent(entity, UUIDComponent) + const documents = getState(GLTFDocumentState) + for (const documentID in documents) { + const document = documents[documentID] + if (!document?.nodes) continue + for (const node of document.nodes) { + if (node.extensions?.[UUIDComponent.jsonID] === uuid) return + } + } removeEntity(entity) } }, [parentEntity]) @@ -362,12 +494,11 @@ const ExtensionReactor = (props: { entity: Entity; extension: string; nodeIndex: const extension = node.extensions![props.extension] useEffect(() => { - return () => { - const Component = ComponentJSONIDMap.get(props.extension) - if (!Component) return console.warn('no component found for extension', props.extension) - - removeComponent(props.entity, Component) - } + // return () => { + // const Component = ComponentJSONIDMap.get(props.extension) + // if (!Component) return console.warn('no component found for extension', props.extension) + // removeComponent(props.entity, Component) + // } }, []) useEffect(() => { diff --git a/packages/engine/src/scene/SceneState.tsx b/packages/engine/src/scene/SceneState.tsx index dd0238478d2..48f3c24c5d0 100644 --- a/packages/engine/src/scene/SceneState.tsx +++ b/packages/engine/src/scene/SceneState.tsx @@ -89,7 +89,6 @@ export const SceneState = defineState({ return createRootEntity(sceneID, data) } }, - unloadScene: (sceneID: string, remove = true) => { const sceneData = getState(SceneState).scenes[sceneID] if (!sceneData) return diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 12a83620a82..b5923b8005e 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -26,7 +26,7 @@ Ethereal Engine. All Rights Reserved. import { FC, useEffect } from 'react' import { AnimationMixer, Group, Scene } from 'three' -import { NO_PROXY, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, dispatchAction, getMutableState, none, useHookstate } from '@etherealengine/hyperflux' import { QueryReactor, UUIDComponent } from '@etherealengine/ecs' import { @@ -41,7 +41,6 @@ import { import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' -import { SceneState } from '@etherealengine/engine/src/scene/SceneState' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' import { RendererComponent } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem' import { GroupComponent, addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' @@ -58,6 +57,9 @@ import { useGLTF } from '../../assets/functions/resourceLoaderHooks' import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' import { AnimationComponent } from '../../avatar/components/AnimationComponent' import { autoconvertMixamoAvatar } from '../../avatar/functions/avatarFunctions' +import { GLTFDocumentState, GLTFSnapshotAction } from '../../gltf/GLTFDocumentState' +import { GLTFSnapshotState } from '../../gltf/GLTFState' +import { SceneJsonType, convertSceneJSONToGLTF } from '../../gltf/convertJsonToGLTF' import { addError, removeError } from '../functions/ErrorFunctions' import { parseGLTFModel, proxifyParentChildRelationships } from '../functions/loadGLTFModel' import { getModelSceneID, useModelSceneID } from '../functions/loaders/ModelFunctions' @@ -79,7 +81,8 @@ export const ModelComponent = defineComponent({ // internal assetTypeOverride: null as null | AssetType, scene: null as Group | null, - asset: null as VRM | GLTF | null + asset: null as VRM | GLTF | null, + dereference: false } }, @@ -108,6 +111,8 @@ export const ModelComponent = defineComponent({ function ModelReactor() { const entity = useEntityContext() const modelComponent = useComponent(entity, ModelComponent) + const gltfDocumentState = useHookstate(getMutableState(GLTFDocumentState)) + const modelSceneID = getModelSceneID(entity) const [gltf, error] = useGLTF(modelComponent.src.value, entity, { forceAssetType: modelComponent.assetTypeOverride.value, @@ -151,7 +156,7 @@ function ModelReactor() { useEffect(() => { const model = modelComponent.get(NO_PROXY)! - const asset = model.asset as GLTF | null + const asset = model.asset as GLTF | VRM | null if (!asset) return const group = getOptionalComponent(entity, GroupComponent) @@ -175,18 +180,21 @@ function ModelReactor() { if (!asset.scene.animations.length && !(asset instanceof VRM)) asset.scene.animations = asset.animations const loadedJsonHierarchy = parseGLTFModel(entity, asset.scene as Scene) - const uuid = getModelSceneID(entity) - - SceneState.loadScene(uuid, { - scene: { - entities: loadedJsonHierarchy, - root: getComponent(entity, UUIDComponent), - version: 0 - }, - name: '', - project: '', - thumbnailUrl: '' - }) + let uuid: string | null = null + uuid = getModelSceneID(entity) + const sceneJson: SceneJsonType = { + entities: loadedJsonHierarchy, + root: getComponent(entity, UUIDComponent), + version: 0 + } + const sceneGLTF = convertSceneJSONToGLTF(sceneJson) + dispatchAction( + GLTFSnapshotAction.createSnapshot({ + source: uuid, + data: sceneGLTF + }) + ) + //} const renderer = getOptionalComponent(Engine.instance.viewerEntity, RendererComponent) @@ -204,7 +212,9 @@ function ModelReactor() { }) } return () => { - SceneState.unloadScene(uuid, false) + if (!uuid) return + getMutableState(GLTFDocumentState)[uuid].set(none) + getMutableState(GLTFSnapshotState)[uuid].set(none) const children = getOptionalComponent(entity, EntityTreeComponent)?.children if (!children) return for (const child of children) { @@ -213,6 +223,18 @@ function ModelReactor() { } }, [modelComponent.scene]) + useEffect(() => { + if (!modelComponent.scene.value) return + if (!modelComponent.dereference.value) return + if (!gltfDocumentState[modelSceneID].value) return + const modelUUID = getComponent(entity, UUIDComponent) + const sourceID = getModelSceneID(entity) + const parentEntity = getComponent(entity, EntityTreeComponent).parentEntity + if (!parentEntity) return + const parentUUID = getComponent(parentEntity, UUIDComponent) + const parentSource = getComponent(parentEntity, SourceComponent) + GLTFSnapshotState.injectSnapshot(modelUUID, sourceID, parentUUID, parentSource) + }, [modelComponent.dereference, gltfDocumentState[modelSceneID]]) return null } diff --git a/packages/engine/src/scene/functions/GLTFConversion.ts b/packages/engine/src/scene/functions/GLTFConversion.ts index b64a0a3a0b5..4af9f5f4c62 100644 --- a/packages/engine/src/scene/functions/GLTFConversion.ts +++ b/packages/engine/src/scene/functions/GLTFConversion.ts @@ -23,12 +23,18 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Object3D } from 'three' +import { Matrix4, Object3D } from 'three' import config from '@etherealengine/common/src/config' -import { generateEntityUUID } from '@etherealengine/ecs' +import { EntityUUID, SerializedComponentType, UUIDComponent, generateEntityUUID } from '@etherealengine/ecs' import { sceneRelativePathIdentifier } from '@etherealengine/common/src/utils/parseSceneJSON' +import { getState } from '@etherealengine/hyperflux' +import { TransformComponent } from '@etherealengine/spatial' +import { GLTF } from '@gltf-transform/core' +import { GLTFDocumentState } from '../../gltf/GLTFDocumentState' +import { GLTFSnapshotState } from '../../gltf/GLTFState' +import { SceneSnapshotState } from '../SceneState' import { EntityJsonType, SceneJsonType } from '../types/SceneTypes' export const nodeToEntityJson = (node: any): EntityJsonType => { @@ -103,3 +109,65 @@ export const handleScenePaths = (gltf: any, mode: 'encode' | 'decode') => { } } } + +export function entityJSONToGLTFNode(entityJson: EntityJsonType, entityUUID: EntityUUID): GLTF.INode { + const node: GLTF.INode = { + name: entityJson.name, + extensions: { + [UUIDComponent.jsonID]: entityUUID + } + } + if (entityJson.components) { + for (const componentJson of entityJson.components) { + //handle transform component map to matrix + if (componentJson.name === TransformComponent.jsonID) { + const transform = componentJson.props as SerializedComponentType + const matrix = new Matrix4().compose(transform.position, transform.rotation, transform.scale) + node.matrix = matrix.toArray() + } else { + node.extensions![componentJson.name] = componentJson.props + } + } + } + return node +} + +export function getGLTFSnapshot(sourceID: string) { + if (getState(GLTFDocumentState)[sourceID]) { + return GLTFSnapshotState.cloneCurrentSnapshot(sourceID) + } else { + const sceneJSONSnapshot = SceneSnapshotState.cloneCurrentSnapshot(sourceID) + const result: GLTF.IGLTF = { + asset: { + version: '2.0', + generator: 'Infinite Reality Engine' + }, + nodes: [] + } + const nodes: GLTF.INode[] = [] + const roots: number[] = [] + for (const [entityUUID, entityJson] of Object.entries(sceneJSONSnapshot.data.entities)) { + const node = entityJSONToGLTFNode(entityJson, entityUUID as EntityUUID) + nodes.push(node) + if (entityJson.parent === sceneJSONSnapshot.data.root) { + roots.push(nodes.length - 1) + } + } + //add parent child info + for (const [entityUUID, entityJson] of Object.entries(sceneJSONSnapshot.data.entities)) { + const node = nodes.find((node) => node.extensions?.[UUIDComponent.jsonID] === entityUUID) + if (!node) continue + const parentUUID = entityJson.parent + if (!parentUUID) continue + const parent = nodes.find((node) => node.extensions?.[UUIDComponent.jsonID] === parentUUID) + if (!parent) continue + if (!parent.children) parent.children = [] + parent.children.push(nodes.indexOf(node)) + } + //add root nodes + result.scene = 0 + result.scenes = [{ nodes: roots }] + result.nodes = nodes + return { data: result, source: sourceID } + } +} diff --git a/packages/engine/src/scene/functions/loadGLTFModel.ts b/packages/engine/src/scene/functions/loadGLTFModel.ts index d263e21c865..dbcb5029d45 100644 --- a/packages/engine/src/scene/functions/loadGLTFModel.ts +++ b/packages/engine/src/scene/functions/loadGLTFModel.ts @@ -341,5 +341,9 @@ export const generateEntityJsonFromObject = (rootEntity: Entity, obj: Object3D, createMaterialInstance(path, objEntity, material) }) mesh.material = isArray(mesh.material) ? materials : materials[0] + + if (!hasComponent(objEntity, MeshComponent)) { + setComponent(objEntity, Object3DComponent, obj) + } return eJson } diff --git a/packages/projects/default-project/geo.prefab.gltf b/packages/projects/default-project/geo.prefab.gltf new file mode 100644 index 00000000000..984dd3207e8 --- /dev/null +++ b/packages/projects/default-project/geo.prefab.gltf @@ -0,0 +1,41 @@ +{ + "asset": { + "version": "2.0", + "generator": "THREE.GLTFExporter" + }, + "scenes": [ + { + "name": "New Object", + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "nodes": [ + { + "name": "New Object", + "extensions": { + "EE_uuid": "852336f7-89fd-48db-a2fa-98443914e4ca", + "EE_visible": true, + "EE_primitive_geometry": { + "geometryType": 0, + "geometryParams": { + "width": 1, + "height": 1, + "depth": 1, + "widthSegments": 1, + "heightSegments": 1, + "depthSegments": 1 + } + } + } + } + ], + "extensionsUsed": [ + "EE_uuid", + "EE_visible", + "EE_primitive_geometry", + "EE_ecs" + ] +} \ No newline at end of file diff --git a/packages/projects/default-project/pointLight.prefab.gltf b/packages/projects/default-project/pointLight.prefab.gltf new file mode 100644 index 00000000000..3dd90fbb208 --- /dev/null +++ b/packages/projects/default-project/pointLight.prefab.gltf @@ -0,0 +1,38 @@ +{ + "asset": { + "version": "2.0", + "generator": "THREE.GLTFExporter" + }, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "scene": 0, + "nodes": [ + { + "name": "EE_transform", + "extensions": { + "EE_uuid": "45a35b2e-0e35-46e4-993a-bcf6831c4fb1", + "EE_visible": true, + "EE_point_light": { + "color": 16777215, + "intensity": 1, + "range": 0, + "decay": 2, + "castShadow": false, + "shadowBias": 0.5, + "shadowRadius": 1 + } + } + } + ], + "extensionsUsed": [ + "EE_uuid", + "EE_visible", + "EE_point_light", + "EE_ecs" + ] +} \ No newline at end of file