diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index a3599dc5b7..40855713ab 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -1228,6 +1228,7 @@ "lbl-collapseAll": "Collapse All", "lbl-explode": "Explode Objects", "lbl-addEntity": "Add Entity", + "lbl-createPrefab": "Create Prefab", "issues": "Issues", "copy-paste": { "no-hierarchy-nodes": "No hierarchy nodes found" diff --git a/packages/editor/src/components/dialogs/CreatePrefabPanelDialog.tsx b/packages/editor/src/components/dialogs/CreatePrefabPanelDialog.tsx new file mode 100644 index 0000000000..3d6da9a4ff --- /dev/null +++ b/packages/editor/src/components/dialogs/CreatePrefabPanelDialog.tsx @@ -0,0 +1,150 @@ +/* +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 { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' +import config from '@etherealengine/common/src/config' +import { staticResourcePath } from '@etherealengine/common/src/schema.type.module' +import { pathJoin } from '@etherealengine/common/src/utils/miscUtils' +import { Engine, Entity, createEntity, getComponent, removeEntity, setComponent } from '@etherealengine/ecs' +import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' +import { proxifyParentChildRelationships } from '@etherealengine/engine/src/scene/functions/loadGLTFModel' +import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' +import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' +import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' +import Button from '@etherealengine/ui/src/primitives/tailwind/Button' +import Input from '@etherealengine/ui/src/primitives/tailwind/Input' +import Modal from '@etherealengine/ui/src/primitives/tailwind/Modal' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Scene } from 'three' +import { EditorControlFunctions } from '../../functions/EditorControlFunctions' +import { exportRelativeGLTF } from '../../functions/exportGLTF' +import { EditorState } from '../../services/EditorServices' +import { SelectionState } from '../../services/SelectionServices' +import { HeirarchyTreeNodeType } from '../hierarchy/HeirarchyTreeWalker' + +export default function CreatePrefabPanel({ node }: { node?: HeirarchyTreeNodeType }) { + const entity = node?.entity as Entity + const defaultPrefabFolder = useHookstate('assets/custom-prefabs') + const prefabName = useHookstate('prefab') + const prefabTag = useHookstate([]) + const { t } = useTranslation() + + const onExportPrefab = async () => { + const editorState = getState(EditorState) + const fileName = defaultPrefabFolder.value + '/' + prefabName.value + '.gltf' + const srcProject = editorState.projectName! + const fileURL = pathJoin(config.client.fileServer, 'projects', srcProject, fileName) + try { + const parentEntity = getComponent(entity, EntityTreeComponent).parentEntity + const prefabEntity = createEntity() + const obj = new Scene() + addObjectToGroup(prefabEntity, obj) + proxifyParentChildRelationships(obj) + setComponent(prefabEntity, EntityTreeComponent, { parentEntity }) + setComponent(prefabEntity, NameComponent, prefabName.value) + setComponent(entity, EntityTreeComponent, { parentEntity: prefabEntity }) + + await exportRelativeGLTF(prefabEntity, srcProject, fileName) + //await exportRelativeGLTF(entity, srcProject, fileName) + //pass tags to static resource + const resources = await Engine.instance.api.service(staticResourcePath).find({ + query: { key: 'projects/' + srcProject + '/' + fileName } + }) + if (resources.data.length === 0) { + throw new Error('User not found') + } + const resource = resources.data[0] + const tags = [...prefabTag.value] + await Engine.instance.api.service(staticResourcePath).patch(resource.id, { tags: tags }) + PopoverState.hidePopupover() + defaultPrefabFolder.set('assets/custom-prefabs') + prefabName.set('prefab') + prefabTag.set([]) + removeEntity(prefabEntity) + const { entityUUID } = EditorControlFunctions.createObjectFromSceneElement( + [{ name: ModelComponent.jsonID, props: { src: fileURL } }], + parentEntity + ) + getMutableState(SelectionState).selectedEntities.set([entityUUID]) + } catch (e) { + console.error(e) + } + } + return ( + + defaultPrefabFolder.set(event.target.value)} + label="Default Save Folder" + /> + prefabName.set(event.target.value)} label="Name" /> + + +
+ {(prefabTag.value ?? []).map((tag, index) => ( +
+ { + const tags = [...prefabTag.value] + tags[index] = event.target.value + prefabTag.set(tags) + }} + value={prefabTag.value[index]} + /> + +
+ ))} +
+
+ ) +} diff --git a/packages/editor/src/components/materials/MaterialLibraryPanelContainer.tsx b/packages/editor/src/components/materials/MaterialLibraryPanelContainer.tsx index 67843163e4..a8eec8c378 100644 --- a/packages/editor/src/components/materials/MaterialLibraryPanelContainer.tsx +++ b/packages/editor/src/components/materials/MaterialLibraryPanelContainer.tsx @@ -126,7 +126,8 @@ export default function MaterialLibraryPanel() { const relativePath = pathJoin('assets', libraryName) const gltf = (await exportMaterialsGLTF([UUIDComponent.getEntityByUUID(materialUUID)], { binary: false, - relativePath + relativePath, + projectName })!) as { [key: string]: any } const blob = [JSON.stringify(gltf)] const file = new File(blob, libraryName) diff --git a/packages/editor/src/functions/assetFunctions.ts b/packages/editor/src/functions/assetFunctions.ts index 0a316bb85d..c9394ff314 100644 --- a/packages/editor/src/functions/assetFunctions.ts +++ b/packages/editor/src/functions/assetFunctions.ts @@ -37,6 +37,7 @@ import { modelResourcesPath } from '@etherealengine/engine/src/assets/functions/ import { Heuristic } from '@etherealengine/engine/src/scene/components/VariantComponent' import { getState } from '@etherealengine/hyperflux' +import { pathJoin } from '@etherealengine/common/src/utils/miscUtils' import { ImportSettingsState } from '../components/assets/ImportSettingsPanel' import { createLODVariants } from '../components/assets/ModelCompressionPanel' import { LODVariantDescriptor } from '../constants/GLTFPresets' @@ -135,7 +136,7 @@ export const uploadProjectFiles = (projectName: string, files: File[], paths: st for (let i = 0; i < files.length; i++) { const file = files[i] const fileDirectory = paths[i].replace('projects/' + projectName + '/', '') - const filePath = (fileDirectory.endsWith('/') ? fileDirectory : fileDirectory + '/') + file.name + const filePath = fileDirectory ? pathJoin(fileDirectory, file.name) : file.name promises.push( uploadToFeathersService( fileBrowserUploadPath, diff --git a/packages/editor/src/functions/exportGLTF.ts b/packages/editor/src/functions/exportGLTF.ts index b02e96511b..57a001a609 100644 --- a/packages/editor/src/functions/exportGLTF.ts +++ b/packages/editor/src/functions/exportGLTF.ts @@ -46,6 +46,6 @@ export async function exportRelativeGLTF(entity: Entity, projectName: string, re }) const blob = isGLTF ? [JSON.stringify(gltf, null, 2)] : [gltf] const file = new File(blob, relativePath) - const urls = await Promise.all(uploadProjectFiles(projectName, [file], [`projects/${projectName}`]).promises) + const urls = await Promise.all(uploadProjectFiles(projectName, [file], [``]).promises) console.log('exported model data to ', ...urls) } diff --git a/packages/engine/src/assets/exporters/gltf/GLTFExporter.d.ts b/packages/engine/src/assets/exporters/gltf/GLTFExporter.d.ts index f2a248af54..613081aef8 100644 --- a/packages/engine/src/assets/exporters/gltf/GLTFExporter.d.ts +++ b/packages/engine/src/assets/exporters/gltf/GLTFExporter.d.ts @@ -75,6 +75,8 @@ export interface GLTFExporterOptions { */ includeCustomExtensions?: boolean; + projectName?: string; + relativePath?: string; resourceURI?: string; diff --git a/packages/engine/src/assets/exporters/gltf/extensions/BasisuExporterExtension.ts b/packages/engine/src/assets/exporters/gltf/extensions/BasisuExporterExtension.ts index 8f11fba20d..fe48e7f1a8 100644 --- a/packages/engine/src/assets/exporters/gltf/extensions/BasisuExporterExtension.ts +++ b/packages/engine/src/assets/exporters/gltf/extensions/BasisuExporterExtension.ts @@ -85,7 +85,7 @@ export default class BasisuExporterExtension extends ExporterExtension implement if (!_texture?.isCompressedTexture) return const writer = this.writer //if we're not embedding images and this image already has a src, just use that - if (!writer.options.embedImages && _texture.userData.src) { + if (!writer.options.embedImages && (_texture.userData.src || _texture.source.data.src)) { textureDef.extensions[this.name] = { source: textureDef.source } writer.extensionsUsed[this.name] = true delete textureDef.source diff --git a/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts b/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts index f2b8c4fd5a..61d1636e85 100644 --- a/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts +++ b/packages/engine/src/assets/exporters/gltf/extensions/ImageRoutingExtension.ts @@ -47,10 +47,16 @@ export default class ImageRoutingExtension extends ExporterExtension implements if (!materialEntity) return const src = getComponent(materialEntity, SourceComponent) const resolvedPath = pathResolver().exec(src)! - let relativeSrc = resolvedPath[2] - relativeSrc = relativeSrc.replace(/\/[^\/]*$/, '') + const projectDst = this.writer.options.projectName! + let projectSrc = this.writer.options.projectName! + let relativeSrc = './assets/' + if (resolvedPath) { + projectSrc = resolvedPath[1] + relativeSrc = resolvedPath[2] + relativeSrc = relativeSrc.replace(/\/[^\/]*$/, '') + } const dst = this.writer.options.relativePath!.replace(/\/[^\/]*$/, '') - const relativeBridge = relativePathTo(dst, relativeSrc) + const relativeBridge = relativePathTo(pathJoin(projectDst, dst), pathJoin(projectSrc, relativeSrc)) for (const [field, value] of Object.entries(material)) { if (field === 'envMap') continue @@ -60,8 +66,15 @@ export default class ImageRoutingExtension extends ExporterExtension implements let oldURI = texture.userData.src if (!oldURI) { const resolved = pathResolver().exec(texture.image.src)! + const oldProject = resolved[1] const relativeOldURL = resolved[2] - oldURI = relativePathTo(relativeSrc, relativeOldURL) + if (oldProject !== projectSrc) { + const srcWithProject = pathJoin(projectSrc, relativeSrc) + const dstWithProject = pathJoin(oldProject, relativeOldURL) + oldURI = relativePathTo(srcWithProject, dstWithProject) + } else { + oldURI = relativePathTo(relativeSrc, relativeOldURL) + } } const newURI = pathJoin(relativeBridge, oldURI) if (!texture.image.src) { diff --git a/packages/engine/src/assets/functions/exportModelGLTF.ts b/packages/engine/src/assets/functions/exportModelGLTF.ts index 2ed621afc0..c29bb57bbf 100644 --- a/packages/engine/src/assets/functions/exportModelGLTF.ts +++ b/packages/engine/src/assets/functions/exportModelGLTF.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 { getComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { getComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' import { GroupComponent } from '@etherealengine/spatial/src/renderer/components/GroupComponent' @@ -41,7 +41,7 @@ export default async function exportModelGLTF( onlyVisible: false } ) { - const scene = getComponent(entity, ModelComponent).scene ?? getComponent(entity, GroupComponent)[0] + const scene = getOptionalComponent(entity, ModelComponent)?.scene ?? getComponent(entity, GroupComponent)[0] const exporter = createGLTFExporter() const modelName = options.relativePath.split('/').at(-1)!.split('.').at(0)! const resourceURI = `model-resources/${modelName}` diff --git a/packages/engine/src/scene/functions/loadGLTFModel.ts b/packages/engine/src/scene/functions/loadGLTFModel.ts index bc6f668c56..0580f49567 100644 --- a/packages/engine/src/scene/functions/loadGLTFModel.ts +++ b/packages/engine/src/scene/functions/loadGLTFModel.ts @@ -50,6 +50,7 @@ import { FrustumCullCameraComponent } from '@etherealengine/spatial/src/transfor import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { computeTransformMatrix } from '@etherealengine/spatial/src/transform/systems/TransformSystem' +import { ColliderComponent } from '@etherealengine/spatial/src/physics/components/ColliderComponent' import { BoneComponent } from '../../avatar/components/BoneComponent' import { SkinnedMeshComponent } from '../../avatar/components/SkinnedMeshComponent' import { GLTFLoadedComponent } from '../components/GLTFLoadedComponent' @@ -293,14 +294,18 @@ export const generateEntityJsonFromObject = (rootEntity: Entity, obj: Object3D, const findColliderData = (obj: Object3D) => { if ( + hasComponent(obj.entity, ColliderComponent) || Object.keys(obj.userData).find( (key) => key.startsWith('xrengine.collider') || key.startsWith('xrengine.EE_collider') ) ) { return true } else if (obj.parent) { - return Object.keys(obj.parent.userData).some( - (key) => key.startsWith('xrengine.collider') || key.startsWith('xrengine.EE_collider') + return ( + hasComponent(obj.parent.entity, ColliderComponent) || + Object.keys(obj.parent.userData).some( + (key) => key.startsWith('xrengine.collider') || key.startsWith('xrengine.EE_collider') + ) ) } return false diff --git a/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx b/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx index e080cd2315..01debba3d3 100644 --- a/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx @@ -43,7 +43,9 @@ import { NotificationService } from '@etherealengine/client-core/src/common/serv import { Engine, EntityUUID, UUIDComponent, entityExists, useQuery } from '@etherealengine/ecs' import { CameraOrbitComponent } from '@etherealengine/spatial/src/camera/components/CameraOrbitComponent' +import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' import useUpload from '@etherealengine/editor/src/components/assets/useUpload' +import CreatePrefabPanel from '@etherealengine/editor/src/components/dialogs/CreatePrefabPanelDialog' import { HeirarchyTreeNodeType, heirarchyTreeWalker @@ -83,6 +85,7 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit const [anchorEl, setAnchorEl] = React.useState(null) const [anchorPosition, setAnchorPosition] = React.useState({ left: 0, top: 0 }) const [anchorPositionPop, setAnchorPositionPop] = React.useState(undefined) + const [prevClickedNode, setPrevClickedNode] = useState(null) const onUpload = useUpload(uploadOptions) const [renamingNode, setRenamingNode] = useState(null) @@ -94,6 +97,9 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit const sourcedEntities = useQuery([SourceComponent]) const rootEntity = UUIDComponent.useEntityByUUID(rootEntityUUID) const rootEntityTree = useComponent(rootEntity, EntityTreeComponent) + const panel = document.getElementById('propertiesPanel') + const anchorElButton = useHookstate(null) + const open = !!anchorElButton.value const MemoTreeNode = useCallback( (props: HierarchyTreeNodeProps) => ( @@ -438,9 +444,7 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit {MemoTreeNode} ) - const panel = document.getElementById('propertiesPanel') - const anchorElButton = useHookstate(null) - const open = !!anchorElButton.value + return ( <> } /> + + + + + {/* )} */} )