diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index a5d1b044fb..4938cc63fd 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -136,6 +136,10 @@ "info-translate": "Translate objects by a unit of measurement.", "info-rotate": "Rotate objects by a specific degrees." }, + "placement": { + "click": "Point & Click Placement", + "drag": "Drag & Drop Placement" + }, "command": { "translate": "Translate", "rotate": "Rotate", @@ -934,7 +938,7 @@ "lbl-torusknot": "Torus Knot" }, "portal": { - "name":"Portal", + "name": "Portal", "lbl-portal": "Linked Portal", "lbl-redirect": "Use Page Redirect", "description": "A Portal to teleport players to specified location", diff --git a/packages/editor/src/EditorModule.ts b/packages/editor/src/EditorModule.ts index 87ccda967d..6e13eded76 100644 --- a/packages/editor/src/EditorModule.ts +++ b/packages/editor/src/EditorModule.ts @@ -24,8 +24,8 @@ Ethereal Engine. All Rights Reserved. */ import { RenderInfoSystem } from '@etherealengine/spatial/src/renderer/RenderInfoSystem' - import { EditorInstanceNetworkingSystem } from './components/realtime/EditorInstanceNetworkingSystem' +import { ClickPlacementSystem } from './systems/ClickPlacementSystem' import { EditorControlSystem } from './systems/EditorControlSystem' import { GizmoSystem } from './systems/GizmoSystem' import { HighlightSystem } from './systems/HighlightSystem' @@ -34,6 +34,7 @@ import { ObjectGridSnapSystem } from './systems/ObjectGridSnapSystem' import { UploadRequestSystem } from './systems/UploadRequestSystem' export { + ClickPlacementSystem, EditorControlSystem, EditorInstanceNetworkingSystem, GizmoSystem, diff --git a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx index 6dc0ffd1dd..e35f0e4934 100644 --- a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx +++ b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx @@ -23,17 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import AddIcon from '@mui/icons-material/Add' -import ArrowBackIcon from '@mui/icons-material/ArrowBack' -import AutorenewIcon from '@mui/icons-material/Autorenew' -import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder' -import DownloadIcon from '@mui/icons-material/Download' -import SettingsIcon from '@mui/icons-material/Settings' -import { Breadcrumbs, Link, Popover, TablePagination } from '@mui/material' -import React, { useEffect, useRef } from 'react' -import { useDrop } from 'react-dnd' -import { useTranslation } from 'react-i18next' - import ConfirmDialog from '@etherealengine/client-core/src/common/components/ConfirmDialog' import InputSlider from '@etherealengine/client-core/src/common/components/InputSlider' import { FileThumbnailJobState } from '@etherealengine/client-core/src/common/services/FileThumbnailJobState' @@ -41,8 +30,8 @@ import { NotificationService } from '@etherealengine/client-core/src/common/serv import { uploadToFeathersService } from '@etherealengine/client-core/src/util/upload' import config from '@etherealengine/common/src/config' import { - archiverPath, FileBrowserContentType, + archiverPath, fileBrowserPath, fileBrowserUploadPath, staticResourcePath @@ -56,7 +45,7 @@ import { ImageConvertDefaultParms, ImageConvertParms } from '@etherealengine/engine/src/assets/constants/ImageConvertParms' -import { getMutableState, NO_PROXY, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { NO_PROXY, getMutableState, getState, useHookstate, useMutableState } from '@etherealengine/hyperflux' import { useFind, useMutation, useSearch } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { useValidProjectForFileBrowser } from '@etherealengine/ui/src/components/editor/panels/Files/container' import Button from '@etherealengine/ui/src/primitives/mui/Button' @@ -64,9 +53,21 @@ import Checkbox from '@etherealengine/ui/src/primitives/mui/Checkbox' import FormControlLabel from '@etherealengine/ui/src/primitives/mui/FormControlLabel' import Typography from '@etherealengine/ui/src/primitives/mui/Typography' import LoadingView from '@etherealengine/ui/src/primitives/tailwind/LoadingView' +import AddIcon from '@mui/icons-material/Add' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' +import AutorenewIcon from '@mui/icons-material/Autorenew' +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder' +import DownloadIcon from '@mui/icons-material/Download' +import SettingsIcon from '@mui/icons-material/Settings' +import { Breadcrumbs, Link, Popover, TablePagination } from '@mui/material' +import React, { useEffect, useRef } from 'react' +import { useDrop } from 'react-dnd' +import { useTranslation } from 'react-i18next' import { SupportedFileTypes } from '../../../constants/AssetTypes' import { downloadBlobAsZip, inputFileWithAddToScene } from '../../../functions/assetFunctions' import { bytesToSize, unique } from '../../../functions/utils' +import { EditorHelperState, PlacementMode } from '../../../services/EditorHelperState' +import { ClickPlacementState } from '../../../systems/ClickPlacementSystem' import BooleanInput from '../../inputs/BooleanInput' import InputGroup from '../../inputs/InputGroup' import StringInput from '../../inputs/StringInput' @@ -76,8 +77,8 @@ import ImageCompressionPanel from '../ImageCompressionPanel' import ImageConvertPanel from '../ImageConvertPanel' import ModelCompressionPanel from '../ModelCompressionPanel' import styles from '../styles.module.scss' -import { canDropItemOverFolder, FileBrowserItem, FileTableWrapper } from './FileBrowserGrid' -import { availableTableColumns, FilesViewModeSettings, FilesViewModeState } from './FileBrowserState' +import { FileBrowserItem, FileTableWrapper, canDropItemOverFolder } from './FileBrowserGrid' +import { FilesViewModeSettings, FilesViewModeState, availableTableColumns } from './FileBrowserState' import { FileDataType } from './FileDataType' import { FilePropertiesPanel } from './FilePropertiesPanel' @@ -209,6 +210,10 @@ const FileBrowserContentPanel: React.FC = (props) contentType: params.type, size: params.size }) + const editorHelperState = getState(EditorHelperState) + if (editorHelperState.placementMode === PlacementMode.CLICK) { + getMutableState(ClickPlacementState).selectedAsset.set(params.url) + } } else { const newPath = `${selectedDirectory.value}${params.name}/` changeDirectoryByPath(newPath) diff --git a/packages/editor/src/components/properties/TransformPropertyGroup.tsx b/packages/editor/src/components/properties/TransformPropertyGroup.tsx index 44a73d54e3..0a633cb56e 100755 --- a/packages/editor/src/components/properties/TransformPropertyGroup.tsx +++ b/packages/editor/src/components/properties/TransformPropertyGroup.tsx @@ -36,7 +36,7 @@ import { } from '@etherealengine/ecs/src/ComponentFunctions' import { SceneDynamicLoadTagComponent } from '@etherealengine/engine/src/scene/components/SceneDynamicLoadTagComponent' import { TransformSpace } from '@etherealengine/engine/src/scene/constants/transformConstants' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' import { TransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' import { EditorControlFunctions } from '../../functions/EditorControlFunctions' @@ -73,9 +73,9 @@ export const TransformPropertyGroup: EditorComponentType = (props) => { transformComponent.matrixWorld.value.decompose(position, rotation, scale) const onRelease = () => { - const bboxSnapState = getMutableState(ObjectGridSnapState) - if (bboxSnapState.enabled.value) { - bboxSnapState.apply.set(true) + const bboxSnapState = getState(ObjectGridSnapState) + if (bboxSnapState.enabled) { + ObjectGridSnapState.apply() } else { EditorControlFunctions.commitTransformSave([props.entity]) } diff --git a/packages/editor/src/components/toolbar/tools/TransformTool.tsx b/packages/editor/src/components/toolbar/tools/TransformTool.tsx index 885bbdf3b3..b67a5903bb 100644 --- a/packages/editor/src/components/toolbar/tools/TransformTool.tsx +++ b/packages/editor/src/components/toolbar/tools/TransformTool.tsx @@ -26,6 +26,10 @@ Ethereal Engine. All Rights Reserved. import HeightIcon from '@mui/icons-material/Height' import OpenWithIcon from '@mui/icons-material/OpenWith' import SyncIcon from '@mui/icons-material/Sync' + +import AdsClickIcon from '@mui/icons-material/AdsClick' +import SwipeRightIcon from '@mui/icons-material/SwipeRight' + import React from 'react' import { useTranslation } from 'react-i18next' @@ -33,7 +37,7 @@ import { TransformMode } from '@etherealengine/engine/src/scene/constants/transf import { useMutableState } from '@etherealengine/hyperflux' import { setTransformMode } from '../../../functions/transformFunctions' -import { EditorHelperState } from '../../../services/EditorHelperState' +import { EditorHelperState, PlacementMode } from '../../../services/EditorHelperState' import { InfoTooltip } from '../../layout/Tooltip' import * as styles from '../styles.module.scss' @@ -44,34 +48,62 @@ const TransformTool = () => { const transformMode = editorHelperState.transformMode.value return ( - + <>
- - - - + - +
-
+ +
+ + + + + + + + + +
+
+ ) } diff --git a/packages/editor/src/functions/gizmoHelper.ts b/packages/editor/src/functions/gizmoHelper.ts index e19dc09f25..4445ab86cb 100644 --- a/packages/editor/src/functions/gizmoHelper.ts +++ b/packages/editor/src/functions/gizmoHelper.ts @@ -39,7 +39,7 @@ import { TransformMode, TransformSpace } from '@etherealengine/engine/src/scene/constants/transformConstants' -import { getMutableState, getState, NO_PROXY } from '@etherealengine/hyperflux' +import { getState, NO_PROXY } from '@etherealengine/hyperflux' import { TransformComponent } from '@etherealengine/spatial' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' import { Axis, Q_IDENTITY, Vector3_Zero } from '@etherealengine/spatial/src/common/constants/MathConstants' @@ -913,7 +913,7 @@ function pointerUp(gizmoEntity) { if (!getState(ObjectGridSnapState).enabled) { EditorControlFunctions.commitTransformSave(gizmoControlComponent.controlledEntities.get(NO_PROXY) as Entity[]) } else { - getMutableState(ObjectGridSnapState).apply.set(true) + ObjectGridSnapState.apply() } } gizmoControlComponent.dragging.set(false) diff --git a/packages/editor/src/services/EditorHelperState.ts b/packages/editor/src/services/EditorHelperState.ts index 6b61650a06..e268f8e36b 100644 --- a/packages/editor/src/services/EditorHelperState.ts +++ b/packages/editor/src/services/EditorHelperState.ts @@ -36,6 +36,11 @@ import { import { defineState, syncStateWithLocalStorage } from '@etherealengine/hyperflux' import { EditorMode, EditorModeType } from '../constants/EditorModeTypes' +export enum PlacementMode { + DRAG, + CLICK +} + export const EditorHelperState = defineState({ name: 'EditorHelperState', initial: () => ({ @@ -47,7 +52,8 @@ export const EditorHelperState = defineState({ gridSnap: SnapMode.Grid as SnapModeType, translationSnap: 0.5, rotationSnap: 10, - scaleSnap: 0.1 + scaleSnap: 0.1, + placementMode: PlacementMode.DRAG }), extension: syncStateWithLocalStorage(['snapMode', 'translationSnap', 'rotationSnap', 'scaleSnap']) }) diff --git a/packages/editor/src/systems/ClickPlacementSystem.tsx b/packages/editor/src/systems/ClickPlacementSystem.tsx new file mode 100644 index 0000000000..b2334a3011 --- /dev/null +++ b/packages/editor/src/systems/ClickPlacementSystem.tsx @@ -0,0 +1,346 @@ +/* +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 { Ray } from '@dimforge/rapier3d-compat' +import { + Engine, + Entity, + UUIDComponent, + UndefinedEntity, + defineQuery, + defineSystem, + getComponent, + getOptionalComponent, + removeEntity, + setComponent, + useComponent, + useOptionalComponent +} from '@etherealengine/ecs' +import { GLTFComponent } from '@etherealengine/engine/src/gltf/GLTFComponent' +import { GLTFDocumentState, GLTFSnapshotAction } from '@etherealengine/engine/src/gltf/GLTFDocumentState' +import { GLTFSnapshotState } from '@etherealengine/engine/src/gltf/GLTFState' +import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' +import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent' +import { entityJSONToGLTFNode } from '@etherealengine/engine/src/scene/functions/GLTFConversion' +import { createSceneEntity } from '@etherealengine/engine/src/scene/functions/createSceneEntity' +import { getModelSceneID } from '@etherealengine/engine/src/scene/functions/loaders/ModelFunctions' +import { toEntityJson } from '@etherealengine/engine/src/scene/functions/serializeWorld' +import { + NO_PROXY, + defineState, + dispatchAction, + getMutableState, + getState, + useHookstate, + useState +} from '@etherealengine/hyperflux' +import { TransformComponent, TransformSystem } from '@etherealengine/spatial' +import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' +import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' +import { InputPointerComponent } from '@etherealengine/spatial/src/input/components/InputPointerComponent' +import { MouseScroll } from '@etherealengine/spatial/src/input/state/ButtonState' +import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' +import { GroupComponent } from '@etherealengine/spatial/src/renderer/components/GroupComponent' +import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' +import { ObjectLayerComponents } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' +import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' +import { HolographicMaterial } from '@etherealengine/spatial/src/renderer/materials/prototypes/HolographicMaterial.mat' +import { EntityTreeComponent, iterateEntityNode } from '@etherealengine/spatial/src/transform/components/EntityTree' +import React, { useEffect } from 'react' +import { Euler, Material, Mesh, Quaternion, Raycaster, Vector3 } from 'three' +import { EditorControlFunctions } from '../functions/EditorControlFunctions' +import { EditorHelperState, PlacementMode } from '../services/EditorHelperState' +import { EditorState } from '../services/EditorServices' +import { SelectionState } from '../services/SelectionServices' +import { ObjectGridSnapState } from './ObjectGridSnapSystem' + +let placedCount = 0 +export const ClickPlacementState = defineState({ + name: 'ClickPlacementState', + initial: { + placementEntity: UndefinedEntity as Entity, + selectedAsset: undefined as undefined | string, + yawOffset: 0, + pitchOffset: 0, + rollOffset: 0, + maxDistance: 25, + materialCache: [] as [Mesh, Material][] + } +}) + +const ClickPlacementReactor = (props: { parentEntity: Entity }) => { + const { parentEntity } = props + const clickState = useState(getMutableState(ClickPlacementState)) + const editorState = useState(getMutableState(EditorHelperState)) + const gltfComponent = useComponent(parentEntity, GLTFComponent) + + // const renderers = defineQuery([RendererComponent]) + + // useEffect(() => { + // const placementMode = editorState.placementMode.value + // const renderer = getComponent(renderers()[0], RendererComponent) + // const canvas = renderer.canvas + // if (placementMode === PlacementMode.CLICK) { + // canvas.addEventListener('click', clickListener) + // } else { + // canvas.removeEventListener('click', clickListener) + // } + // }, [editorState.placementMode]) + + useEffect(() => { + if (gltfComponent.progress.value < 100) return + if (editorState.placementMode.value === PlacementMode.CLICK) { + SelectionState.updateSelection([]) + if (clickState.placementEntity.value) return + clickState.placementEntity.set(createPlacementEntity(parentEntity)) + } else { + if (!clickState.placementEntity.value) return + const selectedEntities = getState(SelectionState).selectedEntities.filter( + (uuid) => uuid !== getComponent(clickState.placementEntity.value, UUIDComponent) + ) + EditorControlFunctions.removeObject([clickState.placementEntity.value]) + removeEntity(clickState.placementEntity.value) + clickState.placementEntity.set(UndefinedEntity) + SelectionState.updateSelection(selectedEntities) + } + }, [editorState.placementMode, gltfComponent.progress]) + + useEffect(() => { + if (!clickState.selectedAsset.value || !clickState.placementEntity.value) return + const assetURL = clickState.selectedAsset.get(NO_PROXY)! + const placementEntity = clickState.placementEntity.value + if (getComponent(placementEntity, ModelComponent)?.src === assetURL) return + updatePlacementEntitySnapshot(placementEntity) + }, [clickState.selectedAsset, clickState.placementEntity]) + + return ( + + ) +} + +const PlacementModelReactor = (props: { placementEntity: Entity }) => { + const clickState = useState(getMutableState(ClickPlacementState)) + const sceneState = useHookstate(getMutableState(GLTFDocumentState)) + const placementModel = useOptionalComponent(props.placementEntity, ModelComponent) + + useEffect(() => { + if (!placementModel) return + const sceneID = getModelSceneID(props.placementEntity) + if (!sceneState.scenes[sceneID]) return + iterateEntityNode(props.placementEntity, (entity) => { + const mesh = getOptionalComponent(entity, MeshComponent) + if (!mesh) return + const material = mesh.material as Material + clickState.materialCache.set((prev) => [...prev, [mesh, material]]) + mesh.material = new HolographicMaterial({}) + }) + }, [placementModel?.scene, sceneState.scenes.keys]) + + return null +} + +const objectLayerQuery = defineQuery([ObjectLayerComponents[ObjectLayers.Scene]]) + +const getParentEntity = () => { + return getState(EditorState).rootEntity +} + +const updatePlacementEntitySnapshot = (placementEntity: Entity) => { + setComponent(placementEntity, ModelComponent, { src: getState(ClickPlacementState).selectedAsset }) + const sceneID = getComponent(placementEntity, SourceComponent) + const snapshot = GLTFSnapshotState.cloneCurrentSnapshot(sceneID) + const uuid = getComponent(placementEntity, UUIDComponent) + const nodeIndex = snapshot.data.nodes!.findIndex( + (value) => value.extensions && value.extensions[UUIDComponent.jsonID] === uuid + ) + const entityJson = toEntityJson(placementEntity) + const entityGLTFNode = entityJSONToGLTFNode(entityJson, uuid) + delete entityGLTFNode.matrix + snapshot.data.nodes![nodeIndex] = entityGLTFNode + dispatchAction(GLTFSnapshotAction.createSnapshot(snapshot)) +} + +const createPlacementEntitySnapshot = (placementEntity: Entity) => { + setComponent(placementEntity, ModelComponent, { src: getState(ClickPlacementState).selectedAsset }) + const sceneID = getComponent(placementEntity, SourceComponent) + const snapshot = GLTFSnapshotState.cloneCurrentSnapshot(sceneID) + const uuid = getComponent(placementEntity, UUIDComponent) + const entityJson = toEntityJson(placementEntity) + const entityGLTFNode = entityJSONToGLTFNode(entityJson, uuid) + delete entityGLTFNode.matrix + const nodeIndex = snapshot.data.nodes!.length + snapshot.data.nodes!.push(entityGLTFNode) + snapshot.data.scenes![0].nodes.push(nodeIndex) + dispatchAction(GLTFSnapshotAction.createSnapshot(snapshot)) +} + +const createPlacementEntity = (parentEntity: Entity) => { + const placementEntity = createSceneEntity('Placement-' + placedCount, parentEntity) + + const sceneID = getComponent(parentEntity, SourceComponent) + setComponent(placementEntity, SourceComponent, sceneID) + setComponent(placementEntity, EntityTreeComponent, { parentEntity }) + createPlacementEntitySnapshot(placementEntity) + + return placementEntity +} + +const clickListener = () => { + const clickState = getMutableState(ClickPlacementState) + if (!clickState.selectedAsset.value) return + const parentEntity = getParentEntity() + if (!parentEntity) return + const placementEntity = clickState.placementEntity.value + if (!placementEntity) return + + if (getState(ObjectGridSnapState).enabled) { + ObjectGridSnapState.apply() + } else { + TransformComponent.updateFromWorldMatrix(placementEntity) + EditorControlFunctions.commitTransformSave([placementEntity]) + } + placedCount += 1 + clickState.placementEntity.set(createPlacementEntity(parentEntity)) + for (const [mesh, material] of clickState.materialCache.value as [Mesh, Material][]) { + mesh.material = material + } + clickState.materialCache.set([]) +} + +export const ClickPlacementSystem = defineSystem({ + uuid: 'ee.studio.ClickPlacementSystem', + insert: { before: TransformSystem }, + reactor: () => { + const parentEntity = useHookstate(getMutableState(EditorState)).rootEntity + + return parentEntity.value ? ( + + ) : null + }, + execute: () => { + const editorHelperState = getState(EditorHelperState) + if (editorHelperState.placementMode !== PlacementMode.CLICK) return + const clickState = getState(ClickPlacementState) + const placementEntity = clickState.placementEntity + if (!placementEntity) return + + //@todo: fix type of `typeof GroupComponent` + const sceneObjects: any[] = [] + const candidates = objectLayerQuery() + for (const entity of candidates) { + const obj = getComponent(entity, GroupComponent)?.[0] + !!obj && sceneObjects.push(obj) + } + //const sceneObjects = Array.from(Engine.instance.objectLayerList[ObjectLayers.Scene] || []) + const camera = getComponent(Engine.instance.cameraEntity, CameraComponent) + const pointerScreenRaycaster = new Raycaster() + + const physicsWorld = getState(PhysicsState).physicsWorld + + let intersectEntity: Entity = UndefinedEntity + let targetIntersection: { point: Vector3; normal: Vector3 } | null = null + + const viewerEntity = Engine.instance.viewerEntity + const mouseEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) + if (!mouseEntity) return + + const buttons = InputComponent.getMergedButtons(viewerEntity) + const axes = InputComponent.getMergedAxes(viewerEntity) + + const zoom = axes[MouseScroll.VerticalScroll] + + if (buttons.SecondaryClick?.pressed) { + clickState.maxDistance -= zoom + } + + if (buttons.KeyE?.up) { + clickState.yawOffset += Math.PI / 4 + } + if (buttons.KeyQ?.up) { + clickState.yawOffset -= Math.PI / 4 + } + if (buttons.PrimaryClick?.up) { + clickListener() + //Wait until next frame is placement entity changed + if (placementEntity !== clickState.placementEntity) return + } + + const pointer = getComponent(mouseEntity, InputPointerComponent) + const mouse = pointer.position + pointerScreenRaycaster.setFromCamera(mouse, camera) // Assuming 'camera' is your Three.js camera + const cameraPosition = pointerScreenRaycaster.ray.origin + const cameraDirection = pointerScreenRaycaster.ray.direction + const physicsIntersection = physicsWorld.castRayAndGetNormal(new Ray(cameraPosition, cameraDirection), 1000, false) + if (physicsIntersection && physicsIntersection.toi < clickState.maxDistance) { + const intersectPosition = cameraPosition + .clone() + .add(cameraDirection.clone().multiplyScalar(physicsIntersection.toi)) + intersectEntity = (physicsIntersection.collider.parent() as { userData: { entity: Entity } }).userData.entity + const intersectNormal = new Vector3( + physicsIntersection.normal.x, + physicsIntersection.normal.y, + physicsIntersection.normal.z + ) + targetIntersection = { + point: intersectPosition, + normal: intersectNormal + } + } + const intersect = pointerScreenRaycaster.intersectObjects(sceneObjects, false) + //if (intersect.length === 0 && !targetIntersection) return + for (let i = 0; i < intersect.length; i++) { + const intersected = intersect[i] + if (intersected.distance > clickState.maxDistance) continue + if (isPlacementDescendant(intersected.object.entity)) continue + targetIntersection = { + point: intersected.point, + normal: intersected.face?.normal ?? new Vector3(0, 1, 0) + } + break + } + + if (!targetIntersection) { + const point = cameraPosition.clone().add(cameraDirection.clone().multiplyScalar(clickState.maxDistance)) + targetIntersection = { point, normal: new Vector3(0, 1, 0) } + } + const position = targetIntersection.point + let rotation = new Quaternion().setFromUnitVectors(new Vector3(), targetIntersection.normal ?? new Vector3(0, 1, 0)) + const offset = new Quaternion().setFromEuler( + new Euler(clickState.pitchOffset, clickState.yawOffset, clickState.rollOffset) + ) + rotation = offset.multiply(rotation) + setComponent(placementEntity, TransformComponent, { position, rotation }) + } +}) + +const isPlacementDescendant = (entity: Entity) => { + const placementEntity = getState(ClickPlacementState).placementEntity + if (!placementEntity) return false + let walker = entity + while (walker) { + if (walker === placementEntity) return true + walker = getComponent(walker, EntityTreeComponent)?.parentEntity ?? null + } + return false +} diff --git a/packages/editor/src/systems/EditorControlSystem.ts b/packages/editor/src/systems/EditorControlSystem.ts index b67c6f0e1e..7a2f8e2277 100644 --- a/packages/editor/src/systems/EditorControlSystem.ts +++ b/packages/editor/src/systems/EditorControlSystem.ts @@ -71,9 +71,12 @@ import { toggleTransformSpace } from '../functions/transformFunctions' import { EditorErrorState } from '../services/EditorErrorServices' -import { EditorHelperState } from '../services/EditorHelperState' + +import { EditorHelperState, PlacementMode } from '../services/EditorHelperState' + import { EditorState } from '../services/EditorServices' import { SelectionState } from '../services/SelectionServices' +import { ClickPlacementState } from './ClickPlacementSystem' import { ObjectGridSnapState } from './ObjectGridSnapSystem' const raycaster = new Raycaster() @@ -130,6 +133,15 @@ const onKeyW = () => { setTransformMode(TransformMode.translate) } +const onKeyP = () => { + const editorHelperState = getMutableState(EditorHelperState) + if (editorHelperState.placementMode.value === PlacementMode.CLICK) { + editorHelperState.placementMode.set(PlacementMode.DRAG) + } else { + editorHelperState.placementMode.set(PlacementMode.CLICK) + } +} + const onKeyE = () => { setTransformMode(TransformMode.rotate) } @@ -253,8 +265,8 @@ const execute = () => { const buttons = InputComponent.getMergedButtonsForInputSources(inputSources) if (buttons.KeyB?.down) onKeyB() - if (buttons.KeyE?.down) onKeyE() + if (buttons.KeyP?.down) onKeyP() if (buttons.KeyR?.down) onKeyR() if (buttons.KeyW?.down) onKeyW() if (buttons.KeyC?.down) onKeyC() @@ -293,7 +305,7 @@ const execute = () => { } } if (buttons.PrimaryClick?.up && !buttons.PrimaryClick?.dragging) { - if (hasComponent(clickStartEntity, SourceComponent)) { + if (hasComponent(clickStartEntity, SourceComponent) && !getState(ClickPlacementState).placementEntity) { const selectedEntities = SelectionState.getSelectedEntities() const modelComponent = getAncestorWithComponent(clickStartEntity, ModelComponent) const ancestorModelEntity = modelComponent || clickStartEntity diff --git a/packages/editor/src/systems/ObjectGridSnapSystem.ts b/packages/editor/src/systems/ObjectGridSnapSystem.ts index 6be8b3e903..019142ac69 100644 --- a/packages/editor/src/systems/ObjectGridSnapSystem.ts +++ b/packages/editor/src/systems/ObjectGridSnapSystem.ts @@ -35,33 +35,35 @@ import { getOptionalMutableComponent, hasComponent, Not, + removeComponent, setComponent, - UndefinedEntity, - useQuery + UndefinedEntity } from '@etherealengine/ecs' -import { AvatarRigComponent } from '@etherealengine/engine/src/avatar/components/AvatarAnimationComponent' -import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' import { BoundingBoxHelperComponent, ObjectGridSnapComponent } from '@etherealengine/engine/src/scene/components/ObjectGridSnapComponent' import { defineState, getMutableState, getState, useMutableState } from '@etherealengine/hyperflux' -import { EngineState } from '@etherealengine/spatial/src/EngineState' -import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { TransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' import { TransformSystem } from '@etherealengine/spatial/src/transform/systems/TransformSystem' +import { AvatarRigComponent } from '@etherealengine/engine/src/avatar/components/AvatarAnimationComponent' +import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' +import { EngineState } from '@etherealengine/spatial/src/EngineState' +import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' import { EditorControlFunctions } from '../functions/EditorControlFunctions' import { SelectionState } from '../services/SelectionServices' - -const objectGridQuery = defineQuery([ObjectGridSnapComponent]) +import { ClickPlacementState } from './ClickPlacementSystem' function isParentSelected(entity: Entity) { let walker: Entity | null = entity + const clickState = getState(ClickPlacementState) + const placementEntity = clickState.placementEntity + const selectedEntities = SelectionState.getSelectedEntities() while (walker) { - if (selectedEntities.includes(walker)) return walker + if (placementEntity === walker || selectedEntities.includes(walker)) return walker walker = getOptionalComponent(walker, EntityTreeComponent)?.parentEntity ?? null } return false @@ -189,14 +191,6 @@ function calculateTranslation(bbox1: Box3, bbox2: Box3): Vector3 { return translation } -export const ObjectGridSnapState = defineState({ - name: 'ObjectGridSnapState', - initial: { - enabled: false, - apply: false - } -}) - function setHelperLayer(entity: Entity, layer: number) { const helper = getOptionalMutableComponent(entity, BoundingBoxHelperComponent) if (helper) { @@ -223,20 +217,56 @@ function resetHelperTransform(entity: Entity) { } } +export const ObjectGridSnapState = defineState({ + name: 'ObjectGridSnapState', + initial: { + enabled: false, + selectedEntities: [] as Entity[], + selectedParents: [] as Entity[] + }, + apply: () => { + const snapState = getState(ObjectGridSnapState) + const selectedEntities = snapState.selectedEntities + const selectedParents = snapState.selectedParents + + const toCommit = [] as Entity[] + + for (let i = 0; i < selectedEntities.length; i++) { + const selectedEntity = selectedEntities[i] + const selectedParent = selectedParents[i] + const helperEntity = getOptionalComponent(selectedEntity, BoundingBoxHelperComponent)?.entity + if (!helperEntity) { + console.warn(`ObjectGridSnapSystem:apply No Helper entity found for selected entity: ${selectedEntity}`) + continue + } + + const helperMatrixWorld = getComponent(helperEntity, TransformComponent).matrixWorld + const parentMatrixWorld = getComponent(selectedParent, TransformComponent).matrixWorld + const transformMat = parentMatrixWorld.clone().invert().multiply(helperMatrixWorld) + parentMatrixWorld.multiply(transformMat) + TransformComponent.updateFromWorldMatrix(selectedParent) + toCommit.push(selectedParent) + } + + EditorControlFunctions.commitTransformSave(toCommit) + } +}) + +const objectGridQuery = defineQuery([ObjectGridSnapComponent]) +const models = defineQuery([ModelComponent, Not(AvatarRigComponent)]) + export const ObjectGridSnapSystem = defineSystem({ uuid: 'ee.engine.scene.ObjectGridSnapSystem', insert: { after: TransformSystem }, reactor: () => { const snapState = useMutableState(ObjectGridSnapState) const selectionState = useMutableState(SelectionState) - const models = useQuery([ModelComponent, Not(AvatarRigComponent)]) useEffect(() => { - if (!snapState.enabled.value) { - for (const entity of objectGridQuery()) { - setHelperColor(entity, new Color(1, 0, 0)) - setHelperLayer(entity, ObjectLayers.NodeHelper) - resetHelperTransform(entity) + if (snapState.enabled.value) { + for (const entity of models()) setComponent(entity, ObjectGridSnapComponent) + return () => { + for (const entity of models()) removeComponent(entity, ObjectGridSnapComponent) } } }, [snapState.enabled]) @@ -253,10 +283,6 @@ export const ObjectGridSnapSystem = defineSystem({ } }, [selectionState.selectedEntities]) - useEffect(() => { - for (const entity of models) setComponent(entity, ObjectGridSnapComponent) - }, []) - return null }, execute: () => { @@ -264,10 +290,20 @@ export const ObjectGridSnapSystem = defineSystem({ if (!engineState.isEditing) return const snapState = getState(ObjectGridSnapState) if (!snapState.enabled) return + + for (const entity of models.enter()) { + setComponent(entity, ObjectGridSnapComponent) + } + + for (const entity of models.exit()) { + removeComponent(entity, ObjectGridSnapComponent) + } + const entities = objectGridQuery() const selectedEntities: Entity[] = [] const selectedParents: Entity[] = [] const nonSelectedEntities: Entity[] = [] + for (const entity of entities) { const parent = isParentSelected(entity) if (parent) { @@ -277,15 +313,21 @@ export const ObjectGridSnapSystem = defineSystem({ setHelperColor(entity, new Color(1, 1, 1)) } else { nonSelectedEntities.push(entity) + resetHelperTransform(entity) setHelperLayer(entity, ObjectLayers.NodeHelper) setHelperColor(entity, new Color(1, 0, 0)) } } + + getMutableState(ObjectGridSnapState).merge({ + selectedEntities: selectedEntities, + selectedParents: selectedParents + }) + if (selectedEntities.length === 0) return for (let i = 0; i < selectedEntities.length; i++) { const selectedEntity = selectedEntities[i] const selectedParent = selectedParents[i] - const selectedBBox = getComponent(selectedEntity, ObjectGridSnapComponent).bbox const selectedMatrixWorld = getComponent(selectedEntity, TransformComponent).matrixWorld const closestEntities: Entity[] = [] @@ -300,25 +342,22 @@ export const ObjectGridSnapSystem = defineSystem({ } } - const helperEntity = getComponent(selectedEntity, BoundingBoxHelperComponent).entity + const helperEntity = getOptionalComponent(selectedEntity, BoundingBoxHelperComponent)?.entity + const commitNoOp = () => { - if (helperEntity) { - //reset helper bbox if exists - setComponent(helperEntity, TransformComponent, { - position: new Vector3(), - rotation: new Quaternion().identity(), - scale: new Vector3(1, 1, 1) - }) - } - if (getState(ObjectGridSnapState).apply) { - EditorControlFunctions.commitTransformSave([selectedParent]) - getMutableState(ObjectGridSnapState).apply.set(false) - } + resetHelperTransform(selectedEntity) } + if (closestEntities.length === 0) { commitNoOp() continue } + + if (!helperEntity) { + commitNoOp() + continue + } + let leastOffset = Infinity let closestEntity = UndefinedEntity for (const candidateEntity of closestEntities) { @@ -348,9 +387,8 @@ export const ObjectGridSnapSystem = defineSystem({ if (closestEntity === UndefinedEntity) { commitNoOp() continue - } else { - setHelperColor(closestEntity, new Color(0, 1, 0)) } + setHelperColor(closestEntity, new Color(0, 1, 0)) const closestBBox = getComponent(closestEntity, ObjectGridSnapComponent).bbox const closestMatrixWorld = getComponent(closestEntity, TransformComponent).matrixWorld const parentMatrixWorld = getComponent(selectedParent, TransformComponent).matrixWorld @@ -359,16 +397,12 @@ export const ObjectGridSnapSystem = defineSystem({ const position = new Vector3() const scale = new Vector3() srcMatrixWorld.decompose(position, new Quaternion(), scale) - const dstEntity = getState(ObjectGridSnapState).apply ? selectedParent : helperEntity - if (!dstEntity) { - commitNoOp() - continue - } - const dstMatrixWorld = getComponent(dstEntity, TransformComponent).matrixWorld + + const dstMatrixWorld = getComponent(helperEntity, TransformComponent).matrixWorld dstMatrixWorld.extractRotation(rotationMatrix) dstMatrixWorld.scale(scale) dstMatrixWorld.setPosition(position) - TransformComponent.updateFromWorldMatrix(dstEntity) + TransformComponent.updateFromWorldMatrix(helperEntity) const translation = boundedTranslation( selectedBBox, closestBBox, @@ -376,12 +410,7 @@ export const ObjectGridSnapSystem = defineSystem({ dstMatrixWorld.clone().invert().multiply(closestMatrixWorld) ) dstMatrixWorld.multiply(new Matrix4().makeTranslation(translation)) - TransformComponent.updateFromWorldMatrix(dstEntity) - if (getState(ObjectGridSnapState).apply) { - EditorControlFunctions.commitTransformSave([dstEntity]) - getMutableState(ObjectGridSnapState).apply.set(false) - } - break + TransformComponent.updateFromWorldMatrix(helperEntity) } } }) diff --git a/packages/engine/src/assets/functions/createGLTFLoader.ts b/packages/engine/src/assets/functions/createGLTFLoader.ts index 6d7a540858..b7267c3355 100644 --- a/packages/engine/src/assets/functions/createGLTFLoader.ts +++ b/packages/engine/src/assets/functions/createGLTFLoader.ts @@ -56,6 +56,7 @@ export const initializeKTX2Loader = (loader: GLTFLoader) => { export const createGLTFLoader = (keepMaterials = false) => { const loader = new GLTFLoader() + if (isClient) initializeKTX2Loader(loader) if (isClient || keepMaterials) { loader.register((parser) => new GPUInstancingExtension(parser)) diff --git a/packages/engine/src/scene/components/ObjectGridSnapComponent.ts b/packages/engine/src/scene/components/ObjectGridSnapComponent.ts index 3a3433bfb9..b4a1d15acb 100644 --- a/packages/engine/src/scene/components/ObjectGridSnapComponent.ts +++ b/packages/engine/src/scene/components/ObjectGridSnapComponent.ts @@ -23,9 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { useEffect } from 'react' -import { Box3, BufferGeometry, ColorRepresentation, LineBasicMaterial, Matrix4, Mesh, Quaternion, Vector3 } from 'three' - import { useDidMount } from '@etherealengine/common/src/utils/useDidMount' import { defineComponent, @@ -39,16 +36,18 @@ import { import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { getMutableState, useState } from '@etherealengine/hyperflux' +import { EngineState } from '@etherealengine/spatial/src/EngineState' import { useHelperEntity } from '@etherealengine/spatial/src/common/debug/DebugComponentUtils' import { matchesColor } from '@etherealengine/spatial/src/common/functions/MatchesUtils' -import { EngineState } from '@etherealengine/spatial/src/EngineState' import { LineSegmentComponent } from '@etherealengine/spatial/src/renderer/components/LineSegmentComponent' import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' -import { ObjectLayerMaskComponent } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' import { ObjectLayerMasks } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' import { EntityTreeComponent, iterateEntityNode } from '@etherealengine/spatial/src/transform/components/EntityTree' import { TransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' import { computeTransformMatrix } from '@etherealengine/spatial/src/transform/systems/TransformSystem' +import { useEffect } from 'react' +import { Box3, BufferGeometry, ColorRepresentation, LineBasicMaterial, Matrix4, Mesh, Quaternion, Vector3 } from 'three' +import { ModelComponent } from './ModelComponent' function createBBoxGridGeometry(matrixWorld: Matrix4, bbox: Box3, density: number): BufferGeometry { const lineSegmentList: Vector3[] = [] @@ -138,15 +137,11 @@ export const BoundingBoxHelperComponent = defineComponent({ const bbox = component.bbox.value const density = component.density.value setComponent(helper, LineSegmentComponent, { - name: 'bbox-line-segment', + name: 'bbox-line-segment-' + entity, geometry: createBBoxGridGeometry(new Matrix4().identity(), bbox, density), material: new LineBasicMaterial({ color: component.color.value }), layerMask: component.layerMask.value }) - - return () => { - removeComponent(helper, LineSegmentComponent) - } }, []) useDidMount(() => { @@ -162,8 +157,9 @@ export const BoundingBoxHelperComponent = defineComponent({ }, [component.color, lineSegment]) useEffect(() => { - setComponent(helper, ObjectLayerMaskComponent, component.layerMask.value) - }, [component.layerMask]) + if (!lineSegment) return + lineSegment.layerMask.set(component.layerMask.value) + }, [component.layerMask, lineSegment]) return null } @@ -187,9 +183,11 @@ export const ObjectGridSnapComponent = defineComponent({ reactor: () => { const entity = useEntityContext() const engineState = useState(getMutableState(EngineState)) + const modelComponent = useComponent(entity, ModelComponent) const snapComponent = useComponent(entity, ObjectGridSnapComponent) useEffect(() => { + if (!modelComponent.scene.value) return const originalPosition = new Vector3() const originalRotation = new Quaternion() const originalScale = new Vector3() @@ -203,9 +201,11 @@ export const ObjectGridSnapComponent = defineComponent({ const meshes: Mesh[] = [] //iterate through children and update their transforms to reflect identity from parent iterateEntityNode(entity, (childEntity: Entity) => { - computeTransformMatrix(childEntity) - if (hasComponent(childEntity, MeshComponent)) { - meshes.push(getComponent(childEntity, MeshComponent)) + if (hasComponent(childEntity, TransformComponent)) { + computeTransformMatrix(childEntity) + if (hasComponent(childEntity, MeshComponent)) { + meshes.push(getComponent(childEntity, MeshComponent)) + } } }) //compute bounding box @@ -224,17 +224,19 @@ export const ObjectGridSnapComponent = defineComponent({ rotation: originalRotation, scale: originalScale }) - iterateEntityNode(entity, computeTransformMatrix) + iterateEntityNode(entity, computeTransformMatrix, (childEntity) => hasComponent(childEntity, TransformComponent)) //set bounding box in component - setComponent(entity, ObjectGridSnapComponent, { - bbox - }) - }, []) + snapComponent.bbox.set(bbox) + }, [modelComponent.scene]) useEffect(() => { if (!engineState.isEditing.value) return const bbox = snapComponent.bbox.value setComponent(entity, BoundingBoxHelperComponent, { bbox }) + + return () => { + removeComponent(entity, BoundingBoxHelperComponent) + } }, [snapComponent.bbox, engineState.isEditing]) return null diff --git a/packages/engine/src/scene/functions/createSceneEntity.ts b/packages/engine/src/scene/functions/createSceneEntity.ts new file mode 100644 index 0000000000..2ffbf5a650 --- /dev/null +++ b/packages/engine/src/scene/functions/createSceneEntity.ts @@ -0,0 +1,66 @@ +/* +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 { + Entity, + UUIDComponent, + UndefinedEntity, + createEntity, + generateEntityUUID, + getComponent, + setComponent +} from '@etherealengine/ecs' +import { TransformComponent } from '@etherealengine/spatial' +import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' +import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' +import { Object3DComponent } from '@etherealengine/spatial/src/renderer/components/Object3DComponent' +import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' +import { Group } from 'three' +import { proxifyParentChildRelationships } from './loadGLTFModel' + +import { SourceComponent } from '../components/SourceComponent' + +export const createSceneEntity = (name: string, parentEntity: Entity = UndefinedEntity): Entity => { + const entity = createEntity() + setComponent(entity, NameComponent, name) + setComponent(entity, VisibleComponent) + setComponent(entity, TransformComponent) + setComponent(entity, EntityTreeComponent, { parentEntity }) + const sceneID = getComponent(parentEntity, SourceComponent) + setComponent(entity, SourceComponent, sceneID) + + setComponent(entity, UUIDComponent, generateEntityUUID()) + + // These additional properties and relations are required for + // the current GLTF exporter to successfully generate a GLTF. + const obj3d = new Group() + obj3d.entity = entity + addObjectToGroup(entity, obj3d) + proxifyParentChildRelationships(obj3d) + setComponent(entity, Object3DComponent, obj3d) + + return entity +} diff --git a/packages/engine/src/scene/functions/serializeWorld.ts b/packages/engine/src/scene/functions/serializeWorld.ts index a4a2aa648f..3ae4eb959f 100644 --- a/packages/engine/src/scene/functions/serializeWorld.ts +++ b/packages/engine/src/scene/functions/serializeWorld.ts @@ -23,11 +23,19 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { getAllComponents, getOptionalComponent, serializeComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { UUIDComponent } from '@etherealengine/ecs' +import { + getAllComponents, + getComponent, + getOptionalComponent, + hasComponent, + serializeComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' - +import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { GLTFLoadedComponent } from '../components/GLTFLoadedComponent' -import { ComponentJsonType } from '../types/SceneTypes' +import { ComponentJsonType, EntityJsonType } from '../types/SceneTypes' export const serializeEntity = (entity: Entity) => { const ignoreComponents = getOptionalComponent(entity, GLTFLoadedComponent) @@ -50,4 +58,17 @@ export const serializeEntity = (entity: Entity) => { return jsonComponents } +export const toEntityJson = (entity: Entity) => { + const components = serializeEntity(entity) + const result: EntityJsonType = { + components, + name: getOptionalComponent(entity, NameComponent) ?? '' + } + const parent = getOptionalComponent(entity, EntityTreeComponent)?.parentEntity + if (parent && hasComponent(parent, UUIDComponent)) { + result.parent = getComponent(parent, UUIDComponent) + } + return result +} + globalThis.serializeEntity = serializeEntity diff --git a/packages/hyperflux/functions/ReactorFunctions.tsx b/packages/hyperflux/functions/ReactorFunctions.tsx index c893467653..cb8d72ba9b 100644 --- a/packages/hyperflux/functions/ReactorFunctions.tsx +++ b/packages/hyperflux/functions/ReactorFunctions.tsx @@ -97,6 +97,7 @@ export function useReactorRootContext(): ReactorRoot { export const ReactorErrorBoundary = createErrorBoundary<{ children: React.ReactNode; reactorRoot: ReactorRoot }>( function error(props, error?: Error) { if (error) { + console.error(error) props.reactorRoot.errors.merge([error]) return null } else { diff --git a/packages/spatial/src/camera/systems/CameraFadeBlackEffectSystem.tsx b/packages/spatial/src/camera/systems/CameraFadeBlackEffectSystem.tsx index 9b9a5cf7ed..f8418350f5 100644 --- a/packages/spatial/src/camera/systems/CameraFadeBlackEffectSystem.tsx +++ b/packages/spatial/src/camera/systems/CameraFadeBlackEffectSystem.tsx @@ -105,7 +105,7 @@ const Reactor = () => { setComponent(entity, NameComponent, mesh.name) addObjectToGroup(entity, mesh) mesh.renderOrder = 1 - setObjectLayers(mesh, ObjectLayers.Scene) + setObjectLayers(mesh, ObjectLayers.Camera) const transition = createTransitionState(0.25, 'OUT') getMutableState(CameraFadeBlackEffectSystemState).set({ diff --git a/packages/spatial/src/renderer/materials/prototypes/HolographicMaterial.mat.ts b/packages/spatial/src/renderer/materials/prototypes/HolographicMaterial.mat.ts new file mode 100644 index 0000000000..9fbc0789dd --- /dev/null +++ b/packages/spatial/src/renderer/materials/prototypes/HolographicMaterial.mat.ts @@ -0,0 +1,280 @@ +/* +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 { Color, Material, MeshStandardMaterial, MeshStandardMaterialParameters, Uniform } from 'three' + +import { createEntity, setComponent } from '@etherealengine/ecs' +import { UpdatableCallback, UpdatableComponent } from '@etherealengine/engine/src/scene/components/UpdatableComponent' +import { SourceType } from '@etherealengine/engine/src/scene/materials/components/MaterialSource' +import { setCallback } from '@etherealengine/spatial/src/common/CallbackComponent' +import { addOBCPlugin } from '@etherealengine/spatial/src/common/functions/OnBeforeCompilePlugin' +import { MaterialPrototypeDefinition } from '../MaterialComponent' +import { BasicArgs } from '../constants/BasicArgs' +import { BoolArg, ColorArg, FloatArg } from '../constants/DefaultArgs' + +export type HolographicMaterialParameters = MeshStandardMaterialParameters & { + speed?: number | undefined + time?: number | undefined + useBlink?: boolean + mix_intensity?: number | undefined + hologramBrightness?: number | undefined + scanlineSize?: number | undefined + hologramOpacity?: number | undefined +} + +export const DefaultArgs = { + ...BasicArgs, + + speed: { + ...FloatArg, + default: 0.1 + }, + scanlineSize: { + ...FloatArg, + default: 15.0 + }, + hologramColor: { + ...ColorArg, + default: new Color(1, 1, 1) + }, + time: FloatArg, + useBlink: BoolArg, + mix_intensity: { + ...FloatArg, + default: 1.0 + }, + hologramBrightness: { + ...FloatArg, + default: 0.5 + }, + hologramOpacity: { + ...FloatArg, + default: 0.5 + } +} + +export class HolographicMaterial extends MeshStandardMaterial { + _uniforms: Record + constructor(args: HolographicMaterialParameters) { + const basicParms: MeshStandardMaterialParameters = Object.fromEntries( + Object.keys(args ?? {}) + .filter((k) => Object.hasOwn(BasicArgs, k)) + .map((k) => [k, args[k]]) + ) + super(basicParms) + const uniform = (k: keyof HolographicMaterialParameters) => new Uniform(args?.[k] ?? DefaultArgs[k].default) + this._uniforms = Object.fromEntries( + [ + 'speed', + 'time', + 'useBlink', + 'mix_intensity', + 'hologramColor', + 'hologramBrightness', + 'scanlineSize', + 'hologramOpacity' + ].map((k: keyof HolographicMaterialParameters) => [k, uniform(k)]) + ) + addOBCPlugin(this as Material, (shader, renderer) => { + Object.entries(this._uniforms).map(([k, v]) => (shader.uniforms[k] = v)) + + shader.vertexShader = + ` + varying vec4 vPos; + varying vec2 myuv; + varying vec3 vPositionNormal; + varying vec3 v_Normal; + ` + shader.vertexShader + + shader.vertexShader = shader.vertexShader.replace( + 'void main() {', + ` + + void main() { + //vec3 transformed = vec3(position); + vPos = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + myuv = uv; + vPositionNormal = normalize(( modelViewMatrix * vec4(position, 1.0) ).xyz); + v_Normal = normalize( normalMatrix * normal ); + ` + ) + + shader.fragmentShader = shader.fragmentShader.replace( + 'void main() {', + ` + uniform float speed; + varying vec3 vPositionNormal; + varying vec3 v_Normal; + uniform float time; + uniform float mix_intensity; + uniform bool useBlink; + uniform vec3 hologramColor; + uniform float hologramBrightness; + uniform float scanlineSize; + uniform float hologramOpacity; + varying vec4 vPos; + varying vec2 myuv; + float flicker( float amt, float time ) {return clamp( fract( cos( time ) * 4358.5453123 ), amt, 1.0 );} + float random(in float a, in float b) { return fract((cos(dot(vec2(a,b) ,vec2(12.9898,78.233))) * 43758.5453)); } + + + + void main() { + + + ` + ) + + const colorFragment = ` + #include + vec2 vCoords = vPos.xy; + vCoords /= vPos.w; + vCoords = vCoords * 0.5 + 0.5; + vec2 myUV = fract( vCoords ); + + // // Defines hologram main color + vec4 hologramColor = vec4(hologramColor, mix(hologramBrightness, myuv.y, 0.5)); + + // // Add scanlines + float scanlines = 10.; + scanlines += 20. * sin(time *speed * 20.8 - myUV.y * 60. * scanlineSize); + scanlines *= smoothstep(1.3 * cos(time *speed + myUV.y * scanlineSize), 0.78, 0.9); + scanlines *= max(0.25, sin(time *speed) * 1.0); + + // // Scanlines offsets + float r = random(myuv.x, myuv.y); + float g = random(myuv.y * 20.2, myuv.y * .2); + float b = random(myuv.y * .9, myuv.y * .2); + + // // Scanline composition + hologramColor += vec4(r*scanlines, b*scanlines, r, 1.0) / 84.; + vec4 scanlineMix = mix(vec4(0.0), hologramColor, hologramColor.a); + + // // Calculates fresnel + float fresnel=pow( (1. + -1. * abs(dot(v_Normal, vPositionNormal)))*2.2, 2.0 ); + + // // Blinkin effect + float blinkValue = 1.0 - speed; + float blink = flicker(blinkValue, time * speed * .002); + + + vec4 initial_diffuse=diffuseColor; + if(useBlink){ + diffuseColor = mix( initial_diffuse,vec4( scanlineMix.rgb+fresnel*blink, 1.0),mix_intensity);} + else{ + diffuseColor = mix( initial_diffuse,vec4( scanlineMix.rgb+fresnel, 1.0),mix_intensity); + } + ` + shader.fragmentShader = shader.fragmentShader.replace('#include ', colorFragment) + + const alphamapFragment = ` + #include + + + diffuseColor.a = hologramOpacity; + + ` + shader.fragmentShader = shader.fragmentShader.replace('#include ', alphamapFragment) + }) + + //@ts-ignore + this.needsUpdate = true + //@ts-ignore + this.userData.type = 'HolographicMaterial' + + // Create an entity with an UpdatableComponent to increment the time uniform + const updater = createEntity() + setCallback(updater, UpdatableCallback, (dt) => { + this._uniforms.time.value += dt + }) + setComponent(updater, UpdatableComponent, true) + } + + get speed() { + return this._uniforms.speed.value + } + set speed(v) { + this._uniforms.speed.value = v + } + + get time() { + return this._uniforms.time.value + } + set time(v) { + this._uniforms.time.value = v + } + get hologramColor() { + return this._uniforms.hologramColor.value + } + set hologramColor(v) { + this._uniforms.hologramColor.value = v + } + get hologramBrightness() { + return this._uniforms.hologramBrightness.value + } + set hologramBrightness(v) { + this._uniforms.hologramBrightness.value = v + } + get hologramOpacity() { + return this._uniforms.hologramOpacity.value + } + set hologramOpacity(v) { + this._uniforms.hologramOpacity.value = v + } + get scanlineSize() { + return this._uniforms.scanlineSize.value + } + set scanlineSize(v) { + this._uniforms.scanlineSize.value = v + } + get mix_intensity() { + return this._uniforms.mix_intensity.value + } + set mix_intensity(v) { + this._uniforms.mix_intensity.value = v + } + get useBlink() { + return this._uniforms.useBlink.value + } + set useBlink(v) { + this._uniforms.useBlink.value = v + } + + clone() { + const result = super.clone() + result._uniforms = Object.fromEntries( + Object.keys(this._uniforms).map((k) => [k, new Uniform(this._uniforms[k].value)]) + ) + return result + } +} + +export const HolographicMaterialPrototype: MaterialPrototypeDefinition = { + prototypeId: 'HolographicMaterial', + // @ts-ignore + baseMaterial: HolographicMaterial as MeshStandardMaterial, + arguments: DefaultArgs, + src: { type: SourceType.PROJECT, path: 'eepro-advanced-materials' } +} diff --git a/packages/ui/src/components/editor/panels/Files/container/index.tsx b/packages/ui/src/components/editor/panels/Files/container/index.tsx index 7873054b4e..6a9e750286 100644 --- a/packages/ui/src/components/editor/panels/Files/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Files/container/index.tsx @@ -53,9 +53,11 @@ import { DndWrapper } from '@etherealengine/editor/src/components/dnd/DndWrapper import { SupportedFileTypes } from '@etherealengine/editor/src/constants/AssetTypes' import { downloadBlobAsZip, inputFileWithAddToScene } from '@etherealengine/editor/src/functions/assetFunctions' import { bytesToSize, unique } from '@etherealengine/editor/src/functions/utils' +import { EditorHelperState, PlacementMode } from '@etherealengine/editor/src/services/EditorHelperState' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' +import { ClickPlacementState } from '@etherealengine/editor/src/systems/ClickPlacementSystem' import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' -import { getMutableState, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { getMutableState, getState, useHookstate, useMutableState } from '@etherealengine/hyperflux' import { useFind, useMutation, useSearch } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import React, { Fragment, useEffect, useRef } from 'react' import { useDrop } from 'react-dnd' @@ -248,6 +250,10 @@ const FileBrowserContentPanel: React.FC = (props) contentType: params.type, size: params.size }) + const editorHelperState = getState(EditorHelperState) + if (editorHelperState.placementMode === PlacementMode.CLICK) { + getMutableState(ClickPlacementState).selectedAsset.set(params.url) + } } else { const newPath = `${selectedDirectory.value}${params.name}/` changeDirectoryByPath(newPath) 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 b9f9437526..1370b63663 100644 --- a/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Hierarchy/container/index.tsx @@ -55,6 +55,7 @@ import { CopyPasteFunctions } from '@etherealengine/editor/src/functions/CopyPas import { EditorControlFunctions } from '@etherealengine/editor/src/functions/EditorControlFunctions' import { addMediaNode } from '@etherealengine/editor/src/functions/addMediaNode' import { cmdOrCtrlString } from '@etherealengine/editor/src/functions/utils' +import { EditorHelperState, PlacementMode } from '@etherealengine/editor/src/services/EditorHelperState' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { SelectionState } from '@etherealengine/editor/src/services/SelectionServices' import { GLTFAssetState, GLTFSnapshotState } from '@etherealengine/engine/src/gltf/GLTFState' @@ -185,6 +186,8 @@ function HierarchyPanelContents(props: { sceneURL: string; rootEntityUUID: Entit const onClick = useCallback( (e: MouseEvent, node: HeirarchyTreeNodeType) => { if (e.detail === 1) { + // Exit click placement mode when anything in the hierarchy is selected + getMutableState(EditorHelperState).placementMode.set(PlacementMode.DRAG) if (e.ctrlKey) { EditorControlFunctions.toggleSelection([getComponent(node.entity, UUIDComponent)]) setSelectedNode(null) diff --git a/packages/ui/src/components/editor/panels/Viewport/tools/TransformSnapTool.tsx b/packages/ui/src/components/editor/panels/Viewport/tools/TransformSnapTool.tsx index b8af4fe35d..6f05d34f99 100644 --- a/packages/ui/src/components/editor/panels/Viewport/tools/TransformSnapTool.tsx +++ b/packages/ui/src/components/editor/panels/Viewport/tools/TransformSnapTool.tsx @@ -29,10 +29,10 @@ import { SnapMode } from '@etherealengine/engine/src/scene/constants/transformCo import { getMutableState, useHookstate } from '@etherealengine/hyperflux' import { toggleSnapMode } from '@etherealengine/editor/src/functions/transformFunctions' -import { EditorHelperState } from '@etherealengine/editor/src/services/EditorHelperState' +import { EditorHelperState, PlacementMode } from '@etherealengine/editor/src/services/EditorHelperState' import { ObjectGridSnapState } from '@etherealengine/editor/src/systems/ObjectGridSnapSystem' import { useTranslation } from 'react-i18next' -import { LuUtilityPole } from 'react-icons/lu' +import { LuMousePointerClick, LuMove3D, LuUtilityPole } from 'react-icons/lu' import { MdOutlineCenterFocusWeak } from 'react-icons/md' import Button from '../../../../../primitives/tailwind/Button' import Select from '../../../../../primitives/tailwind/Select' @@ -116,6 +116,20 @@ const TransformSnapTool = () => { currentValue={editorHelperState.rotationSnap.value} /> {/* */} +