import { DockLayout, DockMode, LayoutData, TabData } from 'rc-dock' import 'rc-dock/dist/rc-dock.css' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { useRouter } from '@xrengine/client-core/src/common/services/RouterService' import { SceneJson } from '@xrengine/common/src/interfaces/SceneInterface' import multiLogger from '@xrengine/common/src/logger' import { Engine } from '@xrengine/engine/src/ecs/classes/Engine' import { getEngineState, useEngineState } from '@xrengine/engine/src/ecs/classes/EngineState' import { gltfToSceneJson, sceneToGLTF } from '@xrengine/engine/src/scene/functions/GLTFConversion' import { dispatchAction } from '@xrengine/hyperflux' import Inventory2Icon from '@mui/icons-material/Inventory2' import Dialog from '@mui/material/Dialog' import { extractZip, uploadProjectFiles } from '../functions/assetFunctions' import { disposeProject, loadProjectScene } from '../functions/projectFunctions' import { createNewScene, getScene, saveScene } from '../functions/sceneFunctions' import { initializeRenderer } from '../functions/sceneRenderFunctions' import { takeScreenshot } from '../functions/takeScreenshot' import { uploadBPCEMBakeToServer } from '../functions/uploadEnvMapBake' import { cmdOrCtrlString } from '../functions/utils' import { useEditorErrorState } from '../services/EditorErrorServices' import { EditorAction, useEditorState } from '../services/EditorServices' import AssetDropZone from './assets/AssetDropZone' import ProjectBrowserPanel from './assets/ProjectBrowserPanel' import ScenesPanel from './assets/ScenesPanel' import { ControlText } from './controlText/ControlText' import ConfirmDialog from './dialogs/ConfirmDialog' import ErrorDialog from './dialogs/ErrorDialog' import { ProgressDialog } from './dialogs/ProgressDialog' import SaveNewSceneDialog from './dialogs/SaveNewSceneDialog' import SaveSceneDialog from './dialogs/SaveSceneDialog' import { DndWrapper } from './dnd/DndWrapper' import DragLayer from './dnd/DragLayer' import ElementList from './element/ElementList' import GraphPanel from './graph/GraphPanel' import { GraphPanelTitle } from './graph/GraphPanelTitle' import HierarchyPanelContainer from './hierarchy/HierarchyPanelContainer' import { HierarchyPanelTitle } from './hierarchy/HierarchyPanelTitle' import { DialogContext } from './hooks/useDialog' import { PanelDragContainer, PanelIcon, PanelTitle } from './layout/Panel' import MaterialLibraryPanel from './materials/MaterialLibraryPanel' import { MaterialLibraryPanelTitle } from './materials/MaterialLibraryPanelTitle' import PropertiesPanelContainer from './properties/PropertiesPanelContainer' import { PropertiesPanelTitle } from './properties/PropertiesPanelTitle' import { AppContext } from './Search/context' import * as styles from './styles.module.scss' import ToolBar from './toolbar/ToolBar' const logger = multiLogger.child({ component: 'editor:EditorContainer' }) /** *Styled component used as dock container. * * @type {type} */ export const DockContainer = (styled as any).div` .dock-panel { background: transparent; pointer-events: auto; border: none; } .dock-panel:first-child { position: relative; z-index: 99; } .dock-panel[data-dockid='+5'] { pointer-events: none; } .dock-panel[data-dockid='+5'] .dock-bar { display: none; } .dock-panel[data-dockid='+5'] .dock { background: transparent; } .dock-divider { pointer-events: auto; background: rgba(1, 1, 1, ${(props) => props.dividerAlpha}); } .dock { border-radius: 4px; background: var(--dockBackground); } .dock-top .dock-bar { font-size: 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.2); background: transparent; } .dock-tab { background: transparent; border-bottom: none; } .dock-tab:hover, .dock-tab-active, .dock-tab-active:hover { border-bottom: 1px solid #ddd; } .dock-tab:hover div, .dock-tab:hover svg { color: var(--textColor); } .dock-tab > div { padding: 2px 12px; } .dock-tab-active { color: var(--textColor); } .dock-ink-bar { background-color: var(--textColor); } .dock-panel-max-btn:before { border-color: var(--iconButtonColor); } ` DockContainer.defaultProps = { dividerAlpha: 0 } /** * EditorContainer class used for creating container for Editor * */ const EditorContainer = () => { const editorState = useEditorState() const projectName = editorState.projectName const sceneName = editorState.sceneName const modified = editorState.sceneModified const sceneLoaded = useEngineState().sceneLoaded const errorState = useEditorErrorState() const editorError = errorState.error const [searchElement, setSearchElement] = React.useState('') const [searchHierarchy, setSearchHierarchy] = React.useState('') const { t } = useTranslation() const [DialogComponent, setDialogComponent] = useState(null) const [toggleRefetchScenes, setToggleRefetchScenes] = useState(false) const route = useRouter() const dockPanelRef = useRef(null) useHotkeys(`${cmdOrCtrlString}+s`, () => onSaveScene() as any) useEffect(() => { return () => { disposeProject() } }, []) const importScene = async (sceneFile: SceneJson) => { setDialogComponent() try { await loadProjectScene({ project: projectName.value!, scene: sceneFile, thumbnailUrl: null!, name: '' }) dispatchAction(EditorAction.sceneModified({ modified: true })) setDialogComponent(null) } catch (error) { logger.error(error) setDialogComponent( ) } } useEffect(() => { if (sceneName.value) { logger.info(`Loading scene ${sceneName.value} via given url`) loadScene(sceneName.value) } }, [sceneName]) const reRouteToLoadScene = async (newSceneName: string) => { if (sceneName.value === newSceneName) return if (!projectName.value || !newSceneName) return route(`/studio/${projectName.value}/${newSceneName}`) } const loadScene = async (sceneName: string) => { setDialogComponent() try { if (!projectName.value) return const project = await getScene(projectName.value, sceneName, false) if (!project.scene) return await loadProjectScene(project) setDialogComponent(null) } catch (error) { logger.error(error) setDialogComponent( ) } } const onNewScene = async () => { if (!projectName.value) return setDialogComponent() try { const sceneData = await createNewScene(projectName.value) if (!sceneData) return reRouteToLoadScene(sceneData.sceneName) setDialogComponent(null) } catch (error) { logger.error(error) setDialogComponent( ) } } /** * Scene Event Handlers */ const onEditorError = (error) => { logger.error(error) if (error['aborted']) { setDialogComponent(null) return } setDialogComponent( ) } const onCloseProject = () => { route('/editor') } const onSaveAs = async () => { const sceneLoaded = getEngineState().sceneLoaded.value // Do not save scene if scene is not loaded or some error occured while loading the scene to prevent data lose if (!sceneLoaded) { setDialogComponent() return } const abortController = new AbortController() try { if (sceneName.value || modified.value) { const blob = await takeScreenshot(512, 320) const result: { name: string } = (await new Promise((resolve) => { setDialogComponent( ) })) as any if (result && projectName.value) { await uploadBPCEMBakeToServer(Engine.instance.currentWorld.sceneEntity) await saveScene(projectName.value, result.name, blob, abortController.signal) dispatchAction(EditorAction.sceneModified({ modified: false })) } } setDialogComponent(null) } catch (error) { logger.error(error) setDialogComponent( ) } setToggleRefetchScenes(!toggleRefetchScenes) } const onImportAsset = async () => { const el = document.createElement('input') el.type = 'file' el.multiple = true el.accept = '.gltf,.glb,.fbx,.vrm,.tga,.png,.jpg,.jpeg,.mp3,.aac,.ogg,.m4a,.zip,.mp4,.mkv,.m3u8,.usdz' el.style.display = 'none' el.onchange = async () => { const pName = projectName.value if (el.files && el.files.length > 0 && pName) { const fList = el.files const files = [...Array(el.files.length).keys()].map((i) => fList[i]) const nuUrl = (await Promise.all(uploadProjectFiles(pName, files, true).promises)).map((url) => url[0]) //process zipped files const zipFiles = nuUrl.filter((url) => /\.zip$/.test(url)) const extractPromises = [...zipFiles.map((zipped) => extractZip(zipped))] Promise.all(extractPromises).then(() => { logger.info('extraction complete') }) } } el.click() el.remove() } const onImportScene = async () => { const confirm = await new Promise((resolve) => { setDialogComponent( resolve(true)} onCancel={() => resolve(false)} /> ) }) setDialogComponent(null) if (!confirm) return const el = document.createElement('input') el.type = 'file' el.accept = '.gltf' el.style.display = 'none' el.onchange = () => { if (el.files && el.files.length > 0) { const fileReader: any = new FileReader() fileReader.onload = () => { const json = JSON.parse(fileReader.result) importScene(gltfToSceneJson(json)) } fileReader.readAsText(el.files[0]) } } el.click() el.remove() } const onExportScene = async () => { const projectFile = await sceneToGLTF([Engine.instance.currentWorld.scene as any]) const projectJson = JSON.stringify(projectFile) const projectBlob = new Blob([projectJson]) const el = document.createElement('a') const fileName = Engine.instance.currentWorld.scene.name.toLowerCase().replace(/\s+/g, '-') el.download = fileName + '.xre.gltf' el.href = URL.createObjectURL(projectBlob) document.body.appendChild(el) el.click() document.body.removeChild(el) } const onSaveScene = async () => { console.log('onSaveScene') const sceneLoaded = getEngineState().sceneLoaded.value // Do not save scene if scene is not loaded or some error occured while loading the scene to prevent data lose if (!sceneLoaded) { setDialogComponent() return } if (!sceneName.value) { if (modified.value) { onSaveAs() } return } const result: { generateThumbnails: boolean } = (await new Promise((resolve) => { setDialogComponent() })) as any if (!result) { setDialogComponent(null) return } const abortController = new AbortController() setDialogComponent( { abortController.abort() setDialogComponent(null) }} /> ) // Wait for 5ms so that the ProgressDialog shows up. await new Promise((resolve) => setTimeout(resolve, 5)) try { if (projectName.value) { if (result.generateThumbnails) { const blob = await takeScreenshot(512, 320) await uploadBPCEMBakeToServer(Engine.instance.currentWorld.sceneEntity) await saveScene(projectName.value, sceneName.value, blob, abortController.signal) } else { await saveScene(projectName.value, sceneName.value, null, abortController.signal) } } dispatchAction(EditorAction.sceneModified({ modified: false })) setDialogComponent(null) } catch (error) { logger.error(error) setDialogComponent( ) } setToggleRefetchScenes(!toggleRefetchScenes) } useEffect(() => { dockPanelRef.current && dockPanelRef.current.updateTab('scenePanel', { id: 'scenePanel', title: ( Scenes ), content: ( ) }) }, [toggleRefetchScenes]) useEffect(() => { if (!dockPanelRef.current) return dockPanelRef.current.updateTab('viewPanel', { id: 'viewPanel', title: 'Viewport', content: viewPortPanelContent(!sceneLoaded.value) }) const activePanel = sceneLoaded.value ? 'filesPanel' : 'scenePanel' dockPanelRef.current.updateTab(activePanel, dockPanelRef.current.find(activePanel) as TabData, true) }, [sceneLoaded]) useEffect(() => { if (editorError) { onEditorError(editorError.value) } }, [editorError]) useEffect(() => { if (editorState.projectLoaded.value === true) { initializeRenderer() } }, [editorState.projectLoaded.value]) const generateToolbarMenu = () => { return [ { name: t('editor:menubar.newScene'), action: onNewScene }, { name: t('editor:menubar.saveScene'), hotkey: `${cmdOrCtrlString}+s`, action: onSaveScene }, { name: t('editor:menubar.saveAs'), action: onSaveAs }, { name: t('editor:menubar.importAsset'), action: onImportAsset }, { name: t('editor:menubar.importScene'), action: onImportScene }, { name: t('editor:menubar.exportScene'), action: onExportScene }, { name: t('editor:menubar.quit'), action: onCloseProject } ] } const viewPortPanelContent = useCallback((shouldDisplay) => { return shouldDisplay ? (

{t('editor:selectSceneMsg')}

) : (
) }, []) const toolbarMenu = generateToolbarMenu() const defaultLayout: LayoutData = { dockbox: { mode: 'horizontal' as DockMode, children: [ { mode: 'vertical' as DockMode, size: 2, children: [ { tabs: [ { id: 'scenePanel', title: ( Scenes ), content: ( ) }, { id: 'filesPanel', title: ( Files ), content: } ] } ] }, { mode: 'vertical' as DockMode, size: 8, children: [ { id: '+5', tabs: [ { id: 'viewPanel', title: 'Viewport', content: viewPortPanelContent(true) } ], size: 1 } ] }, { mode: 'vertical' as DockMode, size: 2, children: [ { tabs: [ { id: 'hierarchyPanel', title: , content: ( ) }, { id: 'materialLibraryPanel', title: , content: }, { id: 'graphPanel', title: , content: } ] }, { tabs: [ { id: 'propertiesPanel', title: , content: } ] } ] } ] } } return ( <>
setDialogComponent(null)} classes={{ root: styles.dialogRoot, paper: styles.dialogPaper }} > {DialogComponent}
) } export default EditorContainer