Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Int (#10872)
Browse files Browse the repository at this point in the history
* scene save static resource fixes (#10857)

* scene save static resource fixes

* fix thumbnail gen and upload

* Fix collider child nodes always being invisible, fix order dependency of rigidbody and collider components (#10856)

Co-authored-by: Josh Field <[email protected]>

* Feature flags for model and transform pivot (#10858)

* feature flags for model and transform pivot

* file properties right click selects intuitively

* Fix recursive reactor render causing memory leak (#10860)

---------

Co-authored-by: Josh Field <[email protected]>
Co-authored-by: lonedevr <[email protected]>
  • Loading branch information
3 people authored Aug 5, 2024
1 parent a7eec53 commit 86152f2
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 153 deletions.
99 changes: 40 additions & 59 deletions packages/client-core/src/common/services/FileThumbnailJobState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
UndefinedEntity,
createEntity,
getComponent,
hasComponent,
removeEntity,
setComponent,
useOptionalComponent
Expand All @@ -48,7 +47,13 @@ import { NO_PROXY, defineState, getMutableState, useHookstate } from '@ethereale
import { DirectionalLightComponent, TransformComponent } from '@etherealengine/spatial'
import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent'
import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent'
import { RendererComponent, initializeEngineRenderer } from '@etherealengine/spatial/src/renderer/WebGLRendererSystem'
import {
RendererComponent,
getNestedVisibleChildren,
getSceneParameters,
initializeEngineRenderer,
render
} from '@etherealengine/spatial/src/renderer/WebGLRendererSystem'
import { ObjectLayerMaskComponent } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent'
import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent'
import createReadableTexture from '@etherealengine/spatial/src/renderer/functions/createReadableTexture'
Expand All @@ -60,13 +65,13 @@ import { computeTransformMatrix } from '@etherealengine/spatial/src/transform/sy
import React, { useEffect } from 'react'
import { Color, Euler, Material, MathUtils, Matrix4, Mesh, Quaternion, Sphere, SphereGeometry, Vector3 } from 'three'

import config from '@etherealengine/common/src/config'
import { ErrorComponent } from '@etherealengine/engine/src/scene/components/ErrorComponent'
import { ShadowComponent } from '@etherealengine/engine/src/scene/components/ShadowComponent'
import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
import { GroupComponent, addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent'
import { addObjectToGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent'
import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent'
import { loadMaterialGLTF } from '@etherealengine/spatial/src/renderer/materials/materialFunctions'
import { iterateEntityNode } from '@etherealengine/spatial/src/transform/components/EntityTree'
import { uploadToFeathersService } from '../../util/upload'
import { getCanvasBlob } from '../utils'

Expand Down Expand Up @@ -106,7 +111,7 @@ 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)
const pathname = new URL(
const thumbnailURL = new URL(
await uploadToFeathersService(fileBrowserUploadPath, [file], {
args: [
{
Expand All @@ -120,10 +125,13 @@ const uploadThumbnail = async (src: string, projectName: string, staticResourceI
}
]
}).promise
).pathname
)
thumbnailURL.search = ''
thumbnailURL.hash = ''
const _thumbnailKey = thumbnailURL.href.replace(config.client.fileServer + '/', '')
await Engine.instance.api
.service(staticResourcePath)
.patch(staticResourceId, { thumbnailKey: pathname.slice(1), thumbnailMode })
.patch(staticResourceId, { thumbnailKey: _thumbnailKey, thumbnailMode })
}

const seenResources = new Set<string>()
Expand Down Expand Up @@ -212,7 +220,6 @@ const ThumbnailJobReactor = () => {
const lightComponent = useOptionalComponent(state.lightEntity.value, DirectionalLightComponent)
const errorComponent = useOptionalComponent(state.modelEntity.value, ErrorComponent)

const rendering = useHookstate(false)
const materialLoaded = useHookstate(false)

const tryCatch = (fn: any) => {
Expand Down Expand Up @@ -390,12 +397,10 @@ const ThumbnailJobReactor = () => {
useEffect(() => {
if (errorComponent?.keys.includes(ModelComponent.name)) {
console.error('failed to load model for thumbnail', src)
rendering.set(false)
jobState.set(jobState.get(NO_PROXY).slice(1))
return
}
if (src === '') return
if (rendering.value) return
if (
(fileType !== 'model' && fileType !== 'material') ||
!state.cameraEntity.value ||
Expand All @@ -409,34 +414,30 @@ const ThumbnailJobReactor = () => {
const modelEntity = state.modelEntity.value
const lightEntity = state.lightEntity.value

const sceneIDs = iterateEntityNode(modelEntity, getModelSceneID, (entity) => hasComponent(entity, ModelComponent))

for (const sceneID of sceneIDs) {
if (!sceneState[sceneID].value) return
}
const sceneID = getModelSceneID(modelEntity)
if (!sceneState.value[sceneID]) return

rendering.set(true)
try {
updateBoundingBox(modelEntity)

const bbox = getComponent(modelEntity, BoundingBoxComponent).box
const length = bbox.getSize(new Vector3(0, 0, 0)).length()
const normalizedSize = new Vector3().setScalar(length / 2)
// const length = bbox.getSize(new Vector3(0, 0, 0)).length()
// const normalizedSize = new Vector3().setScalar(length / 2)

//const canvas = document.getElementById('preview-canvas') as HTMLCanvasElement
// Create the camera entity
const cameraEntity = state.cameraEntity.value
setComponent(cameraEntity, NameComponent, 'thumbnail job camera for ' + src)

// Assuming bbox is already defined
const size = bbox.getSize(new Vector3())
// const size = bbox.getSize(new Vector3())
const center = bbox.getCenter(new Vector3())

// Calculate the bounding sphere radius
const boundingSphere = bbox.getBoundingSphere(new Sphere())
const radius = boundingSphere.radius

const camera = getComponent(cameraEntity, CameraComponent).cameras[0]
const camera = getComponent(cameraEntity, CameraComponent)
const fov = camera.fov * (Math.PI / 180) // convert vertical fov to radians

// Calculate the camera direction vector with the desired angle offsets
Expand Down Expand Up @@ -470,7 +471,7 @@ const ThumbnailJobReactor = () => {
camera.matrixWorldInverse.copy(camera.matrixWorld).invert()

// Update the view camera matrices
const viewCamera = camera
const viewCamera = camera.cameras[0]
viewCamera.matrixWorld.copy(camera.matrixWorld)
viewCamera.matrixWorldInverse.copy(camera.matrixWorldInverse)
viewCamera.projectionMatrix.copy(camera.projectionMatrix)
Expand All @@ -479,49 +480,29 @@ const ThumbnailJobReactor = () => {
viewCamera.layers.mask = getComponent(cameraEntity, ObjectLayerMaskComponent)
setComponent(cameraEntity, RendererComponent, { scenes: [modelEntity, lightEntity] })

const { scene, canvas } = getComponent(cameraEntity, RendererComponent)
scene.children = getComponent(cameraEntity, RendererComponent)
.scenes.map((entity) => getComponent(entity, GroupComponent))
.flat()
const maxTryCount = 10
function doRender(tryCount = 0) {
requestAnimationFrame(() => {
const tmpCanvas = document.createElement('canvas')
tmpCanvas.width = 256
tmpCanvas.height = 256
const ctx = tmpCanvas.getContext('2d')!
ctx.drawImage(canvas!, 0, 0, 256, 256)
//repeat if image is blank
if (ctx.getImageData(0, 0, 256, 256).data.every((v) => v === 0)) {
if (tryCount < maxTryCount) {
doRender(tryCount + 1)
return
}
}
function cleanup() {
tmpCanvas.remove()
jobState.set(jobState.get(NO_PROXY).slice(1))
rendering.set(false)
materialLoaded.set(false)
}

tmpCanvas.toBlob((blob: Blob) => {
try {
uploadThumbnail(src, project, id, blob).then(cleanup)
} catch (e) {
console.error('failed to upload model thumbnail for', src)
console.error(e)
cleanup()
}
})
})
const renderer = getComponent(cameraEntity, RendererComponent)
const { scene, canvas, scenes } = renderer
const entitiesToRender = scenes.map(getNestedVisibleChildren).flat()
const { children } = getSceneParameters(entitiesToRender)
scene.children = children
render(renderer, renderer.scene, getComponent(cameraEntity, CameraComponent), 0, false)
function cleanup() {
jobState.set(jobState.get(NO_PROXY).slice(1))
materialLoaded.set(false)
}
doRender()
canvas!.toBlob((blob: Blob) => {
try {
uploadThumbnail(src, project, id, blob).then(cleanup)
} catch (e) {
console.error('failed to upload model thumbnail for', src)
console.error(e)
cleanup()
}
})
} catch (e) {
console.error('failed to generate model thumbnail for', src)
console.error(e)
jobState.set(jobState.get(NO_PROXY).slice(1))
rendering.set(false)
}
}, [
state.cameraEntity,
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/constants/FeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ export const FeatureFlags = {
}
},
Studio: {
Model: {
Dereference: 'ir.studio.model.dereference',
GLTFTransform: 'ir.studio.model.gltfTransform'
},
Panel: {
VisualScript: 'ir.editor.panel.visualScript'
},
UI: {
TransformPivot: 'ir.editor.ui.transformPivot',
Hierarchy: {
ShowModelChildren: 'ir.editor.ui.hierarchy.showModelChildren'
}
Expand Down
71 changes: 50 additions & 21 deletions packages/editor/src/components/EditorContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Ethereal Engine. All Rights Reserved.
import { PopoverState } from '@etherealengine/client-core/src/common/services/PopoverState'
import { staticResourcePath } from '@etherealengine/common/src/schema.type.module'
import { NO_PROXY, getMutableState, useHookstate, useMutableState } from '@etherealengine/hyperflux'
import { useFind } from '@etherealengine/spatial/src/common/functions/FeathersHooks'
import { AssetsPanelTab } from '@etherealengine/ui/src/components/editor/panels/Assets'
import { FilesPanelTab } from '@etherealengine/ui/src/components/editor/panels/Files'
import { HierarchyPanelTab } from '@etherealengine/ui/src/components/editor/panels/Hierarchy'
Expand All @@ -53,7 +52,7 @@ import DragLayer from './dnd/DragLayer'

import { useZendesk } from '@etherealengine/client-core/src/hooks/useZendesk'
import { FeatureFlags } from '@etherealengine/common/src/constants/FeatureFlags'
import { EntityUUID } from '@etherealengine/ecs'
import { Engine, EntityUUID } from '@etherealengine/ecs'
import useFeatureFlags from '@etherealengine/engine/src/useFeatureFlags'
import { EngineState } from '@etherealengine/spatial/src/EngineState'
import Button from '@etherealengine/ui/src/primitives/tailwind/Button'
Expand Down Expand Up @@ -126,7 +125,55 @@ const defaultLayout = (flags: { visualScriptPanelEnabled: boolean }): LayoutData

const EditorContainer = () => {
const { sceneAssetID, sceneName, projectName, scenePath, uiEnabled, uiAddons } = useMutableState(EditorState)
const sceneQuery = useFind(staticResourcePath, { query: { key: scenePath.value ?? '', type: 'scene' } }).data

const currentLoadedSceneURL = useHookstate(null as string | null)

/**
* what is our source of truth for which scene is loaded?
* EditorState.scenePath
* because we DO NOT want url hashes to trigger a scene reload
*/

/** we don't want to use useFind here, because we don't want all static-resource query refetches to potentially reload the scene */
useEffect(() => {
if (!scenePath.value) return

const abortController = new AbortController()
Engine.instance.api
.service(staticResourcePath)
.find({
query: { key: scenePath.value, type: 'scene', $limit: 1 }
})
.then((result) => {
if (abortController.signal.aborted) return

const scene = result.data[0]
if (!scene) {
console.error('Scene not found')
sceneName.set(null)
sceneAssetID.set(null)
currentLoadedSceneURL.set(null)
return
}

projectName.set(scene.project!)
sceneName.set(scene.key.split('/').pop() ?? null)
sceneAssetID.set(scene.id)
currentLoadedSceneURL.set(scene.url)
})

return () => {
abortController.abort()
}
}, [scenePath.value])

const viewerEntity = useMutableState(EngineState).viewerEntity.value

useEffect(() => {
if (!sceneAssetID.value || !currentLoadedSceneURL.value || !viewerEntity) return
return setCurrentEditorScene(currentLoadedSceneURL.value, sceneAssetID.value as EntityUUID)
}, [viewerEntity, currentLoadedSceneURL.value])

const errorState = useHookstate(getMutableState(EditorErrorState).error)

const dockPanelRef = useRef<DockLayout>(null)
Expand All @@ -136,29 +183,11 @@ const EditorContainer = () => {
PopoverState.showPopupover(<SaveSceneDialog />)
})

const viewerEntity = useMutableState(EngineState).viewerEntity.value

const { initialized, isWidgetVisible, openChat } = useZendesk()
const { t } = useTranslation()

const [visualScriptPanelEnabled] = useFeatureFlags([FeatureFlags.Studio.Panel.VisualScript])

useEffect(() => {
const scene = sceneQuery[0]
if (!scene) return

projectName.set(scene.project!)
sceneName.set(scene.key.split('/').pop() ?? null)
sceneAssetID.set(scene.id)
}, [sceneQuery[0]?.key])

useEffect(() => {
const scene = sceneQuery[0]
if (!sceneAssetID.value || !scene || !viewerEntity) return

return setCurrentEditorScene(scene.url, sceneAssetID.value as EntityUUID)
}, [viewerEntity, sceneAssetID, sceneQuery[0]?.url])

useEffect(() => {
return () => {
getMutableState(SelectionState).selectedEntities.set([])
Expand Down
14 changes: 5 additions & 9 deletions packages/editor/src/functions/assetFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,22 +90,18 @@ export const inputFileWithAddToScene = ({
el.click()
})

export const uploadProjectFiles = (projectName: string, files: File[], paths: string[], onProgress?) => {
export const uploadProjectFiles = (projectName: string, files: File[], paths: string[], args?: object[]) => {
const promises: CancelableUploadPromiseReturnType<string>[] = []

for (let i = 0; i < files.length; i++) {
const file = files[i]
const fileDirectory = paths[i].replace('projects/' + projectName + '/', '')
const filePath = fileDirectory ? pathJoin(fileDirectory, file.name) : file.name
const fileArgs = args?.[i] ?? {}
promises.push(
uploadToFeathersService(
fileBrowserUploadPath,
[file],
{
args: [{ project: projectName, path: filePath, contentType: '' }]
},
onProgress
)
uploadToFeathersService(fileBrowserUploadPath, [file], {
args: [{ contentType: '', ...fileArgs, project: projectName, path: filePath }]
})
)
}

Expand Down
Loading

0 comments on commit 86152f2

Please sign in to comment.