diff --git a/packages/client-core/i18n/en/admin.json b/packages/client-core/i18n/en/admin.json
index 98fef2a4d5..d40aa031d7 100755
--- a/packages/client-core/i18n/en/admin.json
+++ b/packages/client-core/i18n/en/admin.json
@@ -86,7 +86,9 @@
"lastUpdatedBy": "Last updated by user id: {{userId}} on {{updatedAt}}",
"fillRequiredFields": "Please fill all required field",
"fixErrorFields": "Please fix all errors",
- "logOut": "Log Out"
+ "logOut": "Log Out",
+ "newestFirst": "Newest First",
+ "oldestFirst": "Oldest First"
},
"analytics": {
"loading": "Loading analytics...",
@@ -214,8 +216,10 @@
"repo": "Repo",
"access": "Access",
"invalidateCache": "Invalidate Cache",
- "update": "Update"
+ "update": "Update",
+ "history": "History"
},
+ "projectHistory": "Project History",
"addProject": "Add Project",
"updateProject": "Update Project",
"downloadProject": "Download Project",
diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json
index 9544c2e0c7..cbd6e57567 100755
--- a/packages/client-core/i18n/en/editor.json
+++ b/packages/client-core/i18n/en/editor.json
@@ -568,7 +568,7 @@
},
"hemisphere": {
"name": "Hemisphere Light",
- "description": "A light which illuminates the scene from directly overhead.",
+ "description": "A light which illuminates the scene with a sky color from above and a ground color from below.",
"lbl-skyColor": "Sky Color",
"lbl-groundColor": "Ground Color",
"lbl-intensity": "Intensity"
@@ -1177,7 +1177,7 @@
"point-light": "A light which emits in all directions from a single point.",
"spot-light": "Creates a light that shines in a specific direction.",
"directional-light": "Creates a light that emits evenly in a single direction.",
- "hemisphere-light": "A light which illuminates the scene from directly overhead.",
+ "hemisphere-light": "A light which illuminates the scene with a sky color from above and a ground color from below.",
"particle-system": "Creates a particle emitter.",
"system": "Inserts code into the scene by creating a new Entity Component System based on the provided .ts file",
"visual-script": "Customizes state and behavior of entities through a visual node connections.",
@@ -1217,6 +1217,7 @@
"uploadFiles": "Upload Files",
"uploadFolder": "Upload Folder",
"uploadingFiles": "Uploading Files ({{completed}}/{{total}})",
+ "downloadingProject": "Downloading Project ({{completed}}/{{total}})",
"search-placeholder": "Search",
"generatingThumbnails": "Generating Thumbnails ({{count}} remaining)",
"file": "File",
@@ -1360,7 +1361,10 @@
"lbl-thumbnail": "Generate thumbnail & envmap",
"lbl-confirm": "Save Scene",
"info-confirm": "Are you sure you want to save the scene?",
- "info-question": "Do you want to save the current scene?"
+ "info-question": "Do you want to save the current scene?",
+ "unsavedChanges": {
+ "title": "Unsaved Changes"
+ }
},
"saveNewScene": {
"title": "Save As",
diff --git a/packages/client-core/src/admin/components/project/ProjectHistory.tsx b/packages/client-core/src/admin/components/project/ProjectHistory.tsx
new file mode 100644
index 0000000000..0dc57f958f
--- /dev/null
+++ b/packages/client-core/src/admin/components/project/ProjectHistory.tsx
@@ -0,0 +1,302 @@
+/*
+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 { projectHistoryPath } from '@etherealengine/common/src/schema.type.module'
+import { ProjectHistoryType } from '@etherealengine/common/src/schemas/projects/project-history.schema'
+import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
+
+import { toDisplayDateTime } from '@etherealengine/common/src/utils/datetime-sql'
+import AvatarImage from '@etherealengine/ui/src/primitives/tailwind/AvatarImage'
+import Button from '@etherealengine/ui/src/primitives/tailwind/Button'
+import { TablePagination } from '@etherealengine/ui/src/primitives/tailwind/Table'
+import Text from '@etherealengine/ui/src/primitives/tailwind/Text'
+import Tooltip from '@etherealengine/ui/src/primitives/tailwind/Tooltip'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { FaSortAmountDown, FaSortAmountUpAlt } from 'react-icons/fa'
+
+const PROJECT_HISTORY_PAGE_LIMIT = 10
+
+const getRelativeURLFromProject = (projectName: string, url: string) => {
+ const prefix = `projects/${projectName}/`
+ if (url.startsWith(prefix)) {
+ return url.replace(prefix, '')
+ }
+ return url
+}
+
+const getResourceURL = (projectName: string, url: string, resourceType: 'resource' | 'scene') => {
+ const relativeURL = getRelativeURLFromProject(projectName, url)
+ const resourceURL =
+ resourceType === 'resource'
+ ? `/projects/${projectName}/${relativeURL}`
+ : `/studio?project=${projectName}&scenePath=${url}`
+ return {
+ relativeURL,
+ resourceURL
+ }
+}
+
+function HistoryLog({ projectHistory, projectName }: { projectHistory: ProjectHistoryType; projectName: string }) {
+ const { t } = useTranslation()
+
+ const RenderAction = () => {
+ if (projectHistory.action === 'LOCATION_PUBLISHED' || projectHistory.action === 'LOCATION_UNPUBLISHED') {
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ locationName: string
+ sceneURL: string
+ sceneId: string
+ }
+
+ const verb = projectHistory.action === 'LOCATION_PUBLISHED' ? 'published' : 'unpublished'
+
+ const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.sceneURL, 'scene')
+
+ return (
+ <>
+ {verb} the location
+
+ {verb === 'published' ? (
+
+
+ {actionDetail.locationName}
+
+
+ ) : (
+
+ {actionDetail.locationName}
+
+ )}
+
+ from the scene
+
+
+ {relativeURL}.
+
+ >
+ )
+ } else if (projectHistory.action === 'LOCATION_MODIFIED') {
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ locationName: string
+ }
+
+ return (
+ <>
+ modified the location
+
+
+
+ {actionDetail.locationName}
+
+
+ >
+ )
+ } else if (projectHistory.action === 'PERMISSION_CREATED' || projectHistory.action === 'PERMISSION_REMOVED') {
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ userName: string
+ userId: string
+ permissionType: string
+ }
+
+ const verb = projectHistory.action === 'PERMISSION_CREATED' ? 'added' : 'removed'
+
+ return (
+ <>
+ {verb} the
+ {actionDetail.permissionType}
+
+ access to
+
+
+ {actionDetail.userName}
+
+ >
+ )
+ } else if (projectHistory.action === 'PERMISSION_MODIFIED') {
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ userName: string
+ userId: string
+ oldPermissionType: string
+ newPermissionType: string
+ }
+
+ return (
+ <>
+ updated the permission of the user
+
+ {actionDetail.userName}
+
+ from
+ {actionDetail.oldPermissionType}
+ to
+ {actionDetail.newPermissionType}
+ >
+ )
+ } else if (projectHistory.action === 'PROJECT_CREATED') {
+ return created the project
+ } else if (
+ projectHistory.action === 'RESOURCE_CREATED' ||
+ projectHistory.action === 'RESOURCE_REMOVED' ||
+ projectHistory.action === 'SCENE_CREATED' ||
+ projectHistory.action === 'SCENE_REMOVED'
+ ) {
+ const verb =
+ projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'SCENE_CREATED'
+ ? 'created'
+ : 'removed'
+ const object =
+ projectHistory.action === 'RESOURCE_CREATED' || projectHistory.action === 'RESOURCE_REMOVED'
+ ? 'resource'
+ : 'scene'
+
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ url: string
+ }
+
+ const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object)
+
+ return (
+ <>
+
+ {verb} the {object}
+
+
+ {relativeURL}
+
+ >
+ )
+ } else if (projectHistory.action === 'RESOURCE_RENAMED' || projectHistory.action === 'SCENE_RENAMED') {
+ const object = projectHistory.action === 'RESOURCE_RENAMED' ? 'resource' : 'scene'
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ oldURL: string
+ newURL: string
+ }
+
+ const { relativeURL: oldRelativeURL } = getResourceURL(projectName, actionDetail.oldURL, object)
+ const { relativeURL: newRelativeURL, resourceURL: newResourceURL } = getResourceURL(
+ projectName,
+ actionDetail.newURL,
+ object
+ )
+
+ return (
+ <>
+ renamed a {object} from
+
+ {oldRelativeURL}
+ to
+
+ {getRelativeURLFromProject(projectName, newRelativeURL)}
+
+ >
+ )
+ } else if (projectHistory.action === 'RESOURCE_MODIFIED' || projectHistory.action === 'SCENE_MODIFIED') {
+ const object = projectHistory.action === 'RESOURCE_MODIFIED' ? 'resource' : 'scene'
+ const actionDetail = JSON.parse(projectHistory.actionDetail) as {
+ url: string
+ }
+
+ const { relativeURL, resourceURL } = getResourceURL(projectName, actionDetail.url, object)
+
+ return (
+ <>
+ modified the {object}
+
+ {relativeURL}
+
+ >
+ )
+ }
+
+ return null
+ }
+
+ return (
+
+
+
+
+
{projectHistory.userName}
+
+
+
+
+
{toDisplayDateTime(projectHistory.createdAt)}
+
+ )
+}
+
+export const ProjectHistory = ({ projectId, projectName }: { projectId: string; projectName: string }) => {
+ const { t } = useTranslation()
+ const projectHistoryQuery = useFind(projectHistoryPath, {
+ query: {
+ projectId: projectId,
+ $sort: {
+ createdAt: -1
+ },
+ $limit: PROJECT_HISTORY_PAGE_LIMIT
+ }
+ })
+
+ const sortOrder = projectHistoryQuery.sort.createdAt
+
+ const toggleSortOrder = () => {
+ projectHistoryQuery.setSort({
+ createdAt: sortOrder === -1 ? 1 : -1
+ })
+ }
+
+ return (
+
+
:
}
+ >
+ {sortOrder === -1 ? t('admin:components.common.newestFirst') : t('admin:components.common.oldestFirst')}
+
+
+ {projectHistoryQuery.data &&
+ projectHistoryQuery.data.map((projectHistory, index) => (
+
+ ))}
+
+
projectHistoryQuery.setPage(newPage)}
+ />
+
+ )
+}
diff --git a/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx b/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx
new file mode 100644
index 0000000000..033242d548
--- /dev/null
+++ b/packages/client-core/src/admin/components/project/ProjectHistoryModal.tsx
@@ -0,0 +1,45 @@
+/*
+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 Modal from '@etherealengine/ui/src/primitives/tailwind/Modal'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { PopoverState } from '../../../common/services/PopoverState'
+import { ProjectHistory } from './ProjectHistory'
+
+export const ProjectHistoryModal = ({ projectId, projectName }: { projectId: string; projectName: string }) => {
+ const { t } = useTranslation()
+ return (
+ {
+ PopoverState.hidePopupover()
+ }}
+ >
+
+
+ )
+}
diff --git a/packages/client-core/src/admin/components/project/ProjectTable.tsx b/packages/client-core/src/admin/components/project/ProjectTable.tsx
index a664aeff37..f27d9e058b 100644
--- a/packages/client-core/src/admin/components/project/ProjectTable.tsx
+++ b/packages/client-core/src/admin/components/project/ProjectTable.tsx
@@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next'
import { GrGithub } from 'react-icons/gr'
import {
HiOutlineArrowPath,
+ HiOutlineClock,
HiOutlineCommandLine,
HiOutlineExclamationCircle,
HiOutlineFolder,
@@ -55,6 +56,7 @@ import { ProjectRowType, projectsColumns } from '../../common/constants/project'
import { ProjectUpdateState } from '../../services/ProjectUpdateService'
import AddEditProjectModal from './AddEditProjectModal'
import ManageUserPermissionModal from './ManageUserPermissionModal'
+import { ProjectHistoryModal } from './ProjectHistoryModal'
const logger = multiLogger.child({ component: 'client-core:ProjectTable' })
@@ -186,6 +188,16 @@ export default function ProjectTable(props: { search: string }) {
>
{t('admin:components.common.view')}
+ }
+ size="small"
+ className="mr-2 h-min whitespace-pre bg-theme-blue-secondary text-[#214AA6] disabled:opacity-50 dark:text-white"
+ onClick={() => {
+ PopoverState.showPopupover()
+ }}
+ >
+ {t('admin:components.project.actions.history')}
+
}
size="small"
diff --git a/packages/client-core/src/admin/components/scene/SceneItem.tsx b/packages/client-core/src/admin/components/scene/SceneItem.tsx
index 131568e135..d100b0970a 100644
--- a/packages/client-core/src/admin/components/scene/SceneItem.tsx
+++ b/packages/client-core/src/admin/components/scene/SceneItem.tsx
@@ -43,11 +43,18 @@ import { twMerge } from 'tailwind-merge'
type SceneItemProps = {
scene: StaticResourceType
updateEditorState?: boolean
+ moveMenuUp?: boolean
handleOpenScene: () => void
refetchProjectsData: () => void
}
-export const SceneItem = ({ scene, updateEditorState, handleOpenScene, refetchProjectsData }: SceneItemProps) => {
+export const SceneItem = ({
+ scene,
+ updateEditorState,
+ moveMenuUp,
+ handleOpenScene,
+ refetchProjectsData
+}: SceneItemProps) => {
const { t } = useTranslation()
const editorState = useMutableState(EditorState)
@@ -118,7 +125,8 @@ export const SceneItem = ({ scene, updateEditorState, handleOpenScene, refetchPr
{
if (!isClient) return null
const { sceneEntity } = props
- const gltfLoaded = useComponent(sceneEntity, GLTFComponent).progress.value === 100
+ const gltfLoaded = GLTFComponent.useSceneLoaded(sceneEntity)
const searchParams = useMutableState(SearchParamState)
const spawnAvatar = useHookstate(false)
diff --git a/packages/client-core/src/social/services/LocationService.ts b/packages/client-core/src/social/services/LocationService.ts
index 46b1ca2662..689fd5c084 100755
--- a/packages/client-core/src/social/services/LocationService.ts
+++ b/packages/client-core/src/social/services/LocationService.ts
@@ -65,6 +65,7 @@ export const LocationSeed: LocationType = {
},
locationAuthorizedUsers: [],
locationBans: [],
+ updatedBy: '' as UserID,
createdAt: '',
updatedAt: ''
}
diff --git a/packages/client-core/src/systems/LoadingUISystem.tsx b/packages/client-core/src/systems/LoadingUISystem.tsx
index d306f00d61..e44b4442f9 100755
--- a/packages/client-core/src/systems/LoadingUISystem.tsx
+++ b/packages/client-core/src/systems/LoadingUISystem.tsx
@@ -140,9 +140,9 @@ export const LoadingUISystemState = defineState({
const LoadingReactor = (props: { sceneEntity: Entity }) => {
const { sceneEntity } = props
- const gltfComponent = useComponent(props.sceneEntity, GLTFComponent)
+ const gltfComponent = useComponent(sceneEntity, GLTFComponent)
const loadingProgress = gltfComponent.progress.value
- const sceneLoaded = loadingProgress === 100
+ const sceneLoaded = GLTFComponent.useSceneLoaded(sceneEntity)
const locationState = useMutableState(LocationState)
const state = useMutableState(LoadingUISystemState)
diff --git a/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx b/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx
index 4c07cec7ce..b0f78dad6c 100755
--- a/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx
+++ b/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx
@@ -30,7 +30,6 @@ import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'react-router-dom'
import Avatar from '@etherealengine/client-core/src/common/components/Avatar'
-import Button from '@etherealengine/client-core/src/common/components/Button'
import commonStyles from '@etherealengine/client-core/src/common/components/common.module.scss'
import ConfirmDialog from '@etherealengine/client-core/src/common/components/ConfirmDialog'
import { AppleIcon } from '@etherealengine/client-core/src/common/components/Icons/AppleIcon'
@@ -322,51 +321,51 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => {
// console.log('VC Request query result:', result)
}
- async function handleWalletLoginClick() {
- const domain = window.location.origin
- const challenge = '99612b24-63d9-11ea-b99f-4f66f3e4f81a' // TODO: generate
-
- console.log('Sending DIDAuth query...')
-
- const didAuthQuery: any = {
- web: {
- VerifiablePresentation: {
- query: [
- {
- type: 'DIDAuth' // request the controller's DID
- },
- {
- type: 'QueryByExample',
- credentialQuery: [
- {
- example: {
- '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/xr/v1'],
- // contains username and avatar icon
- type: 'LoginDisplayCredential'
- }
- },
- {
- example: {
- '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/xr/v1'],
- // various Ethereal Engine user preferences
- type: 'UserPreferencesCredential'
- }
- }
- ]
- }
- ],
- challenge,
- domain // e.g.: requestingparty.example.com
- }
- }
- }
+ // async function handleWalletLoginClick() {
+ // const domain = window.location.origin
+ // const challenge = '99612b24-63d9-11ea-b99f-4f66f3e4f81a' // TODO: generate
+
+ // console.log('Sending DIDAuth query...')
+
+ // const didAuthQuery: any = {
+ // web: {
+ // VerifiablePresentation: {
+ // query: [
+ // {
+ // type: 'DIDAuth' // request the controller's DID
+ // },
+ // {
+ // type: 'QueryByExample',
+ // credentialQuery: [
+ // {
+ // example: {
+ // '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/xr/v1'],
+ // // contains username and avatar icon
+ // type: 'LoginDisplayCredential'
+ // }
+ // },
+ // {
+ // example: {
+ // '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/xr/v1'],
+ // // various Ethereal Engine user preferences
+ // type: 'UserPreferencesCredential'
+ // }
+ // }
+ // ]
+ // }
+ // ],
+ // challenge,
+ // domain // e.g.: requestingparty.example.com
+ // }
+ // }
+ // }
- // Use Credential Handler API to authenticate and receive basic login display credentials
- const vprResult: any = await navigator.credentials.get(didAuthQuery)
- console.log(vprResult)
+ // // Use Credential Handler API to authenticate and receive basic login display credentials
+ // const vprResult: any = await navigator.credentials.get(didAuthQuery)
+ // console.log(vprResult)
- AuthService.loginUserByXRWallet(vprResult)
- }
+ // AuthService.loginUserByXRWallet(vprResult)
+ // }
const refreshApiKey = () => {
AuthService.updateApiKey()
@@ -752,7 +751,7 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => {
>
)}
- {isGuest && enableWalletLogin && (
+ {/* {isGuest && enableWalletLogin && (
<>
{t('user:usermenu.profile.or')}
@@ -776,7 +775,7 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => {
)}
>
- )}
+ )} */}
{enableSocial && (
<>
diff --git a/packages/client-core/src/user/services/AuthService.ts b/packages/client-core/src/user/services/AuthService.ts
index 1f20161471..0ea9010f87 100755
--- a/packages/client-core/src/user/services/AuthService.ts
+++ b/packages/client-core/src/user/services/AuthService.ts
@@ -317,41 +317,41 @@ export const AuthService = {
*
* @param vprResult {object} - VPR Query result from a user's wallet.
*/
- async loginUserByXRWallet(vprResult: any) {
- const authState = getMutableState(AuthState)
- try {
- authState.merge({ isProcessing: true, error: '' })
-
- const credentials: any = parseUserWalletCredentials(vprResult)
- console.log(credentials)
-
- const walletUser = resolveWalletUser(credentials)
- const authUser = {
- accessToken: '',
- authentication: { strategy: 'did-auth' },
- identityProvider: {
- id: '',
- token: '',
- type: 'didWallet',
- userId: walletUser.id,
- createdAt: '',
- updatedAt: ''
- }
- }
-
- // TODO: This is temp until we move completely to XR wallet #6453
- const oldId = authState.user.id.value
- walletUser.id = oldId
-
- // loadXRAvatarForUpdatedUser(walletUser)
- authState.merge({ isLoggedIn: true, user: walletUser, authUser })
- } catch (err) {
- authState.merge({ error: i18n.t('common:error.login-error') })
- NotificationService.dispatchNotify(err.message, { variant: 'error' })
- } finally {
- authState.merge({ isProcessing: false, error: '' })
- }
- },
+ // async loginUserByXRWallet(vprResult: any) {
+ // const authState = getMutableState(AuthState)
+ // try {
+ // authState.merge({ isProcessing: true, error: '' })
+
+ // const credentials: any = parseUserWalletCredentials(vprResult)
+ // console.log(credentials)
+
+ // const walletUser = resolveWalletUser(credentials)
+ // const authUser = {
+ // accessToken: '',
+ // authentication: { strategy: 'did-auth' },
+ // identityProvider: {
+ // id: '',
+ // token: '',
+ // type: 'didWallet',
+ // userId: walletUser.id,
+ // createdAt: '',
+ // updatedAt: ''
+ // }
+ // }
+
+ // // TODO: This is temp until we move completely to XR wallet #6453
+ // const oldId = authState.user.id.value
+ // walletUser.id = oldId
+
+ // // loadXRAvatarForUpdatedUser(walletUser)
+ // authState.merge({ isLoggedIn: true, user: walletUser, authUser })
+ // } catch (err) {
+ // authState.merge({ error: i18n.t('common:error.login-error') })
+ // NotificationService.dispatchNotify(err.message, { variant: 'error' })
+ // } finally {
+ // authState.merge({ isProcessing: false, error: '' })
+ // }
+ // },
/**
* Logs in the current user based on an OAuth response.
@@ -725,25 +725,25 @@ export const AuthService = {
/**
* @param vprResult {any} See `loginUserByXRWallet()`'s docstring.
*/
-function parseUserWalletCredentials(vprResult: any) {
- console.log('PARSING:', vprResult)
-
- const {
- data: { presentation: vp }
- } = vprResult
- const credentials = Array.isArray(vp.verifiableCredential) ? vp.verifiableCredential : [vp.verifiableCredential]
-
- const { displayName, displayIcon } = parseLoginDisplayCredential(credentials)
-
- return {
- user: {
- id: vp.holder,
- displayName,
- icon: displayIcon
- // session // this will contain the access token and helper methods
- }
- }
-}
+// function parseUserWalletCredentials(vprResult: any) {
+// console.log('PARSING:', vprResult)
+
+// const {
+// data: { presentation: vp }
+// } = vprResult
+// const credentials = Array.isArray(vp.verifiableCredential) ? vp.verifiableCredential : [vp.verifiableCredential]
+
+// const { displayName, displayIcon } = parseLoginDisplayCredential(credentials)
+
+// return {
+// user: {
+// id: vp.holder,
+// displayName,
+// icon: displayIcon
+// // session // this will contain the access token and helper methods
+// }
+// }
+// }
/**
* Parses the user's preferred display name (username) and avatar icon from the
diff --git a/packages/common/src/interfaces/AuthUser.ts b/packages/common/src/interfaces/AuthUser.ts
index 96b5126f00..a0c2572ab4 100755
--- a/packages/common/src/interfaces/AuthUser.ts
+++ b/packages/common/src/interfaces/AuthUser.ts
@@ -41,7 +41,7 @@ export const IdentityProviderSeed: IdentityProviderType = {
accountIdentifier: '',
oauthToken: '',
oauthRefreshToken: '',
- type: '',
+ type: 'guest',
userId: '' as UserID,
createdAt: '',
updatedAt: ''
diff --git a/packages/common/src/schema.type.module.ts b/packages/common/src/schema.type.module.ts
index 6a5acad8ad..1766dbb720 100644
--- a/packages/common/src/schema.type.module.ts
+++ b/packages/common/src/schema.type.module.ts
@@ -119,6 +119,8 @@ export type * from './schemas/user/user-setting.schema'
export type * from './schemas/user/user.schema'
export type * from './schemas/world/spawn-point.schema'
+export type * from './schemas/projects/project-history.schema'
+
export const locationPath = 'location'
export const userRelationshipPath = 'user-relationship'
@@ -310,6 +312,8 @@ export const imageConvertPath = 'image-convert'
export const zendeskPath = 'zendesk'
+export const projectHistoryPath = 'project-history'
+
export const metabaseSettingPath = 'metabase-setting'
export const metabaseUrlPath = 'metabase-url'
diff --git a/packages/common/src/schemas/media/static-resource.schema.ts b/packages/common/src/schemas/media/static-resource.schema.ts
index cf0beef8ed..06cdd9d7f7 100755
--- a/packages/common/src/schemas/media/static-resource.schema.ts
+++ b/packages/common/src/schemas/media/static-resource.schema.ts
@@ -59,6 +59,9 @@ export const staticResourceSchema = Type.Object(
thumbnailKey: Type.Optional(Type.String()),
thumbnailURL: Type.Optional(Type.String()),
thumbnailMode: Type.Optional(Type.String()), // 'automatic' | 'manual'
+ updatedBy: TypedString({
+ format: 'uuid'
+ }),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
diff --git a/packages/common/src/schemas/projects/project-history.schema.ts b/packages/common/src/schemas/projects/project-history.schema.ts
new file mode 100644
index 0000000000..971398724e
--- /dev/null
+++ b/packages/common/src/schemas/projects/project-history.schema.ts
@@ -0,0 +1,110 @@
+/*
+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.
+*/
+
+// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
+import { UserID } from '@etherealengine/common/src/schemas/user/user.schema'
+import { dataValidator, queryValidator } from '@etherealengine/common/src/schemas/validators'
+import { TypedString } from '@etherealengine/common/src/types/TypeboxUtils'
+import { Static, StringEnum, Type, getValidator, querySyntax } from '@feathersjs/typebox'
+
+export const projectHistoryPath = 'project-history'
+export const projectHistoryMethods = ['create', 'find', 'remove'] as const
+
+export const ActionTypes = [
+ 'SCENE_CREATED',
+ 'SCENE_RENAMED',
+ 'SCENE_MODIFIED',
+ 'SCENE_REMOVED',
+ 'RESOURCE_CREATED',
+ 'RESOURCE_RENAMED',
+ 'RESOURCE_MODIFIED',
+ 'RESOURCE_REMOVED',
+ 'PROJECT_CREATED',
+ 'PERMISSION_CREATED',
+ 'PERMISSION_MODIFIED',
+ 'PERMISSION_REMOVED',
+ 'LOCATION_PUBLISHED',
+ 'LOCATION_MODIFIED',
+ 'LOCATION_UNPUBLISHED'
+] as const
+
+export type ActionType = (typeof ActionTypes)[number]
+
+export const ActionIdentifierTypes = ['static-resource', 'project', 'location', 'project-permission'] as const
+
+// Schema for creating new entries
+export const projectHistorySchema = Type.Object(
+ {
+ id: Type.String({
+ format: 'uuid'
+ }),
+ projectId: Type.String({
+ format: 'uuid'
+ }),
+ userId: Type.Union([
+ TypedString({
+ format: 'uuid'
+ }),
+ Type.Null()
+ ]),
+
+ userName: Type.String(),
+ userAvatarURL: Type.String({ format: 'uri' }),
+
+ // @ts-ignore
+ action: StringEnum(ActionTypes),
+ actionIdentifier: Type.String(),
+
+ // @ts-ignore
+ actionIdentifierType: StringEnum(ActionIdentifierTypes),
+ actionDetail: Type.String(),
+
+ createdAt: Type.String({ format: 'date-time' })
+ },
+ { $id: 'ProjectHistory', additionalProperties: false }
+)
+export interface ProjectHistoryType extends Static {}
+
+// Schema for creating new entries
+export const projectHistoryDataSchema = Type.Pick(
+ projectHistorySchema,
+ ['projectId', 'userId', 'action', 'actionIdentifier', 'actionIdentifierType', 'actionDetail'],
+ {
+ $id: 'ProjectHistoryData'
+ }
+)
+export interface ProjectHistoryData extends Static {}
+
+// Schema for allowed query properties
+export const projectHistoryQueryProperties = Type.Pick(projectHistorySchema, ['projectId', 'createdAt'])
+
+export const projectHistoryQuerySchema = Type.Intersect([querySyntax(projectHistoryQueryProperties, {})], {
+ additionalProperties: false
+})
+export interface ProjectHistoryQuery extends Static {}
+
+export const projectHistoryValidator = /* @__PURE__ */ getValidator(projectHistorySchema, dataValidator)
+export const projectHistoryDataValidator = /* @__PURE__ */ getValidator(projectHistoryDataSchema, dataValidator)
+export const projectHistoryQueryValidator = /* @__PURE__ */ getValidator(projectHistoryQuerySchema, queryValidator)
diff --git a/packages/common/src/schemas/projects/project-permission.schema.ts b/packages/common/src/schemas/projects/project-permission.schema.ts
index 2ffb3a4798..f738a6cbdb 100644
--- a/packages/common/src/schemas/projects/project-permission.schema.ts
+++ b/packages/common/src/schemas/projects/project-permission.schema.ts
@@ -50,6 +50,9 @@ export const projectPermissionSchema = Type.Object(
}),
type: Type.String(),
user: Type.Ref(userSchema),
+ updatedBy: TypedString({
+ format: 'uuid'
+ }),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
diff --git a/packages/common/src/schemas/projects/project.schema.ts b/packages/common/src/schemas/projects/project.schema.ts
index dd19c0b566..bda2785e7b 100644
--- a/packages/common/src/schemas/projects/project.schema.ts
+++ b/packages/common/src/schemas/projects/project.schema.ts
@@ -27,8 +27,9 @@ Ethereal Engine. All Rights Reserved.
import type { Static } from '@feathersjs/typebox'
import { getValidator, querySyntax, StringEnum, Type } from '@feathersjs/typebox'
+import { TypedString } from '../../types/TypeboxUtils'
import { projectSettingSchema } from '../setting/project-setting.schema'
-import { UserType } from '../user/user.schema'
+import { UserID, UserType } from '../user/user.schema'
import { dataValidator, queryValidator } from '../validators'
import { projectPermissionSchema } from './project-permission.schema'
@@ -65,6 +66,9 @@ export const projectSchema = Type.Object(
assetsOnly: Type.Boolean(),
visibility: StringEnum(['private', 'public']),
settings: Type.Optional(Type.Array(Type.Ref(projectSettingSchema))),
+ updatedBy: TypedString({
+ format: 'uuid'
+ }),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
diff --git a/packages/common/src/schemas/social/invite.schema.ts b/packages/common/src/schemas/social/invite.schema.ts
index bb8016c8ff..67b4e92008 100644
--- a/packages/common/src/schemas/social/invite.schema.ts
+++ b/packages/common/src/schemas/social/invite.schema.ts
@@ -58,6 +58,8 @@ export const inviteSchema = Type.Object(
format: 'uuid'
}),
token: Type.Optional(Type.String()),
+
+ // @ts-ignore
identityProviderType: Type.Optional(StringEnum(identityProviderTypes)),
passcode: Type.Optional(Type.String()),
targetObjectId: Type.Optional(Type.String()),
diff --git a/packages/common/src/schemas/social/location.schema.ts b/packages/common/src/schemas/social/location.schema.ts
index 6903ad2500..4a155ecd49 100644
--- a/packages/common/src/schemas/social/location.schema.ts
+++ b/packages/common/src/schemas/social/location.schema.ts
@@ -29,6 +29,7 @@ import { getValidator, querySyntax, Type } from '@feathersjs/typebox'
import { OpaqueType } from '@etherealengine/common/src/interfaces/OpaqueType'
+import { UserID } from '../../schema.type.module'
import { TypedString } from '../../types/TypeboxUtils'
import { staticResourceSchema } from '../media/static-resource.schema'
import { dataValidator, queryValidator } from '../validators'
@@ -67,6 +68,9 @@ export const locationSchema = Type.Object(
locationAdmin: Type.Optional(Type.Ref(locationAdminSchema)),
locationAuthorizedUsers: Type.Array(Type.Ref(locationAuthorizedUserSchema)),
locationBans: Type.Array(Type.Ref(locationBanSchema)),
+ updatedBy: TypedString({
+ format: 'uuid'
+ }),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
diff --git a/packages/common/src/schemas/user/generate-token.schema.ts b/packages/common/src/schemas/user/generate-token.schema.ts
index 2e0d928c19..9819a83312 100644
--- a/packages/common/src/schemas/user/generate-token.schema.ts
+++ b/packages/common/src/schemas/user/generate-token.schema.ts
@@ -25,9 +25,10 @@ Ethereal Engine. All Rights Reserved.
// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import type { Static } from '@feathersjs/typebox'
-import { getValidator, querySyntax, Type } from '@feathersjs/typebox'
+import { getValidator, querySyntax, StringEnum, Type } from '@feathersjs/typebox'
import { dataValidator, queryValidator } from '../validators'
+import { identityProviderTypes } from './identity-provider.schema'
export const generateTokenPath = 'generate-token'
@@ -37,7 +38,9 @@ export const generateTokenMethods = ['create'] as const
export const generateTokenSchema = Type.Object(
{
token: Type.String(),
- type: Type.String()
+
+ // @ts-ignore
+ type: Type.Optional(StringEnum(identityProviderTypes))
},
{ $id: 'GenerateToken', additionalProperties: false }
)
diff --git a/packages/common/src/schemas/user/identity-provider.schema.ts b/packages/common/src/schemas/user/identity-provider.schema.ts
index 5405906124..8b64011316 100644
--- a/packages/common/src/schemas/user/identity-provider.schema.ts
+++ b/packages/common/src/schemas/user/identity-provider.schema.ts
@@ -46,8 +46,9 @@ export const identityProviderTypes = [
'facebook',
'twitter',
'linkedin',
- 'auth0'
-]
+ 'auth0',
+ 'guest'
+] as const
// Main data model schema
export const identityProviderSchema = Type.Object(
@@ -61,6 +62,8 @@ export const identityProviderSchema = Type.Object(
accountIdentifier: Type.Optional(Type.String()),
oauthToken: Type.Optional(Type.String()),
oauthRefreshToken: Type.Optional(Type.String()),
+
+ // @ts-ignore
type: StringEnum(identityProviderTypes),
userId: TypedString({
format: 'uuid'
@@ -106,6 +109,9 @@ export const identityProviderQuerySchema = Type.Intersect(
querySyntax(identityProviderQueryProperties, {
accountIdentifier: {
$like: Type.String()
+ },
+ email: {
+ $like: Type.String()
}
}),
// Add additional query properties here
diff --git a/packages/common/src/utils/btyesToSize.ts b/packages/common/src/utils/btyesToSize.ts
new file mode 100644
index 0000000000..edffca23f8
--- /dev/null
+++ b/packages/common/src/utils/btyesToSize.ts
@@ -0,0 +1,43 @@
+/*
+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.
+*/
+
+/**
+ * Converts bytes to a human-readable size
+ * @param bytes The number of bytes
+ * @param decimals The number of decimal places to include
+ * @returns The human-readable size
+ */
+
+export function bytesToSize(bytes: number, decimals = 2) {
+ if (bytes === 0) return '0 Bytes'
+
+ const k = 1024
+ const dm = decimals < 0 ? 0 : decimals
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
+}
diff --git a/packages/common/src/utils/getOS.ts b/packages/common/src/utils/getDeviceStats.ts
similarity index 75%
rename from packages/common/src/utils/getOS.ts
rename to packages/common/src/utils/getDeviceStats.ts
index 40e44c697e..5fa025b4d2 100644
--- a/packages/common/src/utils/getOS.ts
+++ b/packages/common/src/utils/getDeviceStats.ts
@@ -35,3 +35,19 @@ export function getOS() {
}
return 'other'
}
+
+export const isApple = () => {
+ if ('navigator' in globalThis === false) return false
+
+ const iOS_1to12 = /iPad|iPhone|iPod/.test(navigator.platform)
+
+ const iOS13_iPad = navigator.platform === 'MacIntel'
+
+ const iOS1to12quirk = () => {
+ const audio = new Audio() // temporary Audio object
+ audio.volume = 0.5 // has no effect on iOS <= 12
+ return audio.volume === 1
+ }
+
+ return iOS_1to12 || iOS13_iPad || iOS1to12quirk()
+}
diff --git a/packages/common/src/utils/mapToObject.ts b/packages/common/src/utils/mapToObject.ts
index 9db2ca7dfe..6370aa323f 100644
--- a/packages/common/src/utils/mapToObject.ts
+++ b/packages/common/src/utils/mapToObject.ts
@@ -52,3 +52,7 @@ export const iterativeMapToObject = (root: Record) => {
}
return cloneDeep(iterate(root))
}
+
+export function objectToMap(object: object) {
+ return new Map(Object.entries(object))
+}
diff --git a/packages/common/src/utils/miscUtils.ts b/packages/common/src/utils/miscUtils.ts
index 24b6f9ff93..3cdbb44c5c 100644
--- a/packages/common/src/utils/miscUtils.ts
+++ b/packages/common/src/utils/miscUtils.ts
@@ -38,6 +38,11 @@ export function isNumber(value: string | number): boolean {
return value != null && value !== '' && !isNaN(Number(value.toString()))
}
+export function toPrecision(value, precision) {
+ const p = 1 / precision
+ return Math.round(value * p) / p
+}
+
export function combine(first, second, third) {
const res: any[] = []
@@ -47,6 +52,23 @@ export function combine(first, second, third) {
return res
}
+
+export const unique = (arr: T[], keyFinder: (item: T) => S): T[] => {
+ const set = new Set()
+ const newArr = [] as T[]
+ if (!keyFinder) keyFinder = (item: T) => item as any as S
+
+ for (const item of arr) {
+ const key = keyFinder(item)
+ if (set.has(key)) continue
+
+ newArr.push(item)
+ set.add(key)
+ }
+
+ return newArr
+}
+
export function combineArrays(arrays: [[]]) {
const res = []
@@ -59,6 +81,23 @@ export function combineArrays(arrays: [[]]) {
return res
}
+export function insertArraySeparator(children, separatorFn) {
+ if (!Array.isArray(children)) {
+ return children
+ }
+ const length = children.length
+ if (length === 1) {
+ return children[0]
+ }
+ return children.reduce((acc, item, index) => {
+ acc.push(item)
+ if (index !== length - 1) {
+ acc.push(separatorFn(index))
+ }
+ return acc
+ }, [])
+}
+
export function arraysAreEqual(arr1: any[], arr2: any[]): boolean {
if (arr1.length !== arr2.length) return false
@@ -154,3 +193,14 @@ export const toCapitalCase = (source: string) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
})
}
+
+export function toCamelPad(source: string) {
+ return source
+ .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
+ .replace(/([a-zA-Z])(\d)/g, '$1 $2')
+ .replace(/^./, (str) => {
+ return str.toUpperCase()
+ })
+ .trim()
+}
diff --git a/packages/editor/src/components/EditorContainer.tsx b/packages/editor/src/components/EditorContainer.tsx
index 6f374f3b35..e62b06f908 100644
--- a/packages/editor/src/components/EditorContainer.tsx
+++ b/packages/editor/src/components/EditorContainer.tsx
@@ -200,6 +200,16 @@ const EditorContainer = () => {
}
}, [errorState])
+ useEffect(() => {
+ const handleBeforeUnload = async (event: BeforeUnloadEvent) => {
+ if (EditorState.isModified()) {
+ event.preventDefault()
+ }
+ }
+ window.addEventListener('beforeunload', handleBeforeUnload)
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload)
+ }, [])
+
return (
return (
{
PopoverState.hidePopupover()
diff --git a/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts b/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts
index 29b0d85a9d..307507df45 100644
--- a/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts
+++ b/packages/editor/src/components/hierarchy/HierarchyTreeWalker.ts
@@ -23,8 +23,8 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20
Ethereal Engine. All Rights Reserved.
*/
-import { ComponentType, getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions'
-import { Entity } from '@etherealengine/ecs/src/Entity'
+import { getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions'
+import { Entity, EntityUUID } from '@etherealengine/ecs/src/Entity'
import { entityExists } from '@etherealengine/ecs/src/EntityFunctions'
import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent'
import { getState } from '@etherealengine/hyperflux'
@@ -68,7 +68,7 @@ function buildHierarchyTree(
sceneID: string,
showModelChildren: boolean
) {
- const uuid = node.extensions && (node.extensions[UUIDComponent.jsonID] as ComponentType)
+ const uuid = node.extensions && (node.extensions[UUIDComponent.jsonID] as EntityUUID)
const entity = UUIDComponent.getEntityByUUID(uuid!)
if (!entity || !entityExists(entity)) return
diff --git a/packages/editor/src/components/prefabs/PrefabEditors.tsx b/packages/editor/src/components/prefabs/PrefabEditors.tsx
index 2773ed9770..3d56b4ef8d 100644
--- a/packages/editor/src/components/prefabs/PrefabEditors.tsx
+++ b/packages/editor/src/components/prefabs/PrefabEditors.tsx
@@ -98,12 +98,6 @@ export const PrefabShelfState = defineState({
category: 'Collider',
detail: 'Simple cylinder collider'
},
- {
- name: 'Mesh Collider',
- url: `${config.client.fileServer}/projects/default-project/assets/prefabs/mesh-collider.prefab.gltf`,
- category: 'Collider',
- detail: 'Simple mesh collider, drag and drop your mesh'
- },
{
name: 'Text',
url: `${config.client.fileServer}/projects/default-project/assets/prefabs/text.prefab.gltf`,
diff --git a/packages/editor/src/components/toolbar/Toolbar.tsx b/packages/editor/src/components/toolbar/Toolbar.tsx
index 316da9c6c9..4ecf87012e 100644
--- a/packages/editor/src/components/toolbar/Toolbar.tsx
+++ b/packages/editor/src/components/toolbar/Toolbar.tsx
@@ -60,25 +60,21 @@ const onImportAsset = async () => {
}
}
-const onClickNewScene = async () => {
+export const confirmSceneSaveIfModified = async () => {
const isModified = EditorState.isModified()
if (isModified) {
- const confirm = await new Promise((resolve) => {
+ return new Promise((resolve) => {
PopoverState.showPopupover(
- {
- resolve(true)
- }}
- onCancel={() => {
- resolve(false)
- }}
- />
+ resolve(true)} onCancel={() => resolve(false)} />
)
})
- if (!confirm) return
}
+ return true
+}
+
+const onClickNewScene = async () => {
+ if (!(await confirmSceneSaveIfModified())) return
const newSceneUIAddons = getState(EditorState).uiAddons.newScene
if (Object.keys(newSceneUIAddons).length > 0) {
@@ -89,24 +85,7 @@ const onClickNewScene = async () => {
}
const onCloseProject = async () => {
- const isModified = EditorState.isModified()
-
- if (isModified) {
- const confirm = await new Promise((resolve) => {
- PopoverState.showPopupover(
- {
- resolve(true)
- }}
- onCancel={() => {
- resolve(false)
- }}
- />
- )
- })
- if (!confirm) return
- }
+ if (!(await confirmSceneSaveIfModified())) return
const editorState = getMutableState(EditorState)
getMutableState(GLTFModifiedState).set({})
diff --git a/packages/editor/src/functions/EditorControlFunctions.ts b/packages/editor/src/functions/EditorControlFunctions.ts
index e5ff0b648d..8660d40155 100644
--- a/packages/editor/src/functions/EditorControlFunctions.ts
+++ b/packages/editor/src/functions/EditorControlFunctions.ts
@@ -40,7 +40,6 @@ import {
import { Entity } from '@etherealengine/ecs/src/Entity'
import { GLTFDocumentState, GLTFSnapshotAction } from '@etherealengine/engine/src/gltf/GLTFDocumentState'
import { GLTFSnapshotState, GLTFSourceState } from '@etherealengine/engine/src/gltf/GLTFState'
-import { PrimitiveGeometryComponent } from '@etherealengine/engine/src/scene/components/PrimitiveGeometryComponent'
import { SkyboxComponent } from '@etherealengine/engine/src/scene/components/SkyboxComponent'
import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent'
import { TransformSpace } from '@etherealengine/engine/src/scene/constants/transformConstants'
@@ -224,8 +223,7 @@ const overwriteLookdevObject = (
SkyboxComponent,
HemisphereLightComponent,
DirectionalLightComponent,
- PostProcessingComponent,
- PrimitiveGeometryComponent //this component is for test will remove later
+ PostProcessingComponent
]
let overwrited = false
for (const comp of lookDevComponent) {
diff --git a/packages/editor/src/functions/gizmoHelper.ts b/packages/editor/src/functions/gizmoHelper.ts
index e9e80e8f28..77b1ece16e 100644
--- a/packages/editor/src/functions/gizmoHelper.ts
+++ b/packages/editor/src/functions/gizmoHelper.ts
@@ -533,7 +533,9 @@ function pointerDown(gizmoEntity) {
const planeIntersect = intersectObjectWithRay(plane, _raycaster, true)
if (planeIntersect) {
const currenttransform = getComponent(targetEntity, TransformComponent)
- currenttransform.matrix.decompose(_positionStart, _quaternionStart, _scaleStart)
+ _positionStart.copy(currenttransform.position)
+ _quaternionStart.copy(currenttransform.rotation)
+ _scaleStart.copy(currenttransform.scale)
gizmoControlComponent.worldPositionStart.set(_positionStart)
gizmoControlComponent.worldQuaternionStart.set(_quaternionStart)
diff --git a/packages/editor/src/functions/utils.ts b/packages/editor/src/functions/utils.ts
index 38e654987e..f1d2b197f5 100755
--- a/packages/editor/src/functions/utils.ts
+++ b/packages/editor/src/functions/utils.ts
@@ -22,60 +22,7 @@ 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.
*/
-
-export function insertSeparator(children, separatorFn) {
- if (!Array.isArray(children)) {
- return children
- }
- const length = children.length
- if (length === 1) {
- return children[0]
- }
- return children.reduce((acc, item, index) => {
- acc.push(item)
- if (index !== length - 1) {
- acc.push(separatorFn(index))
- }
- return acc
- }, [])
-}
-export function objectToMap(object: object) {
- return new Map(Object.entries(object))
-}
-
-export const unique = (arr: T[], keyFinder: (item: T) => S): T[] => {
- const set = new Set()
- const newArr = [] as T[]
- if (!keyFinder) keyFinder = (item: T) => item as any as S
-
- for (const item of arr) {
- const key = keyFinder(item)
- if (set.has(key)) continue
-
- newArr.push(item)
- set.add(key)
- }
-
- return newArr
-}
-
-export const isApple = () => {
- if ('navigator' in globalThis === false) return false
-
- const iOS_1to12 = /iPad|iPhone|iPod/.test(navigator.platform)
-
- const iOS13_iPad = navigator.platform === 'MacIntel'
-
- const iOS1to12quirk = () => {
- const audio = new Audio() // temporary Audio object
- audio.volume = 0.5 // has no effect on iOS <= 12
- return audio.volume === 1
- }
-
- return iOS_1to12 || iOS13_iPad || iOS1to12quirk()
-}
-
-export const cmdOrCtrlString = isApple() ? 'meta' : 'ctrl'
+import { isApple } from '@etherealengine/common/src/utils/getDeviceStats'
export function getStepSize(event, smallStep, mediumStep, largeStep) {
if (event.altKey) {
@@ -86,29 +33,4 @@ export function getStepSize(event, smallStep, mediumStep, largeStep) {
return mediumStep
}
-export function toPrecision(value, precision) {
- const p = 1 / precision
- return Math.round(value * p) / p
-}
-// https://stackoverflow.com/a/26188910
-export function camelPad(str) {
- return str
- .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
- .replace(/([a-z\d])([A-Z])/g, '$1 $2')
- .replace(/([a-zA-Z])(\d)/g, '$1 $2')
- .replace(/^./, (str) => {
- return str.toUpperCase()
- })
- .trim()
-}
-export function bytesToSize(bytes: number, decimals = 2) {
- if (bytes === 0) return '0 Bytes'
-
- const k = 1024
- const dm = decimals < 0 ? 0 : decimals
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
-
- const i = Math.floor(Math.log(bytes) / Math.log(k))
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
-}
+export const cmdOrCtrlString = isApple() ? 'meta' : 'ctrl'
diff --git a/packages/editor/src/systems/ClickPlacementSystem.tsx b/packages/editor/src/systems/ClickPlacementSystem.tsx
index 475a25b9a5..db23567ede 100644
--- a/packages/editor/src/systems/ClickPlacementSystem.tsx
+++ b/packages/editor/src/systems/ClickPlacementSystem.tsx
@@ -36,7 +36,6 @@ import {
getOptionalComponent,
removeComponent,
setComponent,
- useComponent,
useOptionalComponent
} from '@etherealengine/ecs'
import { GLTFComponent } from '@etherealengine/engine/src/gltf/GLTFComponent'
@@ -115,7 +114,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => {
const { parentEntity } = props
const clickState = useState(getMutableState(ClickPlacementState))
const editorState = useState(getMutableState(EditorHelperState))
- const gltfComponent = useComponent(parentEntity, GLTFComponent)
+ const sceneLoaded = GLTFComponent.useSceneLoaded(parentEntity)
const errors = useEntityErrors(clickState.placementEntity.value, ModelComponent)
// const renderers = defineQuery([RendererComponent])
@@ -132,7 +131,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => {
// }, [editorState.placementMode])
useEffect(() => {
- if (gltfComponent.progress.value < 100) return
+ if (!sceneLoaded) return
if (editorState.placementMode.value === PlacementMode.CLICK) {
SelectionState.updateSelection([])
if (clickState.placementEntity.value) return
@@ -146,7 +145,7 @@ const ClickPlacementReactor = (props: { parentEntity: Entity }) => {
clickState.placementEntity.set(UndefinedEntity)
SelectionState.updateSelection(selectedEntities)
}
- }, [editorState.placementMode, gltfComponent.progress])
+ }, [editorState.placementMode, sceneLoaded])
useEffect(() => {
if (!clickState.placementEntity.value) return
diff --git a/packages/engine/src/gltf/GLTFComponent.tsx b/packages/engine/src/gltf/GLTFComponent.tsx
index c2803003e4..af991ac3ac 100644
--- a/packages/engine/src/gltf/GLTFComponent.tsx
+++ b/packages/engine/src/gltf/GLTFComponent.tsx
@@ -28,6 +28,8 @@ import React, { useEffect } from 'react'
import { parseStorageProviderURLs } from '@etherealengine/common/src/utils/parseSceneJSON'
import {
+ Component,
+ ComponentJSONIDMap,
defineComponent,
Entity,
EntityUUID,
@@ -44,13 +46,36 @@ import { dispatchAction, getState, useHookstate } from '@etherealengine/hyperflu
import { FileLoader } from '../assets/loaders/base/FileLoader'
import { BINARY_EXTENSION_HEADER_MAGIC, EXTENSIONS, GLTFBinaryExtension } from '../assets/loaders/gltf/GLTFExtensions'
-import { ModelComponent } from '../scene/components/ModelComponent'
import { SourceComponent } from '../scene/components/SourceComponent'
import { SceneJsonType } from '../scene/types/SceneTypes'
import { migrateSceneJSONToGLTF } from './convertJsonToGLTF'
import { GLTFDocumentState, GLTFSnapshotAction } from './GLTFDocumentState'
import { ResourcePendingComponent } from './ResourcePendingComponent'
+const loadDependencies = {
+ ['EE_model']: ['scene']
+} as Record
+
+type ComponentDependencies = Record
+
+const buildComponentDependencies = (json: GLTF.IGLTF) => {
+ const dependencies = {} as ComponentDependencies
+ if (!json.nodes) return dependencies
+ for (const node of json.nodes) {
+ if (!node.extensions || !node.extensions[UUIDComponent.jsonID]) continue
+ const uuid = node.extensions[UUIDComponent.jsonID] as EntityUUID
+ const extensions = Object.keys(node.extensions)
+ for (const extension of extensions) {
+ if (loadDependencies[extension]) {
+ if (!dependencies[uuid]) dependencies[uuid] = []
+ dependencies[uuid].push(ComponentJSONIDMap.get(extension)!)
+ }
+ }
+ }
+
+ return dependencies
+}
+
export const GLTFComponent = defineComponent({
name: 'GLTFComponent',
@@ -59,7 +84,8 @@ export const GLTFComponent = defineComponent({
src: '',
// internals
extensions: {},
- progress: 0
+ progress: 0,
+ dependencies: undefined as ComponentDependencies | undefined
}
},
@@ -67,41 +93,61 @@ export const GLTFComponent = defineComponent({
if (typeof json?.src === 'string') component.src.set(json.src)
},
+ useDependenciesLoaded(entity: Entity) {
+ const dependencies = useComponent(entity, GLTFComponent).dependencies
+ return !!(dependencies.value && !dependencies.keys?.length)
+ },
+
+ useSceneLoaded(entity: Entity) {
+ const gltfComponent = useComponent(entity, GLTFComponent)
+ const dependencies = gltfComponent.dependencies
+ const progress = gltfComponent.progress.value
+ return !!(dependencies.value && !dependencies.keys?.length) && progress === 100
+ },
+
+ isSceneLoaded(entity: Entity) {
+ const gltfComponent = getComponent(entity, GLTFComponent)
+ const dependencies = gltfComponent.dependencies
+ const progress = gltfComponent.progress
+ return !!(dependencies && !Object.keys(dependencies).length) && progress === 100
+ },
+
reactor: () => {
const entity = useEntityContext()
const gltfComponent = useComponent(entity, GLTFComponent)
+ const dependencies = gltfComponent.dependencies
useGLTFDocument(gltfComponent.src.value, entity)
const documentID = useComponent(entity, SourceComponent).value
- return
+ return (
+ <>
+
+ {dependencies.value && dependencies.keys?.length ? (
+
+ ) : null}
+ >
+ )
}
})
const ResourceReactor = (props: { documentID: string; entity: Entity }) => {
+ const dependenciesLoaded = GLTFComponent.useDependenciesLoaded(props.entity)
const resourceQuery = useQuery([SourceComponent, ResourcePendingComponent])
const sourceEntities = useHookstate(SourceComponent.entitiesBySourceState[props.documentID])
useEffect(() => {
if (getComponent(props.entity, GLTFComponent).progress === 100) return
if (!getState(GLTFDocumentState)[props.documentID]) return
- const document = getState(GLTFDocumentState)[props.documentID]
- const modelNodes = document.nodes?.filter((node) => !!node.extensions?.[ModelComponent.jsonID])
- if (modelNodes) {
- for (const node of modelNodes) {
- //check if an entity exists for this node, and has a model component
- const uuid = node.extensions![UUIDComponent.jsonID] as EntityUUID
- if (!UUIDComponent.entitiesByUUIDState[uuid]) return
- const entity = UUIDComponent.entitiesByUUIDState[uuid].value
- const model = getOptionalComponent(entity, ModelComponent)
- //ensure that model contents have been loaded into the scene
- if (!model?.scene) return
- }
- }
+
const entities = resourceQuery.filter((e) => getComponent(e, SourceComponent) === props.documentID)
if (!entities.length) {
- getMutableComponent(props.entity, GLTFComponent).progress.set(100)
+ if (dependenciesLoaded) getMutableComponent(props.entity, GLTFComponent).progress.set(100)
return
}
@@ -121,14 +167,83 @@ const ResourceReactor = (props: { documentID: string; entity: Entity }) => {
const progress = resources.reduce((acc, resource) => acc + resource.progress, 0)
const total = resources.reduce((acc, resource) => acc + resource.total, 0)
+ if (!total) return
- const percentage = total === 0 ? 100 : (progress / total) * 100
+ const percentage = Math.floor(Math.min((progress / total) * 100, dependenciesLoaded ? 100 : 99))
getMutableComponent(props.entity, GLTFComponent).progress.set(percentage)
- }, [resourceQuery, sourceEntities])
+ }, [resourceQuery, sourceEntities, dependenciesLoaded])
return null
}
+const ComponentReactor = (props: { gltfComponentEntity: Entity; entity: Entity; component: Component }) => {
+ const { gltfComponentEntity, entity, component } = props
+ const dependencies = loadDependencies[component.jsonID!]
+ const comp = useComponent(entity, component)
+
+ useEffect(() => {
+ const compValue = comp.value
+ for (const key of dependencies) {
+ if (!compValue[key]) return
+ }
+
+ // console.log(`All dependencies loaded for entity: ${entity} on component: ${component.jsonID}`)
+
+ const gltfComponent = getMutableComponent(gltfComponentEntity, GLTFComponent)
+ const uuid = getComponent(entity, UUIDComponent)
+ gltfComponent.dependencies.set((prev) => {
+ const dependencyArr = prev![uuid] as Component[]
+ const index = dependencyArr.findIndex((compItem) => compItem.jsonID === component.jsonID)
+ dependencyArr.splice(index, 1)
+ if (!dependencyArr.length) {
+ delete prev![uuid]
+ }
+ return prev
+ })
+ }, [...dependencies.map((key) => comp[key])])
+
+ return null
+}
+
+const DependencyEntryReactor = (props: { gltfComponentEntity: Entity; uuid: string; components: Component[] }) => {
+ const { gltfComponentEntity, uuid, components } = props
+ const entity = UUIDComponent.useEntityByUUID(uuid as EntityUUID) as Entity | undefined
+ return entity ? (
+ <>
+ {components.map((component) => {
+ return (
+
+ )
+ })}
+ >
+ ) : null
+}
+
+const DependencyReactor = (props: { gltfComponentEntity: Entity; dependencies: ComponentDependencies }) => {
+ const { gltfComponentEntity, dependencies } = props
+ const entries = Object.entries(dependencies)
+
+ return (
+ <>
+ {entries.map(([uuid, components]) => {
+ return (
+
+ )
+ })}
+ >
+ )
+}
+
const onError = (error: ErrorEvent) => {
// console.error(error)
}
@@ -187,6 +302,9 @@ const useGLTFDocument = (url: string, entity: Entity) => {
json = migrateSceneJSONToGLTF(json)
}
+ const dependencies = buildComponentDependencies(json)
+ state.dependencies.set(dependencies)
+
dispatchAction(
GLTFSnapshotAction.createSnapshot({
source: getComponent(entity, SourceComponent),
diff --git a/packages/engine/src/interaction/components/InteractableComponent.ts b/packages/engine/src/interaction/components/InteractableComponent.ts
index 843b39b47d..4be83df0d0 100755
--- a/packages/engine/src/interaction/components/InteractableComponent.ts
+++ b/packages/engine/src/interaction/components/InteractableComponent.ts
@@ -29,7 +29,6 @@ import matches from 'ts-matches'
import { isClient } from '@etherealengine/common/src/utils/getEnvironment'
import {
ECSState,
- Engine,
Entity,
EntityUUID,
getComponent,
@@ -70,10 +69,12 @@ import {
DistanceFromCameraComponent,
DistanceFromLocalClientComponent
} from '@etherealengine/spatial/src/transform/components/DistanceComponents'
+import { useXRUIState } from '@etherealengine/spatial/src/xrui/functions/useXRUIState'
import { useEffect } from 'react'
import { AvatarComponent } from '../../avatar/components/AvatarComponent'
import { createUI } from '../functions/createUI'
import { inFrustum, InteractableState, InteractableTransitions } from '../functions/interactableFunctions'
+import { InteractiveModalState } from '../ui/InteractiveModalView'
/**
* Visibility override for XRUI, none is default behavior, on or off forces that state
@@ -131,7 +132,7 @@ export const updateInteractableUI = (entity: Entity) => {
xruiTransform.position.z = center.z
xruiTransform.position.y = MathUtils.lerp(xruiTransform.position.y, center.y + 0.7 * size.y, alpha)
- const cameraTransform = getComponent(Engine.instance.viewerEntity, TransformComponent)
+ const cameraTransform = getComponent(getState(EngineState).viewerEntity, TransformComponent)
xruiTransform.rotation.copy(cameraTransform.rotation)
}
@@ -145,7 +146,7 @@ export const updateInteractableUI = (entity: Entity) => {
const transition = InteractableTransitions.get(entity)!
let activateUI = false
- const inCameraFrustum = inFrustum(entity)
+ const inCameraFrustum = inFrustum(interactable.uiEntity)
let hovering = false
if (inCameraFrustum) {
@@ -204,9 +205,9 @@ const addInteractableUI = (entity: Entity) => {
const uiEntity = createUI(entity, interactable.label, interactable.uiInteractable).entity
getMutableComponent(entity, InteractableComponent).uiEntity.set(uiEntity)
- setComponent(uiEntity, EntityTreeComponent, { parentEntity: Engine.instance.originEntity })
+ setComponent(uiEntity, EntityTreeComponent, { parentEntity: getState(EngineState).originEntity })
setComponent(uiEntity, ComputedTransformComponent, {
- referenceEntities: [entity, Engine.instance.viewerEntity],
+ referenceEntities: [entity, getState(EngineState).viewerEntity],
computeFunction: () => updateInteractableUI(entity)
})
@@ -297,29 +298,19 @@ export const InteractableComponent = defineComponent({
const entity = useEntityContext()
const interactableComponent = useComponent(entity, InteractableComponent)
const isEditing = useMutableState(EngineState).isEditing
+ const modalState = useXRUIState()
useImmediateEffect(() => {
setComponent(entity, DistanceFromCameraComponent)
setComponent(entity, DistanceFromLocalClientComponent)
-
+ setComponent(entity, BoundingBoxComponent)
return () => {
removeComponent(entity, DistanceFromCameraComponent)
removeComponent(entity, DistanceFromLocalClientComponent)
+ removeComponent(entity, BoundingBoxComponent)
}
}, [])
- useImmediateEffect(() => {
- if (
- interactableComponent.uiActivationType.value === XRUIActivationType.hover ||
- interactableComponent.clickInteract.value
- ) {
- setComponent(entity, BoundingBoxComponent)
- return () => {
- removeComponent(entity, BoundingBoxComponent)
- }
- }
- }, [interactableComponent.uiActivationType, interactableComponent.clickInteract])
-
InputComponent.useExecuteWithInput(
() => {
const buttons = InputComponent.getMergedButtons(entity)
@@ -348,6 +339,12 @@ export const InteractableComponent = defineComponent({
}
}
}, [isEditing.value])
+
+ useEffect(() => {
+ //const xrUI = getMutableComponent(interactableComponent.uiEntity, XRUIComponent)
+ const msg = interactableComponent.label?.value ?? ''
+ modalState.interactMessage?.set(msg)
+ }, [interactableComponent.label]) //TODO just nuke the whole XRUI and recreate....
return null
}
})
diff --git a/packages/engine/src/interaction/functions/createUI.ts b/packages/engine/src/interaction/functions/createUI.ts
index 0148babac1..f5f8253652 100755
--- a/packages/engine/src/interaction/functions/createUI.ts
+++ b/packages/engine/src/interaction/functions/createUI.ts
@@ -30,12 +30,6 @@ import { TransformComponent } from '@etherealengine/spatial/src/transform/compon
import { XRUIComponent } from '@etherealengine/spatial/src/xrui/components/XRUIComponent'
import { WebLayer3D } from '@etherealengine/xrui'
-import { createEntity } from '@etherealengine/ecs'
-import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent'
-import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent'
-import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent'
-import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree'
-import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, Shape, ShapeGeometry, Vector3 } from 'three'
import { createModalView } from '../ui/InteractiveModalView'
/**
@@ -48,22 +42,6 @@ import { createModalView } from '../ui/InteractiveModalView'
export function createUI(entity: Entity, uiMessage: string, isInteractable = true) {
const ui = createModalView(entity, uiMessage, isInteractable)
- const blurMat = new MeshPhysicalMaterial({
- color: new Color('#B9B9B9'),
- transmission: 1,
- roughness: 0.5,
- opacity: 1,
- transparent: true,
- side: DoubleSide
- })
-
- const backgroundEid = createEntity()
- const mesh = new Mesh(roundedRect(-(100 / 1000) / 2, -(100 / 1000) / 2, 100 / 1000, 100 / 1000, 0.01), blurMat)
- setComponent(backgroundEid, EntityTreeComponent, { parentEntity: ui.entity })
- setComponent(backgroundEid, MeshComponent, mesh)
- setComponent(backgroundEid, VisibleComponent)
- const backgroundTransform = setComponent(backgroundEid, TransformComponent, { position: new Vector3(0, 0, -0.001) })
- addObjectToGroup(backgroundEid, mesh) // TODO: this should be managed by the MeshComponent
const nameComponent = getComponent(entity, NameComponent)
setComponent(ui.entity, NameComponent, 'interact-ui-' + uiMessage + '-' + nameComponent)
@@ -77,17 +55,3 @@ export function createUI(entity: Entity, uiMessage: string, isInteractable = tru
return ui
}
-
-function roundedRect(x: number, y: number, width: number, height: number, radius: number): ShapeGeometry {
- const shape = new Shape()
- shape.moveTo(x, y + radius)
- shape.lineTo(x, y + height - radius)
- shape.quadraticCurveTo(x, y + height, x + radius, y + height)
- shape.lineTo(x + width - radius, y + height)
- shape.quadraticCurveTo(x + width, y + height, x + width, y + height - radius)
- shape.lineTo(x + width, y + radius)
- shape.quadraticCurveTo(x + width, y, x + width - radius, y)
- shape.lineTo(x + radius, y)
- shape.quadraticCurveTo(x, y, x, y + radius)
- return new ShapeGeometry(shape)
-}
diff --git a/packages/engine/src/interaction/ui/InteractiveModalView.tsx b/packages/engine/src/interaction/ui/InteractiveModalView.tsx
index b34f3407be..f13ab90866 100755
--- a/packages/engine/src/interaction/ui/InteractiveModalView.tsx
+++ b/packages/engine/src/interaction/ui/InteractiveModalView.tsx
@@ -26,32 +26,91 @@ Ethereal Engine. All Rights Reserved.
import React from 'react'
import { isClient } from '@etherealengine/common/src/utils/getEnvironment'
+import { createEntity, setComponent } from '@etherealengine/ecs'
import { Entity } from '@etherealengine/ecs/src/Entity'
import { hookstate } from '@etherealengine/hyperflux'
+import { TransformComponent } from '@etherealengine/spatial'
+import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent'
+import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent'
+import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent'
+import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree'
import { createXRUI } from '@etherealengine/spatial/src/xrui/functions/createXRUI'
import { useXRUIState } from '@etherealengine/spatial/src/xrui/functions/useXRUIState'
+import { Color, DoubleSide, Mesh, MeshPhysicalMaterial, Shape, ShapeGeometry, Vector3 } from 'three'
export interface InteractiveModalState {
interactMessage: string
}
export const createModalView = (entity: Entity, interactMessage: string, isInteractable = true) => {
+ const uiEntity = createEntity()
const ui = createXRUI(
- InteractiveModalView,
+ () => InteractiveModalView(uiEntity),
hookstate({
interactMessage
} as InteractiveModalState),
- { interactable: isInteractable }
+ { interactable: isInteractable },
+ uiEntity
)
return ui
}
-export const InteractiveModalView = () => {
+function createBackground(parentEntity: Entity, width: number, height: number): Entity {
+ const blurMat = new MeshPhysicalMaterial({
+ color: new Color('#B9B9B9'),
+ transmission: 1,
+ roughness: 0.5,
+ opacity: 1,
+ transparent: true,
+ side: DoubleSide
+ })
+
+ const backgroundEid = createEntity()
+ const calcWidth = width + 30 // 30 accounts for padding and border radius in the Element styling
+ const calcHeight = height + 30
+ const mesh = new Mesh(
+ roundedRect(-(calcWidth / 1000) / 2, -(calcHeight / 1000) / 2, calcWidth / 1000, calcHeight / 1000, 0.01),
+ blurMat
+ )
+ setComponent(backgroundEid, EntityTreeComponent, { parentEntity: parentEntity })
+ setComponent(backgroundEid, MeshComponent, mesh)
+ setComponent(backgroundEid, VisibleComponent)
+ const backgroundTransform = setComponent(backgroundEid, TransformComponent, { position: new Vector3(0, 0, -0.001) })
+ addObjectToGroup(backgroundEid, mesh) // TODO: this should be managed by the MeshComponent
+ return backgroundEid
+}
+
+function roundedRect(x: number, y: number, width: number, height: number, radius: number): ShapeGeometry {
+ const shape = new Shape()
+ shape.moveTo(x, y + radius)
+ shape.lineTo(x, y + height - radius)
+ shape.quadraticCurveTo(x, y + height, x + radius, y + height)
+ shape.lineTo(x + width - radius, y + height)
+ shape.quadraticCurveTo(x + width, y + height, x + width, y + height - radius)
+ shape.lineTo(x + width, y + radius)
+ shape.quadraticCurveTo(x + width, y, x + width - radius, y)
+ shape.lineTo(x + radius, y)
+ shape.quadraticCurveTo(x, y, x, y + radius)
+ return new ShapeGeometry(shape)
+}
+
+export const InteractiveModalView: React.FC = (entity: Entity) => {
const modalState = useXRUIState()
+ const rootElement = React.useRef(null)
+
if (!isClient) return <>>
+
+ React.useLayoutEffect(() => {
+ if (rootElement.current) {
+ createBackground(entity, rootElement.current.clientWidth, rootElement.current.clientHeight)
+ }
+ }, [rootElement.current]) //TODO this isn't firing, not calculating size to add BG
+
return (
-
- E
+
+ {modalState.interactMessage.value && modalState.interactMessage.value !== ''
+ ? modalState.interactMessage.value
+ : 'E'}