diff --git a/.env.local.default b/.env.local.default index b55b6652d9..475a978a8d 100644 --- a/.env.local.default +++ b/.env.local.default @@ -32,6 +32,7 @@ VITE_APP_HOST=localhost VITE_APP_PORT=3000 VITE_ZENDESK_ENABLED=false VITE_ZENDESK_KEY= +VITE_ZENDESK_AUTHENTICATION_ENABLED=false # Use following value for minio s3 provider #VITE_FILE_SERVER=https://localhost:9000/etherealengine-static-resources #VITE_TEST_FILE_SERVER=https://localhost:9000/etherealengine-static-resources-test @@ -220,3 +221,8 @@ OPENSEARCH_HOST=http://localhost:9200 # Switch to `true` to enable local file system operations FS_PROJECT_SYNC_ENABLED=true + +# Zendesk key for user authentication +ZENDESK_KEY_NAME= +ZENDESK_SECRET= +ZENDESK_KID= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c9f3725848..39b1794a21 100755 --- a/Dockerfile +++ b/Dockerfile @@ -66,6 +66,8 @@ ARG VITE_AVATURN_URL ARG VITE_AVATURN_API ARG VITE_ZENDESK_ENABLED ARG VITE_ZENDESK_KEY +ARG VITE_ZENDESK_AUTHENTICATION_ENABLED + ENV MYSQL_HOST=$MYSQL_HOST ENV MYSQL_PORT=$MYSQL_PORT ENV MYSQL_USER=$MYSQL_USER @@ -92,6 +94,7 @@ ENV VITE_AVATURN_URL=$VITE_AVATURN_URL ENV VITE_AVATURN_API=$VITE_AVATURN_API ENV VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED ENV VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY +ENV VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED ARG CACHE_DATE RUN npx cross-env ts-node --swc scripts/check-db-exists.ts diff --git a/dockerfiles/api/Dockerfile-api-client b/dockerfiles/api/Dockerfile-api-client index 05783a8696..b94d74a80b 100755 --- a/dockerfiles/api/Dockerfile-api-client +++ b/dockerfiles/api/Dockerfile-api-client @@ -83,6 +83,7 @@ ARG VITE_AVATURN_URL ARG VITE_AVATURN_API ARG VITE_ZENDESK_ENABLED ARG VITE_ZENDESK_KEY +ARG VITE_ZENDESK_AUTHENTICATION_ENABLED ENV KUBERNETES=$KUBERNETES ENV AUTH_SECRET=$AUTH_SECRET ENV STORAGE_CLOUDFRONT_DOMAIN=$STORAGE_CLOUDFRONT_DOMAIN @@ -114,6 +115,7 @@ ENV VITE_AVATURN_URL=$VITE_AVATURN_URL ENV VITE_AVATURN_API=$VITE_AVATURN_API ENV VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED ENV VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY +ENV VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED RUN npm run build-client diff --git a/dockerfiles/client/Dockerfile-client b/dockerfiles/client/Dockerfile-client index 2c343f08f1..06e9efe87e 100755 --- a/dockerfiles/client/Dockerfile-client +++ b/dockerfiles/client/Dockerfile-client @@ -61,6 +61,7 @@ ARG VITE_AVATURN_API ARG AUTH_SECRET ARG VITE_ZENDESK_ENABLED ARG VITE_ZENDESK_KEY +ARG VITE_ZENDESK_AUTHENTICATION_ENABLED ENV KUBERNETES=$KUBERNETES ENV AUTH_SECRET=$AUTH_SECRET ENV STORAGE_CLOUDFRONT_DOMAIN=$STORAGE_CLOUDFRONT_DOMAIN @@ -92,6 +93,7 @@ ENV VITE_AVATURN_URL=$VITE_AVATURN_URL ENV VITE_AVATURN_API=$VITE_AVATURN_API ENV VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED ENV VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY +ENV VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED RUN npm run build-client diff --git a/dockerfiles/client/Dockerfile-client-serve-static b/dockerfiles/client/Dockerfile-client-serve-static index 5ebfee8a0e..43bda5fd57 100755 --- a/dockerfiles/client/Dockerfile-client-serve-static +++ b/dockerfiles/client/Dockerfile-client-serve-static @@ -60,6 +60,7 @@ ARG VITE_AVATURN_URL ARG VITE_AVATURN_API ARG VITE_ZENDESK_ENABLED ARG VITE_ZENDESK_KEY +ARG VITE_ZENDESK_AUTHENTICATION_ENABLED ARG AUTH_SECRET ENV KUBERNETES=$KUBERNETES ENV AUTH_SECRET=$AUTH_SECRET @@ -93,6 +94,7 @@ ENV VITE_AVATURN_URL=$VITE_AVATURN_URL ENV VITE_AVATURN_API=$VITE_AVATURN_API ENV VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED ENV VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY +ENV VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED RUN npm run build-client diff --git a/packages/client-core/i18n/en/admin.json b/packages/client-core/i18n/en/admin.json index 161eb33b9d..b4c83b4884 100755 --- a/packages/client-core/i18n/en/admin.json +++ b/packages/client-core/i18n/en/admin.json @@ -548,7 +548,13 @@ "processInterval": "Process Interval" }, - "plugins": "Plugins" + "plugins": "Plugins", + "zendesk": { + "header": "Zendesk", + "subtitle": "Edit Zendesk Settings" + }, + "keyName": "key Name", + "kid": "Key Id" }, "avatar": { "columns": { diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index d11117e591..95a7fe60bd 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -27,6 +27,7 @@ "lbl-return": "Return", "loadingScenes": "Loading Scenes", "loadingScenesWithProgress": "Scene Loading... {{progress}}% ({{assetsLeft}} assets left)", + "help": "Help", "menubar": { "newScene": "New Scene", "saveScene": "Save Scene", @@ -469,14 +470,18 @@ "lbl-interactionText": "Interaction Text", "lbl-interactionType": "Interaction Type", "transform": { + "lodLevels": "LOD Levels", + "lodLevelNumber": "LOD Level {{index}}", "compress": "Compress", + "applyPresetConfirmation": "Would you like to apply this preset?", + "savePreset": "Save Preset", "useDraco": "Use DRACO Mesh Compression", "useMeshopt": "Use Meshoptimizer", "useQuantization": "Use Mesh Quantization", "textureFormat": "Image Format", "modelFormat": "Model Format", - "resourceUri": "Resource URI", - "dst": "File Name", + "resourceUri": "Resource URL", + "dst": "File name", "resampleAnimations": "Resample Animations", "maxTextureSize": "Max Texture Size", "simplifyRatio": "Simplify Ratio", @@ -666,6 +671,7 @@ "error-url": "Error Loading From URL" }, "pointLight": { + "name": "Point Light", "description": "A light which emits in all directions from a single point.", "lbl-color": "Color", "lbl-intensity": "Intensity", @@ -986,33 +992,33 @@ }, "text": { "textGroup": "Text", - "textOpacity": "opacity", - "textWidth": "width", - "textIndent": "indent", - "textAlign": "align", - "textWrap": "wrap", - "textAnchor": "anchor", - "textDepthOffset": "depthOffset", - "textCurveRadius": "curveRadius", - "letterSpacing": "letterSpacing", - "lineHeightGroup": "lineHeight", - "lineHeight": "height", - "textDirection": "direction", + "textOpacity": "Opacity", + "textWidth": "Width", + "textIndent": "Indent", + "textAlign": "Align", + "textWrap": "Wrap", + "textAnchor": "Anchor", + "textDepthOffset": "Depth off set", + "textCurveRadius": "Curve radius", + "letterSpacing": "Letter spacing", + "lineHeightGroup": "Line height", + "lineHeight": "Height", + "textDirection": "Direction", "fontGroup": "Font", - "fontFamily": "family", - "fontSize": "size", - "fontColor": "color", - "fontMaterial": "material", + "fontFamily": "Family", + "fontSize": "Size", + "fontColor": "Color", + "fontMaterial": "Material", "outlineGroup": "Outline", - "outlineColor": "color", - "outlineOpacity": "opacity", - "outlineWidth": "width", - "outlineBlur": "blur", - "outlineOffset": "offset", + "outlineColor": "Color", + "outlineOpacity": "Opacity", + "outlineWidth": "Width", + "outlineBlur": "Blur", + "outlineOffset": "Offset", "strokeGroup": "Stroke", - "strokeColor": "color", - "strokeOpacity": "opacity", - "strokeWidth": "width", + "strokeColor": "Color", + "strokeOpacity": "Opacity", + "strokeWidth": "Width", "advancedActive": "Show Advanced", "advancedGroup": "Advanced", "clippingActive": "clip.active", @@ -1214,8 +1220,11 @@ "convert": "Convert", "downloadProject": "Download Project", "uploadAssets": "Upload Assets", + "uploadFiles": "Upload Files", "search-placeholder": "Search folders", "generatingThumbnails": "Generating Thumbnails ({{count}} remaining)", + "file": "File", + "directory": "Directory", "fileProperties": { "name": "Name:", "type": "Type:", @@ -1244,6 +1253,12 @@ "dateModified": "Date Modified", "size": "Size" } + }, + "image-convert": { + "format": "Format", + "resize": "Resize", + "width": "Width", + "height": "Height" } }, "scene-assets": { diff --git a/packages/client-core/i18n/en/user.json b/packages/client-core/i18n/en/user.json index f149ea41ed..ed22e4b4ba 100755 --- a/packages/client-core/i18n/en/user.json +++ b/packages/client-core/i18n/en/user.json @@ -262,7 +262,8 @@ "userIdCopied": "User ID copied", "apiKeyCopied": "API Key copied", "refreshApiKey": "Refresh API Key", - "privacyPolicy": "Privacy Policy" + "privacyPolicy": "Privacy Policy", + "helpChat": "Help Chat" }, "oauth": { "authenticating": "Authenticating...", diff --git a/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx b/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx index 286eac578b..e55e646d14 100644 --- a/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx +++ b/packages/client-core/src/admin/components/project/ManageUserPermissionModal.tsx @@ -32,22 +32,21 @@ import { PopoverState } from '@etherealengine/client-core/src/common/services/Po import { ProjectService } from '@etherealengine/client-core/src/common/services/ProjectService' import { AuthState } from '@etherealengine/client-core/src/user/services/AuthService' import { userHasAccess } from '@etherealengine/client-core/src/user/userHasAccess' -import { InviteCode, ProjectPermissionType, ProjectType } from '@etherealengine/common/src/schema.type.module' +import { + InviteCode, + ProjectPermissionType, + ProjectType, + projectPermissionPath +} from '@etherealengine/common/src/schema.type.module' import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import Button from '@etherealengine/ui/src/primitives/tailwind/Button' import Input from '@etherealengine/ui/src/primitives/tailwind/Input' import Modal from '@etherealengine/ui/src/primitives/tailwind/Modal' import Text from '@etherealengine/ui/src/primitives/tailwind/Text' import Toggle from '@etherealengine/ui/src/primitives/tailwind/Toggle' -export default function ManageUserPermissionModal({ - project, - projectPermissions -}: { - project: ProjectType - projectPermissions: readonly ProjectPermissionType[] -}) { - console.log('ManageUserPermissionModal', project, projectPermissions) +export default function ManageUserPermissionModal({ project }: { project: ProjectType }) { const { t } = useTranslation() const selfUser = useHookstate(getMutableState(AuthState)).user const userInviteCode = useHookstate('' as InviteCode) @@ -58,6 +57,13 @@ export default function ManageUserPermissionModal({ ? 'owner' : 'user' + const projectPermissionsFindQuery = useFind(projectPermissionPath, { + query: { + projectId: project.id, + paginate: false + } + }) + const handleCreatePermission = async () => { if (!userInviteCode.value) { userInviteCodeError.set(t('admin:components.project.inviteCodeCantEmpty')) @@ -65,6 +71,7 @@ export default function ManageUserPermissionModal({ } try { await ProjectService.createPermission(userInviteCode.value, project.id, 'reviewer') + projectPermissionsFindQuery.refetch() } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } @@ -73,6 +80,7 @@ export default function ManageUserPermissionModal({ const handlePatchPermission = async (permission: ProjectPermissionType) => { try { await ProjectService.patchPermission(permission.id, permission.type === 'owner' ? 'user' : 'owner') + projectPermissionsFindQuery.refetch() } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } @@ -81,6 +89,7 @@ export default function ManageUserPermissionModal({ const handleRemovePermission = async (id: string) => { try { await ProjectService.removePermission(id) + projectPermissionsFindQuery.refetch() } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } @@ -105,7 +114,7 @@ export default function ManageUserPermissionModal({ /> )}
- {projectPermissions?.map((permission) => ( + {projectPermissionsFindQuery.data.map((permission) => (
{permission.userId === selfUser.id.value ? `${permission.user?.name} (you)` : permission.user?.name} @@ -119,7 +128,7 @@ export default function ManageUserPermissionModal({ disabled={ selfUserPermission !== 'owner' || selfUser.id.value === permission.userId || - projectPermissions?.length === 1 + projectPermissionsFindQuery.data.length === 1 } /> + +
+
+ + ) +}) + +export default ZendeskTab diff --git a/packages/client-core/src/common/components/TouchGamepad/index.tsx b/packages/client-core/src/common/components/TouchGamepad/index.tsx index 41221e1490..bcdb2ad938 100755 --- a/packages/client-core/src/common/components/TouchGamepad/index.tsx +++ b/packages/client-core/src/common/components/TouchGamepad/index.tsx @@ -62,7 +62,7 @@ const handleStop = () => { const buttonsConfig: Array<{ button: AnyButton; label: React.ReactElement }> = [ { - button: XRStandardGamepadButton.Trigger, + button: XRStandardGamepadButton.XRStandardGamepadTrigger, label: } ] diff --git a/packages/client-core/src/common/services/FileThumbnailJobState.tsx b/packages/client-core/src/common/services/FileThumbnailJobState.tsx index a29015bd0f..71ef1b3625 100644 --- a/packages/client-core/src/common/services/FileThumbnailJobState.tsx +++ b/packages/client-core/src/common/services/FileThumbnailJobState.tsx @@ -106,13 +106,24 @@ const uploadThumbnail = async (src: string, projectName: string, staticResourceI .replaceAll(/[^a-zA-Z0-9\.\-_\s]/g, '') .replaceAll(/\s/g, '-')}-thumbnail.png` const file = new File([blob], thumbnailKey) - await uploadToFeathersService(fileBrowserUploadPath, [file], { - fileName: file.name, - project: projectName, - path: 'public/thumbnails/' + file.name, - contentType: file.type - }).promise - await Engine.instance.api.service(staticResourcePath).patch(staticResourceId, { thumbnailKey, thumbnailMode }) + const pathname = new URL( + await uploadToFeathersService(fileBrowserUploadPath, [file], { + args: [ + { + fileName: file.name, + project: projectName, + path: 'public/thumbnails/' + file.name, + contentType: file.type, + type: 'thumbnail', + thumbnailKey, + thumbnailMode + } + ] + }).promise + ).pathname + await Engine.instance.api + .service(staticResourcePath) + .patch(staticResourceId, { thumbnailKey: pathname.slice(1), thumbnailMode }) } const seenThumbnails = new Set() diff --git a/packages/client-core/src/common/services/NotificationService.tsx b/packages/client-core/src/common/services/NotificationService.tsx index 8a7015fac6..e2b8d67d0d 100755 --- a/packages/client-core/src/common/services/NotificationService.tsx +++ b/packages/client-core/src/common/services/NotificationService.tsx @@ -27,11 +27,9 @@ import { SnackbarKey, SnackbarProvider, VariantType, closeSnackbar } from 'notis import React, { CSSProperties, Fragment, useEffect, useRef } from 'react' import multiLogger from '@etherealengine/common/src/logger' -import { AudioEffectPlayer } from '@etherealengine/engine/src/audio/systems/MediaSystem' import { defineState, getState, useMutableState } from '@etherealengine/hyperflux' import Icon from '@etherealengine/ui/src/primitives/mui/Icon' -import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton' const logger = multiLogger.child({ component: 'client-core:Notification' }) @@ -51,7 +49,7 @@ export const defaultAction = (key: SnackbarKey, content?: React.ReactNode) => { return ( {content} - closeSnackbar(key)} icon={} /> + closeSnackbar(key)} type={'Close'} /> ) } @@ -67,7 +65,6 @@ export const NotificationService = { } const state = getState(NotificationState) - AudioEffectPlayer.instance.play(AudioEffectPlayer.SOUNDS.alert, 0.5) state.snackbar?.enqueueSnackbar(message, { variant: options.variant, action: NotificationActions[options.actionType ?? 'default'] diff --git a/packages/client-core/src/components/InstanceChat/InstanceChat.skiptest.tsx b/packages/client-core/src/components/InstanceChat/InstanceChat.skiptest.tsx index a09f3e30e8..618e544a5f 100644 --- a/packages/client-core/src/components/InstanceChat/InstanceChat.skiptest.tsx +++ b/packages/client-core/src/components/InstanceChat/InstanceChat.skiptest.tsx @@ -29,8 +29,8 @@ import React from 'react' import { createRoot } from 'react-dom/client' import { ChannelID, MessageID, UserID } from '@etherealengine/common/src/schema.type.module' +import { createEngine } from '@etherealengine/ecs' import { getMutableState } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { InstanceChat } from '.' import { createDOM } from '../../../tests/createDOM' @@ -45,8 +45,8 @@ describe('Instance Chat Component', () => { createDOM() rootContainer = document.createElement('div') document.body.appendChild(rootContainer) - API.instance = createMockAPI() createEngine() + API.instance = createMockAPI() }) afterEach(() => { diff --git a/packages/client-core/src/hooks/useZendesk.ts b/packages/client-core/src/hooks/useZendesk.ts index 1b6335c904..d2c23b679d 100644 --- a/packages/client-core/src/hooks/useZendesk.ts +++ b/packages/client-core/src/hooks/useZendesk.ts @@ -24,7 +24,9 @@ Ethereal Engine. All Rights Reserved. */ import config from '@etherealengine/common/src/config' +import { zendeskPath } from '@etherealengine/common/src/schema.type.module' import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { useMutation } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { useEffect } from 'react' import { AuthState } from '../user/services/AuthService' @@ -35,51 +37,88 @@ declare global { } export const useZendesk = () => { + const zendeskMutation = useMutation(zendeskPath) const user = getMutableState(AuthState).user - const initalized = useHookstate(() => { + const authenticated = useHookstate(false) + const initialized = useHookstate(() => { const zendeskScript = document.getElementById(`ze-snippet`) as HTMLScriptElement return !!zendeskScript }) + const isWidgetVisible = useHookstate(false) - const initalize = () => { - if (initalized.value || !config.client.zendeskKey) return + const authenticateUser = () => { + if (authenticated.value || config.client.zendesk.authenticationEnabled !== 'true') return + + window.zE('messenger', 'loginUser', function (callback: any) { + zendeskMutation.create().then(async (token) => { + authenticated.set(true) + await callback(token) + }) + }) + } + + const initialize = () => { + if (initialized.value || !config.client.zendesk.key) return const script = document.createElement('script') script.id = 'ze-snippet' script.async = true - script.src = `https://static.zdassets.com/ekr/snippet.js?key=${config.client.zendeskKey}` + script.src = `https://static.zdassets.com/ekr/snippet.js?key=${config.client.zendesk.key}` document.body.appendChild(script) - initalized.set(true) + initialized.set(true) + + script.addEventListener('load', () => { + if ('zE' in window) { + hideWidget() + authenticateUser() + } + window.zE('messenger:on', 'close', () => { + hideWidget() + }) + window.zE('messenger:on', 'open', function () { + showWidget() + }) + }) } useEffect(() => { - if (config.client.zendeskEnabled !== 'true') return - - if (!user.isGuest.value && !initalized.value) { - initalize() - } else if (!user.isGuest.value && initalized.value) { - showWidget() - } else if (user.isGuest.value && initalized.value) { - hideWidget() + if (config.client.zendesk.enabled !== 'true') return + + if (!user.isGuest.value && !initialized.value) { + initialize() + } else if (!user.isGuest.value && initialized.value) { + authenticateUser() + } else if (user.isGuest.value && initialized.value) { + closeChat() + window.zE('messenger', 'logoutUser') } }, [user.value]) const hideWidget = () => { - if (initalized.value) return + if (!initialized.value) return window.zE('messenger', 'hide') + isWidgetVisible.set(false) } const showWidget = () => { - if (initalized.value) return + if (!initialized.value) return window.zE('messenger', 'show') + isWidgetVisible.set(true) } const openChat = () => { - if (initalized.value) return + if (!initialized.value) return window.zE('messenger', 'open') } + const closeChat = () => { + if (!initialized.value) return + window.zE('messenger', 'close') + } + return { - initialized: initalized.value, + initialized: initialized.value, + isWidgetVisible: isWidgetVisible.value, hideWidget, showWidget, - openChat + openChat, + closeChat } } diff --git a/packages/client-core/src/networking/NetworkInstanceProvisioning.tsx b/packages/client-core/src/networking/NetworkInstanceProvisioning.tsx index 41dc59518c..4f2b011e9c 100644 --- a/packages/client-core/src/networking/NetworkInstanceProvisioning.tsx +++ b/packages/client-core/src/networking/NetworkInstanceProvisioning.tsx @@ -40,11 +40,11 @@ import { } from '@etherealengine/client-core/src/common/services/MediaInstanceConnectionService' import { ChannelService, ChannelState } from '@etherealengine/client-core/src/social/services/ChannelService' import { LocationState } from '@etherealengine/client-core/src/social/services/LocationService' +import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags' import { InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module' +import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState' import { getMutableState, getState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux' import { NetworkState } from '@etherealengine/network' - -import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState' import { FriendService } from '../social/services/FriendService' import { connectToInstance } from '../transports/SocketWebRTCClientFunctions' import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService' @@ -247,7 +247,7 @@ export const SocialMenus = { export const FriendMenus = () => { const { t } = useTranslation() - const socialsEnabled = FeatureFlagsState.useEnabled('ir.client.menu.social') + const socialsEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Social) useEffect(() => { if (!socialsEnabled) return diff --git a/packages/client-core/src/social/services/LocationService.ts b/packages/client-core/src/social/services/LocationService.ts index 335e7ef50f..bf6e900305 100755 --- a/packages/client-core/src/social/services/LocationService.ts +++ b/packages/client-core/src/social/services/LocationService.ts @@ -30,13 +30,16 @@ import { LocationID, locationPath, LocationType, - UserID + UserID, + userPath } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' -import { defineState, getMutableState } from '@etherealengine/hyperflux' +import { defineState, getMutableState, getState } from '@etherealengine/hyperflux' +import { useEffect } from 'react' import { API } from '../../API' import { NotificationService } from '../../common/services/NotificationService' +import { AuthState } from '../../user/services/AuthService' export const LocationSeed: LocationType = { id: '' as LocationID, @@ -184,5 +187,23 @@ export const LocationService = { } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } + }, + useLocationBanListeners: () => { + useEffect(() => { + const locationBanCreatedListener = async (params) => { + const selfUser = getState(AuthState).user + const currentLocation = getState(LocationState).currentLocation.location + const locationBan = params.locationBan + if (selfUser.id === locationBan.userId && currentLocation.id === locationBan.locationId) { + const userId = selfUser.id ?? '' + const user = await Engine.instance.api.service(userPath).get(userId) + getMutableState(AuthState).merge({ user }) + } + } + Engine.instance.api.service(locationBanPath).on('created', locationBanCreatedListener) + return () => { + Engine.instance.api.service(locationBanPath).off('created', locationBanCreatedListener) + } + }, []) } } diff --git a/packages/client-core/src/systems/AvatarUISystem.tsx b/packages/client-core/src/systems/AvatarUISystem.tsx index 6faf49d821..b0fbfec87b 100644 --- a/packages/client-core/src/systems/AvatarUISystem.tsx +++ b/packages/client-core/src/systems/AvatarUISystem.tsx @@ -133,7 +133,7 @@ const raycastComponentData = { const onSecondaryClick = () => { const { physicsWorld } = getState(PhysicsState) - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position const hits = Physics.castRayFromCamera( diff --git a/packages/client-core/src/systems/WidgetUISystem.tsx b/packages/client-core/src/systems/WidgetUISystem.tsx index 75f83f2b15..c96e8a9fc3 100644 --- a/packages/client-core/src/systems/WidgetUISystem.tsx +++ b/packages/client-core/src/systems/WidgetUISystem.tsx @@ -125,7 +125,7 @@ const execute = () => { const inputSource = getComponent(inputSourceEntity, InputSourceComponent) const keys = inputSource.buttons if (inputSource.source.gamepad?.mapping === 'xr-standard') { - if (keys[XRStandardGamepadButton.ButtonA]?.down) + if (keys[XRStandardGamepadButton.XRStandardGamepadButtonA]?.down) toggleWidgetsMenu(inputSource.source.handedness === 'left' ? 'right' : 'left') } /** @todo allow non HMDs to access the widget menu too */ diff --git a/packages/client-core/src/systems/createAnchorWidget.tsx b/packages/client-core/src/systems/createAnchorWidget.tsx index 81d89d7b45..ed5526297f 100644 --- a/packages/client-core/src/systems/createAnchorWidget.tsx +++ b/packages/client-core/src/systems/createAnchorWidget.tsx @@ -77,7 +77,7 @@ export function createAnchorWidget() { if (inputComponent.source.gamepad?.mapping !== 'xr-standard') continue if (inputComponent.source.handedness !== preferredHand) continue - const buttonInputPressed = inputComponent.buttons[XRStandardGamepadButton.Trigger]?.down + const buttonInputPressed = inputComponent.buttons[XRStandardGamepadButton.XRStandardGamepadTrigger]?.down if (buttonInputPressed) { xrState.scenePlacementMode.set('placed') @@ -86,8 +86,8 @@ export function createAnchorWidget() { const { deltaSeconds } = getState(ECSState) - const xAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.ThumbstickX] - const yAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.ThumbstickY] + const xAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.XRStandardGamepadThumbstickX] + const yAxisInput = inputComponent.source.gamepad.axes[XRStandardGamepadAxes.XRStandardGamepadThumbstickY] const xDelta = xAxisInput * Math.PI * deltaSeconds getMutableState(XRState).sceneRotationOffset.set((currentValue) => currentValue + xDelta) @@ -97,7 +97,7 @@ export function createAnchorWidget() { xrState.sceneScaleTarget.set((currentValue) => MathUtils.clamp(currentValue + yDelta, 0.01, 0.2)) } - const triggerButtonPressed = inputComponent.buttons[XRStandardGamepadButton.Stick]?.down + const triggerButtonPressed = inputComponent.buttons[XRStandardGamepadButton.XRStandardGamepadStick]?.down if (triggerButtonPressed) { xrState.sceneScaleAutoMode.set(!xrState.sceneScaleAutoMode.value) diff --git a/packages/client-core/src/user/UserUISystem.tsx b/packages/client-core/src/user/UserUISystem.tsx index 5431660998..5d59bc5c2d 100644 --- a/packages/client-core/src/user/UserUISystem.tsx +++ b/packages/client-core/src/user/UserUISystem.tsx @@ -30,6 +30,7 @@ import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups' import { getMutableState, none } from '@etherealengine/hyperflux' +import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags' import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState' import { InviteService } from '../social/services/InviteService' import { PopupMenuState } from './components/UserMenu/PopupMenuService' @@ -69,9 +70,9 @@ const reactor = () => { const { t } = useTranslation() InviteService.useAPIListeners() - const emotesEnabled = FeatureFlagsState.useEnabled('ir.client.menu.emote') - const avaturnEnabled = FeatureFlagsState.useEnabled('ir.client.menu.avaturn') - const rpmEnabled = FeatureFlagsState.useEnabled('ir.client.menu.readyPlayerMe') + const emotesEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Emote) + const avaturnEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Avaturn) + const rpmEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.ReadyPlayerMe) useEffect(() => { const FaceRetouchingNatural = lazy(() => import('@mui/icons-material/FaceRetouchingNatural')) diff --git a/packages/client-core/src/user/components/MagicLink/AuthMagicLink.tsx b/packages/client-core/src/user/components/MagicLink/AuthMagicLink.tsx index 578ab57c3e..5748720930 100755 --- a/packages/client-core/src/user/components/MagicLink/AuthMagicLink.tsx +++ b/packages/client-core/src/user/components/MagicLink/AuthMagicLink.tsx @@ -28,12 +28,11 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import { InstanceID } from '@etherealengine/common/src/schema.type.module' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' import Box from '@etherealengine/ui/src/primitives/mui/Box' import Container from '@etherealengine/ui/src/primitives/mui/Container' import Typography from '@etherealengine/ui/src/primitives/mui/Typography' -import { AuthService, AuthState } from '../../services/AuthService' +import { AuthService } from '../../services/AuthService' interface Props { //auth: any @@ -45,7 +44,6 @@ interface Props { const AuthMagicLink = ({ token, type, instanceId, path }: Props): JSX.Element => { const { t } = useTranslation() - const user = useHookstate(getMutableState(AuthState)).user useEffect(() => { if (type === 'login') { let redirectSuccess = path ? `${path}` : null @@ -54,10 +52,6 @@ const AuthMagicLink = ({ token, type, instanceId, path }: Props): JSX.Element => AuthService.loginUserByJwt(token, redirectSuccess || '/', '/') } else if (type === 'connection') { AuthService.loginUserMagicLink(token, '/', '/') - // if (user !== null) { - // AuthService.refreshConnections(user.id.value!) - // } - // window.location.href = '/profile-connections' } }, []) diff --git a/packages/client-core/src/user/components/UserMenu/menus/AvatarModifyMenu.tsx b/packages/client-core/src/user/components/UserMenu/menus/AvatarModifyMenu.tsx index 6c9987da95..7bc5cc159e 100644 --- a/packages/client-core/src/user/components/UserMenu/menus/AvatarModifyMenu.tsx +++ b/packages/client-core/src/user/components/UserMenu/menus/AvatarModifyMenu.tsx @@ -52,6 +52,7 @@ import Grid from '@etherealengine/ui/src/primitives/mui/Grid' import Icon from '@etherealengine/ui/src/primitives/mui/Icon' import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton' +import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags' import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState' import { UserMenus } from '../../../UserUISystem' import { AvatarService } from '../../../services/AvatarService' @@ -84,8 +85,8 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => { const [isSaving, setIsSaving] = useState(false) const avatarRef = useRef(null) const thumbnailRef = useRef(null) - const avaturnEnabled = FeatureFlagsState.useEnabled('ir.client.menu.avaturn') - const rpmEnabled = FeatureFlagsState.useEnabled('ir.client.menu.readyPlayerMe') + const avaturnEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.Avaturn) + const rpmEnabled = FeatureFlagsState.useEnabled(FeatureFlags.Client.Menu.ReadyPlayerMe) let thumbnailSrc = state.thumbnailUrl if (state.thumbnailFile) { 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 ecbda1414b..41d5407223 100755 --- a/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx +++ b/packages/client-core/src/user/components/UserMenu/menus/ProfileMenu.tsx @@ -52,8 +52,10 @@ import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton' import { initialAuthState, initialOAuthConnectedState } from '../../../../common/initialAuthState' import { NotificationService } from '../../../../common/services/NotificationService' +import { useZendesk } from '../../../../hooks/useZendesk' import { useUserAvatarThumbnail } from '../../../functions/useUserAvatarThumbnail' import { AuthService, AuthState } from '../../../services/AuthService' +import { AvatarService } from '../../../services/AvatarService' import { useUserHasAccessHook } from '../../../userHasAccess' import { UserMenus } from '../../../UserUISystem' import styles from '../index.module.scss' @@ -93,6 +95,8 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => { const hasAdminAccess = useUserHasAccessHook('admin:admin') const avatarThumbnail = useUserAvatarThumbnail(userId) + const { initialized, openChat } = useZendesk() + useEffect(() => { if (authSetting) { const temp = { ...initialAuthState } @@ -178,7 +182,7 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => { if (!name) return if (selfUser.name.value.trim() !== name) { // @ts-ignore - AuthService.updateUsername(userId, name) + AvatarService.updateUsername(userId, name) } } const handleInputChange = (e) => emailPhone.set(e.target.value) @@ -418,6 +422,43 @@ const ProfileMenu = ({ hideLogin, onClose, isPopover }: Props): JSX.Element => { onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Settings)} /> )} + {!isGuest && initialized && ( + + + + {t('user:usermenu.profile.helpChat')} + + + } + onClick={openChat} + > + )} { - const selfUser = getState(AuthState).user - const currentLocation = getState(LocationState).currentLocation.location - const locationBan = params.locationBan - if (selfUser.id === locationBan.userId && currentLocation.id === locationBan.locationId) { - const userId = selfUser.id ?? '' - const user = await Engine.instance.api.service(userPath).get(userId) - getMutableState(AuthState).merge({ user }) - } - } - Engine.instance.api.service(userPath).on('patched', userPatchedListener) Engine.instance.api.service(userAvatarPath).on('patched', userAvatarPatchedListener) - Engine.instance.api.service(locationBanPath).on('created', locationBanCreatedListener) return () => { Engine.instance.api.service(userPath).off('patched', userPatchedListener) Engine.instance.api.service(userAvatarPath).off('patched', userAvatarPatchedListener) - Engine.instance.api.service(locationBanPath).off('created', locationBanCreatedListener) } }, []) } diff --git a/packages/client-core/src/user/services/AvatarService.ts b/packages/client-core/src/user/services/AvatarService.ts index e86628fee6..51cf5ad12d 100644 --- a/packages/client-core/src/user/services/AvatarService.ts +++ b/packages/client-core/src/user/services/AvatarService.ts @@ -24,6 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import { Paginated } from '@feathersjs/feathers' +import i18n from 'i18next' import { AvatarID, @@ -31,13 +32,21 @@ import { AvatarType, staticResourcePath, StaticResourceType, - uploadAssetPath + uploadAssetPath, + UserID, + UserName, + userPath, + UserType } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' import { AvatarState as AvatarNetworkState } from '@etherealengine/engine/src/avatar/state/AvatarNetworkState' -import { defineState, getMutableState, getState } from '@etherealengine/hyperflux' +import { defineState, dispatchAction, getMutableState, getState } from '@etherealengine/hyperflux' +import { EntityUUID } from '@etherealengine/ecs' +import { AvatarNetworkAction } from '@etherealengine/engine/src/avatar/state/AvatarNetworkActions' +import { NotificationService } from '../../common/services/NotificationService' import { uploadToFeathersService } from '../../util/upload' +import { AuthState } from './AuthService' export const AVATAR_PAGE_LIMIT = 100 @@ -159,5 +168,14 @@ export const AvatarService = { } catch (err) { return null } + }, + + async updateUsername(userId: UserID, name: UserName) { + const { name: updatedName } = (await Engine.instance.api + .service(userPath) + .patch(userId, { name: name })) as UserType + NotificationService.dispatchNotify(i18n.t('user:usermenu.profile.update-msg'), { variant: 'success' }) + getMutableState(AuthState).user.merge({ name: updatedName }) + dispatchAction(AvatarNetworkAction.setName({ entityUUID: (userId + '_avatar') as EntityUUID, name: updatedName })) } } diff --git a/packages/client-core/src/world/Location.tsx b/packages/client-core/src/world/Location.tsx index 939838ecd5..e8293c6477 100755 --- a/packages/client-core/src/world/Location.tsx +++ b/packages/client-core/src/world/Location.tsx @@ -41,6 +41,7 @@ import { t } from 'i18next' import { StyledEngineProvider } from '@mui/material/styles' import { LoadingCircle } from '../components/LoadingCircle' import { useLoadEngineWithScene, useNetwork } from '../components/World/EngineHooks' +import { LocationService } from '../social/services/LocationService' import { LoadingUISystemState } from '../systems/LoadingUISystem' type Props = { @@ -60,6 +61,7 @@ const LocationPage = ({ online }: Props) => { } AuthService.useAPIListeners() + LocationService.useLocationBanListeners() useLoadEngineWithScene() diff --git a/packages/client-core/tests/user/AuthService.test.ts b/packages/client-core/tests/user/AuthService.test.ts index 13420b3abe..786b93ce15 100644 --- a/packages/client-core/tests/user/AuthService.test.ts +++ b/packages/client-core/tests/user/AuthService.test.ts @@ -25,7 +25,7 @@ Ethereal Engine. All Rights Reserved. // import { AuthState, AuthAction, avatarFetchedReceptor } from '../../src/user/services/AuthService' // make browser globals defined -// import { createEngine } from '@etherealengine/spatial/src/initializeEngine' +// import { createEngine } from '@etherealengine/ecs/src/Engine' // import { getMutableState } from '@etherealengine/hyperflux // import { Downgraded } from '@etherealengine/hyperflux/functions/StateFunctions' diff --git a/packages/client/src/engine.tsx b/packages/client/src/engine.tsx index 8e7f706460..13a086d616 100755 --- a/packages/client/src/engine.tsx +++ b/packages/client/src/engine.tsx @@ -30,12 +30,11 @@ import { API } from '@etherealengine/client-core/src/API' import { BrowserRouter, history } from '@etherealengine/client-core/src/common/services/RouterService' import waitForClientAuthenticated from '@etherealengine/client-core/src/util/wait-for-client-authenticated' import { pipeLogs } from '@etherealengine/common/src/logger' -import { Engine } from '@etherealengine/ecs/src/Engine' -import { getMutableState, getState } from '@etherealengine/hyperflux' +import { Engine, createEngine } from '@etherealengine/ecs/src/Engine' +import { getMutableState } from '@etherealengine/hyperflux' import { EngineState } from '@etherealengine/spatial/src/EngineState' +import { startTimer } from '@etherealengine/spatial/src/startTimer' -import { ECSState } from '@etherealengine/ecs' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import LoadingView from '@etherealengine/ui/src/primitives/tailwind/LoadingView' import { initializei18n } from './util' @@ -45,7 +44,7 @@ const initializeLogs = async () => { } createEngine() -getState(ECSState).timer.start() +startTimer() getMutableState(EngineState).publicPath.set( // @ts-ignore import.meta.env.BASE_URL === '/client/' ? location.origin : import.meta.env.BASE_URL!.slice(0, -1) // remove trailing '/' diff --git a/packages/client/src/pages/AppPage.tsx b/packages/client/src/pages/AppPage.tsx index 8127ecef2e..762ea9216e 100755 --- a/packages/client/src/pages/AppPage.tsx +++ b/packages/client/src/pages/AppPage.tsx @@ -34,7 +34,6 @@ import { useThemeProvider } from '@etherealengine/client-core/src/common/service import Debug from '@etherealengine/client-core/src/components/Debug' import InviteToast from '@etherealengine/client-core/src/components/InviteToast' import { LoadWebappInjection } from '@etherealengine/client-core/src/components/LoadWebappInjection' -import { useZendesk } from '@etherealengine/client-core/src/hooks/useZendesk' import { useAuthenticated } from '@etherealengine/client-core/src/user/services/AuthService' import LoadingView from '@etherealengine/ui/src/primitives/tailwind/LoadingView' @@ -46,8 +45,6 @@ const AppPage = (props: { children: React.ReactNode }) => { const { t } = useTranslation() const isLoggedIn = useAuthenticated() - useZendesk() - useEffect(() => { initGA() logPageView() diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index 3e454210dc..7f834b18e4 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -89,8 +89,12 @@ const client = { key8thWall: globalThis.process.env.VITE_8TH_WALL!, featherStoreKey: globalThis.process.env.VITE_FEATHERS_STORE_KEY, gaMeasurementId: globalThis.process.env.VITE_GA_MEASUREMENT_ID, - zendeskEnabled: globalThis.process.env.VITE_ZENDESK_ENABLED, - zendeskKey: globalThis.process.env.VITE_ZENDESK_KEY + + zendesk: { + enabled: globalThis.process.env.VITE_ZENDESK_ENABLED, + authenticationEnabled: globalThis.process.env.VITE_ZENDESK_AUTHENTICATION_ENABLED, + key: globalThis.process.env.VITE_ZENDESK_KEY + } } /** diff --git a/packages/common/src/constants/FeatureFlags.ts b/packages/common/src/constants/FeatureFlags.ts new file mode 100644 index 0000000000..b40192ded6 --- /dev/null +++ b/packages/common/src/constants/FeatureFlags.ts @@ -0,0 +1,35 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +export const FeatureFlags = { + Client: { + Menu: { + Social: 'ir.client.menu.social', + Emote: 'ir.client.menu.emote', + Avaturn: 'ir.client.menu.avaturn', + ReadyPlayerMe: 'ir.client.menu.readyPlayerMe' + } + } +} diff --git a/packages/common/src/interfaces/ResourcesJson.ts b/packages/common/src/interfaces/ResourcesJson.ts index 800de683ec..c0697dbee2 100644 --- a/packages/common/src/interfaces/ResourcesJson.ts +++ b/packages/common/src/interfaces/ResourcesJson.ts @@ -23,17 +23,16 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +export type ResourceType = { + type: string // 'scene' | 'asset' | 'file' | 'thumbnail' | 'avatar' | 'recording' + tags?: string[] + dependencies?: string[] // other keys + licensing?: string + description?: string + attribution?: string + thumbnailKey?: string + thumbnailMode?: string // 'automatic' | 'manual' +} + // key = /path/to/file.ext -export type ResourcesJson = Record< - string, - { - type: string // 'scene' | 'asset' | 'file' | 'thumbnail' | 'avatar' | 'recording' - tags?: string[] - dependencies?: string[] // other keys - licensing?: string - description?: string - attribution?: string - thumbnailKey?: string - thumbnailMode?: string // 'automatic' | 'manual' - } -> +export type ResourcesJson = Record diff --git a/packages/common/src/schema.type.module.ts b/packages/common/src/schema.type.module.ts index 3859172b57..e9830d93aa 100644 --- a/packages/common/src/schema.type.module.ts +++ b/packages/common/src/schema.type.module.ts @@ -33,6 +33,7 @@ export type * from './schemas/cluster/build-status.schema' export type * from './schemas/cluster/logs-api.schema' export type * from './schemas/cluster/migrations-info.schema' export type * from './schemas/cluster/pods.schema' +export type * from './schemas/integrations/zendesk/zendesk.schema' export type * from './schemas/matchmaking/match-instance.schema' export type * from './schemas/matchmaking/match-user.schema' export type * from './schemas/media/archiver.schema' @@ -80,6 +81,7 @@ export type * from './schemas/setting/project-setting.schema' export type * from './schemas/setting/redis-setting.schema' export type * from './schemas/setting/server-setting.schema' export type * from './schemas/setting/task-server-setting.schema' +export type * from './schemas/setting/zendesk-setting.schema' export type * from './schemas/social/channel-user.schema' export type * from './schemas/social/channel.schema' export type * from './schemas/social/invite-code-lookup.schema' @@ -244,6 +246,8 @@ export const analyticsPath = 'analytics' export const serverSettingPath = 'server-setting' +export const zendeskSettingPath = 'zendesk-setting' + export const scopeTypePath = 'scope-type' export const scopePath = 'scope' @@ -297,3 +301,5 @@ export const uploadAssetPath = 'upload-asset' export const invalidationPath = 'invalidation' export const imageConvertPath = 'image-convert' + +export const zendeskPath = 'zendesk' diff --git a/packages/spatial/src/camera/types/CameraMode.ts b/packages/common/src/schemas/integrations/zendesk/zendesk.schema.ts old mode 100755 new mode 100644 similarity index 89% rename from packages/spatial/src/camera/types/CameraMode.ts rename to packages/common/src/schemas/integrations/zendesk/zendesk.schema.ts index 86f2a19323..054cf38e8f --- a/packages/spatial/src/camera/types/CameraMode.ts +++ b/packages/common/src/schemas/integrations/zendesk/zendesk.schema.ts @@ -23,12 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -/** Camera Modes. */ -export enum CameraMode { - FirstPerson, - ShoulderCam, - ThirdPerson, - TopDown, - Strategic, - Dynamic -} +export const zendeskPath = 'zendesk' + +export const zendeskMethods = ['create'] as const diff --git a/packages/common/src/schemas/setting/feature-flag-setting.schema.ts b/packages/common/src/schemas/setting/feature-flag-setting.schema.ts index 504ee6e061..80d7b77606 100644 --- a/packages/common/src/schemas/setting/feature-flag-setting.schema.ts +++ b/packages/common/src/schemas/setting/feature-flag-setting.schema.ts @@ -25,11 +25,9 @@ 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, StringEnum, Type } from '@feathersjs/typebox' +import { getValidator, querySyntax, Type } from '@feathersjs/typebox' import { dataValidator, queryValidator } from '../validators' -export type FeatureFlag = FeatureFlagSettingType['flagName'] - export const featureFlagSettingPath = 'feature-flag-setting' export const featureFlagSettingMethods = ['find', 'get', 'create', 'patch', 'remove'] as const @@ -40,12 +38,7 @@ export const featureFlagSettingSchema = Type.Object( id: Type.String({ format: 'uuid' }), - flagName: StringEnum([ - 'ir.client.menu.social', - 'ir.client.menu.emote', - 'ir.client.menu.avaturn', - 'ir.client.menu.readyPlayerMe' - ]), + flagName: Type.String(), flagValue: Type.Boolean(), createdAt: Type.String({ format: 'date-time' }), updatedAt: Type.String({ format: 'date-time' }) diff --git a/packages/common/src/schemas/setting/zendesk-setting.schema.ts b/packages/common/src/schemas/setting/zendesk-setting.schema.ts new file mode 100644 index 0000000000..f2646c3386 --- /dev/null +++ b/packages/common/src/schemas/setting/zendesk-setting.schema.ts @@ -0,0 +1,80 @@ +/* +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 type { Static } from '@feathersjs/typebox' +import { getValidator, querySyntax, Type } from '@feathersjs/typebox' + +import { dataValidator, queryValidator } from '../validators' + +export const zendeskSettingPath = 'zendesk-setting' + +export const zendeskSettingMethods = ['find', 'get', 'create', 'patch', 'remove'] as const + +// Main data model schema +export const zendeskSettingSchema = Type.Object( + { + id: Type.String({ + format: 'uuid' + }), + name: Type.String(), + secret: Type.String(), + kid: Type.String(), + createdAt: Type.String({ format: 'date-time' }), + updatedAt: Type.String({ format: 'date-time' }) + }, + { $id: 'ZendeskSetting', additionalProperties: false } +) +export interface ZendeskSettingType extends Static {} + +// Schema for creating new entries +export const zendeskSettingDataSchema = Type.Pick(zendeskSettingSchema, ['name', 'secret', 'kid'], { + $id: 'ZendeskSettingData' +}) +export interface ZendeskSettingData extends Static {} + +// Schema for updating existing entries +export const zendeskSettingPatchSchema = Type.Partial(zendeskSettingSchema, { + $id: 'ZendeskSettingPatch' +}) +export interface ZendeskSettingPatch extends Static {} + +// Schema for allowed query properties +export const zendeskSettingQueryProperties = Type.Pick(zendeskSettingSchema, ['id', 'name']) + +export const zendeskSettingQuerySchema = Type.Intersect( + [ + querySyntax(zendeskSettingQueryProperties), + // Add additional query properties here + Type.Object({}, { additionalProperties: false }) + ], + { additionalProperties: false } +) +export interface ZendeskSettingQuery extends Static {} + +export const zendeskSettingValidator = /* @__PURE__ */ getValidator(zendeskSettingSchema, dataValidator) +export const zendeskSettingDataValidator = /* @__PURE__ */ getValidator(zendeskSettingDataSchema, dataValidator) +export const zendeskSettingPatchValidator = /* @__PURE__ */ getValidator(zendeskSettingPatchSchema, dataValidator) +export const zendeskSettingQueryValidator = /* @__PURE__ */ getValidator(zendeskSettingQuerySchema, queryValidator) diff --git a/packages/spatial/src/camera/functions/switchCameraMode.ts b/packages/common/src/utils/getAllStringValueNodes.ts similarity index 53% rename from packages/spatial/src/camera/functions/switchCameraMode.ts rename to packages/common/src/utils/getAllStringValueNodes.ts index f23e9b733a..5e9c2bed61 100644 --- a/packages/spatial/src/camera/functions/switchCameraMode.ts +++ b/packages/common/src/utils/getAllStringValueNodes.ts @@ -23,37 +23,23 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Entity } from '@etherealengine/ecs/src/Entity' - -import { FollowCameraComponent } from '../components/FollowCameraComponent' -import { CameraMode } from '../types/CameraMode' - -type SwitchCameraModeProps = { - cameraMode: CameraMode - pointerLock?: boolean -} - -let changeTimeout: any = undefined -export const switchCameraMode = ( - cameraEntity: Entity, - args: SwitchCameraModeProps = { pointerLock: false, cameraMode: CameraMode.ThirdPerson }, - force = false -): void => { - if (!force) { - if (changeTimeout !== undefined) return - changeTimeout = setTimeout(() => { - clearTimeout(changeTimeout) - changeTimeout = undefined - }, 250) +/** + * Method used to get all leaf node strings from an object. + * https://stackoverflow.com/a/63100031/2077741 + * @param obj + * @returns + */ +export function getAllStringValueNodes(obj: any) { + if (typeof obj === 'string') { + return [obj] } - const cameraFollow = getOptionalComponent(cameraEntity, FollowCameraComponent) - if (!cameraFollow) return - cameraFollow.mode = args.cameraMode - - if (cameraFollow.mode === CameraMode.FirstPerson) { - cameraFollow.phi = 0 - cameraFollow.locked = false + // handle wrong types and null + if (typeof obj !== 'object' || !obj) { + return [] } + + return Object.keys(obj).reduce((acc, key) => { + return [...acc, ...getAllStringValueNodes(obj[key])] + }, []) } diff --git a/packages/ecs/src/ComponentFunctions.test.tsx b/packages/ecs/src/ComponentFunctions.test.tsx index 10dc044678..a9535e7a0f 100644 --- a/packages/ecs/src/ComponentFunctions.test.tsx +++ b/packages/ecs/src/ComponentFunctions.test.tsx @@ -39,14 +39,14 @@ import { useComponent, useOptionalComponent } from './ComponentFunctions' -import { destroyEngine, startEngine } from './Engine' +import { createEngine, destroyEngine } from './Engine' import { Entity, EntityUUID, UndefinedEntity } from './Entity' import { createEntity, removeEntity } from './EntityFunctions' import { UUIDComponent } from './UUIDComponent' describe('ComponentFunctions', async () => { beforeEach(() => { - startEngine() + createEngine() ComponentMap.clear() }) @@ -309,7 +309,7 @@ describe('ComponentFunctions Hooks', async () => { let counter = 0 beforeEach(() => { - startEngine() + createEngine() ComponentMap.clear() testEntity = createEntity() }) @@ -352,7 +352,7 @@ describe('ComponentFunctions Hooks', async () => { let counter = 0 beforeEach(() => { - startEngine() + createEngine() ComponentMap.clear() testEntity = createEntity() }) @@ -402,7 +402,7 @@ describe('ComponentFunctions Hooks', async () => { it('returns different data when the entity is changed', async () => { // Initialize the isolated case - startEngine() + createEngine() ComponentMap.clear() // Initialize the dummy data @@ -447,7 +447,7 @@ describe('ComponentFunctions Hooks', async () => { it('suspense should work', async () => { // Initialize the isolated case - startEngine() + createEngine() ComponentMap.clear() // Initialize the dummy data diff --git a/packages/ecs/src/ComponentFunctions.ts b/packages/ecs/src/ComponentFunctions.ts index cb6fd970e6..e3c9f000a2 100755 --- a/packages/ecs/src/ComponentFunctions.ts +++ b/packages/ecs/src/ComponentFunctions.ts @@ -508,6 +508,7 @@ export function _use(promise) { * Use a component in a reactive context (a React component) */ export function useComponent>(entity: Entity, Component: C) { + if (entity === UndefinedEntity) throw new Error('InvalidUsage: useComponent called with UndefinedEntity') if (!Component.stateMap[entity]) Component.stateMap[entity] = hookstate(none) const componentState = Component.stateMap[entity]! // use() will suspend the component (by throwing a promise) and resume when the promise is resolved diff --git a/packages/ecs/src/Engine.ts b/packages/ecs/src/Engine.ts index a8f0032abb..3a8464df67 100755 --- a/packages/ecs/src/Engine.ts +++ b/packages/ecs/src/Engine.ts @@ -83,7 +83,7 @@ export class Engine { globalThis.Engine = Engine globalThis.Hyperflux = Hyperflux -export function startEngine() { +export function createEngine() { if (Engine.instance) throw new Error('Store already exists') Engine.instance = new Engine() Engine.instance.store = bitECS.createWorld( diff --git a/packages/ecs/src/QueryFunctions.test.tsx b/packages/ecs/src/QueryFunctions.test.tsx index d0e048770d..94126acdd0 100644 --- a/packages/ecs/src/QueryFunctions.test.tsx +++ b/packages/ecs/src/QueryFunctions.test.tsx @@ -28,7 +28,7 @@ import assert from 'assert' import React, { useEffect } from 'react' import { ComponentMap, defineComponent, hasComponent, removeComponent, setComponent } from './ComponentFunctions' -import { destroyEngine, startEngine } from './Engine' +import { createEngine, destroyEngine } from './Engine' import { Entity, UndefinedEntity } from './Entity' import { createEntity, removeEntity } from './EntityFunctions' import { Query, defineQuery, useQuery } from './QueryFunctions' @@ -72,7 +72,7 @@ describe('QueryFunctions', () => { let entity2 = UndefinedEntity beforeEach(() => { - startEngine() + createEngine() entity1 = createEntity() entity2 = createEntity() }) @@ -128,7 +128,7 @@ describe('QueryFunctions Hooks', async () => { let counter = 0 beforeEach(() => { - startEngine() + createEngine() entity1 = createEntity() entity2 = createEntity() }) diff --git a/packages/ecs/src/SystemFunctions.test.ts b/packages/ecs/src/SystemFunctions.test.ts index a47e9b4578..57852d19dc 100755 --- a/packages/ecs/src/SystemFunctions.test.ts +++ b/packages/ecs/src/SystemFunctions.test.ts @@ -30,7 +30,7 @@ import { defineState, getMutableState } from '@etherealengine/hyperflux' import { ECS } from '..' import { ECSState } from './ECSState' -import { destroyEngine, startEngine } from './Engine' +import { createEngine, destroyEngine } from './Engine' import { defineSystem } from './SystemFunctions' import { SimulationSystemGroup } from './SystemGroups' @@ -51,7 +51,7 @@ const MockSystem = defineSystem({ describe('SystemFunctions', () => { beforeEach(() => { - startEngine() + createEngine() }) afterEach(() => { diff --git a/packages/ecs/src/Timer.ts b/packages/ecs/src/Timer.ts index 6830c6729b..b931be706d 100755 --- a/packages/ecs/src/Timer.ts +++ b/packages/ecs/src/Timer.ts @@ -181,6 +181,7 @@ export function Timer(update: TimerUpdateCallback, serverTickRate = 60) { } return { + animation, start: start, stop: stop, clear: clear diff --git a/packages/ecs/src/UUIDComponent.test.tsx b/packages/ecs/src/UUIDComponent.test.tsx index d20b482b88..8883ffabf2 100644 --- a/packages/ecs/src/UUIDComponent.test.tsx +++ b/packages/ecs/src/UUIDComponent.test.tsx @@ -36,7 +36,7 @@ import { serializeComponent, setComponent } from './ComponentFunctions' -import { destroyEngine, startEngine } from './Engine' +import { createEngine, destroyEngine } from './Engine' import { Entity, EntityUUID, UndefinedEntity } from './Entity' import { createEntity, removeEntity } from './EntityFunctions' import { UUIDComponent } from './UUIDComponent' @@ -48,7 +48,7 @@ describe('UUIDComponent', () => { let entity2 = UndefinedEntity beforeEach(() => { - startEngine() + createEngine() ComponentMap.clear() entity1 = createEntity() entity2 = createEntity() @@ -199,7 +199,7 @@ describe('UUIDComponent Hooks', async () => { let counter = 0 beforeEach(() => { - startEngine() + createEngine() ComponentMap.clear() entity1 = createEntity() entity2 = createEntity() diff --git a/packages/ecs/tests/ecs.test.ts b/packages/ecs/tests/ecs.test.ts index 2adf59aab7..a61499dcb6 100644 --- a/packages/ecs/tests/ecs.test.ts +++ b/packages/ecs/tests/ecs.test.ts @@ -37,7 +37,7 @@ import { removeComponent, setComponent } from '../src/ComponentFunctions' -import { destroyEngine, startEngine } from '../src/Engine' +import { createEngine, destroyEngine } from '../src/Engine' import { Entity } from '../src/Entity' import { AnimationSystemGroup } from '../src/SystemGroups' @@ -82,7 +82,7 @@ const MockSystem = ECS.defineSystem({ describe('ECS', () => { beforeEach(() => { - startEngine() + createEngine() }) afterEach(() => { diff --git a/packages/editor/src/components/Editor2Container.tsx b/packages/editor/src/components/Editor2Container.tsx index 97a4d0cf72..97b8dd19a0 100644 --- a/packages/editor/src/components/Editor2Container.tsx +++ b/packages/editor/src/components/Editor2Container.tsx @@ -52,9 +52,13 @@ import { SaveSceneDialog } from './dialogs/SaveSceneDialog2' import { DndWrapper } from './dnd/DndWrapper' import DragLayer from './dnd/DragLayer' +import { useZendesk } from '@etherealengine/client-core/src/hooks/useZendesk' import { EntityUUID } from '@etherealengine/ecs' import { EngineState } from '@etherealengine/spatial/src/EngineState' +import Button from '@etherealengine/ui/src/primitives/tailwind/Button' import 'rc-dock/dist/rc-dock.css' +import { useTranslation } from 'react-i18next' +import { IoHelpCircleOutline } from 'react-icons/io5' import { setCurrentEditorScene } from '../functions/sceneFunctions' import './Editor2Container.css' @@ -126,6 +130,9 @@ const EditorContainer = () => { const viewerEntity = useMutableState(EngineState).viewerEntity.value + const { isWidgetVisible, openChat } = useZendesk() + const { t } = useTranslation() + useEffect(() => { const scene = sceneQuery[0] if (!scene || !viewerEntity) return @@ -177,6 +184,18 @@ const EditorContainer = () => { + {!isWidgetVisible && ( + + )} ) } diff --git a/packages/editor/src/components/EditorContainer.tsx b/packages/editor/src/components/EditorContainer.tsx index 4bdb5f8528..81be1d1b40 100755 --- a/packages/editor/src/components/EditorContainer.tsx +++ b/packages/editor/src/components/EditorContainer.tsx @@ -221,7 +221,7 @@ const onImportAsset = async () => { if (projectName) { try { - await inputFileWithAddToScene({ projectName }) + await inputFileWithAddToScene({ projectName, directoryPath: 'projects/' + projectName + '/assets/' }) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } diff --git a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx index e35f0e4934..6cbef20824 100644 --- a/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx +++ b/packages/editor/src/components/assets/FileBrowser/FileBrowserContentPanel.tsx @@ -75,7 +75,6 @@ import { ToolButton } from '../../toolbar/ToolButton' import { AssetSelectionChangePropsType } from '../AssetsPreviewPanel' import ImageCompressionPanel from '../ImageCompressionPanel' import ImageConvertPanel from '../ImageConvertPanel' -import ModelCompressionPanel from '../ModelCompressionPanel' import styles from '../styles.module.scss' import { FileBrowserItem, FileTableWrapper, canDropItemOverFolder } from './FileBrowserGrid' import { FilesViewModeSettings, FilesViewModeState, availableTableColumns } from './FileBrowserState' @@ -251,9 +250,13 @@ const FileBrowserContentPanel: React.FC = (props) try { const name = processFileName(file.name) await uploadToFeathersService(fileBrowserUploadPath, [file], { - project: projectName, - path: relativePath + name, - contentType: file.type + args: [ + { + project: projectName, + path: relativePath + name, + contentType: file.type + } + ] }).promise } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -638,7 +641,7 @@ const FileBrowserContentPanel: React.FC = (props) { - await inputFileWithAddToScene({ directoryPath: selectedDirectory.value }) + await inputFileWithAddToScene({ projectName, directoryPath: selectedDirectory.value }) .then(refreshDirectory) .catch((err) => { NotificationService.dispatchNotify(err.message, { variant: 'error' }) @@ -688,14 +691,6 @@ const FileBrowserContentPanel: React.FC = (props) /> )} - {openCompress.value && fileProperties.value && fileConsistsOfContentType(fileProperties.value, 'model') && ( - - )} - {openCompress.value && fileProperties.value && fileConsistsOfContentType(fileProperties.value, 'image') && ( { const entity = createEntity() @@ -125,15 +124,13 @@ export const createLODVariants = async ( const variant = createTempEntity('LOD Variant', result) setComponent(variant, ModelComponent, { src: modelSrc }) setComponent(variant, VariantComponent, { - levels: lods.map((lod, lodIndex) => { - return { - src: `${LoaderUtils.extractUrlBase(lod.params.src)}${lod.params.dst}.${lod.params.modelFormat}`, - metadata: { - ...lod.variantMetadata, - ...transformMetadata[lodIndex] - } + levels: lods.map((lod, lodIndex) => ({ + src: `${LoaderUtils.extractUrlBase(lod.params.src)}${lod.params.dst}.${lod.params.modelFormat}`, + metadata: { + ...lod.variantMetadata, + ...transformMetadata[lodIndex] } - }), + })), heuristic }) @@ -143,86 +140,86 @@ export const createLODVariants = async ( } export default function ModelCompressionPanel({ - openCompress, - fileProperties, - onRefreshDirectory + selectedFile, + refreshDirectory }: { - openCompress: State - fileProperties: State - onRefreshDirectory: () => Promise + selectedFile: FileType + refreshDirectory: () => Promise }) { - const [compressionLoading, setCompressionLoading] = useState(false) - const [isClientside, setIsClientSide] = useState(true) - const [isIntegratedPrefab, setIsIntegratedPrefab] = useState(true) - const [selectedLODIndex, setSelectedLODIndex] = useState(0) - const [modalOpen, setModalOpen] = useState(false) - const [selectedPreset, setSelectedPreset] = useState(defaultParams) - const [presetList, setPresetList] = useState(LODList) + const { t } = useTranslation() + const compressionLoading = useHookstate(false) + const selectedLODIndex = useHookstate(0) + const selectedPreset = useHookstate(defaultParams) + const presetList = useHookstate(structuredClone(LODList)) useEffect(() => { const presets = localStorage.getItem('presets') if (presets !== null) { - setPresetList(JSON.parse(presets)) + presetList.set(JSON.parse(presets)) } }, []) const lods = useHookstate([]) const compressContentInBrowser = async () => { - setCompressionLoading(true) + compressionLoading.set(true) await compressModel() - await onRefreshDirectory() - setCompressionLoading(false) - openCompress.set(false) + await refreshDirectory() + compressionLoading.set(false) } const applyPreset = (preset: ModelTransformParameters) => { - setSelectedPreset(preset) - setModalOpen(true) + selectedPreset.set(JSON.parse(JSON.stringify(preset))) + PopoverState.showPopupover( + + ) } const confirmPreset = () => { - const lod = lods[selectedLODIndex].get(NO_PROXY) + const lod = lods[selectedLODIndex.value].get(NO_PROXY) const src = lod.params.src const dst = lod.params.dst const modelFormat = lod.params.modelFormat const uri = lod.params.resourceUri - const presetParams = JSON.parse(JSON.stringify(selectedPreset)) as ModelTransformParameters + const presetParams = JSON.parse(JSON.stringify(selectedPreset.value)) as ModelTransformParameters presetParams.src = src presetParams.dst = dst presetParams.modelFormat = modelFormat presetParams.resourceUri = uri - lods[selectedLODIndex].params.set(presetParams) - - setModalOpen(false) + lods[selectedLODIndex.value].params.set(presetParams) } - const savePresetList = (deleting: boolean) => { - if (!deleting) { - setPresetList([...presetList, lods[selectedLODIndex].value as LODVariantDescriptor]) - } - localStorage.setItem('presets', JSON.stringify(presetList)) + const savePresetList = () => { + presetList.merge([JSON.parse(JSON.stringify(lods[selectedLODIndex.value].value))]) + localStorage.setItem('presets', JSON.stringify(presetList.value)) } const compressModel = async () => { - const modelSrc = fileProperties.url.value - const clientside = isClientside - const exportCombined = isIntegratedPrefab + const clientside = true + const exportCombined = true const heuristic = Heuristic.BUDGET await createLODVariants(lods.value as LODVariantDescriptor[], clientside, heuristic, exportCombined) } - const deletePreset = (idx: number) => { - const newList = [...presetList] - newList.splice(idx, 1) - setPresetList(newList) + const deletePreset = (event: React.MouseEvent, idx: number) => { + event.stopPropagation() + presetList[idx].set(none) + // presetList.set(presetList.value.filter((_, i) => i !== idx)) + localStorage.setItem('presets', JSON.stringify(presetList.value)) + } + + const handleRemoveLOD = (idx: number) => { + lods.set((currentLods) => currentLods.filter((_, i) => i !== idx)) + if (selectedLODIndex.value >= lods.length) { + selectedLODIndex.set(lods.length - 1) + } } useEffect(() => { - const fullSrc = fileProperties.url.value + const fullSrc = selectedFile.url const fileName = fullSrc.split('/').pop()!.split('.').shift()! const defaults = defaultLODs.map((defaultLOD) => { @@ -235,16 +232,12 @@ export default function ModelCompressionPanel({ }) lods.set(defaults) - }, [fileProperties.url]) + }, [selectedFile.url]) - const handleLODSelect = (index) => { - setSelectedLODIndex(Math.min(index, lods.length - 1)) - } - - const handleLODAdd = () => { - const params = JSON.parse(JSON.stringify(lods[selectedLODIndex].params.value)) as ModelTransformParameters + const handleAddLOD = () => { + const params = JSON.parse(JSON.stringify(lods[selectedLODIndex.value].params.value)) as ModelTransformParameters const suffix = '-LOD' + lods.length - params.dst = params.dst.replace(lods[selectedLODIndex].suffix.value, suffix) + params.dst = params.dst.replace(lods[selectedLODIndex.value].suffix.value, suffix) lods.merge([ { params: params, @@ -252,130 +245,93 @@ export default function ModelCompressionPanel({ variantMetadata: {} } ]) - setSelectedLODIndex(lods.length - 1) - } - - const handleLodRemove = () => { - lods.set((lods) => { - lods.pop() - return lods - }) - setSelectedLODIndex(Math.min(selectedLODIndex, lods.length - 1)) + selectedLODIndex.set(lods.length - 1) } return ( - openCompress.set(false)} - showCloseButton={true} - maxWidth={'lg'} - header={fileProperties.value.name} - actions={ - <> - {!compressionLoading ? ( - + {selectedLODIndex.value !== index && ( + + + +
+ {presetList.value.map((lodItem: LODVariantDescriptor, index) => ( + + ))} + +
+ +
+ +
+ +
+ {compressionLoading.value ? ( + ) : ( - + )} - - } - > -
- -
-
LOD Levels
- - {lods.map((lod, lodIndex) => ( - - handleLODSelect(lodIndex)} - > - - {lods.length > 1 && lodIndex == lods.length - 1 && ( - } - > - )} - - - ))} - -
- handleLODAdd()} - icon={} - > -
-
-
- - - {t('editor:properties.model.transform.compress') as string} - - <> - {}} /> - - { - // isClientside.set(val) - }} - disabled={true} - /> - - - { - setIsIntegratedPrefab(val) - }} - /> - - - - - - LOD Presets - - - - {presetList.map((lodItem: LODVariantDescriptor, idx) => ( - - applyPreset(lodItem.params)}> - {lodItem.params.dst} - - {!LODList.find((l) => l.params.dst === lodItem.params.dst) && ( - deletePreset(idx)}>x - )} - - ))} - - - - +
- - - Would you like to apply this preset? - {selectedPreset.dst} - - - - - - -
+ ) } diff --git a/packages/editor/src/components/assets/SceneAssetsPanel.tsx b/packages/editor/src/components/assets/SceneAssetsPanel.tsx index f3fab087b4..d55a18ef61 100644 --- a/packages/editor/src/components/assets/SceneAssetsPanel.tsx +++ b/packages/editor/src/components/assets/SceneAssetsPanel.tsx @@ -35,7 +35,7 @@ import { useTranslation } from 'react-i18next' import { staticResourcePath, StaticResourceType } from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' -import { getState, NO_PROXY, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { getState, NO_PROXY, useHookstate } from '@etherealengine/hyperflux' import { DockContainer } from '../EditorContainer' import StringInput from '../inputs/StringInput' @@ -45,7 +45,6 @@ import { FileIcon } from './FileBrowser/FileIcon' import { AssetsPanelCategories } from './AssetsPanelCategories' -import { EditorState } from '../../services/EditorServices' import styles from './styles.module.scss' const ResourceFile = ({ resource }: { resource: StaticResourceType }) => { @@ -124,7 +123,6 @@ const SceneAssetsPanel = () => { const searchText = useHookstate('') const searchTimeoutCancelRef = useRef<(() => void) | null>(null) const searchedStaticResources = useHookstate([]) - const { projectName } = useMutableState(EditorState) const AssetCategory = useCallback( (props: { @@ -191,8 +189,7 @@ const SceneAssetsPanel = () => { const query = { key: { $like: `%${searchText.value}%` }, $sort: { mimeType: 1 }, - $limit: 10000, - project: projectName.value! + $limit: 10000 } if (selectedCategory.value) { diff --git a/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx b/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx index e64e262849..0b49e06785 100644 --- a/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx +++ b/packages/editor/src/components/properties/CameraPropertiesNodeEditor.tsx @@ -31,7 +31,7 @@ import { getOptionalComponent, useComponent } from '@etherealengine/ecs/src/Comp import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { CameraSettingsComponent } from '@etherealengine/engine/src/scene/components/CameraSettingsComponent' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' import { iterateEntityNode } from '@etherealengine/spatial/src/transform/components/EntityTree' @@ -46,28 +46,29 @@ import { commitProperties, commitProperty, EditorComponentType, updateProperty } const cameraModeSelect = [ { label: 'First Person', - value: CameraMode.FirstPerson + value: FollowCameraMode.FirstPerson }, { label: 'Shoulder Cam', - value: CameraMode.ShoulderCam + value: FollowCameraMode.ShoulderCam }, { label: 'Third Person', - value: CameraMode.ThirdPerson + value: FollowCameraMode.ThirdPerson }, { label: 'Top Down', - value: CameraMode.TopDown - }, - { - label: 'Strategic', - value: CameraMode.Strategic - }, - { - label: 'Dynamic', - value: CameraMode.Dynamic + value: FollowCameraMode.TopDown } + // These are not currently defined or implemented: + // { + // label: 'Strategic', + // value: FollowCameraMode.Strategic + // }, + // { + // label: 'Dynamic', + // value: FollowCameraMode.Dynamic + // } ] /** Types copied from Camera Modes of engine. */ diff --git a/packages/editor/src/components/properties/GLTFTransformProperties.tsx b/packages/editor/src/components/properties/GLTFTransformProperties.tsx index 342e163ad4..5dc0066741 100644 --- a/packages/editor/src/components/properties/GLTFTransformProperties.tsx +++ b/packages/editor/src/components/properties/GLTFTransformProperties.tsx @@ -23,406 +23,271 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { t } from 'i18next' -import React, { useCallback } from 'react' +import React from 'react' -import { - ImageTransformParameters, - ModelTransformParameters -} from '@etherealengine/engine/src/assets/classes/ModelTransform' +import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' import { State } from '@etherealengine/hyperflux' +import Accordion from '@etherealengine/ui/src/primitives/tailwind/Accordion' +import Checkbox from '@etherealengine/ui/src/primitives/tailwind/Checkbox' +import Input from '@etherealengine/ui/src/primitives/tailwind/Input' +import Select from '@etherealengine/ui/src/primitives/tailwind/Select' +import Text from '@etherealengine/ui/src/primitives/tailwind/Text' +import { useTranslation } from 'react-i18next' +import { HiMinus, HiPlusSmall } from 'react-icons/hi2' +import { twMerge } from 'tailwind-merge' -import BooleanInput from '../inputs/BooleanInput' -import InputGroup from '../inputs/InputGroup' -import NumericInput from '../inputs/NumericInput' -import NumericInputGroup from '../inputs/NumericInputGroup' -import ParameterInput from '../inputs/ParameterInput' -import SelectInput from '../inputs/SelectInput' -import StringInput from '../inputs/StringInput' -import CollapsibleBlock from '../layout/CollapsibleBlock' -import PaginatedList from '../layout/PaginatedList' +function CheckBoxParam({ label, state }: { label: string; state: State }) { + return ( +
+
+ {label} +
+ +
+ { + state.set((v) => !v) + }} + /> +
+
+ ) +} + +function TextParam({ + label, + state, + parseFunction = (value: string) => value +}: { + label: string + state: State + parseFunction?: (value: string) => string | number +}) { + return ( +
+
+ + {label} + +
+ +
+ { + state.set(parseFunction(e.target.value)) + }} + /> +
+
+ ) +} export default function GLTFTransformProperties({ - transformParms, - onChange + transformParms }: { transformParms: State - onChange: (transformParms: ModelTransformParameters) => void }) { - const onChangeTransformParm = useCallback((scope: State) => { - return (value: typeof scope.value) => { - scope.set(value) - onChange(JSON.parse(JSON.stringify(transformParms.value))) - } - }, []) - - const onChangeTransformStringParm = useCallback((scope: State) => { - return (value: string) => { - scope.set(value) - } - }, []) - - const onChangeParameter = useCallback( - (scope: State, key: string) => (val: any) => { - scope[key].set(val) - onChange(JSON.parse(JSON.stringify(transformParms.value))) - }, - [] - ) + const { t } = useTranslation() return ( - <> -
- - - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - {transformParms.join.enabled.value && ( - <> - - - )} - - - - {transformParms.palette.enabled.value && ( - <> - +
+
+ + {t('editor:properties.model.transform.dst')} + + + {t('editor:properties.model.transform.resourceUri')} + +
+
+ { + transformParms.dst.set(e.target.value) + }} + className="px-2 py-0.5 font-['Figtree'] text-sm text-[#9CA0AA]" /> - - )} - - - - - - - - - - - {transformParms.weld.enabled.value && ( - <> - { + transformParms.resourceUri.set(e.target.value) + }} + className="px-2 py-0.5 font-['Figtree'] text-sm text-[#9CA0AA]" /> - - )} - - - - - +
+ + } + shrinkIcon={} + titleFontSize="sm" + className="mb-2 rounded bg-theme-highlight p-2" + > +
+
+ + {t('editor:properties.model.transform.textureFormat')} + +
+ +
+ { + // @ts-ignore + transformParms.textureCompressionType.set(value) + }} + currentValue={transformParms.textureCompressionType.value} /> - - - {transformParms.textureCompressionType.value === 'uastc' && ( - <> - - - - - )} - {transformParms.textureCompressionType.value === 'etc1' && ( - <> - - - - - - )} - - )} - - ) => { - return ( - <> -
- - - -
- {image.enabled.value && ( -
- - - {image.parameters.textureFormat.enabled.value && ( - - )} - - - - {image.parameters.maxTextureSize.enabled.value && ( - - )} - - - - {image.parameters.textureCompressionType.enabled.value && ( - - )} - - - - {image.parameters.textureCompressionQuality.enabled.value && ( - - )} - - - - {image.parameters.flipY.enabled.value && ( - - )} - -
- )} - - ) - }} +
+
+ + - -
- + + + + + + + + + + + + + + + } + shrinkIcon={} + titleFontSize="sm" + className="mb-2 rounded bg-theme-highlight p-2" + > + + + + + + + + + + + + } + shrinkIcon={} + titleFontSize="sm" + className="mb-2 rounded bg-theme-highlight p-2" + > + + + + + + + } + shrinkIcon={} + titleFontSize="sm" + className="mb-2 rounded bg-theme-highlight p-2" + > + + + + ) ) } diff --git a/packages/editor/src/components/properties/ModelTransformProperties.tsx b/packages/editor/src/components/properties/ModelTransformProperties.tsx index eca7062802..94b6b5b024 100644 --- a/packages/editor/src/components/properties/ModelTransformProperties.tsx +++ b/packages/editor/src/components/properties/ModelTransformProperties.tsx @@ -211,10 +211,7 @@ export default function ModelTransformProperties({ entity, onChangeModel }: { en
- {}} - /> + {!transforming.value && ( <> diff --git a/packages/editor/src/components/toolbar/Toolbar2.tsx b/packages/editor/src/components/toolbar/Toolbar2.tsx index b6c649f3e7..21ce492c18 100644 --- a/packages/editor/src/components/toolbar/Toolbar2.tsx +++ b/packages/editor/src/components/toolbar/Toolbar2.tsx @@ -52,7 +52,7 @@ const onImportAsset = async () => { if (projectName) { try { - await inputFileWithAddToScene({ projectName }) + await inputFileWithAddToScene({ projectName, directoryPath: 'projects/' + projectName + '/assets/' }) } catch (err) { NotificationService.dispatchNotify(err.message, { variant: 'error' }) } diff --git a/packages/editor/src/functions/EditorControlFunctions.test.tsx b/packages/editor/src/functions/EditorControlFunctions.test.tsx index d51c6568df..6e0bc35ad3 100644 --- a/packages/editor/src/functions/EditorControlFunctions.test.tsx +++ b/packages/editor/src/functions/EditorControlFunctions.test.tsx @@ -29,7 +29,7 @@ import { Cache, Color, MathUtils } from 'three' import { UserID } from '@etherealengine/common/src/schema.type.module' import { getComponent, UUIDComponent } from '@etherealengine/ecs' -import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' import { EntityUUID } from '@etherealengine/ecs/src/Entity' import { GLTFSnapshotState, GLTFSourceState } from '@etherealengine/engine/src/gltf/GLTFState' import { SourceComponent } from '@etherealengine/engine/src/scene/components/SourceComponent' @@ -37,7 +37,6 @@ import { SplineComponent } from '@etherealengine/engine/src/scene/components/Spl import { applyIncomingActions, getMutableState, getState } from '@etherealengine/hyperflux' import { HemisphereLightComponent, TransformComponent } from '@etherealengine/spatial' import { EngineState } from '@etherealengine/spatial/src/EngineState' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' diff --git a/packages/editor/src/functions/EditorControlFunctions.ts b/packages/editor/src/functions/EditorControlFunctions.ts index c3b5cd13e5..cddc905970 100644 --- a/packages/editor/src/functions/EditorControlFunctions.ts +++ b/packages/editor/src/functions/EditorControlFunctions.ts @@ -197,6 +197,7 @@ const modifyMaterial = (nodes: string[], materialId: EntityUUID, properties: { [ material[k] = v } }) + material.needsUpdate = true } } const overwriteLookdevObject = ( diff --git a/packages/editor/src/functions/assetFunctions.ts b/packages/editor/src/functions/assetFunctions.ts index 3f4a3b3ea8..16b0e123d8 100644 --- a/packages/editor/src/functions/assetFunctions.ts +++ b/packages/editor/src/functions/assetFunctions.ts @@ -53,8 +53,8 @@ export const inputFileWithAddToScene = async ({ projectName, directoryPath }: { - projectName?: string - directoryPath?: string + projectName: string + directoryPath: string }): Promise => new Promise((resolve, reject) => { const el = document.createElement('input') @@ -103,9 +103,13 @@ export const inputFileWithAddToScene = async ({ files.map( (file) => uploadToFeathersService(fileBrowserUploadPath, [file], { - project: projectName, - path: directoryPath.replace('projects/' + projectName + '/', '') + file.name, - contentType: file.type + args: [ + { + project: projectName, + path: directoryPath.replace('projects/' + projectName + '/', '') + file.name, + contentType: file.type + } + ] }).promise ) ) @@ -142,7 +146,9 @@ export const uploadProjectFiles = (projectName: string, files: File[], paths: st uploadToFeathersService( fileBrowserUploadPath, [file], - { project: projectName, path: filePath, contentType: '' }, + { + args: [{ project: projectName, path: filePath, contentType: '' }] + }, onProgress ) ) diff --git a/packages/editor/src/functions/gizmoHelper.ts b/packages/editor/src/functions/gizmoHelper.ts index 4445ab86cb..7e4a1511a6 100644 --- a/packages/editor/src/functions/gizmoHelper.ts +++ b/packages/editor/src/functions/gizmoHelper.ts @@ -483,7 +483,7 @@ export function controlUpdate(gizmoEntity: Entity) { function pointerHover(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -509,7 +509,7 @@ function pointerHover(gizmoEntity) { function pointerDown(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -768,7 +768,7 @@ function applyPivotRotation(entity, pivotToOriginMatrix, originToPivotMatrix, ro function pointerMove(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) const gizmoControlComponent = getMutableComponent(gizmoEntity, TransformGizmoControlComponent) @@ -901,7 +901,7 @@ function pointerMove(gizmoEntity) { function pointerUp(gizmoEntity) { // TODO support gizmos in multiple viewports - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointer = getComponent(inputPointerEntity, InputPointerComponent) diff --git a/packages/editor/src/systems/ClickPlacementSystem.tsx b/packages/editor/src/systems/ClickPlacementSystem.tsx index b2334a3011..3404d1d9d4 100644 --- a/packages/editor/src/systems/ClickPlacementSystem.tsx +++ b/packages/editor/src/systems/ClickPlacementSystem.tsx @@ -241,7 +241,7 @@ export const ClickPlacementSystem = defineSystem({ execute: () => { const editorHelperState = getState(EditorHelperState) if (editorHelperState.placementMode !== PlacementMode.CLICK) return - const clickState = getState(ClickPlacementState) + const clickState = getMutableState(ClickPlacementState) const placementEntity = clickState.placementEntity if (!placementEntity) return @@ -262,7 +262,7 @@ export const ClickPlacementSystem = defineSystem({ let targetIntersection: { point: Vector3; normal: Vector3 } | null = null const viewerEntity = Engine.instance.viewerEntity - const mouseEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) + const mouseEntity = InputPointerComponent.getPointersForCamera(viewerEntity)[0] if (!mouseEntity) return const buttons = InputComponent.getMergedButtons(viewerEntity) @@ -271,14 +271,14 @@ export const ClickPlacementSystem = defineSystem({ const zoom = axes[MouseScroll.VerticalScroll] if (buttons.SecondaryClick?.pressed) { - clickState.maxDistance -= zoom + clickState.maxDistance.set(clickState.maxDistance.value - zoom) } if (buttons.KeyE?.up) { - clickState.yawOffset += Math.PI / 4 + clickState.yawOffset.set(clickState.yawOffset.value + Math.PI / 4) } if (buttons.KeyQ?.up) { - clickState.yawOffset -= Math.PI / 4 + clickState.yawOffset.set(clickState.yawOffset.value - Math.PI / 4) } if (buttons.PrimaryClick?.up) { clickListener() @@ -292,7 +292,7 @@ export const ClickPlacementSystem = defineSystem({ 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) { + if (physicsIntersection && physicsIntersection.toi < clickState.maxDistance.value) { const intersectPosition = cameraPosition .clone() .add(cameraDirection.clone().multiplyScalar(physicsIntersection.toi)) @@ -311,7 +311,7 @@ export const ClickPlacementSystem = defineSystem({ //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 (intersected.distance > clickState.maxDistance.value) continue if (isPlacementDescendant(intersected.object.entity)) continue targetIntersection = { point: intersected.point, @@ -321,16 +321,16 @@ export const ClickPlacementSystem = defineSystem({ } if (!targetIntersection) { - const point = cameraPosition.clone().add(cameraDirection.clone().multiplyScalar(clickState.maxDistance)) + const point = cameraPosition.clone().add(cameraDirection.clone().multiplyScalar(clickState.maxDistance.value)) 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) + new Euler(clickState.pitchOffset.value, clickState.yawOffset.value, clickState.rollOffset.value) ) rotation = offset.multiply(rotation) - setComponent(placementEntity, TransformComponent, { position, rotation }) + setComponent(placementEntity.value, TransformComponent, { position, rotation }) } }) diff --git a/packages/editor/src/systems/EditorControlSystem.ts b/packages/editor/src/systems/EditorControlSystem.ts index 7a2f8e2277..f2848c24a0 100644 --- a/packages/editor/src/systems/EditorControlSystem.ts +++ b/packages/editor/src/systems/EditorControlSystem.ts @@ -26,7 +26,7 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { Intersection, Layers, Object3D, Raycaster } from 'three' -import { PresentationSystemGroup, UndefinedEntity, UUIDComponent } from '@etherealengine/ecs' +import { Entity, PresentationSystemGroup, UndefinedEntity, UUIDComponent } from '@etherealengine/ecs' import { getComponent, getMutableComponent, @@ -51,12 +51,8 @@ import { InputComponent } from '@etherealengine/spatial/src/input/components/Inp import { InputSourceComponent } from '@etherealengine/spatial/src/input/components/InputSourceComponent' import { InfiniteGridComponent } from '@etherealengine/spatial/src/renderer/components/InfiniteGridHelper' import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' -import { - EntityTreeComponent, - getAncestorWithComponent -} from '@etherealengine/spatial/src/transform/components/EntityTree' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' -import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' import { EngineState } from '@etherealengine/spatial/src/EngineState' import { InputState } from '@etherealengine/spatial/src/input/state/InputState' import { TransformGizmoControlComponent } from '../classes/TransformGizmoControlComponent' @@ -249,6 +245,35 @@ const findIntersectObjects = (object: Object3D, excludeObjects?: Object3D[], exc } } +const findTopLevelParent = (entity: Entity) => { + while ( + getOptionalComponent( + getOptionalComponent(entity, EntityTreeComponent)?.parentEntity || UndefinedEntity, + EntityTreeComponent + )?.parentEntity + ) { + entity = getComponent(entity, EntityTreeComponent).parentEntity! + } + return entity +} + +const findNextSelectionEntity = (topLevelParent: Entity, child: Entity): Entity => { + // Check for adjacent child + const childTree = getComponent(child, EntityTreeComponent) + const parentTree = getComponent(childTree.parentEntity, EntityTreeComponent) + if (topLevelParent !== child) { + const children = parentTree.children + const currentChildIndex = children.findIndex((entity) => child === entity) + if (children.length > currentChildIndex + 1) return children[currentChildIndex + 1] + } + + // Otherwise if child has children traverse down + if (childTree.children.length) return childTree.children[0] + + if (childTree.parentEntity === topLevelParent || parentTree.parentEntity === topLevelParent) return topLevelParent + return findNextSelectionEntity(topLevelParent, parentTree.parentEntity) +} + const inputQuery = defineQuery([InputSourceComponent]) let clickStartEntity = UndefinedEntity @@ -290,14 +315,31 @@ const execute = () => { } if (buttons.PrimaryClick?.pressed) { + let closestIntersection = { + entity: UndefinedEntity, + distance: Infinity + } if (buttons.PrimaryClick?.down) { - clickStartEntity = InputSourceComponent.getClosestIntersectedEntity(inputSources[0]) - while ( - !hasComponent(clickStartEntity, SourceComponent) && - getOptionalComponent(clickStartEntity, EntityTreeComponent)?.parentEntity - ) { - clickStartEntity = getComponent(clickStartEntity, EntityTreeComponent).parentEntity! + for (const inputSourceEntity of inputSources) { + const intersection = InputSourceComponent.getClosestIntersection(inputSourceEntity) + if (intersection && intersection.distance < closestIntersection.distance) { + closestIntersection = intersection + } } + + // Get top most parent entity that isn't the scene entity + const selectedParentEntity = findTopLevelParent(closestIntersection.entity) + // If entity is already selected set closest intersection, otherwise set top parent + clickStartEntity = selectedParentEntity === clickStartEntity ? closestIntersection.entity : selectedParentEntity + + /** @todo decide how we want selection to work with heirarchies */ + // Walks object heirarchy everytime a selected object is clicked again + // const prevParentEntity = findTopLevelParent(clickStartEntity) + // if (selectedParentEntity === prevParentEntity) { + // clickStartEntity = findNextSelectionEntity(prevParentEntity, clickStartEntity) + // } else { + // clickStartEntity = selectedParentEntity + // } } const capturingEntity = getState(InputState).capturingEntity if (capturingEntity !== UndefinedEntity && capturingEntity !== clickStartEntity) { @@ -307,13 +349,13 @@ const execute = () => { if (buttons.PrimaryClick?.up && !buttons.PrimaryClick?.dragging) { if (hasComponent(clickStartEntity, SourceComponent) && !getState(ClickPlacementState).placementEntity) { const selectedEntities = SelectionState.getSelectedEntities() - const modelComponent = getAncestorWithComponent(clickStartEntity, ModelComponent) - const ancestorModelEntity = modelComponent || clickStartEntity - const targetSelection = selectedEntities[0] === ancestorModelEntity ? clickStartEntity : ancestorModelEntity //only update selection if the selection actually changed (prevents unnecessarily creating new transform gizmos in edit mode) - if (selectedEntities.length !== 1 || (selectedEntities.length === 1 && selectedEntities[0] !== targetSelection)) { - SelectionState.updateSelection([getComponent(targetSelection, UUIDComponent)]) + if ( + selectedEntities.length !== 1 || + (selectedEntities.length === 1 && selectedEntities[0] !== clickStartEntity) + ) { + SelectionState.updateSelection([getComponent(clickStartEntity, UUIDComponent)]) } } } diff --git a/packages/engine/src/FeatureFlagsState.tsx b/packages/engine/src/FeatureFlagsState.tsx index 0d1ce4c2eb..c85b4f784c 100644 --- a/packages/engine/src/FeatureFlagsState.tsx +++ b/packages/engine/src/FeatureFlagsState.tsx @@ -23,19 +23,19 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { FeatureFlag, featureFlagSettingPath } from '@etherealengine/common/src/schema.type.module' +import { featureFlagSettingPath } from '@etherealengine/common/src/schema.type.module' import { defineState, getMutableState, useHookstate } from '@etherealengine/hyperflux/functions/StateFunctions' import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks' import { useEffect } from 'react' export const FeatureFlagsState = defineState({ name: 'ee.engine.FeatureFlagsState', - initial: {} as Record, - enabled(flagName: FeatureFlag) { + initial: {} as Record, + enabled(flagName: string) { const state = getMutableState(FeatureFlagsState)[flagName].value return typeof state === 'boolean' ? state : true }, - useEnabled(flagName: FeatureFlag) { + useEnabled(flagName: string) { const state = useHookstate(getMutableState(FeatureFlagsState)[flagName]).value return typeof state === 'boolean' ? state : true }, diff --git a/packages/engine/src/assets/classes/AssetLoader.test.ts b/packages/engine/src/assets/classes/AssetLoader.test.ts index 91a9ef207d..ad1b23b25d 100644 --- a/packages/engine/src/assets/classes/AssetLoader.test.ts +++ b/packages/engine/src/assets/classes/AssetLoader.test.ts @@ -29,8 +29,7 @@ import Sinon from 'sinon' // hack to make tests happy import '../../EngineModule' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { AssetExt, AssetType } from '@etherealengine/common/src/constants/AssetType' import { AssetLoader } from './AssetLoader' diff --git a/packages/engine/src/assets/functions/createGLTFLoader.ts b/packages/engine/src/assets/functions/createGLTFLoader.ts index b7267c3355..2ba03fed14 100644 --- a/packages/engine/src/assets/functions/createGLTFLoader.ts +++ b/packages/engine/src/assets/functions/createGLTFLoader.ts @@ -61,7 +61,7 @@ export const createGLTFLoader = (keepMaterials = false) => { if (isClient || keepMaterials) { loader.register((parser) => new GPUInstancingExtension(parser)) loader.register((parser) => new HubsLightMapExtension(parser)) - loader.register((parser) => new EEMaterialImporterExtension(parser)) + loader.registerFirst((parser) => new EEMaterialImporterExtension(parser)) } else { loader.register((parser) => new RemoveMaterialsExtension(parser)) } diff --git a/packages/engine/src/assets/functions/resourceLoaderFunctions.test.tsx b/packages/engine/src/assets/functions/resourceLoaderFunctions.test.tsx index 35d38c8abd..a747565abc 100644 --- a/packages/engine/src/assets/functions/resourceLoaderFunctions.test.tsx +++ b/packages/engine/src/assets/functions/resourceLoaderFunctions.test.tsx @@ -26,8 +26,8 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' import { createEntity, destroyEngine } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getState } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { ResourceManager, ResourceState, diff --git a/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx b/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx index 39a8310d42..fef1c79a37 100644 --- a/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx +++ b/packages/engine/src/assets/functions/resourceLoaderHooks.test.tsx @@ -28,8 +28,8 @@ import assert from 'assert' import React, { useEffect } from 'react' import { createEntity, destroyEngine } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getState, useHookstate } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { ResourceState } from '@etherealengine/spatial/src/resources/ResourceState' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.ts b/packages/engine/src/assets/loaders/gltf/GLTFLoader.ts index 0ff0f17778..41d9da87c6 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.ts +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.ts @@ -248,6 +248,14 @@ export class GLTFLoader extends Loader { return this } + registerFirst(callback) { + if (this.pluginCallbacks.indexOf(callback) === -1) { + this.pluginCallbacks.unshift(callback) + } + + return this + } + register(callback) { if (this.pluginCallbacks.indexOf(callback) === -1) { this.pluginCallbacks.push(callback) diff --git a/packages/engine/src/assets/state/ResourceLoadingManager.test.tsx b/packages/engine/src/assets/state/ResourceLoadingManager.test.tsx index 2ef482ae60..86565a80ab 100644 --- a/packages/engine/src/assets/state/ResourceLoadingManager.test.tsx +++ b/packages/engine/src/assets/state/ResourceLoadingManager.test.tsx @@ -27,8 +27,8 @@ import assert from 'assert' import { LoadingManager } from 'three' import { createEntity, destroyEngine } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getState } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { ResourceState, ResourceType } from '@etherealengine/spatial/src/resources/ResourceState' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' diff --git a/packages/engine/src/avatar/AvatarModule.ts b/packages/engine/src/avatar/AvatarModule.ts index 9a7299a4cf..9c64b60e4a 100644 --- a/packages/engine/src/avatar/AvatarModule.ts +++ b/packages/engine/src/avatar/AvatarModule.ts @@ -28,7 +28,6 @@ import { AvatarState } from './state/AvatarNetworkState' import { AnimationSystem } from './systems/AnimationSystem' import { AvatarAnimationSystem } from './systems/AvatarAnimationSystem' import { AvatarAutopilotSystem } from './systems/AvatarAutopilotSystem' -import { AvatarCameraInputSystem } from './systems/AvatarCameraInputSystem' import { AvatarControllerSystem } from './systems/AvatarControllerSystem' import { AvatarInputSystem } from './systems/AvatarInputSystem' import { AvatarLoadingSystem } from './systems/AvatarLoadingSystem' @@ -42,7 +41,6 @@ export default { AnimationSystem, AvatarAnimationSystem, AvatarAutopilotSystem, - AvatarCameraInputSystem, AvatarControllerSystem, AvatarIKTargetState, AvatarInputSystem, diff --git a/packages/engine/src/avatar/components/AvatarControllerComponent.ts b/packages/engine/src/avatar/components/AvatarControllerComponent.ts index e4662de174..f95a58fc3a 100755 --- a/packages/engine/src/avatar/components/AvatarControllerComponent.ts +++ b/packages/engine/src/avatar/components/AvatarControllerComponent.ts @@ -48,6 +48,8 @@ import { CameraComponent } from '../../../../spatial/src/camera/components/Camer import { setAvatarColliderTransform } from '../functions/spawnAvatarReceptor' import { AvatarComponent } from './AvatarComponent' +export const eyeOffset = 0.25 + export const AvatarControllerComponent = defineComponent({ name: 'AvatarControllerComponent', @@ -109,7 +111,8 @@ export const AvatarControllerComponent = defineComponent({ const cameraEntity = avatarControllerComponent.cameraEntity.value if (cameraEntity && entityExists(cameraEntity) && hasComponent(cameraEntity, FollowCameraComponent)) { const cameraComponent = getComponent(cameraEntity, FollowCameraComponent) - cameraComponent.offset.set(0, avatarComponent.eyeHeight.value, 0) + cameraComponent.firstPersonOffset.set(0, avatarComponent.eyeHeight.value, eyeOffset) + cameraComponent.thirdPersonOffset.set(0, avatarComponent.eyeHeight.value, 0) } }, [avatarComponent.avatarHeight, camera.near]) diff --git a/packages/engine/src/avatar/functions/autopilotFunctions.ts b/packages/engine/src/avatar/functions/autopilotFunctions.ts index 78d2a40e2c..b463afbbc1 100644 --- a/packages/engine/src/avatar/functions/autopilotFunctions.ts +++ b/packages/engine/src/avatar/functions/autopilotFunctions.ts @@ -65,7 +65,7 @@ export const autopilotSetPosition = (entity: Entity) => { const { physicsWorld } = getState(PhysicsState) - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(Engine.instance.viewerEntity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(Engine.instance.viewerEntity)[0] if (!inputPointerEntity) return const pointerPosition = getComponent(inputPointerEntity, InputPointerComponent).position diff --git a/packages/engine/src/avatar/functions/avatarFunctions.test.ts b/packages/engine/src/avatar/functions/avatarFunctions.test.ts index bde9b1ba5e..60e367f908 100644 --- a/packages/engine/src/avatar/functions/avatarFunctions.test.ts +++ b/packages/engine/src/avatar/functions/avatarFunctions.test.ts @@ -33,7 +33,7 @@ // import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' // import { addComponent, getComponent, setComponent } from '@etherealengine/ecs/srcComponentFunctions' // import { createEntity } from '@etherealengine/ecs/srcEntityFunctions' -// import { createEngine } from '../../initializeEngine' +// import { createEngine } from '@etherealengine/ecs/src/Engine' // import { GroupComponent } from '@etherealengine/spatial/src/renderer/components/GroupComponent' // import { setTransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' // import { AnimationManager } from '../AnimationManager' diff --git a/packages/engine/src/avatar/functions/moveAvatar.test.tsx b/packages/engine/src/avatar/functions/moveAvatar.test.tsx index 6dc5f671f0..25ba5f3445 100644 --- a/packages/engine/src/avatar/functions/moveAvatar.test.tsx +++ b/packages/engine/src/avatar/functions/moveAvatar.test.tsx @@ -32,12 +32,12 @@ import { AvatarID, UserID } from '@etherealengine/common/src/schema.type.module' import { Entity, EntityUUID, SystemDefinitions, UUIDComponent } from '@etherealengine/ecs' import { getComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' +import { Engine, createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { applyIncomingActions, dispatchAction, getMutableState, getState } from '@etherealengine/hyperflux' import { Network, NetworkPeerFunctions, NetworkState, NetworkWorldUserStateSystem } from '@etherealengine/network' import { createMockNetwork } from '@etherealengine/network/tests/createMockNetwork' import { EventDispatcher } from '@etherealengine/spatial/src/common/classes/EventDispatcher' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' diff --git a/packages/engine/src/avatar/functions/spawnAvatarReceptor.test.tsx b/packages/engine/src/avatar/functions/spawnAvatarReceptor.test.tsx index c98afa4618..90ba23ff98 100644 --- a/packages/engine/src/avatar/functions/spawnAvatarReceptor.test.tsx +++ b/packages/engine/src/avatar/functions/spawnAvatarReceptor.test.tsx @@ -31,12 +31,12 @@ import { Quaternion, Vector3 } from 'three' import { AvatarID, UserID } from '@etherealengine/common/src/schema.type.module' import { Entity, EntityUUID, SystemDefinitions, UUIDComponent } from '@etherealengine/ecs' import { getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' +import { Engine, createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { ReactorReconciler, applyIncomingActions, dispatchAction, getMutableState } from '@etherealengine/hyperflux' import { Network, NetworkPeerFunctions, NetworkState, NetworkWorldUserStateSystem } from '@etherealengine/network' import { createMockNetwork } from '@etherealengine/network/tests/createMockNetwork' import { EventDispatcher } from '@etherealengine/spatial/src/common/classes/EventDispatcher' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { RigidBodyComponent, diff --git a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts index 67d1f187b4..0e2a2b15b8 100644 --- a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts +++ b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts @@ -63,8 +63,7 @@ import { proxifyParentChildRelationships } from '../../scene/functions/loadGLTFM import { AnimationComponent } from '../components/AnimationComponent' import { AvatarAnimationComponent, AvatarRigComponent } from '../components/AvatarAnimationComponent' import { AvatarComponent } from '../components/AvatarComponent' -import { AvatarColliderComponent, AvatarControllerComponent } from '../components/AvatarControllerComponent' -import { eyeOffset } from '../systems/AvatarTransparencySystem' +import { AvatarColliderComponent, AvatarControllerComponent, eyeOffset } from '../components/AvatarControllerComponent' export const spawnAvatarReceptor = (entityUUID: EntityUUID) => { const entity = UUIDComponent.getEntityByUUID(entityUUID) diff --git a/packages/engine/src/avatar/systems/AvatarInputSystem.ts b/packages/engine/src/avatar/systems/AvatarInputSystem.ts index e554cb0acf..42c41ec8d4 100755 --- a/packages/engine/src/avatar/systems/AvatarInputSystem.ts +++ b/packages/engine/src/avatar/systems/AvatarInputSystem.ts @@ -62,20 +62,11 @@ import { AvatarComponent } from '../components/AvatarComponent' import { applyInputSourcePoseToIKTargets } from '../functions/applyInputSourcePoseToIKTargets' import { setIkFootTarget } from '../functions/avatarFootHeuristics' -const _quat = new Quaternion() +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { getThumbstickOrThumbpadAxes } from '@etherealengine/spatial/src/input/functions/getThumbstickOrThumbpadAxes' -/** - * On 'xr-standard' mapping, get thumbstick input [2,3], fallback to thumbpad input [0,1] - * On 'standard' mapping, get thumbstick input [0,1] - */ -export function getThumbstickOrThumbpadAxes(inputSource: XRInputSource, handedness: XRHandedness, deadZone = 0.05) { - const gamepad = inputSource.gamepad - const axes = gamepad!.axes - const axesIndex = inputSource.gamepad?.mapping === 'xr-standard' || handedness === 'right' ? 2 : 0 - const xAxis = Math.abs(axes[axesIndex]) > deadZone ? axes[axesIndex] : 0 - const zAxis = Math.abs(axes[axesIndex + 1]) > deadZone ? axes[axesIndex + 1] : 0 - return [xAxis, zAxis] as [number, number] -} +const _quat = new Quaternion() export const InputSourceAxesDidReset = new WeakMap() @@ -244,14 +235,16 @@ const execute = () => { controller.gamepadLocalInput.set(0, 0, 0) const viewerEntity = Engine.instance.viewerEntity - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(viewerEntity) + + const inputPointerEntity = InputPointerComponent.getPointersForCamera(viewerEntity)[0] + if (!inputPointerEntity && !xrState.session) return const buttons = InputComponent.getMergedButtons(viewerEntity) if (buttons.ShiftLeft?.down) onShiftLeft() - const gamepadJump = buttons[StandardGamepadButton.ButtonA]?.down + const gamepadJump = buttons[StandardGamepadButton.StandardGamepadButtonA]?.down //** touch input (only for avatar jump)*/ const doubleClicked = isCameraAttachedToAvatar ? false : getAvatarDoubleClick(buttons) @@ -259,15 +252,22 @@ const execute = () => { const keyDeltaX = (buttons.KeyA?.pressed ? -1 : 0) + (buttons.KeyD?.pressed ? 1 : 0) + - (buttons[StandardGamepadButton.DPadLeft]?.pressed ? -1 : 0) + - (buttons[StandardGamepadButton.DPadRight]?.pressed ? 1 : 0) + (buttons[StandardGamepadButton.StandardGamepadDPadLeft]?.pressed ? -1 : 0) + + (buttons[StandardGamepadButton.StandardGamepadDPadRight]?.pressed ? 1 : 0) const keyDeltaZ = (buttons.KeyW?.pressed ? -1 : 0) + (buttons.KeyS?.pressed ? 1 : 0) + (buttons.ArrowUp?.pressed ? -1 : 0) + (buttons.ArrowDown?.pressed ? 1 : 0) + - (buttons[StandardGamepadButton.DPadUp]?.pressed ? -1 : 0) + - (buttons[StandardGamepadButton.DPadDown]?.pressed ? -1 : 0) + (buttons[StandardGamepadButton.StandardGamepadDPadUp]?.pressed ? -1 : 0) + + (buttons[StandardGamepadButton.StandardGamepadDPadDown]?.pressed ? -1 : 0) + + if (keyDeltaZ === 1) { + // todo: auto-adjust target distance in follow camera system based on target velocity + const follow = getOptionalComponent(controller.cameraEntity, FollowCameraComponent) + if (follow?.mode === FollowCameraMode.ThirdPerson || follow?.mode === FollowCameraMode.ShoulderCam) + follow.targetDistance = Math.max(follow.targetDistance, follow.effectiveMaxDistance * 0.5) + } controller.gamepadLocalInput.set(keyDeltaX, 0, keyDeltaZ).normalize() diff --git a/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx b/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx index 81202402fb..8f388ae826 100644 --- a/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx +++ b/packages/engine/src/avatar/systems/AvatarTransparencySystem.tsx @@ -50,7 +50,6 @@ import React, { useEffect } from 'react' import { SourceComponent } from '../../scene/components/SourceComponent' import { useModelSceneID } from '../../scene/functions/loaders/ModelFunctions' import { AvatarComponent } from '../components/AvatarComponent' -import { AvatarHeadDecapComponent } from '../components/AvatarIKComponents' const headDithering = 0 const cameraDithering = 1 @@ -85,25 +84,12 @@ const execute = () => { } } -export const eyeOffset = 0.25 - export const AvatarTransparencySystem = defineSystem({ uuid: 'AvatarTransparencySystem', execute, insert: { with: PresentationSystemGroup }, reactor: () => { const selfEid = AvatarComponent.useSelfAvatarEntity() - const hasDecapComponent = !!useOptionalComponent(selfEid, AvatarHeadDecapComponent) - const hasFollowCamera = !!useOptionalComponent(Engine.instance.viewerEntity, FollowCameraComponent) - useEffect(() => { - const followCamera = getOptionalComponent(Engine.instance.viewerEntity, FollowCameraComponent) - if (!followCamera) return - const prevOffsetZ = followCamera.offset.z - followCamera.offset.setZ(eyeOffset) - return () => { - followCamera.offset.setZ(prevOffsetZ) - } - }, [hasFollowCamera, hasDecapComponent, selfEid]) const sceneInstanceID = useModelSceneID(selfEid) const childEntities = useHookstate(SourceComponent.entitiesBySourceState[sceneInstanceID]) diff --git a/packages/engine/src/gltf/GLTFState.test.tsx b/packages/engine/src/gltf/GLTFState.test.tsx index bed1140b02..67e3d191c4 100644 --- a/packages/engine/src/gltf/GLTFState.test.tsx +++ b/packages/engine/src/gltf/GLTFState.test.tsx @@ -28,11 +28,10 @@ import assert from 'assert' import { Cache, Color, Euler, MathUtils, Matrix4, Quaternion, Vector3 } from 'three' import { defineComponent, EntityUUID, getComponent, UUIDComponent } from '@etherealengine/ecs' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { applyIncomingActions, dispatchAction, getMutableState, getState } from '@etherealengine/hyperflux' import { HemisphereLightComponent, TransformComponent } from '@etherealengine/spatial' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' diff --git a/packages/engine/src/interaction/functions/createUI.ts b/packages/engine/src/interaction/functions/createUI.ts index ca9f169a69..0148babac1 100755 --- a/packages/engine/src/interaction/functions/createUI.ts +++ b/packages/engine/src/interaction/functions/createUI.ts @@ -52,7 +52,7 @@ export function createUI(entity: Entity, uiMessage: string, isInteractable = tru color: new Color('#B9B9B9'), transmission: 1, roughness: 0.5, - opacity: 0.95, + opacity: 1, transparent: true, side: DoubleSide }) diff --git a/packages/engine/src/interaction/systems/GrabbableSystem.test.ts b/packages/engine/src/interaction/systems/GrabbableSystem.test.ts index 7911cca3d9..cc010dcae0 100644 --- a/packages/engine/src/interaction/systems/GrabbableSystem.test.ts +++ b/packages/engine/src/interaction/systems/GrabbableSystem.test.ts @@ -30,7 +30,7 @@ import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { AvatarID, UserID } from '@etherealengine/common/src/schema.type.module' import { Entity, EntityUUID, UUIDComponent } from '@etherealengine/ecs' import { getComponent, hasComponent, removeComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' +import { Engine, createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' import { PeerID, @@ -40,7 +40,6 @@ import { getMutableState } from '@etherealengine/hyperflux' import { NetworkObjectComponent, NetworkPeerFunctions, NetworkState } from '@etherealengine/network' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { ColliderComponent } from '@etherealengine/spatial/src/physics/components/ColliderComponent' import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' diff --git a/packages/engine/src/scene/components/CameraSettingsComponent.ts b/packages/engine/src/scene/components/CameraSettingsComponent.ts index b77fbd6d57..03f1ebdfa5 100755 --- a/packages/engine/src/scene/components/CameraSettingsComponent.ts +++ b/packages/engine/src/scene/components/CameraSettingsComponent.ts @@ -47,7 +47,8 @@ export const CameraSettingsComponent = defineComponent({ if (typeof json.cameraNearClip === 'number') component.cameraNearClip.set(json.cameraNearClip) if (typeof json.cameraFarClip === 'number') component.cameraFarClip.set(json.cameraFarClip) if (typeof json.projectionType === 'number') component.projectionType.set(json.projectionType) - if (typeof json.minCameraDistance === 'number') component.minCameraDistance.set(json.minCameraDistance) + if (typeof json.minCameraDistance === 'number') + component.minCameraDistance.set(Math.max(json.minCameraDistance, 1.5)) if (typeof json.maxCameraDistance === 'number') component.maxCameraDistance.set(json.maxCameraDistance) if (typeof json.startCameraDistance === 'number') component.startCameraDistance.set(json.startCameraDistance) if (typeof json.cameraMode === 'number') component.cameraMode.set(json.cameraMode) diff --git a/packages/engine/src/scene/components/LinkComponent.ts b/packages/engine/src/scene/components/LinkComponent.ts index adc2bbb82d..eea99f3ce7 100755 --- a/packages/engine/src/scene/components/LinkComponent.ts +++ b/packages/engine/src/scene/components/LinkComponent.ts @@ -32,7 +32,6 @@ import { Entity } from '@etherealengine/ecs/src/Entity' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { defineState, getMutableState, getState, matches } from '@etherealengine/hyperflux' import { setCallback } from '@etherealengine/spatial/src/common/CallbackComponent' -import { XRStandardGamepadButton } from '@etherealengine/spatial/src/input/state/ButtonState' import { XRState } from '@etherealengine/spatial/src/xr/XRState' import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' @@ -49,7 +48,7 @@ const linkLogic = (linkComponent, xrState) => { const linkCallback = (linkEntity: Entity) => { const linkComponent = getComponent(linkEntity, LinkComponent) const buttons = InputComponent.getMergedButtons(linkEntity) - if (buttons[XRStandardGamepadButton.Trigger]?.down) { + if (buttons.XRStandardGamepadTrigger?.down) { const xrState = getState(XRState) linkLogic(linkComponent, xrState) } else { diff --git a/packages/engine/src/scene/components/MediaComponent.ts b/packages/engine/src/scene/components/MediaComponent.ts index 8f7e07db0c..d6e4064c53 100644 --- a/packages/engine/src/scene/components/MediaComponent.ts +++ b/packages/engine/src/scene/components/MediaComponent.ts @@ -448,8 +448,8 @@ export function MediaReactor() { if (isHLS(path)) { setupHLS(entity, path).then((hls) => { mediaElementState.hls.set(hls) + mediaElementState.hls.value!.attachMedia(mediaElementState.element.value as HTMLMediaElement) }) - mediaElementState.hls.value!.attachMedia(mediaElementState.element.value as HTMLMediaElement) } else { mediaElementState.element.src.set(path) } diff --git a/packages/engine/src/scene/functions/loadGLTFModel.ts b/packages/engine/src/scene/functions/loadGLTFModel.ts index 0580f49567..73501d0f87 100644 --- a/packages/engine/src/scene/functions/loadGLTFModel.ts +++ b/packages/engine/src/scene/functions/loadGLTFModel.ts @@ -358,5 +358,10 @@ export const generateEntityJsonFromObject = (rootEntity: Entity, obj: Object3D, if (!hasComponent(objEntity, MeshComponent)) { setComponent(objEntity, Object3DComponent, obj) } + + delete mesh.userData['componentJson'] + delete mesh.userData['gltfExtensions'] + delete mesh.userData['useVisible'] + return eJson } diff --git a/packages/engine/src/scene/materials/functions/materialSourcingFunctions.ts b/packages/engine/src/scene/materials/functions/materialSourcingFunctions.ts index 1971f2f00e..3dcc40fc14 100644 --- a/packages/engine/src/scene/materials/functions/materialSourcingFunctions.ts +++ b/packages/engine/src/scene/materials/functions/materialSourcingFunctions.ts @@ -116,7 +116,7 @@ export const createMaterialEntity = (material: Material, path?: string, user?: E const materialEntity = createEntity() setComponent(materialEntity, UUIDComponent, material.uuid as EntityUUID) if (path) setComponent(materialEntity, SourceComponent, path) - const prototypeEntity = getPrototypeEntityFromName(material.type) + const prototypeEntity = getPrototypeEntityFromName(material.userData.type || material.type) if (!prototypeEntity) throw new PrototypeNotFoundError(`Material prototype ${material.type} not found`) setComponent(materialEntity, MaterialStateComponent, { material, diff --git a/packages/engine/src/scene/materials/systems/MaterialLibrarySystem.tsx b/packages/engine/src/scene/materials/systems/MaterialLibrarySystem.tsx index 4ba3fda155..8095e94ee0 100644 --- a/packages/engine/src/scene/materials/systems/MaterialLibrarySystem.tsx +++ b/packages/engine/src/scene/materials/systems/MaterialLibrarySystem.tsx @@ -41,6 +41,7 @@ import { } from '@etherealengine/spatial/src/renderer/materials/MaterialComponent' import { createMaterialPrototype, + materialPrototypeMatches, setMeshMaterial, updateMaterialPrototype } from '@etherealengine/spatial/src/renderer/materials/materialFunctions' @@ -96,7 +97,7 @@ const MaterialEntityReactor = () => { }, [materialComponent.material]) useEffect(() => { - if (materialComponent.prototypeEntity.value) updateMaterialPrototype(entity) + if (materialComponent.prototypeEntity.value && !materialPrototypeMatches(entity)) updateMaterialPrototype(entity) }, [materialComponent.prototypeEntity]) useEffect(() => { diff --git a/packages/engine/src/scene/systems/PortalSystem.ts b/packages/engine/src/scene/systems/PortalSystem.ts index 157c648875..2ce40391fd 100644 --- a/packages/engine/src/scene/systems/PortalSystem.ts +++ b/packages/engine/src/scene/systems/PortalSystem.ts @@ -26,15 +26,15 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { UUIDComponent } from '@etherealengine/ecs' -import { getComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { getComponent, getMutableComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups' import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' import { SpawnPoseState } from '@etherealengine/spatial' -import { switchCameraMode } from '@etherealengine/spatial/src/camera/functions/switchCameraMode' -import { CameraMode } from '@etherealengine/spatial/src/camera/types/CameraMode' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' import { AvatarComponent } from '../../avatar/components/AvatarComponent' import { AvatarControllerComponent } from '../../avatar/components/AvatarControllerComponent' import { PortalComponent, PortalState } from '../components/PortalComponent' @@ -46,7 +46,7 @@ const reactor = () => { const activePortalEntity = activePortalEntityState.value if (!activePortalEntity) return const activePortal = getComponent(activePortalEntity, PortalComponent) - switchCameraMode(Engine.instance.cameraEntity, { cameraMode: CameraMode.ShoulderCam }) + getMutableComponent(Engine.instance.cameraEntity, FollowCameraComponent).mode.set(FollowCameraMode.ShoulderCam) const selfAvatarEntity = AvatarComponent.getSelfAvatarEntity() AvatarControllerComponent.captureMovement(selfAvatarEntity, activePortalEntity) diff --git a/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts b/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts index 08cb4eedac..06cf08cfbe 100644 --- a/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts +++ b/packages/engine/src/visualscript/nodes/profiles/engine/values/CustomNodes.ts @@ -416,7 +416,7 @@ export const setCameraZoom = makeFlowNodeDefinition({ triggered: ({ read, commit }) => { const entity = Engine.instance.cameraEntity const zoom = read('zoom') - setComponent(entity, FollowCameraComponent, { zoomLevel: zoom }) + setComponent(entity, FollowCameraComponent, { targetDistance: zoom }) commit('flow') } }) diff --git a/packages/engine/tests/SoA.test.ts b/packages/engine/tests/SoA.test.ts index 69e26669ca..5b8e7c5e60 100644 --- a/packages/engine/tests/SoA.test.ts +++ b/packages/engine/tests/SoA.test.ts @@ -26,10 +26,9 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' import { getComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' import { proxifyQuaternion, proxifyVector3 } from '@etherealengine/spatial/src/common/proxies/createThreejsProxy' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { TransformComponent } from '@etherealengine/spatial/src/transform/components/TransformComponent' describe('Structure of Array Synchronization', () => { diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json index 097f7ff881..c1db9208f9 100755 --- a/packages/engine/tsconfig.json +++ b/packages/engine/tsconfig.json @@ -35,4 +35,4 @@ "./**/*.ts", "./**/*.tsx" ] -} \ No newline at end of file +} diff --git a/packages/instanceserver/src/start.ts b/packages/instanceserver/src/start.ts index e49ed5fcb8..38b5b5fa74 100755 --- a/packages/instanceserver/src/start.ts +++ b/packages/instanceserver/src/start.ts @@ -42,6 +42,7 @@ import { import multiLogger from '@etherealengine/server-core/src/ServerLogger' import { ServerMode, ServerState } from '@etherealengine/server-core/src/ServerState' +import { startTimer } from '@etherealengine/spatial/src/startTimer' import channels from './channels' import { setupSocketFunctions } from './SocketFunctions' @@ -69,6 +70,8 @@ export const instanceServerPipe = pipe(configureOpenAPI(), configurePrimus(true) export const start = async (): Promise => { const app = createFeathersKoaApp(ServerMode.Instance, instanceServerPipe) + startTimer() + const serverState = getMutableState(ServerState) const agonesSDK = new AgonesSDK() diff --git a/packages/network/src/EntityNetworkState.test.tsx b/packages/network/src/EntityNetworkState.test.tsx index 7426565ff2..34429a97f7 100644 --- a/packages/network/src/EntityNetworkState.test.tsx +++ b/packages/network/src/EntityNetworkState.test.tsx @@ -31,12 +31,12 @@ import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { UserID } from '@etherealengine/common/src/schema.type.module' import { EntityUUID, generateEntityUUID, UUIDComponent } from '@etherealengine/ecs' import { getComponent, hasComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { SystemDefinitions } from '@etherealengine/ecs/src/SystemFunctions' import { PeerID, ReactorReconciler } from '@etherealengine/hyperflux' import { applyIncomingActions, dispatchAction } from '@etherealengine/hyperflux/functions/ActionFunctions' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { createMockNetwork } from '../tests/createMockNetwork' import { Network, NetworkTopics } from './Network' diff --git a/packages/network/src/functions/NetworkPeerFunctions.test.tsx b/packages/network/src/functions/NetworkPeerFunctions.test.tsx index e80dc9c4bd..23797dc425 100644 --- a/packages/network/src/functions/NetworkPeerFunctions.test.tsx +++ b/packages/network/src/functions/NetworkPeerFunctions.test.tsx @@ -28,9 +28,9 @@ import assert from 'assert' import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { InstanceID, UserID } from '@etherealengine/common/src/schema.type.module' import { EntityUUID, UUIDComponent, getComponent } from '@etherealengine/ecs' -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' +import { Engine, createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { PeerID, applyIncomingActions, dispatchAction, getMutableState } from '@etherealengine/hyperflux' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { SpawnObjectActions } from '../../../spatial/src/transform/SpawnObjectActions' import { createMockNetwork } from '../../tests/createMockNetwork' diff --git a/packages/network/src/serialization/DataReader.test.ts b/packages/network/src/serialization/DataReader.test.ts index a417f43325..b1fda4239d 100644 --- a/packages/network/src/serialization/DataReader.test.ts +++ b/packages/network/src/serialization/DataReader.test.ts @@ -30,12 +30,11 @@ import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { UserID } from '@etherealengine/common/src/schema.type.module' import { getComponent, removeComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getMutableState, getState, PeerID } from '@etherealengine/hyperflux' import { TransformComponent } from '@etherealengine/spatial' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' import { readPosition, diff --git a/packages/network/src/serialization/DataWriter.test.ts b/packages/network/src/serialization/DataWriter.test.ts index 0ccd30f291..ad7cf10361 100644 --- a/packages/network/src/serialization/DataWriter.test.ts +++ b/packages/network/src/serialization/DataWriter.test.ts @@ -30,12 +30,11 @@ import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { UserID } from '@etherealengine/common/src/schema.type.module' import { setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getMutableState, getState, PeerID } from '@etherealengine/hyperflux' import { TransformComponent } from '@etherealengine/spatial' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' import { readRotation, diff --git a/packages/network/src/serialization/ViewCursor.test.ts b/packages/network/src/serialization/ViewCursor.test.ts index fc2b503d32..0086b46c1d 100644 --- a/packages/network/src/serialization/ViewCursor.test.ts +++ b/packages/network/src/serialization/ViewCursor.test.ts @@ -27,10 +27,9 @@ import assert, { strictEqual } from 'assert' import { NetworkId } from '@etherealengine/common/src/interfaces/NetworkId' import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' import { getMutableState } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { NetworkObjectComponent } from '../NetworkObjectComponent' import { diff --git a/packages/network/src/systems/IncomingActionSystem.test.ts b/packages/network/src/systems/IncomingActionSystem.test.ts index e1264c4f38..313ed7ef05 100644 --- a/packages/network/src/systems/IncomingActionSystem.test.ts +++ b/packages/network/src/systems/IncomingActionSystem.test.ts @@ -28,9 +28,9 @@ import assert, { strictEqual } from 'assert' import { UserID } from '@etherealengine/common/src/schema.type.module' import { EntityUUID, getComponent, UUIDComponent } from '@etherealengine/ecs' import { ECSState } from '@etherealengine/ecs/src/ECSState' -import { destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine, Engine } from '@etherealengine/ecs/src/Engine' import { ActionRecipients, applyIncomingActions, getMutableState, getState } from '@etherealengine/hyperflux' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { SpawnObjectActions } from '@etherealengine/spatial/src/transform/SpawnObjectActions' import { createMockNetwork } from '../../tests/createMockNetwork' diff --git a/packages/projects/default-project/assets/platform.glb b/packages/projects/default-project/assets/platform.glb index ea9d4a3530..3d359dcc33 100644 Binary files a/packages/projects/default-project/assets/platform.glb and b/packages/projects/default-project/assets/platform.glb differ diff --git a/packages/projects/default-project/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsSkybase.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsSkybase.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsSkybase.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsSkybase.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsUV.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsUV.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsUV.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsUV.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsanimationsemotes.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsanimationsemotes.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsanimationsemotes.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsanimationsemotes.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsapartment-CubemapBake.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsapartment-CubemapBake.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsapartment-CubemapBake.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsapartment-CubemapBake.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsapartment.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsapartment.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsapartment.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsapartment.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_01.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_01.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_01.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_01.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_02.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_02.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_02.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_02.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_02.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_02.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_02.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_02.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscloud.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscloud.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscloud.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscloud.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetscontrollersright_controller.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetscontrollersright_controller.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetscontrollersright_controller.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetscontrollersright_controller.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetskeycard.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetskeycard.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetskeycard.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetskeycard.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegy.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegy.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegy.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegy.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposz.jpg-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposz.jpg-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetsskyboxsun25degposz.jpg-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposz.jpg-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png diff --git a/packages/projects/default-project/thumbnails/default-projectplatform.glb-thumbnail.png b/packages/projects/default-project/public/thumbnails/default-projectplatform.glb-thumbnail.png similarity index 100% rename from packages/projects/default-project/thumbnails/default-projectplatform.glb-thumbnail.png rename to packages/projects/default-project/public/thumbnails/default-projectplatform.glb-thumbnail.png diff --git a/packages/projects/default-project/resources.json b/packages/projects/default-project/resources.json index 9151f641ed..a1db95015d 100644 --- a/packages/projects/default-project/resources.json +++ b/packages/projects/default-project/resources.json @@ -1,187 +1,217 @@ { - "public/scenes/default.loadingscreen.ktx2": { + "assets/animations/default_skeleton.vrm": { "type": "file", "tags": [ - "Image" + "Model" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/avatars/male_03.vrm": { - "type": "avatar", + "assets/animations/emotes.glb": { + "type": "file", "tags": [ "Model" ], - "dependencies": [ - "projects/default-project/assets/avatars/male_03.png" + "dependencies": [] + }, + "assets/animations/locomotion.glb": { + "type": "file", + "tags": [ + "Model" ], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png", + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/apartment.gltf": { - "type": "scene", + "assets/animations/optional/seated.fbx": { + "type": "file", "tags": [ "Model" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/public/scenes/apartment.thumbnail.jpg" + "dependencies": [] }, - "assets/prefabs/text.prefab.gltf": { + "assets/apartment_skybox.jpg": { "type": "file", "tags": [ - "Prefab" + "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/male_02.png": { - "type": "thumbnail", + "assets/apartment-CubemapBake.png": { + "type": "file", "tags": [ "Image" ], "dependencies": [] }, - "assets/avatars/female_03.vrm": { - "type": "avatar", + "assets/apartment.glb": { + "type": "file", "tags": [ "Model" ], - "dependencies": [ - "projects/default-project/assets/avatars/female_03.png" - ], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png", + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsapartment.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/male_03.png": { - "type": "thumbnail", + "assets/avatars/female_01.png": { + "type": "file", "tags": [ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/animations/optional/seated.fbx": { - "type": "file", + "assets/avatars/female_01.vrm": { + "type": "avatar", "tags": [ "Model" ], - "dependencies": [] + "dependencies": [ + "projects/default-project/assets/avatars/female_01.png" + ] }, - "public/scenes/apartment-New-EnvMap Bake.ktx2": { + "assets/avatars/female_02.png": { "type": "file", "tags": [ "Image" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/prefabs/geo.prefab.gltf": { + "assets/avatars/female_02.vrm": { + "type": "avatar", + "tags": [ + "Model" + ], + "dependencies": [ + "projects/default-project/assets/avatars/female_02.png" + ] + }, + "assets/avatars/female_03.png": { "type": "file", "tags": [ - "Prefab" + "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/controllers/right_controller.glb": { - "type": "file", + "assets/avatars/female_03.vrm": { + "type": "avatar", "tags": [ "Model" ], - "dependencies": [] + "dependencies": [ + "projects/default-project/assets/avatars/female_03.png" + ], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsfemale_03.vrm-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/SampleVideo.mp4": { + "assets/avatars/male_01.png": { "type": "file", "tags": [ - "Video" + "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/default.thumbnail.jpg": { - "type": "file", + "assets/avatars/male_01.vrm": { + "type": "avatar", "tags": [ - "Image" + "Model" ], - "dependencies": [] + "dependencies": [ + "projects/default-project/assets/avatars/male_01.png" + ], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png", + "thumbnailMode": "automatic" }, - "public/scenes/sky-station.envmap.ktx2": { + "assets/avatars/male_02.png": { "type": "file", "tags": [ "Image" ], "dependencies": [] }, - "assets/prefabs/point-light.prefab.gltf": { - "type": "file", + "assets/avatars/male_02.vrm": { + "type": "avatar", "tags": [ - "Prefab" + "Model" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png", + "dependencies": [ + "projects/default-project/assets/avatars/male_02.png" + ], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/sample_etc1s.ktx2": { + "assets/avatars/male_03.png": { "type": "file", "tags": [ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.png-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/drop-shadow.png": { - "type": "file", + "assets/avatars/male_03.vrm": { + "type": "avatar", "tags": [ - "Image" + "Model" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png", + "dependencies": [ + "projects/default-project/assets/avatars/male_03.png" + ], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsavatarsmale_03.vrm-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/sky-station.loadingscreen.ktx2": { + "assets/cloud.png": { "type": "file", "tags": [ "Image" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetscloud.png-thumbnail.png", + "thumbnailMode": "automatic" }, - "public/scenes/sky-station.gltf": { - "type": "scene", + "assets/collisioncube.glb": { + "type": "file", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/public/scenes/sky-station.thumbnail.jpg" + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/skyboxsun25deg/negx.jpg": { + "assets/controllers/left_controller.glb": { "type": "file", "tags": [ - "Image" + "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/Skybase.glb": { + "assets/controllers/left.glb": { "type": "file", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsSkybase.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/female_01.vrm": { - "type": "avatar", + "assets/controllers/right_controller.glb": { + "type": "file", "tags": [ "Model" ], - "dependencies": [ - "projects/default-project/assets/avatars/female_01.png" - ] + "dependencies": [] }, "assets/controllers/right.glb": { "type": "file", @@ -189,93 +219,91 @@ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetscontrollersright.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/apartment_skybox.jpg": { + "assets/default-silhouette.svg": { + "type": "file", + "tags": [ + "unknown" + ], + "dependencies": [] + }, + "assets/drop-shadow.png": { "type": "file", "tags": [ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsapartment_skybox.jpg-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsdrop-shadow.png-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/apartment.loadingscreen.ktx2": { + "assets/galaxyTexture.jpg": { "type": "file", "tags": [ "Image" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/animations/locomotion.glb": { + "assets/keycard.glb": { "type": "file", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsanimationslocomotion.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetskeycard.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/apartment.thumbnail.jpg": { + "assets/platform.glb": { "type": "file", "tags": [ - "Image" + "Model" ], "dependencies": [] }, - "assets/test-equippable.glb": { + "assets/portal_frame.glb": { "type": "file", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/prefabs/hemisphere-light.prefab.gltf": { + "assets/prefabs/3d-model.prefab.gltf": { "type": "file", "tags": [ "Prefab" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png", - "thumbnailMode": "automatic" - }, - "assets/apartment.glb": { - "type": "file", - "tags": [ - "Model" - ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsapartment.glb-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "assets/prefabs/spot-light.prefab.gltf": { + "assets/prefabs/ambient-light.prefab.gltf": { "type": "file", "tags": [ "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/cloud.png": { + "assets/prefabs/box-collider.prefab.gltf": { "type": "file", "tags": [ - "Image" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetscloud.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/skyboxsun25deg/posx.jpg": { + "assets/prefabs/cylinder-collider.prefab.gltf": { "type": "file", "tags": [ - "Image" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, "assets/prefabs/directional-light.prefab.gltf": { @@ -284,65 +312,50 @@ "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsdirectional-light.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/collisioncube.glb": { + "assets/prefabs/fog.prefab.gltf": { "type": "file", "tags": [ - "Model" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetscollisioncube.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/sky-station-Portal-- to Apartment.ktx2": { + "assets/prefabs/geo.prefab.gltf": { "type": "file", "tags": [ - "Image" - ], - "dependencies": [] - }, - "assets/avatars/female_02.png": { - "type": "thumbnail", - "tags": [ - "Image" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsfemale_02.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsgeo.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/keycard.glb": { + "assets/prefabs/hemisphere-light.prefab.gltf": { "type": "file", "tags": [ - "Model" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetskeycard.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabshemisphere-light.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/default.gltf": { - "type": "scene", - "tags": [ - "Model" - ], - "dependencies": [], - "thumbnailKey": "projects/default-project/public/scenes/default.thumbnail.jpg" - }, - "assets/skyboxsun25deg/negy.jpg": { + "assets/prefabs/mesh-collider.prefab.gltf": { "type": "file", "tags": [ - "Image" + "Prefab" ], "dependencies": [] }, - "assets/avatars/female_03.png": { - "type": "thumbnail", + "assets/prefabs/point-light.prefab.gltf": { + "type": "file", "tags": [ - "Image" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsfemale_03.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabspoint-light.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, "assets/prefabs/postprocessing.prefab.gltf": { @@ -351,151 +364,146 @@ "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabspostprocessing.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/skyboxsun25deg/posz.jpg": { + "assets/prefabs/skybox.prefab.gltf": { "type": "file", "tags": [ - "Image" + "Prefab" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/controllers/left_controller.glb": { + "assets/prefabs/sphere-collider.prefab.gltf": { "type": "file", "tags": [ - "Model" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetscontrollersleft_controller.glb-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/prefabs/box-collider.prefab.gltf": { + "assets/prefabs/spot-light.prefab.gltf": { "type": "file", "tags": [ "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsbox-collider.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabsspot-light.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/male_01.png": { - "type": "thumbnail", + "assets/prefabs/text.prefab.gltf": { + "type": "file", "tags": [ - "Image" + "Prefab" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsmale_01.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsprefabstext.prefab.gltf-thumbnail.png", "thumbnailMode": "automatic" }, - "public/scenes/apartment.envmap.ktx2": { + "assets/sample_etc1s.ktx2": { "type": "file", "tags": [ "Image" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetssample_etc1s.ktx2-thumbnail.png", + "thumbnailMode": "automatic" }, - "public/scenes/sky-station.thumbnail.jpg": { + "assets/SampleAudio.mp3": { "type": "file", "tags": [ - "Image" + "Audio" ], "dependencies": [] }, - "assets/prefabs/fog.prefab.gltf": { + "assets/SampleVideo.mp4": { "type": "file", "tags": [ - "Prefab" + "Video" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsfog.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsSampleVideo.mp4-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/male_02.vrm": { - "type": "avatar", + "assets/sky_skybox.jpg": { + "type": "file", "tags": [ - "Model" - ], - "dependencies": [ - "projects/default-project/assets/avatars/male_02.png" + "Image" ], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsmale_02.vrm-thumbnail.png", + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/default-silhouette.svg": { + "assets/Skybase.glb": { "type": "file", "tags": [ - "unknown" + "Model" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsSkybase.glb-thumbnail.png", + "thumbnailMode": "automatic" }, - "assets/sky_skybox.jpg": { + "assets/skyboxsun25deg/negx.jpg": { "type": "file", "tags": [ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetssky_skybox.jpg-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegx.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/female_01.png": { - "type": "thumbnail", + "assets/skyboxsun25deg/negy.jpg": { + "type": "file", "tags": [ "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsfemale_01.png-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "assets/prefabs/cylinder-collider.prefab.gltf": { + "assets/skyboxsun25deg/negz.jpg": { "type": "file", "tags": [ - "Prefab" + "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabscylinder-collider.prefab.gltf-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/avatars/female_02.vrm": { - "type": "avatar", - "tags": [ - "Model" - ], - "dependencies": [ - "projects/default-project/assets/avatars/female_02.png" - ] - }, - "assets/skyboxsun25deg/posy.jpg": { + "assets/skyboxsun25deg/posx.jpg": { "type": "file", "tags": [ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposx.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/animations/default_skeleton.vrm": { + "assets/skyboxsun25deg/posy.jpg": { "type": "file", "tags": [ - "Model" + "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsanimationsdefault_skeleton.vrm-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsskyboxsun25degposy.jpg-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/prefabs/mesh-collider.prefab.gltf": { + "assets/skyboxsun25deg/posz.jpg": { "type": "file", "tags": [ - "Prefab" + "Image" ], "dependencies": [] }, - "public/scenes/default.envmap.ktx2": { + "assets/test-equippable.glb": { "type": "file", "tags": [ - "Image" + "Model" ], - "dependencies": [] + "dependencies": [], + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetstest-equippable.glb-thumbnail.png", + "thumbnailMode": "automatic" }, "assets/UV.png": { "type": "file", @@ -503,17 +511,15 @@ "Image" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsUV.png-thumbnail.png", + "thumbnailKey": "projects/default-project/public/thumbnails/default-projectassetsUV.png-thumbnail.png", "thumbnailMode": "automatic" }, - "assets/prefabs/ambient-light.prefab.gltf": { + "public/scenes/apartment-New-EnvMap Bake.ktx2": { "type": "file", "tags": [ - "Prefab" + "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsambient-light.prefab.gltf-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, "public/scenes/apartment-Portal.ktx2": { "type": "file", @@ -522,111 +528,91 @@ ], "dependencies": [] }, - "public/scenes/sky-station-Portal-- Sky Station Exterior.ktx2": { + "public/scenes/apartment.envmap.ktx2": { "type": "file", "tags": [ "Image" ], "dependencies": [] }, - "assets/avatars/male_01.vrm": { - "type": "avatar", + "public/scenes/apartment.gltf": { + "type": "scene", "tags": [ "Model" ], - "dependencies": [ - "projects/default-project/assets/avatars/male_01.png" - ], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsavatarsmale_01.vrm-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [], + "thumbnailKey": "projects/default-project/public/scenes/apartment.thumbnail.jpg" }, - "assets/prefabs/sphere-collider.prefab.gltf": { + "public/scenes/apartment.loadingscreen.ktx2": { "type": "file", "tags": [ - "Prefab" + "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabssphere-collider.prefab.gltf-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "assets/prefabs/skybox.prefab.gltf": { + "public/scenes/default.envmap.ktx2": { "type": "file", "tags": [ - "Prefab" + "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsprefabsskybox.prefab.gltf-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "assets/portal_frame.glb": { - "type": "file", + "public/scenes/default.gltf": { + "type": "scene", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsportal_frame.glb-thumbnail.png", - "thumbnailMode": "automatic" + "thumbnailKey": "projects/default-project/public/scenes/default.thumbnail.jpg" }, - "assets/animations/emotes.glb": { + "public/scenes/default.loadingscreen.ktx2": { "type": "file", "tags": [ - "Model" + "Image" ], "dependencies": [] }, - "assets/galaxyTexture.jpg": { + "public/scenes/sky-station-Portal-- Sky Station Exterior.ktx2": { "type": "file", "tags": [ "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsgalaxyTexture.jpg-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "assets/SampleAudio.mp3": { + "public/scenes/sky-station-Portal-- Sky Station Interior.ktx2": { "type": "file", "tags": [ - "Audio" + "Image" ], "dependencies": [] }, - "assets/skyboxsun25deg/negz.jpg": { + "public/scenes/sky-station-Portal-- to Apartment.ktx2": { "type": "file", "tags": [ "Image" ], - "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetsskyboxsun25degnegz.jpg-thumbnail.png", - "thumbnailMode": "automatic" + "dependencies": [] }, - "public/scenes/sky-station-Portal-- Sky Station Interior.ktx2": { + "public/scenes/sky-station.envmap.ktx2": { "type": "file", "tags": [ "Image" ], "dependencies": [] }, - "assets/controllers/left.glb": { - "type": "file", + "public/scenes/sky-station.gltf": { + "type": "scene", "tags": [ "Model" ], "dependencies": [], - "thumbnailKey": "projects/default-project/thumbnails/default-projectassetscontrollersleft.glb-thumbnail.png", - "thumbnailMode": "automatic" + "thumbnailKey": "projects/default-project/public/scenes/sky-station.thumbnail.jpg" }, - "assets/apartment-CubemapBake.png": { + "public/scenes/sky-station.loadingscreen.ktx2": { "type": "file", "tags": [ "Image" ], "dependencies": [] - }, - "assets/prefabs/3d-model.prefab.gltf": { - "type": "file", - "tags": [ - "Prefab" - ], - "dependencies": [] } } \ No newline at end of file diff --git a/packages/server-core/src/appconfig.ts b/packages/server-core/src/appconfig.ts index 624f7dc5fd..706b4193bf 100755 --- a/packages/server-core/src/appconfig.ts +++ b/packages/server-core/src/appconfig.ts @@ -398,6 +398,12 @@ const ipfs = { enabled: process.env.USE_IPFS } +const zendesk = { + name: process.env.ZENDESK_KEY_NAME, + secret: process.env.ZENDESK_SECRET, + kid: process.env.ZENDESK_KID +} + /** * Full config */ @@ -428,7 +434,8 @@ const config = { /** @todo when project versioning is fully implemented, remove 'undefined' check here */ allowOutOfDateProjects: typeof process.env.ALLOW_OUT_OF_DATE_PROJECTS === 'undefined' || process.env.ALLOW_OUT_OF_DATE_PROJECTS === 'true', - fsProjectSyncEnabled: process.env.FS_PROJECT_SYNC_ENABLED === 'false' ? false : true + fsProjectSyncEnabled: process.env.FS_PROJECT_SYNC_ENABLED === 'false' ? false : true, + zendesk } chargebeeInst.configure({ diff --git a/packages/server-core/src/createApp.ts b/packages/server-core/src/createApp.ts index 0d6f1a1baa..4c06f35a40 100644 --- a/packages/server-core/src/createApp.ts +++ b/packages/server-core/src/createApp.ts @@ -40,12 +40,10 @@ import healthcheck from 'koa-simple-healthcheck' import { pipeLogs } from '@etherealengine/common/src/logger' import { pipe } from '@etherealengine/common/src/utils/pipe' -import { Engine } from '@etherealengine/ecs/src/Engine' -import { getMutableState, getState } from '@etherealengine/hyperflux' +import { Engine, createEngine } from '@etherealengine/ecs/src/Engine' +import { getMutableState } from '@etherealengine/hyperflux' import { EngineState } from '@etherealengine/spatial/src/EngineState' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' -import { ECSState } from '@etherealengine/ecs' import { Application } from '../declarations' import { logger } from './ServerLogger' import { ServerMode, ServerState, ServerTypeMode } from './ServerState' @@ -176,7 +174,6 @@ export const createFeathersKoaApp = ( configurationPipe = serverPipe ): Application => { createEngine() - getState(ECSState).timer.start() const serverState = getMutableState(ServerState) serverState.serverMode.set(serverMode) diff --git a/packages/server-core/src/hooks/resolve-projects-by-permission.ts b/packages/server-core/src/hooks/resolve-projects-by-permission.ts new file mode 100644 index 0000000000..f698d4a11d --- /dev/null +++ b/packages/server-core/src/hooks/resolve-projects-by-permission.ts @@ -0,0 +1,61 @@ +/* +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 { UserType, projectPath, projectPermissionPath } from '@etherealengine/common/src/schema.type.module' +import { Forbidden } from '@feathersjs/errors' +import { Application, HookContext } from '../../declarations' +/** + * if project is not provided query the project permission table for all projects the user has permissions for. + * Then add the projects to the $in of the query + * @param context + * @returns + */ +export default () => { + return async (context: HookContext) => { + if (!context.params.query?.project) { + const loggedInUser = context.params.user as UserType + const data = await context.app.service(projectPermissionPath).find({ + query: { + userId: loggedInUser.id + }, + paginate: false + }) + + if (data.length === 0) { + console.error(`No Project permissions found. UserId: ${loggedInUser.id}`) + throw new Forbidden(`Project permissions not found`) + } + + context.params.query.project = { $in: [] } + + for (const projP of data) { + const project = await context.app.service(projectPath).get(projP.projectId) + if (project !== undefined) context.params.query.project.$in.push(project.name) + } + return context + } + return context + } +} diff --git a/packages/server-core/src/integrations/services.ts b/packages/server-core/src/integrations/services.ts new file mode 100644 index 0000000000..2986d54ba6 --- /dev/null +++ b/packages/server-core/src/integrations/services.ts @@ -0,0 +1,28 @@ +/* +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 ZendeskAuthentication from './zendesk/zendesk' + +export default [ZendeskAuthentication] diff --git a/packages/server-core/src/integrations/zendesk/zendesk.class.ts b/packages/server-core/src/integrations/zendesk/zendesk.class.ts new file mode 100755 index 0000000000..02281c699b --- /dev/null +++ b/packages/server-core/src/integrations/zendesk/zendesk.class.ts @@ -0,0 +1,38 @@ +/* +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 { Params } from '@feathersjs/feathers' +import { KnexAdapterParams } from '@feathersjs/knex' +import { BaseService } from '../../BaseService' + +export interface ZendeskAuthenticationParams extends KnexAdapterParams {} + +/** + * A class for ZendeskAuthentication service + */ +export class ZendeskAuthenticationService< + T = string, + ServiceParams extends Params = ZendeskAuthenticationParams +> extends BaseService {} diff --git a/packages/spatial/src/xrui/XRUIState.ts b/packages/server-core/src/integrations/zendesk/zendesk.docs.ts old mode 100644 new mode 100755 similarity index 82% rename from packages/spatial/src/xrui/XRUIState.ts rename to packages/server-core/src/integrations/zendesk/zendesk.docs.ts index 9eff38fe78..4a94d2a461 --- a/packages/spatial/src/xrui/XRUIState.ts +++ b/packages/server-core/src/integrations/zendesk/zendesk.docs.ts @@ -23,13 +23,12 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { Object3D, Ray } from 'three' - -import { defineState } from '@etherealengine/hyperflux' - -export const XRUIState = defineState({ - name: 'XRUIState', - initial: () => ({ - interactionRays: [] as Array - }) +import { createSwaggerServiceOptions } from 'feathers-swagger' + +export default createSwaggerServiceOptions({ + schemas: {}, + docs: { + description: 'Zendesk Authentication service description', + securities: ['all'] + } }) diff --git a/packages/server-core/src/integrations/zendesk/zendesk.hooks.ts b/packages/server-core/src/integrations/zendesk/zendesk.hooks.ts new file mode 100755 index 0000000000..4ce5ad2896 --- /dev/null +++ b/packages/server-core/src/integrations/zendesk/zendesk.hooks.ts @@ -0,0 +1,77 @@ +/* +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 appConfig from '@etherealengine/server-core/src/appconfig' +import { Application, HookContext } from '@feathersjs/feathers/lib/declarations' +import { disallow } from 'feathers-hooks-common' +import { sign } from 'jsonwebtoken' + +const getZendeskToken = (context: HookContext) => { + context.result = sign( + { + scope: 'user', + external_id: context.params.user.id, + name: context.params.user.name + }, + appConfig.zendesk.secret!, + { + header: { + alg: 'HS256', + kid: appConfig.zendesk.kid + } + } + ) + return context +} + +export default { + before: { + all: [], + find: [disallow()], + get: [disallow()], + create: [getZendeskToken], + update: [disallow()], + patch: [disallow()], + remove: [disallow()] + }, + after: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + }, + error: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + } +} as any diff --git a/packages/server-core/src/integrations/zendesk/zendesk.ts b/packages/server-core/src/integrations/zendesk/zendesk.ts new file mode 100755 index 0000000000..b30df9f557 --- /dev/null +++ b/packages/server-core/src/integrations/zendesk/zendesk.ts @@ -0,0 +1,50 @@ +/* +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 { zendeskMethods, zendeskPath } from '@etherealengine/common/src/schemas/integrations/zendesk/zendesk.schema' + +import { Application } from '../../../declarations' +import { ZendeskAuthenticationService } from './zendesk.class' +import zendeskAuthenticationDocs from './zendesk.docs' +import hooks from './zendesk.hooks' + +declare module '@etherealengine/common/declarations' { + interface ServiceTypes { + [zendeskPath]: ZendeskAuthenticationService + } +} + +export default (app: Application): void => { + app.use(zendeskPath, new ZendeskAuthenticationService(), { + // A list of all methods this service exposes externally + methods: zendeskMethods, + // You can add additional custom events to be sent to clients here + events: [], + docs: zendeskAuthenticationDocs + }) + + const service = app.service(zendeskPath) + service.hooks(hooks) +} diff --git a/packages/server-core/src/media/FileUtil.test.ts b/packages/server-core/src/media/FileUtil.test.ts index 78e090a2ec..2effa93ac9 100644 --- a/packages/server-core/src/media/FileUtil.test.ts +++ b/packages/server-core/src/media/FileUtil.test.ts @@ -27,8 +27,7 @@ import assert from 'assert' import fs from 'fs' import path from 'path/posix' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { projectsRootFolder } from './file-browser/file-browser.class' import { copyRecursiveSync, getIncrementalName } from './FileUtil' diff --git a/packages/server-core/src/media/file-browser-upload/file-browser-upload.class.ts b/packages/server-core/src/media/file-browser-upload/file-browser-upload.class.ts index 66e8f0342a..74fe4e4406 100755 --- a/packages/server-core/src/media/file-browser-upload/file-browser-upload.class.ts +++ b/packages/server-core/src/media/file-browser-upload/file-browser-upload.class.ts @@ -35,12 +35,6 @@ export interface FileBrowserUploadParams extends KnexAdapterParams { files: UploadFile[] } -export interface FileBrowserUploadData { - project: string - path: string - args: string -} - /** * A class for File Browser Upload service */ @@ -51,19 +45,20 @@ export class FileBrowserUploadService implements ServiceInterface - this.app.service(fileBrowserPath).patch(null, { - project: data.project, - path: data.path, + params.files.map((file, i) => { + const args = data[i] + return this.app.service(fileBrowserPath).patch(null, { + ...args, + project: args.project, + path: args.path, body: file.buffer as Buffer, contentType: file.mimetype }) - ) + }) ) ).map((result) => result.url) diff --git a/packages/server-core/src/media/file-browser/file-browser.class.ts b/packages/server-core/src/media/file-browser/file-browser.class.ts index 79b82f3ca9..de69bd0ace 100755 --- a/packages/server-core/src/media/file-browser/file-browser.class.ts +++ b/packages/server-core/src/media/file-browser/file-browser.class.ts @@ -194,8 +194,10 @@ export class FileBrowserService const storageProvider = getStorageProvider(storageProviderName) /** @todo future proofing for when projects include orgname */ - if (!data.oldPath.startsWith('projects/' + data.oldProject)) throw new Error('Not allowed to access this directory') - if (!data.newPath.startsWith('projects/' + data.newProject)) throw new Error('Not allowed to access this directory') + if (!data.oldPath.startsWith('projects/' + data.oldProject)) + throw new Error('Not allowed to access this directory ' + data.oldPath + ' ' + data.oldProject) + if (!data.newPath.startsWith('projects/' + data.newProject)) + throw new Error('Not allowed to access this directory ' + data.newPath + ' ' + data.newProject) const oldDirectory = data.oldPath.split('/').slice(0, -1).join('/') const newDirectory = data.newPath.split('/').slice(0, -1).join('/') @@ -262,7 +264,7 @@ export class FileBrowserService const arr = await response.arrayBuffer() data.body = Buffer.from(arr) } catch (error) { - throw new Error('Invalid URL ' + url) + throw new Error('Failure in fetching source URL: ' + url + 'Error: ' + error) } } diff --git a/packages/server-core/src/media/static-resource/static-resource-helper.ts b/packages/server-core/src/media/static-resource/static-resource-helper.ts index a3d649e7c7..fb5dd99dbd 100644 --- a/packages/server-core/src/media/static-resource/static-resource-helper.ts +++ b/packages/server-core/src/media/static-resource/static-resource-helper.ts @@ -205,7 +205,7 @@ export const StatFunctions = { export const regenerateProjectResourcesJson = async (app: Application, projectName: string) => { const resources: StaticResourceType[] = await app.service(staticResourcePath).find({ - query: { project: projectName }, + query: { project: projectName, type: { $ne: 'thumbnail' } }, paginate: false }) if (resources.length === 0) return @@ -258,6 +258,7 @@ export const regenerateProjectResourcesJson = async (app: Application, projectNa export const patchSingleProjectResourcesJson = async (app: Application, id: string) => { // refetch resource since after hooks have not run resolvers yet to parse strings into objects const resource = (await app.service(staticResourcePath).get(id)) as StaticResourceType + if (resource.type === 'thumbnail') return const projectName = resource.project diff --git a/packages/server-core/src/media/static-resource/static-resource.hooks.ts b/packages/server-core/src/media/static-resource/static-resource.hooks.ts index 68a2661023..3819a73d43 100755 --- a/packages/server-core/src/media/static-resource/static-resource.hooks.ts +++ b/packages/server-core/src/media/static-resource/static-resource.hooks.ts @@ -26,13 +26,15 @@ import { BadRequest, Forbidden, NotFound } from '@feathersjs/errors' import { hooks as schemaHooks } from '@feathersjs/schema' import { discardQuery, iff, iffElse, isProvider } from 'feathers-hooks-common' -import { staticResourcePath } from '@etherealengine/common/src/schemas/media/static-resource.schema' +import { StaticResourceType, staticResourcePath } from '@etherealengine/common/src/schemas/media/static-resource.schema' import { HookContext } from '../../../declarations' import checkScope from '../../hooks/check-scope' import collectAnalytics from '../../hooks/collect-analytics' import enableClientPagination from '../../hooks/enable-client-pagination' +import isAction from '../../hooks/is-action' import resolveProjectId from '../../hooks/resolve-project-id' +import resolveProjectsByPermission from '../../hooks/resolve-projects-by-permission' import setLoggedinUserInBody from '../../hooks/set-loggedin-user-in-body' import verifyProjectPermission from '../../hooks/verify-project-permission' import verifyScope from '../../hooks/verify-scope' @@ -152,6 +154,23 @@ const getProjectName = async (context: HookContext) => { return context } +const hasProjectField = (context: HookContext) => { + return context.params.query?.project != undefined +} + +const isKeyPublic = (context: HookContext) => { + if (context.method !== 'get') throw new BadRequest('isKeyPublic hook only works for get method') + const result = context.result as StaticResourceType + + if (!result.project) return + + const projectRelativeKey = result.key.replace(`projects/${result.project}/`, '') + if (!projectRelativeKey.startsWith('public/') && !projectRelativeKey.startsWith('assets/')) + throw new Forbidden('Cannot access this resource') + + return context +} + export default { around: { all: [schemaHooks.resolveResult(staticResourceResolver)] @@ -163,27 +182,23 @@ export default { iff( isProvider('external'), iffElse( - checkScope('static_resource', 'read'), + (ctx: HookContext) => isAction('admin')(ctx) && checkScope('static_resource', 'read')(ctx), [], - [verifyScope('editor', 'write'), resolveProjectId(), verifyProjectPermission(['owner', 'editor', 'reviewer'])] + [ + verifyScope('editor', 'write'), + iffElse( + hasProjectField, + [resolveProjectId(), verifyProjectPermission(['owner', 'editor', 'reviewer'])], + [resolveProjectsByPermission()] + ) as any + ] ) ), enableClientPagination() /** @todo we should either constrain this only for when type='scene' or remove it in favour of comprehensive front end pagination */, discardQuery('action', 'projectId'), collectAnalytics() ], - get: [ - iff( - isProvider('external'), - iffElse( - checkScope('static_resource', 'read'), - [], - [verifyScope('editor', 'write'), resolveProjectId(), verifyProjectPermission(['owner', 'editor', 'reviewer'])] - ) - ), - discardQuery('action', 'projectId'), - collectAnalytics() - ], + get: [collectAnalytics()], create: [ ensureProject, iff( @@ -251,7 +266,16 @@ export default { after: { all: [], find: [], - get: [], + get: [ + iff( + isProvider('external'), + iffElse( + (ctx: HookContext) => checkScope('static_resource', 'read')(ctx) || verifyScope('editor', 'write')(ctx), + [], + [isKeyPublic] + ) + ) + ], create: [updateResourcesJson], update: [updateResourcesJson], patch: [updateResourcesJson], diff --git a/packages/server-core/src/media/static-resource/static-resource.resolvers.ts b/packages/server-core/src/media/static-resource/static-resource.resolvers.ts index ffca6788e1..b8c1a57235 100644 --- a/packages/server-core/src/media/static-resource/static-resource.resolvers.ts +++ b/packages/server-core/src/media/static-resource/static-resource.resolvers.ts @@ -85,6 +85,7 @@ export const staticResourceResolver = resolve( /** @todo optimize this */ const thumbnailStaticResource = await context.app.service('static-resource').find({ query: { + type: 'thumbnail', key: staticResource.thumbnailKey } }) diff --git a/packages/server-core/src/projects/project/project-helper.ts b/packages/server-core/src/projects/project/project-helper.ts index 8fdc0ca5ca..0d87447f2e 100644 --- a/packages/server-core/src/projects/project/project-helper.ts +++ b/packages/server-core/src/projects/project/project-helper.ts @@ -49,7 +49,7 @@ import { INSTALLATION_SIGNED_REGEX, PUBLIC_SIGNED_REGEX } from '@etherealengine/ import { ManifestJson } from '@etherealengine/common/src/interfaces/ManifestJson' import { ProjectPackageJsonType } from '@etherealengine/common/src/interfaces/ProjectPackageJsonType' -import { ResourcesJson } from '@etherealengine/common/src/interfaces/ResourcesJson' +import { ResourcesJson, ResourceType } from '@etherealengine/common/src/interfaces/ResourcesJson' import { apiJobPath } from '@etherealengine/common/src/schemas/cluster/api-job.schema' import { invalidationPath } from '@etherealengine/common/src/schemas/media/invalidation.schema' import { staticResourcePath, StaticResourceType } from '@etherealengine/common/src/schemas/media/static-resource.schema' @@ -1636,6 +1636,15 @@ const migrateResourcesJson = (projectName: string, resourceJsonPath: string) => if (newManifest) fs.writeFileSync(resourceJsonPath, Buffer.from(JSON.stringify(newManifest, null, 2))) } +const getResourceType = (key: string, resource?: ResourceType) => { + // TODO: figure out a better way of handling thumbnails rather than by convention + if (key.startsWith('public/thumbnails') || key.endsWith('.thumbnail.jpg')) return 'thumbnail' + if (!resource) return 'file' + if (resource.type) return resource.type + if (resource.tags) return 'asset' + return 'file' +} + const staticResourceClasses = [ AssetType.Audio, AssetType.Image, @@ -1735,7 +1744,7 @@ export const uploadLocalProjectToProvider = async ( const hash = createStaticResourceHash(fileResult) const stats = await getStats(fileResult, contentType) const resourceInfo = resourcesJson?.[filePathRelative] - const type = isScene ? 'scene' : resourceInfo?.type ? resourceInfo?.type : resourceInfo?.tags ? 'asset' : 'file' // assume if it has already been given tag metadata that it is an asset + const type = isScene ? 'scene' : getResourceType(filePathRelative, resourceInfo!) const thumbnailKey = resourceInfo?.thumbnailKey ?? (isScene ? key.split('.').slice(0, -1).join('.') + '.thumbnail.jpg' : undefined) diff --git a/packages/server-core/src/projects/project/project.class.ts b/packages/server-core/src/projects/project/project.class.ts index d85d009250..c19f227261 100644 --- a/packages/server-core/src/projects/project/project.class.ts +++ b/packages/server-core/src/projects/project/project.class.ts @@ -31,7 +31,13 @@ import path from 'path' import { v4 as uuidv4 } from 'uuid' import { DefaultUpdateSchedule } from '@etherealengine/common/src/interfaces/ProjectPackageJsonType' -import { staticResourcePath } from '@etherealengine/common/src/schema.type.module' +import { + ScopeData, + ScopeType, + projectPermissionPath, + scopePath, + staticResourcePath +} from '@etherealengine/common/src/schema.type.module' import { ProjectBuildUpdateItemType } from '@etherealengine/common/src/schemas/projects/project-build.schema' import { ProjectData, @@ -43,6 +49,7 @@ import { import { getDateTimeSql, toDateTimeSql } from '@etherealengine/common/src/utils/datetime-sql' import { getState } from '@etherealengine/hyperflux' +import { isDev } from '@etherealengine/common/src/config' import { Application } from '../../../declarations' import logger from '../../ServerLogger' import { ServerMode, ServerState } from '../../ServerState' @@ -137,6 +144,20 @@ export class ProjectService { ...RouteService, ...RecordingServices, ...MatchMakingServices, - ...WorldServices + ...WorldServices, + ...IntegrationServices ] .concat(...installedProjects) .forEach((service) => { diff --git a/packages/server-core/src/setting/feature-flag-setting/feature-flag-setting.resolvers.ts b/packages/server-core/src/setting/feature-flag-setting/feature-flag-setting.resolvers.ts index f17ed6e10a..8f4aba465f 100644 --- a/packages/server-core/src/setting/feature-flag-setting/feature-flag-setting.resolvers.ts +++ b/packages/server-core/src/setting/feature-flag-setting/feature-flag-setting.resolvers.ts @@ -40,7 +40,9 @@ export const featureFlagSettingResolver = resolve fromDateTimeSql(featureFlagSettings.updatedAt)) }) -export const featureFlagSettingExternalResolver = resolve({}) +export const featureFlagSettingExternalResolver = resolve({ + flagValue: async (_, setting) => !!setting.flagValue +}) export const featureFlagSettingDataResolver = resolve({ id: async () => { diff --git a/packages/server-core/src/setting/seeder-config.ts b/packages/server-core/src/setting/seeder-config.ts index 2ec6a5e2c7..d61a1f7a2f 100644 --- a/packages/server-core/src/setting/seeder-config.ts +++ b/packages/server-core/src/setting/seeder-config.ts @@ -36,6 +36,7 @@ import * as instanceServerSeed from './instance-server-setting/instance-server-s import * as redisSeed from './redis-setting/redis-setting.seed' import * as serverSeed from './server-setting/server-setting.seed' import * as taskServerSeed from './task-server-setting/task-server-setting.seed' +import * as zendeskSeed from './zendesk-setting/zendesk-setting.seed' export const settingSeeds: Array = [ authenticationSeed, @@ -48,5 +49,7 @@ export const settingSeeds: Array = [ emailSeed, redisSeed, awsSeed, - helmSeed + helmSeed, + zendeskSeed, + zendeskSeed ] diff --git a/packages/server-core/src/setting/service.ts b/packages/server-core/src/setting/service.ts index b15ff7958c..fb14a62183 100644 --- a/packages/server-core/src/setting/service.ts +++ b/packages/server-core/src/setting/service.ts @@ -36,6 +36,7 @@ import ProjectServer from './project-setting/project-setting' import RedisSetting from './redis-setting/redis-setting' import ServerSetting from './server-setting/server-setting' import TaskServer from './task-server-setting/task-server-setting' +import ZendeskSetting from './zendesk-setting/zendesk-setting' export default [ ProjectServer, @@ -50,5 +51,6 @@ export default [ Coil, RedisSetting, TaskServer, - Helm + Helm, + ZendeskSetting ] diff --git a/packages/server-core/src/setting/zendesk-setting/migrations/20240625222127_zendesk-keys.ts b/packages/server-core/src/setting/zendesk-setting/migrations/20240625222127_zendesk-keys.ts new file mode 100644 index 0000000000..a417738654 --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/migrations/20240625222127_zendesk-keys.ts @@ -0,0 +1,63 @@ +/* +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 { zendeskSettingPath } from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' +import { Knex } from 'knex' + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function up(knex: Knex): Promise { + const tableExists = await knex.schema.hasTable(zendeskSettingPath) + + if (!tableExists) { + await knex.schema.createTable(zendeskSettingPath, (table) => { + //@ts-ignore + table.uuid('id').collate('utf8mb4_bin').primary() + table.string('name', 255).nullable() + table.string('secret', 255).nullable() + table.string('kid', 255).nullable() + table.dateTime('createdAt').notNullable() + table.dateTime('updatedAt').notNullable() + }) + } +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export async function down(knex: Knex): Promise { + await knex.raw('SET FOREIGN_KEY_CHECKS=0') + + const tableExists = await knex.schema.hasTable(zendeskSettingPath) + + if (tableExists) { + await knex.schema.dropTable(zendeskSettingPath) + } + + await knex.raw('SET FOREIGN_KEY_CHECKS=1') +} diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.class.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.class.ts new file mode 100644 index 0000000000..4161b007dd --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.class.ts @@ -0,0 +1,41 @@ +/* +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 type { Params } from '@feathersjs/feathers' +import { KnexAdapterParams, KnexService } from '@feathersjs/knex' + +import { + ZendeskSettingData, + ZendeskSettingPatch, + ZendeskSettingQuery, + ZendeskSettingType +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' + +export interface ZendeskSettingParams extends KnexAdapterParams {} + +export class ZendeskSettingService< + T = ZendeskSettingType, + ServiceParams extends Params = ZendeskSettingParams +> extends KnexService {} diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.docs.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.docs.ts new file mode 100644 index 0000000000..c58df9df41 --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.docs.ts @@ -0,0 +1,46 @@ +/* +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 { createSwaggerServiceOptions } from 'feathers-swagger' + +import { + zendeskSettingDataSchema, + zendeskSettingPatchSchema, + zendeskSettingQuerySchema, + zendeskSettingSchema +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' + +export default createSwaggerServiceOptions({ + schemas: { + zendeskSettingDataSchema, + zendeskSettingPatchSchema, + zendeskSettingQuerySchema, + zendeskSettingSchema + }, + docs: { + description: 'Zendesk setting service description', + securities: ['all'] + } +}) diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.hooks.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.hooks.ts new file mode 100644 index 0000000000..2689173a3f --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.hooks.ts @@ -0,0 +1,93 @@ +/* +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 { hooks as schemaHooks } from '@feathersjs/schema' +import { iff, isProvider } from 'feathers-hooks-common' + +import { + zendeskSettingDataValidator, + zendeskSettingPatchValidator, + zendeskSettingQueryValidator +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' + +import verifyScope from '../../hooks/verify-scope' +import { + zendeskSettingDataResolver, + zendeskSettingExternalResolver, + zendeskSettingPatchResolver, + zendeskSettingQueryResolver, + zendeskSettingResolver +} from './zendesk-setting.resolvers' + +export default { + around: { + all: [ + schemaHooks.resolveExternal(zendeskSettingExternalResolver), + schemaHooks.resolveResult(zendeskSettingResolver) + ] + }, + + before: { + all: [ + iff(isProvider('external'), verifyScope('admin', 'admin')), + () => schemaHooks.validateQuery(zendeskSettingQueryValidator), + schemaHooks.resolveQuery(zendeskSettingQueryResolver) + ], + find: [iff(isProvider('external'), verifyScope('settings', 'read'))], + get: [iff(isProvider('external'), verifyScope('settings', 'read'))], + create: [ + iff(isProvider('external'), verifyScope('settings', 'write')), + () => schemaHooks.validateData(zendeskSettingDataValidator), + schemaHooks.resolveData(zendeskSettingDataResolver) + ], + update: [iff(isProvider('external'), verifyScope('settings', 'write'))], + patch: [ + iff(isProvider('external'), verifyScope('settings', 'write')), + () => schemaHooks.validateData(zendeskSettingPatchValidator), + schemaHooks.resolveData(zendeskSettingPatchResolver) + ], + remove: [iff(isProvider('external'), verifyScope('settings', 'write'))] + }, + + after: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + }, + + error: { + all: [], + find: [], + get: [], + create: [], + update: [], + patch: [], + remove: [] + } +} as any diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.resolvers.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.resolvers.ts new file mode 100644 index 0000000000..7627328686 --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.resolvers.ts @@ -0,0 +1,56 @@ +/* +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 { resolve, virtual } from '@feathersjs/schema' +import { v4 as uuidv4 } from 'uuid' + +import { + ZendeskSettingQuery, + ZendeskSettingType +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' +import { fromDateTimeSql, getDateTimeSql } from '@etherealengine/common/src/utils/datetime-sql' +import type { HookContext } from '@etherealengine/server-core/declarations' + +export const zendeskSettingResolver = resolve({ + createdAt: virtual(async (zendeskSetting) => fromDateTimeSql(zendeskSetting.createdAt)), + updatedAt: virtual(async (zendeskSetting) => fromDateTimeSql(zendeskSetting.updatedAt)) +}) + +export const zendeskSettingExternalResolver = resolve({}) + +export const zendeskSettingDataResolver = resolve({ + id: async () => { + return uuidv4() + }, + createdAt: getDateTimeSql, + updatedAt: getDateTimeSql +}) + +export const zendeskSettingPatchResolver = resolve({ + updatedAt: getDateTimeSql +}) + +export const zendeskSettingQueryResolver = resolve({}) diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.seed.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.seed.ts new file mode 100644 index 0000000000..fa619a9545 --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.seed.ts @@ -0,0 +1,70 @@ +/* +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 { Knex } from 'knex' +import { v4 as uuidv4 } from 'uuid' + +import { + zendeskSettingPath, + ZendeskSettingType +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' +import { getDateTimeSql } from '@etherealengine/common/src/utils/datetime-sql' +import appConfig from '@etherealengine/server-core/src/appconfig' + +export async function seed(knex: Knex): Promise { + const { testEnabled } = appConfig + const { forceRefresh } = appConfig.db + + const seedData: ZendeskSettingType[] = await Promise.all( + [ + { + name: process.env.ZENDESK_KEY_NAME!, + secret: process.env.ZENDESK_SECRET!, + kid: process.env.ZENDESK_KID! + } + ].map(async (item) => ({ + ...item, + id: uuidv4(), + createdAt: await getDateTimeSql(), + updatedAt: await getDateTimeSql() + })) + ) + + if (forceRefresh || testEnabled) { + // Deletes ALL existing entries + await knex(zendeskSettingPath).del() + + // Inserts seed entries + await knex(zendeskSettingPath).insert(seedData) + } else { + const existingData = await knex(zendeskSettingPath).count({ count: '*' }) + + if (existingData.length === 0 || existingData[0].count === 0) { + for (const item of seedData) { + await knex(zendeskSettingPath).insert(item) + } + } + } +} diff --git a/packages/server-core/src/setting/zendesk-setting/zendesk-setting.ts b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.ts new file mode 100644 index 0000000000..894fe43522 --- /dev/null +++ b/packages/server-core/src/setting/zendesk-setting/zendesk-setting.ts @@ -0,0 +1,69 @@ +/* +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 { + zendeskSettingMethods, + zendeskSettingPath +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' + +import { Application } from '../../../declarations' +import { updateAppConfig } from '../../updateAppConfig' +import { ZendeskSettingService } from './zendesk-setting.class' +import zendeskSettingDocs from './zendesk-setting.docs' +import hooks from './zendesk-setting.hooks' + +declare module '@etherealengine/common/declarations' { + interface ServiceTypes { + [zendeskSettingPath]: ZendeskSettingService + } +} + +export default (app: Application): void => { + const options = { + name: zendeskSettingPath, + paginate: app.get('paginate'), + Model: app.get('knexClient'), + multi: true + } + + app.use(zendeskSettingPath, new ZendeskSettingService(options), { + // A list of all methods this service exposes externally + methods: zendeskSettingMethods, + // You can add additional custom events to be sent to clients here + events: [], + docs: zendeskSettingDocs + }) + + const service = app.service(zendeskSettingPath) + service.hooks(hooks) + + service.on('patched', () => { + updateAppConfig() + }) + + service.on('created', () => { + updateAppConfig() + }) +} diff --git a/packages/server-core/src/updateAppConfig.ts b/packages/server-core/src/updateAppConfig.ts index e04b3cf929..10e8daacba 100644 --- a/packages/server-core/src/updateAppConfig.ts +++ b/packages/server-core/src/updateAppConfig.ts @@ -57,6 +57,10 @@ import { TaskServerSettingType } from '@etherealengine/common/src/schemas/setting/task-server-setting.schema' +import { + zendeskSettingPath, + ZendeskSettingType +} from '@etherealengine/common/src/schemas/setting/zendesk-setting.schema' import appConfig from './appconfig' import logger from './ServerLogger' import { authenticationDbToSchema } from './setting/authentication-setting/authentication-setting.resolvers' @@ -282,5 +286,21 @@ export const updateAppConfig = async (): Promise => { }) promises.push(serverSettingPromise) + const zendeskSettingPromise = knexClient + .select() + .from(zendeskSettingPath) + .then(([dbZendesk]) => { + if (dbZendesk) { + appConfig.zendesk = { + ...appConfig.zendesk, + ...dbZendesk + } + } + }) + .catch((e) => { + logger.error(e, `[updateAppConfig]: Failed to read zendesk setting: ${e.message}`) + }) + promises.push(zendeskSettingPromise) + await Promise.all(promises) } diff --git a/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts b/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts index 0ecf839d09..1df96b01ac 100755 --- a/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts +++ b/packages/server-core/src/user/identity-provider/identity-provider.hooks.ts @@ -44,6 +44,7 @@ import { import { userPath } from '@etherealengine/common/src/schemas/user/user.schema' import { checkScope } from '@etherealengine/spatial/src/common/functions/checkScope' +import { projectPath, projectPermissionPath, UserType } from '@etherealengine/common/src/schema.type.module' import { HookContext } from '../../../declarations' import appConfig from '../../appconfig' import persistData from '../../hooks/persist-data' @@ -197,6 +198,31 @@ async function addScopes(context: HookContext) { } } +const addDevProjectPermissions = async (context: HookContext) => { + if (!isDev || !(await checkScope(context.existingUser, 'admin', 'admin'))) return + + const user = context.existingUser as UserType + + const projects = await context.app.service(projectPath).find({ paginate: false }) + + const staticResourcePermission = await context.app.service(scopePath).find({ + query: { + userId: user.id, + type: 'static_resource:write' as ScopeType + } + }) + + if (staticResourcePermission.total > 0) { + for (const project of projects) { + await context.app.service(projectPermissionPath).create({ + projectId: project.id, + userId: user.id, + type: 'owner' + }) + } + } +} + async function createAccessToken(context: HookContext) { if (!(context.result as IdentityProviderType).accessToken) { ;(context.result as IdentityProviderType).accessToken = await context.app @@ -248,7 +274,7 @@ export default { all: [], find: [], get: [], - create: [addScopes, createAccessToken], + create: [addScopes, addDevProjectPermissions, createAccessToken], update: [], patch: [], remove: [] diff --git a/packages/server-core/tests/storageprovider/storageprovider.test.ts b/packages/server-core/tests/storageprovider/storageprovider.test.ts index fbd2385c06..bee09620d7 100644 --- a/packages/server-core/tests/storageprovider/storageprovider.test.ts +++ b/packages/server-core/tests/storageprovider/storageprovider.test.ts @@ -31,8 +31,7 @@ import fetch from 'node-fetch' import path from 'path/posix' import { v4 as uuidv4 } from 'uuid' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import LocalStorage from '../../src/media/storageprovider/local.storage' import S3Provider from '../../src/media/storageprovider/s3.storage' diff --git a/packages/spatial/src/camera/CameraModule.ts b/packages/spatial/src/camera/CameraModule.ts index 3eccc6495b..12d04679de 100644 --- a/packages/spatial/src/camera/CameraModule.ts +++ b/packages/spatial/src/camera/CameraModule.ts @@ -26,5 +26,6 @@ Ethereal Engine. All Rights Reserved. import { CameraFadeBlackEffectSystem } from './systems/CameraFadeBlackEffectSystem' import { CameraOrbitSystem } from './systems/CameraOrbitSystem' import { CameraSystem } from './systems/CameraSystem' +import { FollowCameraInputSystem } from './systems/FollowCameraInputSystem' -export default { CameraFadeBlackEffectSystem, CameraSystem, CameraOrbitSystem } +export default { CameraFadeBlackEffectSystem, CameraSystem, CameraOrbitSystem, FollowCameraInputSystem } diff --git a/packages/spatial/src/camera/CameraSceneMetadata.ts b/packages/spatial/src/camera/CameraSceneMetadata.ts index 223f0cc586..fcb1936c43 100644 --- a/packages/spatial/src/camera/CameraSceneMetadata.ts +++ b/packages/spatial/src/camera/CameraSceneMetadata.ts @@ -25,9 +25,10 @@ Ethereal Engine. All Rights Reserved. import { defineState } from '@etherealengine/hyperflux' -import { CameraMode } from './types/CameraMode' +import { FollowCameraMode } from './types/FollowCameraMode' import { ProjectionType } from './types/ProjectionType' +// TODO: don't mix camera settings and follow camera settings export const CameraSettingsState = defineState({ name: 'CameraSettingsState', initial: { @@ -35,11 +36,11 @@ export const CameraSettingsState = defineState({ cameraNearClip: 0.1, cameraFarClip: 1000, projectionType: ProjectionType.Perspective, - minCameraDistance: 1, + minCameraDistance: 1.5, maxCameraDistance: 50, startCameraDistance: 3, - cameraMode: CameraMode.Dynamic, - cameraModeDefault: CameraMode.ThirdPerson, + cameraMode: FollowCameraMode.Dynamic, + cameraModeDefault: FollowCameraMode.ThirdPerson, minPhi: -70, maxPhi: 85 } diff --git a/packages/spatial/src/camera/CameraState.ts b/packages/spatial/src/camera/CameraState.ts index fe12c4d109..a7a84af22f 100644 --- a/packages/spatial/src/camera/CameraState.ts +++ b/packages/spatial/src/camera/CameraState.ts @@ -32,7 +32,7 @@ import { SpawnObjectActions } from '../transform/SpawnObjectActions' export const CameraSettings = defineState({ name: 'xre.engine.CameraSettings', initial: () => ({ - cameraRotationSpeed: 100 + cameraRotationSpeed: 200 }) }) diff --git a/packages/spatial/src/camera/components/FollowCameraComponent.ts b/packages/spatial/src/camera/components/FollowCameraComponent.ts index a81c04ac17..d7f482b14c 100755 --- a/packages/spatial/src/camera/components/FollowCameraComponent.ts +++ b/packages/spatial/src/camera/components/FollowCameraComponent.ts @@ -30,12 +30,14 @@ import { defineQuery, ECSState, Engine, useEntityContext } from '@etherealengine import { defineComponent, getComponent, + getMutableComponent, getOptionalComponent, removeComponent, - setComponent + setComponent, + useComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' -import { getState } from '@etherealengine/hyperflux' +import { getState, matches, useImmediateEffect } from '@etherealengine/hyperflux' import { createConeOfVectors } from '../../common/functions/MathFunctions' import { smoothDamp, smootheLerpAlpha } from '../../common/functions/MathLerpFunctions' @@ -43,10 +45,11 @@ import { MeshComponent } from '../../renderer/components/MeshComponent' import { ObjectLayerComponents } from '../../renderer/components/ObjectLayerComponent' import { VisibleComponent } from '../../renderer/components/VisibleComponent' import { ObjectLayers } from '../../renderer/constants/ObjectLayers' -import { TransformComponent } from '../../SpatialModule' import { ComputedTransformComponent } from '../../transform/components/ComputedTransformComponent' +import { TransformComponent } from '../../transform/components/TransformComponent' import { CameraSettingsState } from '../CameraSceneMetadata' -import { CameraMode } from '../types/CameraMode' +import { setTargetCameraRotation } from '../functions/CameraFunctions' +import { FollowCameraMode, FollowCameraShoulderSide } from '../types/FollowCameraMode' import { TargetCameraRotationComponent } from './TargetCameraRotationComponent' export const coneDebugHelpers: ArrowHelper[] = [] @@ -71,7 +74,7 @@ export const FollowCameraComponent = defineComponent({ // } const cameraRays = [] as Vector3[] - const rayConeAngle = Math.PI / 6 + const rayConeAngle = Math.PI / 12 const camRayCastClock = new Clock() const camRayCastCache = { maxDistance: -1, @@ -94,54 +97,71 @@ export const FollowCameraComponent = defineComponent({ } return { - offset: new Vector3(), + firstPersonOffset: new Vector3(), + thirdPersonOffset: new Vector3(), + currentOffset: new Vector3(), + offsetSmoothness: 0.1, targetEntity: UndefinedEntity, currentTargetPosition: new Vector3(), targetPositionSmoothness: 0, - mode: CameraMode.ThirdPerson, + mode: FollowCameraMode.ThirdPerson, + allowedModes: [ + FollowCameraMode.ThirdPerson, + FollowCameraMode.FirstPerson, + FollowCameraMode.TopDown, + FollowCameraMode.ShoulderCam + ], distance: cameraSettings.startCameraDistance, - zoomLevel: 5, + targetDistance: 5, zoomVelocity: { value: 0 }, - minDistance: cameraSettings.minCameraDistance, - maxDistance: cameraSettings.maxCameraDistance, + thirdPersonMinDistance: cameraSettings.minCameraDistance, + thirdPersonMaxDistance: cameraSettings.maxCameraDistance, + effectiveMinDistance: cameraSettings.minCameraDistance, + effectiveMaxDistance: cameraSettings.maxCameraDistance, theta: 180, phi: 10, minPhi: cameraSettings.minPhi, maxPhi: cameraSettings.maxPhi, - shoulderSide: true, locked: false, enabled: true, - raycastProps + shoulderSide: FollowCameraShoulderSide.Left, + raycastProps, + accumulatedZoomTriggerDebounceTime: -1, + lastZoomStartDistance: (cameraSettings.minCameraDistance + cameraSettings.minCameraDistance) / 2 } }, onSet: (entity, component, json) => { if (!json) return - if (typeof json.offset !== 'undefined') component.offset.set(json.offset) + if (typeof json.firstPersonOffset !== 'undefined') component.firstPersonOffset.set(json.firstPersonOffset) + if (typeof json.thirdPersonOffset !== 'undefined') component.thirdPersonOffset.set(json.thirdPersonOffset) if (typeof json.targetEntity !== 'undefined') component.targetEntity.set(json.targetEntity) - if (typeof json.mode !== 'undefined') component.mode.set(json.mode) + if (typeof json.mode === 'string') component.mode.set(json.mode) + if (matches.arrayOf(matches.string).test(json.allowedModes)) component.allowedModes.set(json.allowedModes) if (typeof json.distance !== 'undefined') component.distance.set(json.distance) - if (typeof json.zoomLevel !== 'undefined') component.zoomLevel.set(json.zoomLevel) + if (typeof json.targetDistance !== 'undefined') component.targetDistance.set(json.targetDistance) if (typeof json.zoomVelocity !== 'undefined') component.zoomVelocity.set(json.zoomVelocity) - if (typeof json.minDistance !== 'undefined') component.minDistance.set(json.minDistance) - if (typeof json.maxDistance !== 'undefined') component.maxDistance.set(json.maxDistance) + if (typeof json.thirdPersonMinDistance !== 'undefined') + component.thirdPersonMinDistance.set(json.thirdPersonMinDistance) + if (typeof json.thirdPersonMaxDistance !== 'undefined') + component.thirdPersonMaxDistance.set(json.thirdPersonMaxDistance) if (typeof json.theta !== 'undefined') component.theta.set(json.theta) if (typeof json.phi !== 'undefined') component.phi.set(json.phi) if (typeof json.minPhi !== 'undefined') component.minPhi.set(json.minPhi) if (typeof json.maxPhi !== 'undefined') component.maxPhi.set(json.maxPhi) if (typeof json.shoulderSide !== 'undefined') component.shoulderSide.set(json.shoulderSide) - if (typeof json.locked !== 'undefined') component.locked.set(json.locked) }, reactor: () => { const entity = useEntityContext() + const follow = useComponent(entity, FollowCameraComponent) useEffect(() => { - const followCamera = getComponent(entity, FollowCameraComponent) + const follow = getComponent(entity, FollowCameraComponent) setComponent(entity, ComputedTransformComponent, { - referenceEntities: [followCamera.targetEntity], - computeFunction: () => computeCameraFollow(entity, followCamera.targetEntity) + referenceEntities: [follow.targetEntity], + computeFunction: () => computeCameraFollow(entity, follow.targetEntity) }) return () => { @@ -149,6 +169,12 @@ export const FollowCameraComponent = defineComponent({ } }, []) + useImmediateEffect(() => { + if (follow.mode.value === FollowCameraMode.FirstPerson) { + follow.targetDistance.set(0) + } + }, [follow.mode]) + return null } }) @@ -161,61 +187,186 @@ const mx = new Matrix4() const tempVec1 = new Vector3() const raycaster = new Raycaster() -export const computeCameraFollow = (cameraEntity: Entity, referenceEntity: Entity) => { - const followCamera = getComponent(cameraEntity, FollowCameraComponent) +const MODE_SWITCH_DEBOUNCE = 0.03 + +const computeCameraFollow = (cameraEntity: Entity, referenceEntity: Entity) => { + const follow = getComponent(cameraEntity, FollowCameraComponent) + const followState = getMutableComponent(cameraEntity, FollowCameraComponent) const cameraTransform = getComponent(cameraEntity, TransformComponent) const targetTransform = getComponent(referenceEntity, TransformComponent) - if (!targetTransform || !followCamera || !followCamera?.enabled) return + if (!targetTransform || !follow || !follow?.enabled) return // Limit the pitch - followCamera.phi = Math.min(followCamera.maxPhi, Math.max(followCamera.minPhi, followCamera.phi)) + follow.phi = Math.min(follow.maxPhi, Math.max(follow.minPhi, follow.phi)) - let maxDistance = followCamera.zoomLevel let isInsideWall = false + const offsetAlpha = smootheLerpAlpha(follow.offsetSmoothness, getState(ECSState).deltaSeconds) + const targetOffset = + follow.mode === FollowCameraMode.FirstPerson ? follow.firstPersonOffset : follow.thirdPersonOffset + follow.currentOffset.lerp(targetOffset, offsetAlpha) + targetPosition - .copy(followCamera.offset) + .copy(follow.currentOffset) .applyQuaternion(TransformComponent.getWorldRotation(referenceEntity, targetTransform.rotation)) .add(TransformComponent.getWorldPosition(referenceEntity, new Vector3())) - const alpha = smootheLerpAlpha(followCamera.targetPositionSmoothness, getState(ECSState).deltaSeconds) - followCamera.currentTargetPosition.lerp(targetPosition, alpha) + const targetPositionAlpha = smootheLerpAlpha(follow.targetPositionSmoothness, getState(ECSState).deltaSeconds) + follow.currentTargetPosition.lerp(targetPosition, targetPositionAlpha) // Run only if not in first person mode - if (followCamera.raycastProps.enabled && followCamera.zoomLevel >= followCamera.minDistance) { - const distanceResults = getMaxCamDistance(cameraEntity, followCamera.currentTargetPosition) - maxDistance = distanceResults.maxDistance + let obstacleDistance = Infinity + if (follow.raycastProps.enabled && follow.mode !== FollowCameraMode.FirstPerson) { + const distanceResults = getMaxCamDistance(cameraEntity, follow.currentTargetPosition) + obstacleDistance = distanceResults.maxDistance isInsideWall = distanceResults.targetHit } - const newZoomDistance = Math.min(followCamera.zoomLevel, maxDistance) + if (follow.mode === FollowCameraMode.FirstPerson) { + follow.effectiveMinDistance = follow.effectiveMaxDistance = 0 + } else if (follow.mode === FollowCameraMode.ThirdPerson || follow.mode === FollowCameraMode.ShoulderCam) { + follow.effectiveMaxDistance = Math.min(obstacleDistance * 0.8, follow.thirdPersonMaxDistance) + follow.effectiveMinDistance = Math.min(follow.thirdPersonMinDistance, follow.effectiveMaxDistance) + } else if (follow.mode === FollowCameraMode.TopDown) { + follow.effectiveMinDistance = follow.effectiveMaxDistance = Math.min( + obstacleDistance * 0.9, + follow.thirdPersonMaxDistance + ) + } + + let newZoomDistance = Math.max( + Math.min(follow.targetDistance, follow.effectiveMaxDistance), + follow.effectiveMinDistance + ) + + const constrainTargetDistance = follow.accumulatedZoomTriggerDebounceTime === -1 + + if (constrainTargetDistance) { + follow.targetDistance = newZoomDistance + } + + const triggerZoomShift = follow.accumulatedZoomTriggerDebounceTime > MODE_SWITCH_DEBOUNCE + + const minSpringFactor = + Math.min( + Math.sqrt(Math.abs(follow.targetDistance - follow.effectiveMinDistance)) * + Math.sign(follow.targetDistance - follow.effectiveMinDistance), + 0 + ) * 0.5 + + const maxSpringFactor = + Math.max( + Math.sqrt(Math.abs(follow.targetDistance - follow.effectiveMaxDistance)) * + Math.sign(follow.targetDistance - follow.effectiveMaxDistance), + 0 + ) * 0.5 + + if (follow.mode === FollowCameraMode.FirstPerson) { + newZoomDistance = Math.sqrt(follow.targetDistance) * 0.5 + // Move from first person mode to third person mode + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + follow.allowedModes.includes(FollowCameraMode.ThirdPerson) && + newZoomDistance > 0.1 * follow.thirdPersonMinDistance + ) { + // setup third person mode + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.ThirdPerson) + follow.targetDistance = newZoomDistance = follow.thirdPersonMinDistance + } else { + // reset first person mode + follow.targetDistance = newZoomDistance = 0 + } + } + } else if (follow.mode === FollowCameraMode.ThirdPerson) { + newZoomDistance = newZoomDistance + minSpringFactor + maxSpringFactor + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + // Move from third person mode to first person mode + follow.allowedModes.includes(FollowCameraMode.FirstPerson) && + follow.targetDistance < follow.effectiveMinDistance - follow.effectiveMaxDistance * 0.05 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMinDistance) < follow.effectiveMaxDistance * 0.05 + ) { + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.FirstPerson) + follow.targetDistance = newZoomDistance = 0 + } else if ( + // Move from third person mode to top down mode + follow.allowedModes.includes(FollowCameraMode.TopDown) && + follow.targetDistance > follow.effectiveMaxDistance + follow.effectiveMaxDistance * 0.02 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMaxDistance) < follow.effectiveMaxDistance * 0.02 + ) { + setTargetCameraRotation(cameraEntity, 85, follow.theta) + followState.mode.set(FollowCameraMode.TopDown) + follow.targetDistance = newZoomDistance = follow.effectiveMaxDistance + } else { + follow.targetDistance = newZoomDistance = Math.max( + Math.min(follow.targetDistance, follow.effectiveMaxDistance), + follow.effectiveMinDistance + ) + } + } + } else if (follow.mode === FollowCameraMode.TopDown) { + newZoomDistance += minSpringFactor + maxSpringFactor * 0.1 + // Move from top down mode to third person mode + if (triggerZoomShift) { + follow.accumulatedZoomTriggerDebounceTime = -1 + if ( + follow.allowedModes.includes(FollowCameraMode.ThirdPerson) && + newZoomDistance < follow.effectiveMaxDistance * 0.98 && + Math.abs(follow.lastZoomStartDistance - follow.effectiveMaxDistance) < 0.05 * follow.effectiveMaxDistance + ) { + setTargetCameraRotation(cameraEntity, 0, follow.theta) + followState.mode.set(FollowCameraMode.ThirdPerson) + } + follow.targetDistance = newZoomDistance = follow.effectiveMaxDistance + } + } + + // // Move from third person mode to top down mode + // if (allowModeShift && follow.mode === FollowCameraMode.ThirdPerson && + // follow.allowedModes.includes(FollowCameraMode.TopDown) && + // follow.targetDistance >= 1.1 * follow.thirdPersonMaxDistance) { + // setTargetCameraRotation(cameraEntity, 90, follow.theta) + // followState.mode.set(FollowCameraMode.TopDown) + // } + + // // Rotate camera to the top but let the player rotate if he/she desires + // if (Math.abs(follow.thirdPersonMaxDistance - nextTargetDistance) <= 1.0 && scrollDelta > 0 && follow) { + // setTargetCameraRotation(cameraEntity, 85, follow.theta) + // } + + // // Rotate from top + // if (Math.abs(follow.thirdPersonMaxDistance - follow.targetDistance) <= 1.0 && scrollDelta < 0 && follow.phi >= 80) { + // setTargetCameraRotation(cameraEntity, 45, follow.theta) + // } + + // if (Math.abs(follow.targetDistance - nextTargetDistance) > epsilon) { + // follow.targetDistance = nextTargetDistance + // } // Zoom smoothing const smoothingSpeed = isInsideWall ? 0.1 : 0.3 const deltaSeconds = getState(ECSState).deltaSeconds - followCamera.distance = smoothDamp( - followCamera.distance, - newZoomDistance, - followCamera.zoomVelocity, - smoothingSpeed, - deltaSeconds - ) + follow.distance = smoothDamp(follow.distance, newZoomDistance, follow.zoomVelocity, smoothingSpeed, deltaSeconds) - const theta = followCamera.theta + const theta = follow.theta const thetaRad = MathUtils.degToRad(theta) - const phiRad = MathUtils.degToRad(followCamera.phi) + const phiRad = MathUtils.degToRad(follow.phi) + + direction.set(Math.sin(thetaRad) * Math.cos(phiRad), Math.sin(phiRad), Math.cos(thetaRad) * Math.cos(phiRad)) cameraTransform.position.set( - followCamera.currentTargetPosition.x + followCamera.distance * Math.sin(thetaRad) * Math.cos(phiRad), - followCamera.currentTargetPosition.y + followCamera.distance * Math.sin(phiRad), - followCamera.currentTargetPosition.z + followCamera.distance * Math.cos(thetaRad) * Math.cos(phiRad) + follow.currentTargetPosition.x + follow.distance * direction.x, + follow.currentTargetPosition.y + follow.distance * direction.y, + follow.currentTargetPosition.z + follow.distance * direction.z ) - direction.copy(cameraTransform.position).sub(followCamera.currentTargetPosition).normalize() mx.lookAt(direction, empty, upVector) - cameraTransform.rotation.setFromRotationMatrix(mx) updateCameraTargetRotation(cameraEntity) @@ -223,33 +374,33 @@ export const computeCameraFollow = (cameraEntity: Entity, referenceEntity: Entit const updateCameraTargetRotation = (cameraEntity: Entity) => { if (!cameraEntity) return - const followCamera = getComponent(cameraEntity, FollowCameraComponent) + const follow = getComponent(cameraEntity, FollowCameraComponent) const target = getOptionalComponent(cameraEntity, TargetCameraRotationComponent) if (!target) return const epsilon = 0.001 - target.phi = Math.min(followCamera.maxPhi, Math.max(followCamera.minPhi, target.phi)) + target.phi = Math.min(follow.maxPhi, Math.max(follow.minPhi, target.phi)) - if (Math.abs(target.phi - followCamera.phi) < epsilon && Math.abs(target.theta - followCamera.theta) < epsilon) { - removeComponent(followCamera.targetEntity, TargetCameraRotationComponent) + if (Math.abs(target.phi - follow.phi) < epsilon && Math.abs(target.theta - follow.theta) < epsilon) { + removeComponent(follow.targetEntity, TargetCameraRotationComponent) return } const delta = getState(ECSState).deltaSeconds - if (!followCamera.locked) { - followCamera.phi = smoothDamp(followCamera.phi, target.phi, target.phiVelocity, target.time, delta) - followCamera.theta = smoothDamp(followCamera.theta, target.theta, target.thetaVelocity, target.time, delta) + if (!follow.locked) { + follow.phi = smoothDamp(follow.phi, target.phi, target.phiVelocity, target.time, delta) + follow.theta = smoothDamp(follow.theta, target.theta, target.thetaVelocity, target.time, delta) } } const cameraLayerQuery = defineQuery([VisibleComponent, ObjectLayerComponents[ObjectLayers.Camera], MeshComponent]) const getMaxCamDistance = (cameraEntity: Entity, target: Vector3) => { - const followCamera = getComponent(cameraEntity, FollowCameraComponent) + const follow = getComponent(cameraEntity, FollowCameraComponent) // Cache the raycast result for 0.1 seconds - const raycastProps = followCamera.raycastProps + const raycastProps = follow.raycastProps const { camRayCastCache, camRayCastClock, cameraRays, rayConeAngle } = raycastProps if (camRayCastCache.maxDistance != -1 && camRayCastClock.getElapsedTime() < raycastProps.rayFrequency) { return camRayCastCache @@ -266,13 +417,13 @@ const getMaxCamDistance = (cameraEntity: Entity, target: Vector3) => { createConeOfVectors(targetToCamVec, cameraRays, rayConeAngle) - let maxDistance = Math.min(followCamera.maxDistance, raycastProps.rayLength) + let maxDistance = Math.min(follow.thirdPersonMaxDistance, raycastProps.rayLength) // Check hit with mid ray raycaster.layers.set(ObjectLayers.Camera) // Ignore avatars // @ts-ignore - todo figure out why typescript freaks out at this raycaster.firstHitOnly = true // three-mesh-bvh setting - raycaster.far = followCamera.maxDistance + raycaster.far = follow.thirdPersonMaxDistance raycaster.set(target, targetToCamVec.normalize()) const hits = raycaster.intersectObjects(sceneObjects, false) diff --git a/packages/spatial/src/camera/functions/CameraFunctions.ts b/packages/spatial/src/camera/functions/CameraFunctions.ts index 82abeb4a13..7c389a9d17 100644 --- a/packages/spatial/src/camera/functions/CameraFunctions.ts +++ b/packages/spatial/src/camera/functions/CameraFunctions.ts @@ -23,12 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { clamp } from 'lodash' - import { ComponentType, getOptionalComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Entity } from '@etherealengine/ecs/src/Entity' -import { FollowCameraComponent } from '../components/FollowCameraComponent' import { TargetCameraRotationComponent } from '../components/TargetCameraRotationComponent' export const setTargetCameraRotation = (entity: Entity, phi: number, theta: number, time = 0.3) => { @@ -49,53 +46,3 @@ export const setTargetCameraRotation = (entity: Entity, phi: number, theta: numb cameraRotationTransition.time = time } } - -/** - * Change camera distance. - * @param cameraEntity Entity holding camera and input component. - */ -export const handleCameraZoom = (cameraEntity: Entity, scrollDelta: number): void => { - if (scrollDelta === 0) { - return - } - - const followComponent = getOptionalComponent(cameraEntity, FollowCameraComponent) as - | ComponentType - | undefined - - if (!followComponent) { - return - } - - const epsilon = 0.001 - const nextZoomLevel = clamp(followComponent.zoomLevel + scrollDelta, epsilon, followComponent.maxDistance) - - // Move out of first person mode - if (followComponent.zoomLevel <= epsilon && scrollDelta > 0) { - followComponent.zoomLevel = followComponent.minDistance - return - } - - // Move to first person mode - if (nextZoomLevel < followComponent.minDistance) { - followComponent.zoomLevel = epsilon - setTargetCameraRotation(cameraEntity, 0, followComponent.theta) - return - } - - // Rotate camera to the top but let the player rotate if he/she desires - if (Math.abs(followComponent.maxDistance - nextZoomLevel) <= 1.0 && scrollDelta > 0) { - setTargetCameraRotation(cameraEntity, 85, followComponent.theta) - } - - // Rotate from top - if ( - Math.abs(followComponent.maxDistance - followComponent.zoomLevel) <= 1.0 && - scrollDelta < 0 && - followComponent.phi >= 80 - ) { - setTargetCameraRotation(cameraEntity, 45, followComponent.theta) - } - - followComponent.zoomLevel = nextZoomLevel -} diff --git a/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx b/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx index 72a26977ef..04471145f2 100644 --- a/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx +++ b/packages/spatial/src/camera/systems/CameraOrbitSystem.tsx @@ -81,7 +81,7 @@ const execute = () => { * assign active orbit camera based on which input source registers input */ for (const cameraEid of orbitCameraQuery()) { - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(cameraEid) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(cameraEid)[0] const cameraOrbit = getMutableComponent(cameraEid, CameraOrbitComponent) diff --git a/packages/spatial/src/camera/systems/CameraSystem.test.tsx b/packages/spatial/src/camera/systems/CameraSystem.test.tsx index 17944236bd..259b8d4b1e 100755 --- a/packages/spatial/src/camera/systems/CameraSystem.test.tsx +++ b/packages/spatial/src/camera/systems/CameraSystem.test.tsx @@ -41,6 +41,7 @@ import { removeEntity, setComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { PeerID, applyIncomingActions, dispatchAction } from '@etherealengine/hyperflux' import { Network, @@ -50,7 +51,6 @@ import { NetworkWorldUserStateSystem } from '@etherealengine/network' import { createMockNetwork } from '../../../../network/tests/createMockNetwork' -import { createEngine } from '../../initializeEngine' import { CameraActions } from '../CameraState' import { CameraComponent } from '../components/CameraComponent' diff --git a/packages/spatial/src/camera/systems/CameraSystem.tsx b/packages/spatial/src/camera/systems/CameraSystem.tsx index 3c507c009c..fa84058594 100755 --- a/packages/spatial/src/camera/systems/CameraSystem.tsx +++ b/packages/spatial/src/camera/systems/CameraSystem.tsx @@ -33,6 +33,7 @@ import { Engine, EntityUUID, getComponent, + getOptionalMutableComponent, setComponent, UUIDComponent } from '@etherealengine/ecs' @@ -45,7 +46,7 @@ import { TransformComponent } from '../../transform/components/TransformComponen import { CameraSettingsState } from '../CameraSceneMetadata' import { CameraActions } from '../CameraState' import { CameraComponent } from '../components/CameraComponent' -import { switchCameraMode } from '../functions/switchCameraMode' +import { FollowCameraComponent } from '../components/FollowCameraComponent' export const CameraEntityState = defineState({ name: 'CameraEntityState', @@ -92,12 +93,23 @@ function CameraReactor() { if (!cameraSettings?.cameraNearClip) return const camera = getComponent(Engine.instance.cameraEntity, CameraComponent) as PerspectiveCamera if (camera?.isPerspectiveCamera) { + camera.fov = cameraSettings.fov.value camera.near = cameraSettings.cameraNearClip.value camera.far = cameraSettings.cameraFarClip.value camera.updateProjectionMatrix() } - switchCameraMode(Engine.instance.cameraEntity, cameraSettings.value) - }, [cameraSettings.cameraNearClip, cameraSettings.cameraFarClip]) + }, [cameraSettings.fov, cameraSettings.cameraNearClip, cameraSettings.cameraFarClip]) + + // TODO: this is messy and not properly reactive; we need a better way to handle camera settings + useEffect(() => { + if (!cameraSettings?.fov) return + const follow = getOptionalMutableComponent(Engine.instance.cameraEntity, FollowCameraComponent) + if (follow) { + follow.thirdPersonMinDistance.set(cameraSettings.minCameraDistance.value) + follow.thirdPersonMaxDistance.set(cameraSettings.maxCameraDistance.value) + follow.distance.set(cameraSettings.startCameraDistance.value) + } + }, [cameraSettings]) return null } diff --git a/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts b/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts new file mode 100644 index 0000000000..3b36b56c5e --- /dev/null +++ b/packages/spatial/src/camera/systems/FollowCameraInputSystem.ts @@ -0,0 +1,170 @@ +/* +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 { Vector2 } from 'three' + +import { Entity } from '@etherealengine/ecs' +import { getComponent, getMutableComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { ECSState } from '@etherealengine/ecs/src/ECSState' +import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' +import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' +import { InputSystemGroup } from '@etherealengine/ecs/src/SystemGroups' +import { getState } from '@etherealengine/hyperflux' +import { CameraSettings } from '@etherealengine/spatial/src/camera/CameraState' +import { FollowCameraComponent } from '@etherealengine/spatial/src/camera/components/FollowCameraComponent' +import { TargetCameraRotationComponent } from '@etherealengine/spatial/src/camera/components/TargetCameraRotationComponent' +import { setTargetCameraRotation } from '@etherealengine/spatial/src/camera/functions/CameraFunctions' +import { FollowCameraMode } from '@etherealengine/spatial/src/camera/types/FollowCameraMode' +import { DefaultAxisAlias, InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' +import { InputPointerComponent } from '@etherealengine/spatial/src/input/components/InputPointerComponent' +import { InputSourceComponent } from '@etherealengine/spatial/src/input/components/InputSourceComponent' +import { getThumbstickOrThumbpadAxes } from '@etherealengine/spatial/src/input/functions/getThumbstickOrThumbpadAxes' +import { AxisValueMap } from '@etherealengine/spatial/src/input/state/ButtonState' +import { InputState } from '@etherealengine/spatial/src/input/state/InputState' +import { XRState } from '@etherealengine/spatial/src/xr/XRState' +import { RendererComponent } from '../../renderer/WebGLRendererSystem' + +// const throttleHandleCameraZoom = throttle(handleFollowCameraZoom, 30, { leading: true, trailing: false }) + +const pointerPositionDelta = new Vector2() +const rendererQuery = defineQuery([RendererComponent]) +const epsilon = 0.001 + +const followCameraModeCycle = [ + FollowCameraMode.FirstPerson, + FollowCameraMode.ShoulderCam, + FollowCameraMode.ThirdPerson, + FollowCameraMode.TopDown +] as FollowCameraMode[] + +const onFollowCameraModeCycle = (cameraEntity: Entity) => { + const follow = getMutableComponent(cameraEntity, FollowCameraComponent) + const mode = follow.mode.value + const currentModeIdx = followCameraModeCycle.includes(mode) ? followCameraModeCycle.indexOf(mode) : 0 + const nextModeIdx = (currentModeIdx + 1) % followCameraModeCycle.length + const nextMode = followCameraModeCycle[nextModeIdx] + follow.mode.set(nextMode) +} + +const onFollowCameraFirstPerson = (cameraEntity: Entity) => { + const followComponent = getMutableComponent(cameraEntity, FollowCameraComponent) + followComponent.mode.set(FollowCameraMode.FirstPerson) +} + +const onFollowCameraShoulderCam = (cameraEntity: Entity) => { + const follow = getMutableComponent(cameraEntity, FollowCameraComponent) + follow.mode.set(FollowCameraMode.ShoulderCam) +} + +/** + * Change camera distance. + * @param cameraEntity Entity holding camera and input component. + */ +export const handleFollowCameraScroll = ( + cameraEntity: Entity, + axes: AxisValueMap, + deltaTime: number +): void => { + const follow = getComponent(cameraEntity, FollowCameraComponent) + + const zoomDelta = axes.FollowCameraZoomScroll ?? 0 + const shoulderDelta = axes.FollowCameraShoulderCamScroll ?? 0 + + follow.targetDistance = Math.max(follow.targetDistance + zoomDelta, 0) + + // Math.min( + // Math.max(follow.targetDistance + zoomDelta, follow.effectiveMinDistance * 0.8), + // follow.effectiveMaxDistance * 1.2 + // ) + + const outsideMinMaxRange = + follow.targetDistance < follow.effectiveMinDistance || follow.targetDistance > follow.effectiveMaxDistance + + if (zoomDelta === 0 && shoulderDelta === 0 && follow.accumulatedZoomTriggerDebounceTime >= 0 && outsideMinMaxRange) { + follow.accumulatedZoomTriggerDebounceTime += deltaTime + } else if (Math.abs(zoomDelta) > 0 || Math.abs(shoulderDelta) > 0) { + if (follow.accumulatedZoomTriggerDebounceTime === -1) { + follow.lastZoomStartDistance = follow.distance + } + follow.accumulatedZoomTriggerDebounceTime = 0 + } +} + +const execute = () => { + if (getState(XRState).xrFrame) return + + const deltaSeconds = getState(ECSState).deltaSeconds + const cameraSettings = getState(CameraSettings) + + for (const cameraEntity of rendererQuery()) { + const buttons = InputComponent.getMergedButtons(cameraEntity) + const axes = InputComponent.getMergedAxes(cameraEntity) + + const inputPointerEntities = InputPointerComponent.getPointersForCamera(cameraEntity) + const inputState = getState(InputState) + + const follow = getOptionalComponent(cameraEntity, FollowCameraComponent) + if (!follow) continue + + let { theta, phi } = getOptionalComponent(cameraEntity, TargetCameraRotationComponent) ?? follow + let time = 0.3 + + if (buttons?.PrimaryClick?.pressed && buttons?.PrimaryClick?.dragging) { + InputState.setCapturingEntity(cameraEntity) + } + if (buttons?.FollowCameraModeCycle?.down) onFollowCameraModeCycle(cameraEntity) + if (buttons?.FollowCameraFirstPerson?.down) onFollowCameraFirstPerson(cameraEntity) + if (buttons?.FollowCameraShoulderCam?.down) onFollowCameraShoulderCam(cameraEntity) + + const keyDelta = (buttons?.ArrowLeft ? 1 : 0) + (buttons?.ArrowRight ? -1 : 0) + theta += 100 * deltaSeconds * keyDelta + + for (const inputPointerEid of inputPointerEntities) { + const inputSource = getComponent(inputPointerEid, InputSourceComponent) + const [x, y] = getThumbstickOrThumbpadAxes(inputSource.source, inputState.preferredHand) + theta -= x * 2 + phi += y * 2 + const pointerDragging = inputSource.buttons?.PrimaryClick?.dragging + if (pointerDragging) { + const inputPointer = getComponent(inputPointerEid, InputPointerComponent) + pointerPositionDelta.copy(inputPointer.movement) + phi -= pointerPositionDelta.y * cameraSettings.cameraRotationSpeed + theta -= pointerPositionDelta.x * cameraSettings.cameraRotationSpeed + time = 0.1 + } + } + + if (getState(InputState).capturingEntity === cameraEntity) { + setTargetCameraRotation(cameraEntity, phi, theta, time) + } + handleFollowCameraScroll(cameraEntity, axes, deltaSeconds) + } +} + +export const FollowCameraInputSystem = defineSystem({ + uuid: 'ee.engine.FollowCameraInputSystem', + insert: { after: InputSystemGroup }, + execute +}) diff --git a/packages/spatial/src/camera/systems/SpectateSystem.test.tsx b/packages/spatial/src/camera/systems/SpectateSystem.test.tsx index bf97a59c0f..9168acb6d1 100644 --- a/packages/spatial/src/camera/systems/SpectateSystem.test.tsx +++ b/packages/spatial/src/camera/systems/SpectateSystem.test.tsx @@ -38,6 +38,7 @@ import { removeEntity, setComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { PeerID, applyIncomingActions, dispatchAction, getState } from '@etherealengine/hyperflux' import { Network, @@ -47,7 +48,6 @@ import { NetworkWorldUserStateSystem } from '@etherealengine/network' import { createMockNetwork } from '../../../../network/tests/createMockNetwork' -import { createEngine } from '../../initializeEngine' import { SpectateActions, SpectateEntityState } from './SpectateSystem' describe('SpectateSystem', async () => { diff --git a/packages/spatial/src/camera/systems/SpectateSystem.tsx b/packages/spatial/src/camera/systems/SpectateSystem.tsx index 30cfff4a54..8bc88f615e 100644 --- a/packages/spatial/src/camera/systems/SpectateSystem.tsx +++ b/packages/spatial/src/camera/systems/SpectateSystem.tsx @@ -47,8 +47,8 @@ import { } from '@etherealengine/hyperflux' import { matchesUserID, NetworkObjectComponent, NetworkTopics, WorldNetworkAction } from '@etherealengine/network' -import { TransformComponent } from '../../SpatialModule' import { ComputedTransformComponent } from '../../transform/components/ComputedTransformComponent' +import { TransformComponent } from '../../transform/components/TransformComponent' import { CameraComponent } from '../components/CameraComponent' import { FlyControlComponent } from '../components/FlyControlComponent' diff --git a/packages/spatial/src/camera/types/FollowCameraMode.ts b/packages/spatial/src/camera/types/FollowCameraMode.ts new file mode 100755 index 0000000000..6d04ea1477 --- /dev/null +++ b/packages/spatial/src/camera/types/FollowCameraMode.ts @@ -0,0 +1,39 @@ +/* +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. +*/ + +/** Camera Modes. */ +export enum FollowCameraMode { + FirstPerson = 'FirstPerson', + ShoulderCam = 'ShoulderCam', + ThirdPerson = 'ThirdPerson', + TopDown = 'TopDown', + Strategic = 'Strategic', + Dynamic = 'Dynamic' +} + +export enum FollowCameraShoulderSide { + Left = 'Left', + Right = 'Right' +} diff --git a/packages/spatial/src/common/functions/FeathersHooks.test.ts b/packages/spatial/src/common/functions/FeathersHooks.test.ts index ccaabf7bd2..ce8a7d70b3 100644 --- a/packages/spatial/src/common/functions/FeathersHooks.test.ts +++ b/packages/spatial/src/common/functions/FeathersHooks.test.ts @@ -32,7 +32,7 @@ import { AvatarID, UserName, userPath } from '@etherealengine/common/src/schema. import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { createState } from '@etherealengine/hyperflux' -import { createEngine } from '../../initializeEngine' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { EventDispatcher } from '../classes/EventDispatcher' import { useFind, useGet, useMutation } from './FeathersHooks' diff --git a/packages/spatial/src/common/functions/FeathersHooks.tsx b/packages/spatial/src/common/functions/FeathersHooks.tsx index 7927828263..db3e7e64dc 100644 --- a/packages/spatial/src/common/functions/FeathersHooks.tsx +++ b/packages/spatial/src/common/functions/FeathersHooks.tsx @@ -76,6 +76,7 @@ export const FeathersState = defineState({ QueryHash, { fetch: () => void + query: any response: unknown status: 'pending' | 'success' | 'error' error: string @@ -118,11 +119,13 @@ export const useService = ( const service = Engine.instance.api.service(serviceName) const state = useMutableState(FeathersState) - const queryId = `${method.substring(0, 1)}:${hashObject({ + const queryParams = { serviceName, method, args - })}` as QueryHash + } + + const queryId = `${method.substring(0, 1)}:${hashObject(queryParams)}` as QueryHash const fetch = () => { if (method === 'get' && !args) { @@ -159,6 +162,7 @@ export const useService = ( state[serviceName].merge({ [queryId]: { fetch, + query: queryParams, response: null, status: 'pending', error: '' diff --git a/packages/spatial/src/initializeEngine.ts b/packages/spatial/src/initializeEngine.ts index 24934fd1dc..6dc50cef89 100644 --- a/packages/spatial/src/initializeEngine.ts +++ b/packages/spatial/src/initializeEngine.ts @@ -27,17 +27,13 @@ import { BoxGeometry, Group, Mesh, MeshNormalMaterial } from 'three' import { createEntity, - ECSState, - executeSystems, getComponent, getMutableComponent, removeEntity, setComponent, UUIDComponent } from '@etherealengine/ecs' -import { Engine, startEngine } from '@etherealengine/ecs/src/Engine' import { EntityUUID, UndefinedEntity } from '@etherealengine/ecs/src/Entity' -import { Timer } from '@etherealengine/ecs/src/Timer' import { getMutableState, getState } from '@etherealengine/hyperflux' import { CameraComponent } from './camera/components/CameraComponent' @@ -53,22 +49,6 @@ import { PerformanceManager } from './renderer/PerformanceState' import { RendererComponent } from './renderer/WebGLRendererSystem' import { EntityTreeComponent } from './transform/components/EntityTree' import { TransformComponent } from './transform/components/TransformComponent' -import { XRState } from './xr/XRState' - -/** - * Creates a new instance of the engine and engine renderer. This initializes all properties and state for the engine, - * adds action receptors and creates a new world. - * @returns {Engine} - */ -export const createEngine = () => { - startEngine() - const timer = Timer((time, xrFrame) => { - getMutableState(XRState).xrFrame.set(xrFrame) - executeSystems(time) - getMutableState(XRState).xrFrame.set(null) - }) - getMutableState(ECSState).timer.set(timer) -} export const initializeSpatialEngine = (canvas?: HTMLCanvasElement) => { const originEntity = createEntity() diff --git a/packages/spatial/src/input/components/InputComponent.test.tsx b/packages/spatial/src/input/components/InputComponent.test.tsx index 002593eb5e..37be9291b5 100644 --- a/packages/spatial/src/input/components/InputComponent.test.tsx +++ b/packages/spatial/src/input/components/InputComponent.test.tsx @@ -34,7 +34,8 @@ import { import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { ReactorReconciler } from '@etherealengine/hyperflux' -import { createEngine, initializeSpatialEngine } from '../../initializeEngine' +import { createEngine } from '@etherealengine/ecs/src/Engine' +import { initializeSpatialEngine } from '../../initializeEngine' import { HighlightComponent } from '../../renderer/components/HighlightComponent' import { InputComponent } from './InputComponent' diff --git a/packages/spatial/src/input/components/InputComponent.ts b/packages/spatial/src/input/components/InputComponent.ts index ce3d2ffdf0..ee0209b1c8 100644 --- a/packages/spatial/src/input/components/InputComponent.ts +++ b/packages/spatial/src/input/components/InputComponent.ts @@ -46,16 +46,39 @@ import { EngineState } from '../../EngineState' import { HighlightComponent } from '../../renderer/components/HighlightComponent' import { getAncestorWithComponent, isAncestor } from '../../transform/components/EntityTree' -import { ButtonState, ButtonStateMap, KeyboardButton, MouseButton, XRStandardGamepadButton } from '../state/ButtonState' +import { + AnyAxis, + AnyButton, + AxisMapping, + AxisValueMap, + ButtonStateMap, + KeyboardButton, + MouseButton, + MouseScroll, + XRStandardGamepadAxes, + XRStandardGamepadButton +} from '../state/ButtonState' import { InputState } from '../state/InputState' import { InputSinkComponent } from './InputSinkComponent' import { InputSourceComponent } from './InputSourceComponent' export type InputAlias = Record -export const DefaultInputAlias = { - Interact: [MouseButton.PrimaryClick, XRStandardGamepadButton.Trigger, KeyboardButton.KeyE] -} +export const DefaultButtonAlias = { + Interact: [MouseButton.PrimaryClick, XRStandardGamepadButton.XRStandardGamepadTrigger, KeyboardButton.KeyE], + FollowCameraModeCycle: [KeyboardButton.KeyV], + FollowCameraFirstPerson: [KeyboardButton.KeyF], + FollowCameraShoulderCam: [KeyboardButton.KeyC] +} satisfies Record> + +export const DefaultAxisAlias = { + FollowCameraZoomScroll: [ + MouseScroll.VerticalScroll, + XRStandardGamepadAxes.XRStandardGamepadThumbstickY, + XRStandardGamepadAxes.XRStandardGamepadTouchpadY + ], + FollowCameraShoulderCamScroll: [MouseScroll.HorizontalScroll] +} satisfies Record> export const InputComponent = defineComponent({ name: 'InputComponent', @@ -76,7 +99,7 @@ export const InputComponent = defineComponent({ onSet(entity, component, json) { if (!json) return - if (typeof json.inputSinks === 'object') component.inputSinks.set(json.inputSinks) + if (Array.isArray(json.inputSinks)) component.inputSinks.set(json.inputSinks) if (typeof json.highlight === 'boolean') component.highlight.set(json.highlight) if (json.activationDistance) component.activationDistance.set(json.activationDistance) if (typeof json.grow === 'boolean') component.grow.set(json.grow) @@ -124,32 +147,32 @@ export const InputComponent = defineComponent({ }, []) }, - getMergedButtons( + getMergedButtons( entityContext: Entity, - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultButtonAlias as unknown as AliasType ) { const inputSourceEntities = InputComponent.getInputSourceEntities(entityContext) return InputComponent.getMergedButtonsForInputSources(inputSourceEntities, inputAlias) }, - getMergedAxes( + getMergedAxes( entityContext: Entity, - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultAxisAlias as unknown as AliasType ) { const inputSourceEntities = InputComponent.getInputSourceEntities(entityContext) return InputComponent.getMergedAxesForInputSources(inputSourceEntities, inputAlias) }, - getMergedButtonsForInputSources( + getMergedButtonsForInputSources( inputSourceEntities: Entity[], - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultButtonAlias as unknown as AliasType ) { const buttons = Object.assign( - {} as ButtonStateMap, + {}, ...inputSourceEntities.map((eid) => { return getComponent(eid, InputSourceComponent).buttons }) - ) as ButtonStateMap & Partial> + ) as ButtonStateMap for (const key of Object.keys(inputAlias)) { const k = key as keyof AliasType @@ -159,34 +182,36 @@ export const InputComponent = defineComponent({ return buttons }, - getMergedAxesForInputSources( + getMergedAxesForInputSources( inputSourceEntities: Entity[], - inputAlias: AliasType = DefaultInputAlias as unknown as AliasType + inputAlias: AliasType = DefaultAxisAlias as unknown as AliasType ) { const axes = { 0: 0, 1: 0, 2: 0, 3: 0 - } as Record + } as any for (const eid of inputSourceEntities) { const inputSource = getComponent(eid, InputSourceComponent) if (inputSource.source.gamepad?.axes) { + const mapping = AxisMapping[inputSource.source.gamepad.mapping] for (let i = 0; i < 4; i++) { const newAxis = inputSource.source.gamepad.axes[i] ?? 0 - axes[i] = getLargestMagnitudeNumber(axes[i], newAxis) + axes[i] = getLargestMagnitudeNumber(axes[i] ?? 0, newAxis) + axes[mapping[i]] = axes[i] } } } for (const key of Object.keys(inputAlias)) { - axes[key] = inputAlias[key].reduce((prev, alias) => { - return getLargestMagnitudeNumber(prev, axes[alias]) + axes[key as any] = inputAlias[key].reduce((prev, alias) => { + return getLargestMagnitudeNumber(prev, axes[alias] ?? 0) }, 0) } - return axes + return axes as AxisValueMap }, useHasFocus() { diff --git a/packages/spatial/src/input/components/InputPointerComponent.ts b/packages/spatial/src/input/components/InputPointerComponent.ts index ef03416ef2..ac4f528439 100644 --- a/packages/spatial/src/input/components/InputPointerComponent.ts +++ b/packages/spatial/src/input/components/InputPointerComponent.ts @@ -26,6 +26,16 @@ Ethereal Engine. All Rights Reserved. import { Vector2 } from 'three' import { defineComponent, defineQuery, Entity, getComponent, UndefinedEntity } from '@etherealengine/ecs' +import { defineState, getState } from '@etherealengine/hyperflux' + +export const InputPointerState = defineState({ + name: 'InputPointerState', + initial() { + return { + pointers: new Map() + } + } +}) export const InputPointerComponent = defineComponent({ name: 'InputPointerComponent', @@ -36,17 +46,29 @@ export const InputPointerComponent = defineComponent({ position: new Vector2(), lastPosition: new Vector2(), movement: new Vector2(), - canvasEntity: UndefinedEntity + cameraEntity: UndefinedEntity } }, - onSet(entity, component, args: { pointerId: number; canvasEntity: Entity }) { + onSet(entity, component, args: { pointerId: number; cameraEntity: Entity }) { component.pointerId.set(args.pointerId) - component.canvasEntity.set(args.canvasEntity) + component.cameraEntity.set(args.cameraEntity) + const pointerHash = `canvas-${args.cameraEntity}.pointer-${args.pointerId}` + getState(InputPointerState).pointers.set(pointerHash, entity) + }, + + onRemove(entity, component) { + const pointerHash = `canvas-${component.cameraEntity}.pointer-${component.pointerId}` + getState(InputPointerState).pointers.delete(pointerHash) + }, + + getPointersForCamera(cameraEntity: Entity) { + return pointerQuery().filter((entity) => getComponent(entity, InputPointerComponent).cameraEntity === cameraEntity) }, - getPointerForCanvas(canvasEntity: Entity) { - return pointerQuery().find((entity) => getComponent(entity, InputPointerComponent).canvasEntity === canvasEntity) + getPointerByID(cameraEntity: Entity, pointerId: number) { + const pointerHash = `canvas-${cameraEntity}.pointer-${pointerId}` + return getState(InputPointerState).pointers.get(pointerHash) ?? UndefinedEntity } }) diff --git a/packages/spatial/src/input/components/InputSourceComponent.tsx b/packages/spatial/src/input/components/InputSourceComponent.tsx index 9c093fcc2e..5df66226c7 100644 --- a/packages/spatial/src/input/components/InputSourceComponent.tsx +++ b/packages/spatial/src/input/components/InputSourceComponent.tsx @@ -34,6 +34,7 @@ import { XRHandComponent, XRSpaceComponent } from '../../xr/XRComponents' import { ReferenceSpace, XRState } from '../../xr/XRState' import { ButtonStateMap } from '../state/ButtonState' import { InputState } from '../state/InputState' +import { DefaultButtonAlias } from './InputComponent' export const InputSourceComponent = defineComponent({ name: 'InputSourceComponent', @@ -41,7 +42,7 @@ export const InputSourceComponent = defineComponent({ onInit: () => { return { source: {} as XRInputSource, - buttons: {} as Readonly, + buttons: {} as Readonly>, raycaster: new Raycaster(), intersections: [] as Array<{ entity: Entity @@ -67,7 +68,7 @@ export const InputSourceComponent = defineComponent({ hapticActuators: [], id: 'emulated-gamepad-' + entity, index: 0, - mapping: 'standard', + mapping: '', timestamp: performance.now(), vibrationActuator: null } as Gamepad), @@ -118,6 +119,10 @@ export const InputSourceComponent = defineComponent({ return getComponent(inputSourceEntity, InputSourceComponent).intersections[0]?.entity }, + getClosestIntersection(inputSourceEntity: Entity) { + return getComponent(inputSourceEntity, InputSourceComponent).intersections[0] + }, + entitiesByInputSource: new WeakMap() }) diff --git a/packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts b/packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts new file mode 100644 index 0000000000..34f3a72aad --- /dev/null +++ b/packages/spatial/src/input/functions/getThumbstickOrThumbpadAxes.ts @@ -0,0 +1,37 @@ +/* +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. +*/ + +/** + * On 'xr-standard' mapping, get thumbstick input [2,3], fallback to thumbpad input [0,1] + * On 'standard' mapping, get thumbstick input [0,1] + */ +export function getThumbstickOrThumbpadAxes(inputSource: XRInputSource, handedness: XRHandedness, deadZone = 0.05) { + const gamepad = inputSource.gamepad + const axes = gamepad!.axes + const axesIndex = inputSource.gamepad?.mapping === 'xr-standard' || handedness === 'right' ? 2 : 0 + const xAxis = Math.abs(axes[axesIndex]) > deadZone ? axes[axesIndex] : 0 + const zAxis = Math.abs(axes[axesIndex + 1]) > deadZone ? axes[axesIndex + 1] : 0 + return [xAxis, zAxis] as [number, number] +} diff --git a/packages/spatial/src/input/state/ButtonState.ts b/packages/spatial/src/input/state/ButtonState.ts index 2d98fecdb8..5eeb0b9518 100644 --- a/packages/spatial/src/input/state/ButtonState.ts +++ b/packages/spatial/src/input/state/ButtonState.ts @@ -196,30 +196,30 @@ export enum KeyboardButton { * https://www.w3.org/TR/gamepad/#dfn-standard-gamepad */ export enum StandardGamepadButton { - 'ButtonA' = 0, // X - 'ButtonB' = 1, // Circle - 'ButtonX' = 2, // Square - 'ButtonY' = 3, // Triangle - 'Left1' = 4, - 'Right1' = 5, - 'Left2' = 6, - 'Right2' = 7, - 'ButtonBack' = 8, - 'ButtonStart' = 9, - 'LeftStick' = 10, - 'RightStick' = 11, - 'DPadUp' = 12, - 'DPadDown' = 13, - 'DPadLeft' = 14, - 'DPadRight' = 15, - 'ButtonHome' = 16 + 'StandardGamepadButtonA' = 0, // X + 'StandardGamepadButtonB' = 1, // Circle + 'StandardGamepadButtonX' = 2, // Square + 'StandardGamepadButtonY' = 3, // Triangle + 'StandardGamepadLeft1' = 4, + 'StandardGamepadRight1' = 5, + 'StandardGamepadLeft2' = 6, + 'StandardGamepadRight2' = 7, + 'StandardGamepadButtonBack' = 8, + 'StandardGamepadButtonStart' = 9, + 'StandardGamepadLeftStick' = 10, + 'StandardGamepadRightStick' = 11, + 'StandardGamepadDPadUp' = 12, + 'StandardGamepadDPadDown' = 13, + 'StandardGamepadDPadLeft' = 14, + 'StandardGamepadDPadRight' = 15, + 'StandardGamepadButtonHome' = 16 } export enum StandardGamepadAxes { - 'LeftStickX' = 0, - 'LeftStickY' = 1, - 'RightStickX' = 2, - 'RightStickY' = 3 + 'StandardGamepadLeftStickX' = 0, + 'StandardGamepadLeftStickY' = 1, + 'StandardGamepadRightStickX' = 2, + 'StandardGamepadRightStickY' = 3 } /** @@ -227,24 +227,53 @@ export enum StandardGamepadAxes { * https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping */ export enum XRStandardGamepadButton { - 'Trigger' = 0, - 'Squeeze' = 1, - 'Pad' = 2, - 'Stick' = 3, - 'ButtonA' = 4, - 'ButtonB' = 5 + 'XRStandardGamepadTrigger' = 0, + 'XRStandardGamepadSqueeze' = 1, + 'XRStandardGamepadPad' = 2, + 'XRStandardGamepadStick' = 3, + 'XRStandardGamepadButtonA' = 4, + 'XRStandardGamepadButtonB' = 5 } export enum XRStandardGamepadAxes { - 'TouchpadX' = 0, - 'TouchpadY' = 1, - 'ThumbstickX' = 2, - 'ThumbstickY' = 3 + 'XRStandardGamepadTouchpadX' = 0, + 'XRStandardGamepadTouchpadY' = 1, + 'XRStandardGamepadThumbstickX' = 2, + 'XRStandardGamepadThumbstickY' = 3 } -export type AnyButton = MouseButton | KeyboardButton | StandardGamepadButton | XRStandardGamepadButton +export type AnyButton = + | keyof typeof MouseButton + | keyof typeof KeyboardButton + | keyof typeof StandardGamepadButton + | keyof typeof XRStandardGamepadButton + | StandardGamepadButton + | XRStandardGamepadButton +export type AnyAxis = + | keyof typeof MouseScroll + | keyof typeof StandardGamepadAxes + | keyof typeof XRStandardGamepadAxes + | MouseScroll + | StandardGamepadAxes + | XRStandardGamepadAxes -export type ButtonStateMap = Partial> +export type ButtonStateMap> = Partial< + Record +> +export type AxisValueMap> = Partial> + +export const ButtonMapping = { + '': MouseButton, + keyboard: KeyboardButton, + standard: StandardGamepadButton, + 'xr-standard': XRStandardGamepadButton +} satisfies Record> + +export const AxisMapping = { + '': MouseScroll, + 'xr-standard': XRStandardGamepadAxes, + standard: StandardGamepadAxes +} satisfies Record> export const DefaultBooleanButtonState = Object.freeze({ down: true, diff --git a/packages/spatial/src/input/systems/ClientInputSystem.tsx b/packages/spatial/src/input/systems/ClientInputSystem.tsx index 9cf2be0e24..0ca3304eef 100755 --- a/packages/spatial/src/input/systems/ClientInputSystem.tsx +++ b/packages/spatial/src/input/systems/ClientInputSystem.tsx @@ -34,7 +34,6 @@ import { getMutableComponent, getOptionalComponent, hasComponent, - removeComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' @@ -52,6 +51,7 @@ import { } from '@etherealengine/spatial/src/transform/components/EntityTree' import { UUIDComponent } from '@etherealengine/ecs' +import { InteractableComponent } from '@etherealengine/engine/src/interaction/components/InteractableComponent' import { CameraComponent } from '../../camera/components/CameraComponent' import { ObjectDirection, PI, Q_IDENTITY, Vector3_Zero } from '../../common/constants/MathConstants' import { NameComponent } from '../../common/NameComponent' @@ -71,11 +71,11 @@ import { XRSpaceComponent } from '../../xr/XRComponents' import { XRScenePlacementComponent } from '../../xr/XRScenePlacementComponent' import { XRControlsState, XRState } from '../../xr/XRState' import { XRUIComponent } from '../../xrui/components/XRUIComponent' -import { InputComponent } from '../components/InputComponent' +import { DefaultButtonAlias, InputComponent } from '../components/InputComponent' import { InputPointerComponent } from '../components/InputPointerComponent' import { InputSourceComponent } from '../components/InputSourceComponent' import normalizeWheel from '../functions/normalizeWheel' -import { ButtonStateMap, createInitialButtonState, MouseButton } from '../state/ButtonState' +import { AnyButton, ButtonState, ButtonStateMap, createInitialButtonState, MouseButton } from '../state/ButtonState' import { InputState } from '../state/InputState' /** squared distance threshold for dragging state */ @@ -101,7 +101,7 @@ const preventDefaultKeyDown = (evt) => { export function updateGamepadInput(eid: Entity) { const inputSource = getComponent(eid, InputSourceComponent) const gamepad = inputSource.source.gamepad - const buttons = inputSource.buttons as ButtonStateMap + const buttons = inputSource.buttons // const buttonDownPos = inputSource.buttonDownPositions as WeakMap // log buttons // if (source.gamepad) { @@ -113,7 +113,7 @@ export function updateGamepadInput(eid: Entity) { if (!gamepad) return const gamepadButtons = gamepad.buttons - if (gamepadButtons) { + if (gamepadButtons.length) { const pointer = getOptionalComponent(eid, InputPointerComponent) const xrTransform = getOptionalComponent(eid, TransformComponent) @@ -180,7 +180,7 @@ const inputs = defineQuery([InputComponent]) const worldPosInputSourceComponent = new Vector3() const worldPosInputComponent = new Vector3() -const inputXRUIs = defineQuery([InputComponent, VisibleComponent, XRUIComponent]) +const xruiQuery = defineQuery([VisibleComponent, XRUIComponent]) const boundingBoxesQuery = defineQuery([VisibleComponent, BoundingBoxComponent]) const meshesQuery = defineQuery([VisibleComponent, MeshComponent]) @@ -227,8 +227,7 @@ const execute = () => { for (const eid of pointers()) { const pointer = getComponent(eid, InputPointerComponent) const inputSource = getComponent(eid, InputSourceComponent) - const viewerEntity = pointer.canvasEntity - const camera = getComponent(viewerEntity, CameraComponent) + const camera = getComponent(pointer.cameraEntity, CameraComponent) pointer.movement.copy(pointer.position).sub(pointer.lastPosition) pointer.lastPosition.copy(pointer.position) inputSource.raycaster.setFromCamera(pointer.position, camera) @@ -263,6 +262,11 @@ const execute = () => { } } + const interactionRays = inputSourceQuery().map((eid) => getComponent(eid, InputSourceComponent).raycaster.ray) + for (const xrui of xruiQuery()) { + getComponent(xrui, XRUIComponent).interactionRays = interactionRays + } + // assign input sources (InputSourceComponent) to input sinks (InputComponent), foreach on InputSourceComponents for (const sourceEid of inputSourceQuery()) { const isSpatialInput = hasComponent(sourceEid, TransformComponent) @@ -281,7 +285,7 @@ const execute = () => { TransformComponent.getWorldPosition(sourceEid, inputRaycast.origin).addScaledVector(inputRaycast.direction, -0.01) inputRay.set(inputRaycast.origin, inputRaycast.direction) raycaster.set(inputRaycast.origin, inputRaycast.direction) - raycaster.layers.enable(ObjectLayers.Default) + raycaster.layers.enable(ObjectLayers.Scene) const inputState = getState(InputState) const isEditing = getState(EngineState).isEditing @@ -305,7 +309,7 @@ const execute = () => { } } else { // 1st heuristic is XRUI - for (const entity of inputXRUIs()) { + for (const entity of xruiQuery()) { const xrui = getComponent(entity, XRUIComponent) const layerHit = xrui.hitTest(inputRay) if ( @@ -330,7 +334,8 @@ const execute = () => { // 3rd heuristic is bboxes for (const entity of inputState.inputBoundingBoxes) { - const boundingBox = getComponent(entity, BoundingBoxComponent) + const boundingBox = getOptionalComponent(entity, BoundingBoxComponent) + if (!boundingBox) continue const hit = inputRay.intersectBox(boundingBox.box, bboxHitTarget) if (hit) { intersectionData.add({ entity, distance: inputRay.origin.distanceTo(bboxHitTarget) }) @@ -371,9 +376,6 @@ const execute = () => { sortedIntersections.length === 0 && !hasComponent(sourceEid, InputPointerComponent) ) { - let closestEntity = UndefinedEntity - let closestDistanceSquared = Infinity - //use sourceEid if controller (one InputSource per controller), otherwise use avatar rather than InputSource-emulated-pointer const selfAvatarEntity = UUIDComponent.getEntityByUUID((Engine.instance.userID + '_avatar') as EntityUUID) //would prefer a better way to do this const inputSourceEntity = @@ -388,27 +390,42 @@ const execute = () => { const inputComponent = getComponent(inputEntity, InputComponent) TransformComponent.getWorldPosition(inputEntity, worldPosInputComponent) - const distSquared = worldPosInputSourceComponent.distanceToSquared(worldPosInputComponent) //closer than our current closest AND within inputSource's activation distance - if ( - distSquared < closestDistanceSquared && - inputComponent.activationDistance * inputComponent.activationDistance > distSquared - ) { - closestDistanceSquared = distSquared - closestEntity = inputEntity + if (inputComponent.activationDistance * inputComponent.activationDistance > distSquared) { + //using this object type out of convenience (intersectionsData is also guaranteed empty in this flow) + intersectionData.add({ entity: inputEntity, distance: distSquared }) //keeping it as distSquared for now to avoid extra square root calls } } - if (closestEntity !== UndefinedEntity) { - sortedIntersections.push({ entity: closestEntity, distance: Math.sqrt(closestDistanceSquared) }) + const closestEntities = Array.from(intersectionData) + if (closestEntities.length > 0) { + if (closestEntities.length === 1) { + sortedIntersections.push({ + entity: closestEntities[0].entity, + distance: Math.sqrt(closestEntities[0].distance) + }) + } else { + //sort if more than 1 entry + closestEntities.sort((a, b) => { + //prioritize anything with an InteractableComponent if otherwise equal + const aNum = hasComponent(a.entity, InteractableComponent) ? -1 : 0 + const bNum = hasComponent(b.entity, InteractableComponent) ? -1 : 0 + //aNum - bNum : 0 if equal, -1 if a has tag and b doesn't, 1 if a doesnt have tag and b does + return Math.sign(a.distance - b.distance) + (aNum - bNum) + }) + sortedIntersections.push({ + entity: closestEntities[0].entity, + distance: Math.sqrt(closestEntities[0].distance) + }) + } } } } const inputPointerComponent = getOptionalComponent(sourceEid, InputPointerComponent) if (inputPointerComponent) { - sortedIntersections.push({ entity: inputPointerComponent.canvasEntity, distance: 0 }) + sortedIntersections.push({ entity: inputPointerComponent.cameraEntity, distance: 0 }) } sourceState.intersections.set(sortedIntersections) @@ -463,7 +480,7 @@ const useNonSpatialInputSources = () => { const code = event.code const down = event.type === 'keydown' - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons if (down) buttonState[code] = createInitialButtonState(eid) else if (buttonState[code]) buttonState[code].up = true } @@ -481,30 +498,21 @@ const useNonSpatialInputSources = () => { document.addEventListener('touchstickmove', handleTouchDirectionalPad) document.addEventListener('touchgamepadbuttondown', (event: CustomEvent) => { - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons buttonState[event.detail.button] = createInitialButtonState(eid) }) document.addEventListener('touchgamepadbuttonup', (event: CustomEvent) => { - const buttonState = inputSourceComponent.buttons as ButtonStateMap + const buttonState = inputSourceComponent.buttons if (buttonState[event.detail.button]) buttonState[event.detail.button].up = true }) - const onWheelEvent = (event: WheelEvent) => { - const normalizedValues = normalizeWheel(event) - const axes = inputSourceComponent.source.gamepad!.axes as number[] - axes[0] = normalizedValues.spinX - axes[1] = normalizedValues.spinY - } - document.addEventListener('wheel', onWheelEvent, { passive: true, capture: true }) - return () => { document.removeEventListener('DOMMouseScroll', preventDefault, false) document.removeEventListener('gesturestart', preventDefault) document.removeEventListener('keyup', onKeyEvent) document.removeEventListener('keydown', onKeyEvent) document.removeEventListener('touchstickmove', handleTouchDirectionalPad) - document.removeEventListener('wheel', onWheelEvent) removeEntity(eid) } }, []) @@ -532,75 +540,67 @@ const useGamepadInputSources = () => { } const CanvasInputReactor = () => { - const canvasEntity = useEntityContext() + const cameraEntity = useEntityContext() const xrState = useMutableState(XRState) useEffect(() => { if (xrState.session.value) return // pointer input sources are automatically handled by webxr - const rendererComponent = getComponent(canvasEntity, RendererComponent) + const rendererComponent = getComponent(cameraEntity, RendererComponent) const canvas = rendererComponent.canvas - canvas.addEventListener('dragstart', preventDefault, false) - canvas.addEventListener('contextmenu', preventDefault) - - // TODO: follow this spec more closely https://immersive-web.github.io/webxr/#transient-input - // const pointerEntities = new Map() - - const emulatedInputSourceEntity = createEntity() - setComponent(emulatedInputSourceEntity, NameComponent, 'InputSource-emulated-pointer') - setComponent(emulatedInputSourceEntity, TransformComponent) - setComponent(emulatedInputSourceEntity, InputSourceComponent) - const inputSourceComponent = getComponent(emulatedInputSourceEntity, InputSourceComponent) - /** Clear mouse events */ - const pointerButtons = ['PrimaryClick', 'AuxiliaryClick', 'SecondaryClick'] - const clearPointerState = () => { - const state = inputSourceComponent.buttons as ButtonStateMap + const pointerButtons = ['PrimaryClick', 'AuxiliaryClick', 'SecondaryClick'] as AnyButton[] + const clearPointerState = (entity: Entity) => { + const inputSourceComponent = getComponent(entity, InputSourceComponent) + const state = inputSourceComponent.buttons for (const button of pointerButtons) { - const val = state[button] - if (!val?.up && val?.pressed) state[button].up = true + const val = state[button] as ButtonState + if (!val?.up && val?.pressed) (state[button] as ButtonState).up = true } } - const pointerEnter = (event: PointerEvent) => { - setComponent(emulatedInputSourceEntity, InputPointerComponent, { + const onPointerEnter = (event: PointerEvent) => { + const pointerEntity = createEntity() + setComponent(pointerEntity, NameComponent, 'InputSource-emulated-pointer') + setComponent(pointerEntity, TransformComponent) + setComponent(pointerEntity, InputSourceComponent) + setComponent(pointerEntity, InputPointerComponent, { pointerId: event.pointerId, - canvasEntity: canvasEntity + cameraEntity }) + redirectPointerEventsToXRUI(cameraEntity, event) } - const pointerLeave = (event: PointerEvent) => { - const pointerComponent = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) - if (!pointerComponent || pointerComponent?.pointerId !== event.pointerId) return - clearPointerState() - removeComponent(emulatedInputSourceEntity, InputPointerComponent) + const onPointerOver = (event: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, event) } - canvas.addEventListener('pointerenter', pointerEnter) - canvas.addEventListener('pointerleave', pointerLeave) + const onPointerOut = (event: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, event) + } - canvas.addEventListener('blur', clearPointerState) - canvas.addEventListener('mouseleave', clearPointerState) - const handleVisibilityChange = (event: Event) => { - if (document.visibilityState === 'hidden') clearPointerState() + const onPointerLeave = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + redirectPointerEventsToXRUI(cameraEntity, event) + removeEntity(pointerEntity) } - canvas.addEventListener('visibilitychange', handleVisibilityChange) - const handleMouseClick = (event: MouseEvent) => { - const down = event.type === 'mousedown' || event.type === 'touchstart' + const onPointerClick = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + const inputSourceComponent = getOptionalComponent(pointerEntity, InputSourceComponent) + if (!inputSourceComponent) return + + const down = event.type === 'pointerdown' let button = MouseButton.PrimaryClick if (event.button === 1) button = MouseButton.AuxiliaryClick else if (event.button === 2) button = MouseButton.SecondaryClick - const inputSourceComponent = getOptionalComponent(emulatedInputSourceEntity, InputSourceComponent) - if (!inputSourceComponent) return - - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap if (down) { - state[button] = createInitialButtonState(emulatedInputSourceEntity) //down, pressed, touched = true + state[button] = createInitialButtonState(pointerEntity) //down, pressed, touched = true - const pointer = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const pointer = getOptionalComponent(pointerEntity, InputPointerComponent) if (pointer) { state[button]!.downPosition = new Vector3(pointer.position.x, pointer.position.y, 0) //rotation will never be defined for the mouse or touch @@ -608,50 +608,78 @@ const CanvasInputReactor = () => { } else if (state[button]) { state[button]!.up = true } - } - - const handleMouseMove = (event: MouseEvent) => { - handleMouseOrTouchMovement(event.clientX, event.clientY, event) - } - const handleTouchMove = (event: TouchEvent) => { - const touch = event.touches[0] - handleMouseOrTouchMovement(touch.clientX, touch.clientY, event) + redirectPointerEventsToXRUI(cameraEntity, event) } - const handleMouseOrTouchMovement = (clientX: number, clientY: number, event: MouseEvent | TouchEvent) => { - const pointerComponent = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const onPointerMove = (event: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, event.pointerId) + const pointerComponent = getOptionalComponent(pointerEntity, InputPointerComponent) if (!pointerComponent) return + pointerComponent.position.set( - ((clientX - canvas.getBoundingClientRect().x) / canvas.clientWidth) * 2 - 1, - ((clientY - canvas.getBoundingClientRect().y) / canvas.clientHeight) * -2 + 1 + ((event.clientX - canvas.getBoundingClientRect().x) / canvas.clientWidth) * 2 - 1, + ((event.clientY - canvas.getBoundingClientRect().y) / canvas.clientHeight) * -2 + 1 ) - updateMouseOrTouchDragging(emulatedInputSourceEntity, event) + updatePointerDragging(pointerEntity, event) + redirectPointerEventsToXRUI(cameraEntity, event) + } + + const onVisibilityChange = (event: Event) => { + if ( + document.visibilityState === 'hidden' || + !canvas.checkVisibility({ + checkOpacity: true, + checkVisibilityCSS: true + }) + ) { + InputPointerComponent.getPointersForCamera(cameraEntity).forEach(clearPointerState) + } + } + + const onClick = (evt: PointerEvent) => { + redirectPointerEventsToXRUI(cameraEntity, evt) + } + + const onWheelEvent = (event: WheelEvent) => { + const pointer = InputPointerComponent.getPointersForCamera(cameraEntity)[0] + if (!pointer) return + const inputSourceComponent = getComponent(pointer, InputSourceComponent) + const normalizedValues = normalizeWheel(event) + const axes = inputSourceComponent.source.gamepad!.axes as number[] + axes[0] = normalizedValues.spinX + axes[1] = normalizedValues.spinY } - canvas.addEventListener('touchmove', handleTouchMove, { passive: true, capture: true }) - canvas.addEventListener('mousemove', handleMouseMove, { passive: true, capture: true }) - canvas.addEventListener('mouseup', handleMouseClick) - canvas.addEventListener('mousedown', handleMouseClick) - canvas.addEventListener('touchstart', handleMouseClick) - canvas.addEventListener('touchend', handleMouseClick) + canvas.addEventListener('dragstart', preventDefault, false) + canvas.addEventListener('contextmenu', preventDefault) + canvas.addEventListener('pointerenter', onPointerEnter) + canvas.addEventListener('pointerover', onPointerOver) + canvas.addEventListener('pointerout', onPointerOut) + canvas.addEventListener('pointerleave', onPointerLeave) + canvas.addEventListener('pointermove', onPointerMove, { passive: true, capture: true }) + canvas.addEventListener('pointerup', onPointerClick) + canvas.addEventListener('pointerdown', onPointerClick) + canvas.addEventListener('blur', onVisibilityChange) + canvas.addEventListener('visibilitychange', onVisibilityChange) + canvas.addEventListener('click', onClick) + canvas.addEventListener('wheel', onWheelEvent, { passive: true, capture: true }) return () => { canvas.removeEventListener('dragstart', preventDefault, false) canvas.removeEventListener('contextmenu', preventDefault) - canvas.removeEventListener('pointerenter', pointerEnter) - canvas.removeEventListener('pointerleave', pointerLeave) - canvas.removeEventListener('blur', clearPointerState) - canvas.removeEventListener('mouseleave', clearPointerState) - canvas.removeEventListener('visibilitychange', handleVisibilityChange) - canvas.removeEventListener('touchmove', handleTouchMove) - canvas.removeEventListener('mousemove', handleMouseMove) - canvas.removeEventListener('mouseup', handleMouseClick) - canvas.removeEventListener('mousedown', handleMouseClick) - canvas.removeEventListener('touchstart', handleMouseClick) - canvas.removeEventListener('touchend', handleMouseClick) - removeEntity(emulatedInputSourceEntity) + canvas.removeEventListener('pointerenter', onPointerEnter) + canvas.removeEventListener('pointerover', onPointerOver) + canvas.removeEventListener('pointerout', onPointerOut) + canvas.removeEventListener('pointerleave', onPointerLeave) + canvas.removeEventListener('pointermove', onPointerMove) + canvas.removeEventListener('pointerup', onPointerClick) + canvas.removeEventListener('pointerdown', onPointerClick) + canvas.removeEventListener('blur', onVisibilityChange) + canvas.removeEventListener('visibilitychange', onVisibilityChange) + canvas.removeEventListener('click', onClick) + canvas.removeEventListener('wheel', onWheelEvent) } }, [xrState.session]) @@ -695,7 +723,7 @@ const useXRInputSources = () => { if (!eid) return const inputSourceComponent = getComponent(eid, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap state.PrimaryClick = createInitialButtonState(eid) } const onXRSelectEnd = (event: XRInputSourceEvent) => { @@ -703,7 +731,7 @@ const useXRInputSources = () => { if (!eid) return const inputSourceComponent = getComponent(eid, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap if (!state.PrimaryClick) return state.PrimaryClick.up = true } @@ -766,20 +794,20 @@ export const ClientInputSystem = defineSystem({ reactor }) -function updateMouseOrTouchDragging(emulatedInputSourceEntity: Entity, event: MouseEvent | TouchEvent) { - const inputSourceComponent = getOptionalComponent(emulatedInputSourceEntity, InputSourceComponent) +function updatePointerDragging(pointerEntity: Entity, event: PointerEvent) { + const inputSourceComponent = getOptionalComponent(pointerEntity, InputSourceComponent) if (!inputSourceComponent) return - const state = inputSourceComponent.buttons as ButtonStateMap + const state = inputSourceComponent.buttons as ButtonStateMap let button = MouseButton.PrimaryClick - if (event.type === 'mousemove') { + if (event.type === 'pointermove') { if ((event as MouseEvent).button === 1) button = MouseButton.AuxiliaryClick else if ((event as MouseEvent).button === 2) button = MouseButton.SecondaryClick } const btn = state[button] if (btn && !btn.dragging) { - const pointer = getOptionalComponent(emulatedInputSourceEntity, InputPointerComponent) + const pointer = getOptionalComponent(pointerEntity, InputPointerComponent) if (btn.pressed && btn.downPosition) { //if not yet dragging, compare distance to drag threshold and begin if appropriate @@ -797,7 +825,11 @@ function updateMouseOrTouchDragging(emulatedInputSourceEntity: Entity, event: Mo } } -function cleanupButton(key: string, buttons: ButtonStateMap, hasFocus: boolean) { +function cleanupButton( + key: string, + buttons: ButtonStateMap>>, + hasFocus: boolean +) { const button = buttons[key] if (button?.down) button.down = false if (button?.up || !hasFocus) delete buttons[key] @@ -825,3 +857,22 @@ export const ClientInputCleanupSystem = defineSystem({ insert: { after: PresentationSystemGroup }, execute: cleanupInputs }) + +const redirectPointerEventsToXRUI = (cameraEntity: Entity, evt: PointerEvent) => { + const pointerEntity = InputPointerComponent.getPointerByID(cameraEntity, evt.pointerId) + const inputSource = getOptionalComponent(pointerEntity, InputSourceComponent) + if (!inputSource) return + for (const i of inputSource.intersections) { + const entity = i.entity + const xrui = getOptionalComponent(entity, XRUIComponent) + if (!xrui) continue + xrui.updateWorldMatrix(true, true) + const raycaster = inputSource.raycaster + const hit = xrui.hitTest(raycaster.ray) + if (hit && hit.intersection.object.visible) { + hit.target.dispatchEvent(new (evt.constructor as any)(evt.type, evt)) + hit.target.focus() + return + } + } +} diff --git a/packages/spatial/src/input/systems/FlyControlSystem.ts b/packages/spatial/src/input/systems/FlyControlSystem.ts index 206d62f5fd..c7fb925245 100644 --- a/packages/spatial/src/input/systems/FlyControlSystem.ts +++ b/packages/spatial/src/input/systems/FlyControlSystem.ts @@ -87,7 +87,7 @@ const execute = () => { /** Since we have nothing that specifies whether we should use orbit/fly controls or not, just tie it to the camera orbit component for the studio */ for (const entity of cameraQuery()) { - const inputPointerEntity = InputPointerComponent.getPointerForCanvas(entity) + const inputPointerEntity = InputPointerComponent.getPointersForCamera(entity) if (!inputPointerEntity) continue if (hasComponent(entity, CameraOrbitComponent)) { if (buttons.SecondaryClick?.down) onSecondaryClick(entity) diff --git a/packages/spatial/src/physics/classes/Physics.test.ts b/packages/spatial/src/physics/classes/Physics.test.ts index 145ac52687..3aa2b8f323 100644 --- a/packages/spatial/src/physics/classes/Physics.test.ts +++ b/packages/spatial/src/physics/classes/Physics.test.ts @@ -40,8 +40,8 @@ import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getMutableState, getState } from '@etherealengine/hyperflux' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { ObjectDirection, Vector3_Zero } from '../../common/constants/MathConstants' -import { createEngine } from '../../initializeEngine' import { TransformComponent } from '../../transform/components/TransformComponent' import { computeTransformMatrix } from '../../transform/systems/TransformSystem' import { ColliderComponent } from '../components/ColliderComponent' diff --git a/packages/spatial/src/physics/components/ColliderComponent.test.ts b/packages/spatial/src/physics/components/ColliderComponent.test.ts index 132d329012..2e9cf9f94f 100644 --- a/packages/spatial/src/physics/components/ColliderComponent.test.ts +++ b/packages/spatial/src/physics/components/ColliderComponent.test.ts @@ -39,9 +39,9 @@ import { import { getMutableState } from '@etherealengine/hyperflux' import { World } from '@dimforge/rapier3d-compat' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { Vector3 } from 'three' import { TransformComponent } from '../../SpatialModule' -import { createEngine } from '../../initializeEngine' import { EntityTreeComponent, getAncestorWithComponent } from '../../transform/components/EntityTree' import { Physics } from '../classes/Physics' import { assertVecAllApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' diff --git a/packages/spatial/src/physics/components/CollisionComponent.test.ts b/packages/spatial/src/physics/components/CollisionComponent.test.ts index 8168db4c50..28ccbcfbb5 100644 --- a/packages/spatial/src/physics/components/CollisionComponent.test.ts +++ b/packages/spatial/src/physics/components/CollisionComponent.test.ts @@ -26,6 +26,7 @@ Ethereal Engine. All Rights Reserved. import { Entity, UndefinedEntity, + createEngine, createEntity, destroyEngine, getComponent, @@ -33,7 +34,6 @@ import { setComponent } from '@etherealengine/ecs' import assert from 'assert' -import { createEngine } from '../../initializeEngine' import { ColliderHitEvent } from '../types/PhysicsTypes' import { CollisionComponent } from './CollisionComponent' diff --git a/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx b/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx index 29d555da92..c859082d64 100644 --- a/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx +++ b/packages/spatial/src/physics/components/RigidBodyComponent.test.tsx @@ -29,6 +29,7 @@ import { RigidBodyType, World } from '@dimforge/rapier3d-compat' import { SystemDefinitions, UndefinedEntity, + createEngine, createEntity, destroyEngine, getComponent, @@ -42,7 +43,6 @@ import { getMutableState } from '@etherealengine/hyperflux' import { Vector3 } from 'three' import { PhysicsSystem, TransformComponent } from '../../SpatialModule' import { Vector3_Zero } from '../../common/constants/MathConstants' -import { createEngine } from '../../initializeEngine' import { Physics } from '../classes/Physics' import { assertFloatApproxEq, diff --git a/packages/spatial/src/physics/components/TriggerComponent.test.ts b/packages/spatial/src/physics/components/TriggerComponent.test.ts index 2c626228a3..0cd95bc378 100644 --- a/packages/spatial/src/physics/components/TriggerComponent.test.ts +++ b/packages/spatial/src/physics/components/TriggerComponent.test.ts @@ -27,6 +27,7 @@ import { World } from '@dimforge/rapier3d-compat' import { EntityUUID, UndefinedEntity, + createEngine, createEntity, destroyEngine, getComponent, @@ -39,7 +40,6 @@ import { getMutableState } from '@etherealengine/hyperflux' import assert from 'assert' import { Vector3 } from 'three' import { TransformComponent } from '../../SpatialModule' -import { createEngine } from '../../initializeEngine' import { Physics } from '../classes/Physics' import { CollisionGroups, DefaultCollisionMask } from '../enums/CollisionGroups' import { PhysicsState } from '../state/PhysicsState' diff --git a/packages/spatial/src/physics/systems/PhysicsSystem.test.ts b/packages/spatial/src/physics/systems/PhysicsSystem.test.ts index 62c1c49a9b..1526dc3184 100644 --- a/packages/spatial/src/physics/systems/PhysicsSystem.test.ts +++ b/packages/spatial/src/physics/systems/PhysicsSystem.test.ts @@ -39,12 +39,12 @@ import { removeEntity, setComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import assert from 'assert' import { Quaternion, Vector3 } from 'three' import { TransformComponent } from '../../SpatialModule' import { Vector3_Zero } from '../../common/constants/MathConstants' import { smootheLerpAlpha } from '../../common/functions/MathLerpFunctions' -import { createEngine } from '../../initializeEngine' import { Physics } from '../classes/Physics' import { assertVecAllApproxNotEq, assertVecAnyApproxNotEq, assertVecApproxEq } from '../classes/Physics.test' import { ColliderComponent } from '../components/ColliderComponent' diff --git a/packages/spatial/src/physics/systems/TriggerSystem.test.ts b/packages/spatial/src/physics/systems/TriggerSystem.test.ts index d8ab5feecc..477dff23f3 100644 --- a/packages/spatial/src/physics/systems/TriggerSystem.test.ts +++ b/packages/spatial/src/physics/systems/TriggerSystem.test.ts @@ -32,6 +32,7 @@ import { SystemUUID, UUIDComponent, UndefinedEntity, + createEngine, createEntity, destroyEngine, getComponent, @@ -43,7 +44,6 @@ import { import { getMutableState } from '@etherealengine/hyperflux' import { TransformComponent } from '../../SpatialModule' import { setCallback } from '../../common/CallbackComponent' -import { createEngine } from '../../initializeEngine' import { Physics } from '../classes/Physics' import { ColliderComponent } from '../components/ColliderComponent' import { CollisionComponent } from '../components/CollisionComponent' diff --git a/packages/spatial/src/renderer/PerformanceState.test.tsx b/packages/spatial/src/renderer/PerformanceState.test.tsx index 0cc87cc334..f175d4c6f0 100644 --- a/packages/spatial/src/renderer/PerformanceState.test.tsx +++ b/packages/spatial/src/renderer/PerformanceState.test.tsx @@ -32,8 +32,9 @@ import sinon from 'sinon' import { destroyEngine } from '@etherealengine/ecs' import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { EngineState } from '../EngineState' -import { createEngine, initializeSpatialEngine } from '../initializeEngine' +import { initializeSpatialEngine } from '../initializeEngine' import { PerformanceManager, PerformanceState } from './PerformanceState' import { RendererState } from './RendererState' import { EngineRenderer, RenderSettingsState } from './WebGLRendererSystem' diff --git a/packages/spatial/src/renderer/WebGLRendererSystem.test.tsx b/packages/spatial/src/renderer/WebGLRendererSystem.test.tsx index b2986d4e98..1bb36d6d30 100644 --- a/packages/spatial/src/renderer/WebGLRendererSystem.test.tsx +++ b/packages/spatial/src/renderer/WebGLRendererSystem.test.tsx @@ -34,6 +34,7 @@ import { getMutableComponent, setComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getMutableState } from '@etherealengine/hyperflux' import { act, render } from '@testing-library/react' import assert from 'assert' @@ -43,7 +44,6 @@ import { Color, Group, MathUtils, Texture } from 'three' import { MockEngineRenderer } from '../../tests/util/MockEngineRenderer' import { EngineState } from '../EngineState' import { CameraComponent } from '../camera/components/CameraComponent' -import { createEngine } from '../initializeEngine' import { EntityTreeComponent } from '../transform/components/EntityTree' import { RendererState } from './RendererState' import { diff --git a/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx b/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx index d7fd55a85d..8974d32603 100644 --- a/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx +++ b/packages/spatial/src/renderer/components/FogSettingsComponent.test.tsx @@ -33,12 +33,12 @@ import { getMutableComponent, setComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { act, render } from '@testing-library/react' import assert from 'assert' import React from 'react' import { Fog, FogExp2, MathUtils, ShaderChunk } from 'three' import { CameraComponent } from '../../camera/components/CameraComponent' -import { createEngine } from '../../initializeEngine' import { EntityTreeComponent } from '../../transform/components/EntityTree' import { RendererComponent } from '../WebGLRendererSystem' import { FogSettingsComponent, FogType } from './FogSettingsComponent' diff --git a/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx b/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx index 1538bc25ca..e44b197320 100644 --- a/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx +++ b/packages/spatial/src/renderer/components/LineSegmentComponent.test.tsx @@ -34,7 +34,7 @@ import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getState } from '@etherealengine/hyperflux' -import { createEngine } from '../../initializeEngine' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { ResourceState } from '../../resources/ResourceState' import { ObjectLayerMasks, ObjectLayers } from '../constants/ObjectLayers' import { GroupComponent } from './GroupComponent' diff --git a/packages/spatial/src/renderer/components/MeshComponent.test.tsx b/packages/spatial/src/renderer/components/MeshComponent.test.tsx index 8bd58ee36b..4936e7f26d 100644 --- a/packages/spatial/src/renderer/components/MeshComponent.test.tsx +++ b/packages/spatial/src/renderer/components/MeshComponent.test.tsx @@ -34,8 +34,8 @@ import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { State, getState } from '@etherealengine/hyperflux' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { Geometry } from '../../common/constants/Geometry' -import { createEngine } from '../../initializeEngine' import { ResourceState } from '../../resources/ResourceState' import { MeshComponent, useMeshComponent } from './MeshComponent' diff --git a/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx b/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx index 3a514f66bf..8acf7cfecc 100644 --- a/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx +++ b/packages/spatial/src/renderer/components/ObjectLayerComponent.test.tsx @@ -30,7 +30,7 @@ import { getComponent, hasComponent, setComponent } from '@etherealengine/ecs/sr import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' -import { createEngine } from '../../initializeEngine' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { addObjectToGroup } from './GroupComponent' import { Layer, ObjectLayerComponents, ObjectLayerMaskComponent } from './ObjectLayerComponent' diff --git a/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx b/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx index 65d3dc1e19..3172521d9f 100644 --- a/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx +++ b/packages/spatial/src/renderer/components/PostProcessingComponent.test.tsx @@ -35,11 +35,10 @@ import { hasComponent, setComponent } from '@etherealengine/ecs' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { getMutableState, getState, none } from '@etherealengine/hyperflux' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { RendererComponent } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem' import { SceneComponent } from '@etherealengine/spatial/src/renderer/components/SceneComponents' import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' diff --git a/packages/spatial/src/renderer/components/RenderOrderComponent.test.tsx b/packages/spatial/src/renderer/components/RenderOrderComponent.test.tsx index 4cb70abb67..95b41331c9 100644 --- a/packages/spatial/src/renderer/components/RenderOrderComponent.test.tsx +++ b/packages/spatial/src/renderer/components/RenderOrderComponent.test.tsx @@ -29,7 +29,7 @@ import { BoxGeometry, Mesh, MeshBasicMaterial } from 'three' import { destroyEngine } from '@etherealengine/ecs/src/Engine' import { createEntity } from '@etherealengine/ecs/src/EntityFunctions' -import { createEngine } from '../../initializeEngine' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { addObjectToGroup } from './GroupComponent' import { RenderOrderComponent } from './RenderOrderComponent' diff --git a/packages/spatial/src/renderer/functions/useUpdateLight.ts b/packages/spatial/src/renderer/functions/useUpdateLight.ts index 778c85e433..dba035b1b1 100644 --- a/packages/spatial/src/renderer/functions/useUpdateLight.ts +++ b/packages/spatial/src/renderer/functions/useUpdateLight.ts @@ -26,8 +26,7 @@ Ethereal Engine. All Rights Reserved. import { DirectionalLight, SpotLight, Vector3 } from 'three' import { useExecute } from '@etherealengine/ecs/src/SystemFunctions' - -import { TransformSystem } from '../../transform/TransformModule' +import { TransformSystem } from '../../transform/systems/TransformSystem' export const useUpdateLight = (light: DirectionalLight | SpotLight) => { useExecute( diff --git a/packages/spatial/src/renderer/materials/materialFunctions.ts b/packages/spatial/src/renderer/materials/materialFunctions.ts index d3ac452357..3a94ab16d2 100644 --- a/packages/spatial/src/renderer/materials/materialFunctions.ts +++ b/packages/spatial/src/renderer/materials/materialFunctions.ts @@ -115,6 +115,17 @@ export const removePlugin = (material: Material, callback) => { if (pluginIndex !== undefined) material.plugins?.splice(pluginIndex, 1) } +export const materialPrototypeMatches = (materialEntity: Entity) => { + const materialComponent = getComponent(materialEntity, MaterialStateComponent) + const prototypeEntity = materialComponent.prototypeEntity + if (!prototypeEntity) return false + const prototypeComponent = getComponent(prototypeEntity, MaterialPrototypeComponent) + const prototypeName = Object.keys(prototypeComponent.prototypeConstructor)[0] + const material = materialComponent.material + const materialType = material.userData.type || material.type + return materialType === prototypeName +} + /**Updates the material entity's threejs material prototype to match its * current prototype entity */ export const updateMaterialPrototype = (materialEntity: Entity) => { diff --git a/packages/spatial/src/resources/resourceHooks.test.tsx b/packages/spatial/src/resources/resourceHooks.test.tsx index fd5e99dde1..8f5d2bce9e 100644 --- a/packages/spatial/src/resources/resourceHooks.test.tsx +++ b/packages/spatial/src/resources/resourceHooks.test.tsx @@ -30,8 +30,8 @@ import sinon from 'sinon' import { AmbientLight, DirectionalLight } from 'three' import { createEntity, destroyEngine } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getState } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { useDisposable, useResource } from './resourceHooks' import { ResourceState } from './ResourceState' diff --git a/packages/spatial/src/startTimer.ts b/packages/spatial/src/startTimer.ts new file mode 100644 index 0000000000..abb93fef62 --- /dev/null +++ b/packages/spatial/src/startTimer.ts @@ -0,0 +1,38 @@ +/* +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 { ECSState, Timer, executeSystems } from '@etherealengine/ecs' +import { getMutableState } from '@etherealengine/hyperflux' +import { XRState } from './xr/XRState' + +export const startTimer = () => { + const timer = Timer((time, xrFrame) => { + getMutableState(XRState).xrFrame.set(xrFrame) + executeSystems(time) + getMutableState(XRState).xrFrame.set(null) + }) + getMutableState(ECSState).timer.set(timer) + timer.start() +} diff --git a/packages/spatial/src/transform/components/BoundingBoxComponents.ts b/packages/spatial/src/transform/components/BoundingBoxComponents.ts index 64a4dd5268..ac49effc9d 100755 --- a/packages/spatial/src/transform/components/BoundingBoxComponents.ts +++ b/packages/spatial/src/transform/components/BoundingBoxComponents.ts @@ -98,8 +98,14 @@ export const BoundingBoxComponent = defineComponent({ }) export const updateBoundingBox = (entity: Entity) => { - const box = getComponent(entity, BoundingBoxComponent).box + const boxComponent = getOptionalComponent(entity, BoundingBoxComponent) + if (!boxComponent) { + console.error('BoundingBoxComponent not found in updateBoundingBox') + return + } + + const box = boxComponent.box box.makeEmpty() const callback = (child: Entity) => { diff --git a/packages/spatial/src/transform/components/EntityTree.test.tsx b/packages/spatial/src/transform/components/EntityTree.test.tsx index dd0a47a148..7bde1c9859 100644 --- a/packages/spatial/src/transform/components/EntityTree.test.tsx +++ b/packages/spatial/src/transform/components/EntityTree.test.tsx @@ -29,11 +29,10 @@ import React, { useEffect } from 'react' import { EntityUUID, UUIDComponent } from '@etherealengine/ecs' import { getComponent, hasComponent, removeComponent, setComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { destroyEngine } from '@etherealengine/ecs/src/Engine' +import { createEngine, destroyEngine } from '@etherealengine/ecs/src/Engine' import { Entity, UndefinedEntity } from '@etherealengine/ecs/src/Entity' import { createEntity, removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { startReactor } from '@etherealengine/hyperflux' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' import { NameComponent } from '../../common/NameComponent' import { HighlightComponent } from '../../renderer/components/HighlightComponent' diff --git a/packages/spatial/src/xr/WebXRManager.ts b/packages/spatial/src/xr/WebXRManager.ts index d4dfbf1eae..aa8af8b36d 100644 --- a/packages/spatial/src/xr/WebXRManager.ts +++ b/packages/spatial/src/xr/WebXRManager.ts @@ -43,7 +43,7 @@ import { getComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { defineState, getMutableState, getState } from '@etherealengine/hyperflux' -import { createAnimationLoop } from '@etherealengine/ecs' +import { createAnimationLoop, ECSState } from '@etherealengine/ecs' import { CameraComponent } from '../camera/components/CameraComponent' import { XRState } from './XRState' @@ -100,6 +100,9 @@ export const XRRendererState = defineState({ }) export function createWebXRManager(renderer: WebGLRenderer) { + const ecsState = getState(ECSState) + const { animation } = ecsState.timer + const xrState = getState(XRState) const xrRendererState = getMutableState(XRRendererState) @@ -305,22 +308,7 @@ export function createWebXRManager(renderer: WebGLRenderer) { } } - // Animation Loop - - let onAnimationFrameCallback = null as typeof onAnimationFrame | null - - function onAnimationFrame(time: number, frame: XRFrame) { - if (onAnimationFrameCallback) onAnimationFrameCallback(time, frame) - } - - const animation = createAnimationLoop() - - animation.setAnimationLoop(onAnimationFrame) - - scope.setAnimationLoop = function (callback: typeof onAnimationFrame) { - onAnimationFrameCallback = callback - } - + scope.setAnimationLoop = function () {} scope.dispose = function () {} scope.addEventListener = function (type: string, listener: EventListener) {} scope.hasEventListener = function (type: string, listener: EventListener) {} diff --git a/packages/spatial/src/xr/XRHapticsSystem.ts b/packages/spatial/src/xr/XRHapticsSystem.ts index 20347617bd..ed246388ea 100644 --- a/packages/spatial/src/xr/XRHapticsSystem.ts +++ b/packages/spatial/src/xr/XRHapticsSystem.ts @@ -35,15 +35,13 @@ import { XRAction } from './XRState' /** haptic typings are currently incomplete */ declare global { - interface Gamepad { - /** @deprecated - old meta quest API */ - hapticActuators?: Array<{ - /** - * @param value A double representing the intensity of the pulse. This can vary depending on the hardware type, but generally takes a value between 0.0 (no intensity) and 1.0 (full intensity). - * @param duration A double representing the duration of the pulse, in milliseconds. - */ - pulse: (value: number, duration: number) => void - }> + interface GamepadHapticActuator { + /** + * @deprecated - old meta quest API + * @param value A double representing the intensity of the pulse. This can vary depending on the hardware type, but generally takes a value between 0.0 (no intensity) and 1.0 (full intensity). + * @param duration A double representing the duration of the pulse, in milliseconds. + */ + pulse: (value: number, duration: number) => void } } diff --git a/packages/spatial/src/xr/XRSessionFunctions.ts b/packages/spatial/src/xr/XRSessionFunctions.ts index f309f5a7e1..4121263469 100644 --- a/packages/spatial/src/xr/XRSessionFunctions.ts +++ b/packages/spatial/src/xr/XRSessionFunctions.ts @@ -81,16 +81,16 @@ export const setupXRSession = async (requestedMode?: 'inline' | 'immersive-ar' | isXREAL ? undefined : 'dom-overlay', // dom overlay crashes nreal 'hit-test', 'light-estimation', - 'depth-sensing', + // 'depth-sensing', // TODO: crashes meta quest 'anchors', 'plane-detection', 'mesh-detection', 'camera-access' ].filter(Boolean), - depthSensing: { - usagePreference: ['cpu-optimized', 'gpu-optimized'], - dataFormatPreference: ['luminance-alpha', 'float32'] - }, + // depthSensing: { + // usagePreference: ['cpu-optimized', 'gpu-optimized'], + // dataFormatPreference: ['luminance-alpha', 'float32'] + // }, domOverlay: isXREAL ? undefined : { root: document.body } } as XRSessionInit const mode = diff --git a/packages/spatial/src/xrui/components/XRUIComponent.ts b/packages/spatial/src/xrui/components/XRUIComponent.ts index 4e398afed5..1895877889 100644 --- a/packages/spatial/src/xrui/components/XRUIComponent.ts +++ b/packages/spatial/src/xrui/components/XRUIComponent.ts @@ -24,11 +24,8 @@ Ethereal Engine. All Rights Reserved. */ import { defineComponent } from '@etherealengine/ecs/src/ComponentFunctions' -import { getState } from '@etherealengine/hyperflux' import type { WebContainer3D } from '@etherealengine/xrui' -import { XRUIState } from '../XRUIState' - export const XRUIComponent = defineComponent({ name: 'XRUIComponent', @@ -39,7 +36,6 @@ export const XRUIComponent = defineComponent({ onSet: (entity, component, json: WebContainer3D) => { if (typeof json !== 'undefined') { component.set(json) - json.interactionRays = getState(XRUIState).interactionRays } }, diff --git a/packages/spatial/src/xrui/systems/XRUISystem.ts b/packages/spatial/src/xrui/systems/XRUISystem.ts index f7155bee88..13fe616662 100644 --- a/packages/spatial/src/xrui/systems/XRUISystem.ts +++ b/packages/spatial/src/xrui/systems/XRUISystem.ts @@ -32,19 +32,14 @@ import { Entity } from '@etherealengine/ecs/src/Entity' import { removeEntity } from '@etherealengine/ecs/src/EntityFunctions' import { defineQuery } from '@etherealengine/ecs/src/QueryFunctions' import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions' -import { getMutableState, getState } from '@etherealengine/hyperflux' import { WebContainer3D } from '@etherealengine/xrui' import { InputComponent } from '../../input/components/InputComponent' import { InputSourceComponent } from '../../input/components/InputSourceComponent' -import { XRStandardGamepadButton } from '../../input/state/ButtonState' -import { InputState } from '../../input/state/InputState' import { VisibleComponent } from '../../renderer/components/VisibleComponent' import { TransformSystem } from '../../transform/systems/TransformSystem' -import { XRState } from '../../xr/XRState' import { PointerComponent, PointerObject } from '../components/PointerComponent' import { XRUIComponent } from '../components/XRUIComponent' -import { XRUIState } from '../XRUIState' const hitColor = new Color(0x00e6e6) const normalColor = new Color(0xffffff) @@ -56,10 +51,11 @@ const inputSourceQuery = defineQuery([InputSourceComponent]) // redirect DOM events from the canvas, to the 3D scene, // to the appropriate child Web3DLayer, and finally (back) to the // DOM to dispatch an event on the intended DOM target -const redirectDOMEvent = (evt) => { +const redirectDOMEvent = (evt: PointerEvent) => { for (const entity of visibleInteractableXRUIQuery()) { const layer = getComponent(entity, XRUIComponent) - const inputSources = getComponent(entity, InputComponent).inputSources + const inputSources = InputComponent.getInputSourceEntities(entity) + // const inputSources = getComponent(entity, InputComponent).inputSources if (!inputSources.length) continue const inputSource = getComponent(inputSources[0], InputSourceComponent) // assume only one input source per XRUI if (inputSource.intersections.length && inputSource.intersections[0].entity !== entity) continue // only handle events for the first intersection @@ -67,7 +63,7 @@ const redirectDOMEvent = (evt) => { const raycaster = inputSource.raycaster const hit = layer.hitTest(raycaster.ray) if (hit && hit.intersection.object.visible) { - hit.target.dispatchEvent(new evt.constructor(evt.type, evt)) + hit.target.dispatchEvent(new (evt.constructor as any)(evt.type, evt)) hit.target.focus() return } @@ -133,17 +129,6 @@ const updateClickEventsForController = (entity: Entity) => { const execute = () => { if (!isClient) return - const xruiState = getState(XRUIState) - const xrFrame = getState(XRState).xrFrame - - /** Update the objects to use for intersection tests */ - const pointerScreenRaycaster = getState(InputState).pointerScreenRaycaster - if (xrFrame && xruiState.interactionRays[0] === pointerScreenRaycaster.ray) - xruiState.interactionRays = [...PointerComponent.getPointers(), pointerScreenRaycaster.ray] // todo, replace pointerScreenRaycaster with input sources - - if (!xrFrame && xruiState.interactionRays[0] !== pointerScreenRaycaster.ray) - xruiState.interactionRays = [pointerScreenRaycaster.ray] - const interactableXRUIEntities = visibleInteractableXRUIQuery() const inputSourceEntities = inputSourceQuery() @@ -166,7 +151,7 @@ const execute = () => { if (!pointer) continue if ( - buttons[XRStandardGamepadButton.Trigger]?.down && + buttons.XRStandardGamepadTrigger?.down && (inputSource.handedness === 'left' || inputSource.handedness === 'right') ) updateClickEventsForController(pointerEntity) @@ -222,10 +207,6 @@ const reactor = () => { document.body.addEventListener('contextmenu', redirectDOMEvent) document.body.addEventListener('dblclick', redirectDOMEvent) - const pointerScreenRaycaster = getState(InputState).pointerScreenRaycaster - - getMutableState(XRUIState).interactionRays.set([pointerScreenRaycaster.ray]) - return () => { document.body.removeEventListener('pointerdown', redirectDOMEvent) document.body.removeEventListener('click', redirectDOMEvent) diff --git a/packages/ui/src/components/editor/layout/ContextMenu.tsx b/packages/ui/src/components/editor/layout/ContextMenu.tsx index 811362fbd4..2147e93774 100644 --- a/packages/ui/src/components/editor/layout/ContextMenu.tsx +++ b/packages/ui/src/components/editor/layout/ContextMenu.tsx @@ -33,6 +33,7 @@ type ContextMenuProps = { anchorPosition?: undefined | { left: number; top: number } onClose: () => void className?: string + anchorEl?: HTMLElement } export const ContextMenu = ({ @@ -41,12 +42,15 @@ export const ContextMenu = ({ panelId, anchorPosition: propAnchorPosition, onClose, - className + className, + ...prop }: React.PropsWithChildren) => { const [open, setOpen] = React.useState(false) const panel = document.getElementById(panelId) const menuRef = useRef(null) + const { anchorEl } = prop + // use custom anchorPosition if explicity provided, otherwise use default anchor position when anchorEvent is defined const anchorPosition = propAnchorPosition ? propAnchorPosition @@ -59,7 +63,12 @@ export const ContextMenu = ({ // Calculate the Y position of the context menu based on the menu height and space to the bottom of the viewport in order to avoid overflow const calculatePositionY = () => { - let positionY = anchorPosition ? anchorPosition.top - panel?.getBoundingClientRect().top! : 0 + let positionY = anchorPosition + ? anchorPosition.top - panel?.getBoundingClientRect().top! + : anchorEl + ? anchorEl.getBoundingClientRect().bottom! + : 0 + // let positionY = if (open && menuRef.current) { const menuHeight = menuRef.current.offsetHeight @@ -69,6 +78,16 @@ export const ContextMenu = ({ if (offset < 0) { positionY = positionY + offset } + + const viewportHeight = window.innerHeight + + // Adjust Y position to avoid overflow + if (positionY + menuHeight > viewportHeight) { + positionY = viewportHeight - menuHeight - 10 // 10px for padding + } + if (positionY < 0) { + positionY = 10 // 10px for padding + } } return positionY @@ -76,7 +95,11 @@ export const ContextMenu = ({ // Calculate the X position of the context menu based on the menu width and space to the right of the panel in order to avoid overflow const calculatePositionX = () => { - let positionX = anchorPosition ? anchorPosition.left - panel?.getBoundingClientRect().left! : 0 + let positionX = anchorPosition + ? anchorPosition.left - panel?.getBoundingClientRect().left! + : anchorEl + ? anchorEl.getBoundingClientRect().left! + : 0 if (open && menuRef.current) { const menuWidth = menuRef.current.offsetWidth @@ -86,6 +109,16 @@ export const ContextMenu = ({ if (offset < 0) { positionX = positionX + offset } + + const viewportWidth = window.innerWidth + + // Adjust X position to avoid overflow + if (positionX + menuWidth > viewportWidth) { + positionX = viewportWidth - menuWidth - 10 // 10px for padding + } + if (positionX < 0) { + positionX = 10 // 10px for padding + } } return positionX diff --git a/packages/ui/src/components/editor/panels/Assets/container/index.tsx b/packages/ui/src/components/editor/panels/Assets/container/index.tsx index 954b2c9588..180237bf05 100644 --- a/packages/ui/src/components/editor/panels/Assets/container/index.tsx +++ b/packages/ui/src/components/editor/panels/Assets/container/index.tsx @@ -27,13 +27,19 @@ import { clone, debounce, isEmpty, last } from 'lodash' import React, { createContext, useContext, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { staticResourcePath, StaticResourceType } from '@etherealengine/common/src/schema.type.module' +import { NotificationService } from '@etherealengine/client-core/src/common/services/NotificationService' +import { + StaticResourceQuery, + StaticResourceType, + staticResourcePath +} from '@etherealengine/common/src/schema.type.module' import { Engine } from '@etherealengine/ecs/src/Engine' import { AssetsPanelCategories } from '@etherealengine/editor/src/components/assets/AssetsPanelCategories' import { AssetSelectionChangePropsType } from '@etherealengine/editor/src/components/assets/AssetsPreviewPanel' +import { inputFileWithAddToScene } from '@etherealengine/editor/src/functions/assetFunctions' import { EditorState } from '@etherealengine/editor/src/services/EditorServices' import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' -import { getState, State, useHookstate, useMutableState } from '@etherealengine/hyperflux' +import { State, getState, useHookstate, useMutableState } from '@etherealengine/hyperflux' import { ContextMenu } from '@etherealengine/ui/src/components/editor/layout/ContextMenu' import { useDrag } from 'react-dnd' import { getEmptyImage } from 'react-dnd-html5-backend' @@ -63,6 +69,7 @@ type Category = { isLeaf: boolean depth: number } + const AssetsPreviewContext = createContext({ onAssetSelectionChanged: (props: AssetSelectionChangePropsType) => {} }) const generateAssetsBreadcrumb = (categories: Category[], target: string) => { @@ -312,7 +319,7 @@ const AssetPanel = () => { const searchedStaticResources = useHookstate([]) const searchText = useHookstate('') const breadcrumbPath = useHookstate('') - const { projectName } = useMutableState(EditorState) + const originalPath = useMutableState(EditorState).projectName.value const CategoriesList = () => { return ( @@ -369,30 +376,35 @@ const AssetPanel = () => { useEffect(() => { const staticResourcesFindApi = () => { + const tags = selectedCategory.value + ? [selectedCategory.value.name, ...iterativelyListTags(selectedCategory.value.object)] + : [] + const query = { key: { $like: `%${searchText.value}%` }, - type: 'asset', - project: projectName.value!, + type: { + $or: [{ type: 'file' }, { type: 'asset' }] + }, + tags: selectedCategory.value + ? { + $or: tags.flatMap((tag) => [ + { tags: { $like: `%${tag.toLowerCase()}%` } }, + { tags: { $like: `%${tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase()}%` } } + ]) + } + : undefined, $sort: { mimeType: 1 }, - $limit: 10000 - } + $paginate: false + } as StaticResourceQuery - if (selectedCategory.value) { - const tags = [selectedCategory.value.name, ...iterativelyListTags(selectedCategory.value.object)] - query['tags'] = { - $or: tags.flatMap((tag) => [ - { tags: { $like: `%${tag.toLowerCase()}%` } }, - { tags: { $like: `%${tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase()}%` } } - ]) - } - } Engine.instance.api .service(staticResourcePath) .find({ query }) .then((resources) => { - searchedStaticResources.set(resources.data) + // cast type due to temporary server-side pagination + searchedStaticResources.set(resources as any as StaticResourceType[]) }) .then(() => { loading.set(false) @@ -466,10 +478,6 @@ const AssetPanel = () => { // TODO: add settings functionality } - const handleUpdateAsset = () => { - // TODO: add upload asset functionality - } - return ( <>
@@ -522,7 +530,16 @@ const AssetPanel = () => { rounded="none" className="h-full whitespace-nowrap bg-[#375DAF] px-2" size="small" - onClick={handleUpdateAsset} + onClick={() => + inputFileWithAddToScene({ + projectName: originalPath as string, + directoryPath: `projects/${originalPath}/assets/` + }) + .then(handleRefresh) + .catch((err) => { + NotificationService.dispatchNotify(err.message, { variant: 'error' }) + }) + } > {t('editor:layout.filebrowser.uploadAssets')} diff --git a/packages/ui/src/components/editor/panels/Files/browserGrid/ImageConvertModal.tsx b/packages/ui/src/components/editor/panels/Files/browserGrid/ImageConvertModal.tsx new file mode 100644 index 0000000000..50f1a75206 --- /dev/null +++ b/packages/ui/src/components/editor/panels/Files/browserGrid/ImageConvertModal.tsx @@ -0,0 +1,126 @@ +/* +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 React from 'react' +import { useTranslation } from 'react-i18next' + +import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState' +import { imageConvertPath } from '@etherealengine/common/src/schema.type.module' +import { FileDataType } from '@etherealengine/editor/src/components/assets/FileBrowser/FileDataType' +import { + ImageConvertDefaultParms, + ImageConvertParms +} from '@etherealengine/engine/src/assets/constants/ImageConvertParms' +import { useHookstate } from '@etherealengine/hyperflux' +import { useMutation } from '@etherealengine/spatial/src/common/functions/FeathersHooks' +import Checkbox from '../../../../../primitives/tailwind/Checkbox' +import Label from '../../../../../primitives/tailwind/Label' +import Modal from '../../../../../primitives/tailwind/Modal' +import Select from '../../../../../primitives/tailwind/Select' +import Text from '../../../../../primitives/tailwind/Text' +import NumericInput from '../../../input/Numeric' + +export default function ImageConvertModal({ + file, + refreshDirectory +}: { + file: FileDataType + refreshDirectory: () => Promise +}) { + const { t } = useTranslation() + const modalProcessing = useHookstate(false) + + const convertProperties = useHookstate(ImageConvertDefaultParms) + const imageConvertMutation = useMutation(imageConvertPath) + + const handleSubmit = async () => { + convertProperties.src.set(file.isFolder ? `${file.url}/${file.key}` : file.url) + imageConvertMutation + .create({ + ...convertProperties.value + }) + .then(() => { + refreshDirectory() + PopoverState.hidePopupover() + }) + } + + return ( + +
+ + {file.name} {file.isFolder ? t('editor:layout.filebrowser.directory') : t('editor:layout.filebrowser.file')} + +
+ + onRelease(value)} + onPointerUp={() => onRelease && onRelease(value)} step={step} type="range" style={sliderStyle} className={twMerge( - `w-[${width}px] h-8 cursor-pointer appearance-none overflow-hidden rounded bg-[#111113] from-[#214AA6] via-[#214AA6] focus:outline-none - - disabled:pointer-events-none - disabled:opacity-50 + `w-[${width}px] h-8 cursor-pointer appearance-none overflow-hidden rounded bg-[#111113] focus:outline-none + disabled:pointer-events-none disabled:opacity-50 [&::-moz-range-progress]:bg-[#214AA6] [&::-moz-range-thumb]:h-full [&::-moz-range-thumb]:w-4 diff --git a/packages/ui/src/primitives/tailwind/Text/index.tsx b/packages/ui/src/primitives/tailwind/Text/index.tsx index 13deefc682..daa2fc1066 100644 --- a/packages/ui/src/primitives/tailwind/Text/index.tsx +++ b/packages/ui/src/primitives/tailwind/Text/index.tsx @@ -60,7 +60,7 @@ const Text = ({ const twClassName = twMerge( 'inline-block leading-normal', - `font-${fontWeight} font-[${fontFamily}] text-${fontSize} text-theme-${theme}`, + `font-${fontWeight} font-['${fontFamily}'] text-${fontSize} text-theme-${theme}`, className ) diff --git a/packages/visual-script/tests/visualScript.test.tsx b/packages/visual-script/tests/visualScript.test.tsx index 7717aa4af8..a3e0f97b5f 100644 --- a/packages/visual-script/tests/visualScript.test.tsx +++ b/packages/visual-script/tests/visualScript.test.tsx @@ -44,6 +44,7 @@ import { SystemDefinitions, UUIDComponent } from '@etherealengine/ecs' +import { createEngine } from '@etherealengine/ecs/src/Engine' import { getOnAsyncExecuteSystemUUID, getOnExecuteSystemUUID, @@ -54,7 +55,7 @@ import { VisualScriptDomain } from '@etherealengine/engine' import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' -import { createEngine, initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' +import { initializeSpatialEngine } from '@etherealengine/spatial/src/initializeEngine' import { InputComponent } from '@etherealengine/spatial/src/input/components/InputComponent' import { GraphJSON, VisualScriptState } from '../src/VisualScriptModule' diff --git a/scripts/build_and_publish_package.sh b/scripts/build_and_publish_package.sh index 89dc05643b..9a041c7677 100755 --- a/scripts/build_and_publish_package.sh +++ b/scripts/build_and_publish_package.sh @@ -81,7 +81,8 @@ then --build-arg VITE_AVATURN_URL=$VITE_AVATURN_URL \ --build-arg VITE_AVATURN_API=$VITE_AVATURN_API \ --build-arg VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED \ - --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY . + --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY \ + --build-arg VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED . else docker buildx build \ --builder etherealengine-$PACKAGE \ @@ -129,7 +130,8 @@ else --build-arg VITE_AVATURN_URL=$VITE_AVATURN_URL \ --build-arg VITE_AVATURN_API=$VITE_AVATURN_API \ --build-arg VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED \ - --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY . + --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY \ + --build-arg VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED . fi if [ $PRIVATE_REPO == "true" ] diff --git a/scripts/build_microk8s.sh b/scripts/build_microk8s.sh index 61aece2684..0c4bc3313a 100755 --- a/scripts/build_microk8s.sh +++ b/scripts/build_microk8s.sh @@ -138,6 +138,13 @@ else VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY fi +if [ -z "$VITE_ZENDESK_AUTHENTICATION_ENABLED" ] +then + VITE_ZENDESK_AUTHENTICATION_ENABLED=false +else + VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED +fi + if [ -z "$NODE_ENV" ] then NODE_ENV=development @@ -196,7 +203,8 @@ docker buildx build \ --build-arg VITE_AVATURN_URL=$VITE_AVATURN_URL \ --build-arg VITE_AVATURN_API=$VITE_AVATURN_API \ --build-arg VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED \ - --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY . + --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY \ + --build-arg VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED . docker tag $REGISTRY_HOST:32000/etherealengine $REGISTRY_HOST:32000/etherealengine:$TAG docker push $REGISTRY_HOST:32000/etherealengine:$TAG diff --git a/scripts/build_minikube.sh b/scripts/build_minikube.sh index 3e6b31884a..967ea10595 100755 --- a/scripts/build_minikube.sh +++ b/scripts/build_minikube.sh @@ -135,6 +135,13 @@ else VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY fi +if [ -z "$VITE_ZENDESK_AUTHENTICATION_ENABLED" ] +then + VITE_ZENDESK_AUTHENTICATION_ENABLED=false +else + VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED +fi + # ./generate-certs.sh docker start etherealengine_minikube_db @@ -168,6 +175,7 @@ docker buildx build \ --build-arg VITE_AVATURN_URL=$VITE_AVATURN_URL \ --build-arg VITE_AVATURN_API=$VITE_AVATURN_API \ --build-arg VITE_ZENDESK_ENABLED=$VITE_ZENDESK_ENABLED \ - --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY . + --build-arg VITE_ZENDESK_KEY=$VITE_ZENDESK_KEY \ + --build-arg VITE_ZENDESK_AUTHENTICATION_ENABLED=$VITE_ZENDESK_AUTHENTICATION_ENABLED . #DOCKER_BUILDKIT=1 docker build -t etherealengine-testbot -f ./dockerfiles/testbot/Dockerfile-testbot . \ No newline at end of file diff --git a/scripts/resave-all-scenes.ts b/scripts/resave-all-scenes.ts deleted file mode 100644 index 05f99a61f1..0000000000 --- a/scripts/resave-all-scenes.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* -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 appRootPath from 'app-root-path' -import cli from 'cli' -import fs from 'fs' -import path from 'path' - -import { createDOM } from '@etherealengine/client-core/tests/createDOM' -import { ComponentJSONIDMap, ComponentMap } from '@etherealengine/ecs/src/ComponentFunctions' -import { Engine, destroyEngine } from '@etherealengine/ecs/src/Engine' -import { getMutableState } from '@etherealengine/hyperflux' -import { loadEngineInjection } from '@etherealengine/projects/loadEngineInjection' -import { EngineState } from '@etherealengine/spatial/src/EngineState' -import { createEngine } from '@etherealengine/spatial/src/initializeEngine' -import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' -import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' - -require('fix-esm').register() - -/** - * USAGE: `npx ts-node --swc scripts/resave-all-scenes.ts --write` - */ - -// @TODO - this does not support most of our projects, so should not be used for production - -createDOM() -// import client systems so we know we have all components registered -// behave graph breaks import somehow, so use require... -require('@etherealengine/client-core/src/world/startClientSystems') -console.log(ComponentJSONIDMap.keys()) - -cli.enable('status') - -const options = cli.parse({ - write: [false, 'Write', 'boolean'] -}) - -console.log(options) - -// manually disable all component reactors - we dont need any logic to actually run -for (const component of ComponentMap.values()) component.reactor = undefined - -const resaveAllProjects = async () => { - // get list of project folders in /packages/projects/projects - const projectsPath = path.join(appRootPath.path, 'packages', 'projects', 'projects') - const projects = fs.readdirSync(projectsPath) - - // for each project, get a list of .scene.json files and flatten them into a single array - const scenes = projects - .map((project) => { - const projectPath = path.join(projectsPath, project) - const projectScenes = fs.readdirSync(projectPath).filter((file) => file.endsWith('.scene.json')) - return projectScenes.map((scene) => path.join(projectPath, scene)) - }) - .flat() - - for (const scene of scenes) { - if (Engine.instance) { - await destroyEngine() - } - - const sceneName = path.basename(scene, '.scene.json') - const projectname = path.basename(path.dirname(scene)) - - console.log('') - cli.info(`Project: ${projectname}, Scene: ${sceneName}`) - - createEngine() - getMutableState(PhysicsState).physicsWorld.set(Physics.createWorld()) - await loadEngineInjection() - - getMutableState(EngineState).isEditor.set(true) - - // read scene file - // const sceneJson = JSON.parse(fs.readFileSync(scene, { encoding: 'utf-8' })) as SceneJson - // getMutableState(SceneState).sceneData.set({ - // scene: sceneJson, - // name: scene - // } as any) - - // const sceneState = getState(SceneState) - // setComponent(sceneState.sceneEntity, EntityTreeComponent, { parentEntity: UndefinedEntity, uuid: sceneJson.root }) - // updateSceneEntity(sceneJson.root, sceneJson.entities[sceneJson.root]) - // updateSceneEntitiesFromJSON(sceneJson.root) - - // if ((sceneJson as any).metadata) { - // for (const [key, val] of Object.entries((sceneJson as any).metadata) as any) { - // switch (key) { - // case 'renderSettings': - // setComponent(sceneState.sceneEntity, RenderSettingsComponent, val) - // break - // case 'postprocessing': - // setComponent(sceneState.sceneEntity, PostProcessingComponent, val) - // break - // case 'mediaSettings': - // setComponent(sceneState.sceneEntity, MediaSettingsComponent, val) - // break - // case 'fog': - // setComponent(sceneState.sceneEntity, FogSettingsComponent, val) - // break - // } - // } - // } - - // await delay(1) - - // const newScene = serializeWorld(UUIDComponent.getEntityByUUID(sceneJson.root)) as SceneJson - - // // log each component diff - // const changes = JSON.parse(JSON.stringify(diff(sceneJson, newScene))) as SceneJson - // console.log('changes to', scene) - // if (changes.entities) - // for (const entity of Object.values(changes.entities)) { - // console.log(...Object.values(entity.components).map((val) => JSON.stringify(val, null, 2))) - // } - - // // save file - // if (options.write) fs.writeFileSync(scene, JSON.stringify(newScene, null, 2)) - } - - cli.ok('Done') -} - -Physics.load().then(() => { - resaveAllProjects() -})