Implement dynamic scene trees in r3f

This makes it possible to add/remove objects on the fly, do hot-module reloading, change object configs on the fly, and more.
This commit is contained in:
Aria Minaei 2022-10-18 10:18:10 +02:00 committed by Aria
parent 151fcce298
commit a9c3c00153
4 changed files with 95 additions and 41 deletions

View file

@ -43,21 +43,21 @@ const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
useLayoutEffect(() => {
const originalVisibility = object.visible
if (editable.visibleOnlyInEditor) {
if (editable?.visibleOnlyInEditor) {
object.visible = true
}
return () => {
object.visible = originalVisibility
}
}, [editable.visibleOnlyInEditor, object.visible])
}, [editable?.visibleOnlyInEditor, object.visible])
const [hovered, setHovered] = useState(false)
// Helpers
const scene = useThree((state) => state.scene)
const helper = useMemo<Helper | undefined>(
() => editable.objectConfig.createHelper?.(object),
() => editable?.objectConfig.createHelper?.(object),
[object],
)
useEffect(() => {
@ -92,6 +92,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
// subscribe to external changes
useEffect(() => {
if (!editable) return
const sheetObject = editable.sheetObject
const objectConfig = editable.objectConfig
@ -114,6 +115,8 @@ const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
}
}, [editable])
if (!editable) return null
return (
<>
<group

View file

@ -58,18 +58,19 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
sceneProxy.traverse((object) => {
if (object.userData.__editable) {
// there are duplicate theatreKeys in the scene, only display one instance in the editor
if (editableProxies[object.userData.__storeKey]) {
object.parent!.remove(object)
} else {
const theatreKey = object.userData.__storeKey
if (
// there are duplicate theatreKeys in the scene, only display one instance in the editor
editableProxies[theatreKey] ||
// this object has been unmounted
!editables[theatreKey]
) {
object.parent!.remove(object)
} else {
editableProxies[theatreKey] = {
portal: createPortal(
<EditableProxy
storeKey={object.userData.__storeKey}
object={object}
/>,
<EditableProxy storeKey={theatreKey} object={object} />,
object.parent!,
),
object: object,

View file

@ -1,4 +1,11 @@
import type {ComponentProps, ComponentType, Ref, RefAttributes} from 'react'
import {
ComponentProps,
ComponentType,
Ref,
RefAttributes,
useMemo,
useState,
} from 'react'
import React, {forwardRef, useEffect, useLayoutEffect, useRef} from 'react'
import {allRegisteredObjects, editorStore} from './store'
import mergeRefs from 'react-merge-refs'
@ -7,7 +14,8 @@ import {useCurrentSheet} from './SheetProvider'
import defaultEditableFactoryConfig from './defaultEditableFactoryConfig'
import type {EditableFactoryConfig} from './editableFactoryConfigUtils'
import {makeStoreKey} from './utils'
import type {$FixMe} from '../types'
import type {$FixMe, $IntentionalAny} from '../types'
import type {ISheetObject} from '@theatre/core'
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
config: EditableFactoryConfig,
@ -49,21 +57,18 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
const sheet = useCurrentSheet()!
const sheetObject = sheet.object(
theatreKey,
Object.assign(
{
...additionalProps,
},
// @ts-ignore
...Object.values(config[actualType].props).map(
// @ts-ignore
(value) => value.type,
),
),
)
const [sheetObject, setSheetObject] = useState<
undefined | ISheetObject<$FixMe>
>(undefined)
const storeKey = makeStoreKey(sheetObject.address)
const storeKey = useMemo(
() =>
makeStoreKey({
...sheet.address,
objectKey: theatreKey as $IntentionalAny,
}),
[sheet, theatreKey],
)
const invalidate = useInvalidate()
@ -102,8 +107,38 @@ Then you can use it in your JSX like any other editable component. Note the make
// create sheet object and add editable to store
useLayoutEffect(() => {
if (!sheet) return
if (sheetObject) {
sheet.object(
theatreKey,
Object.assign(
{
...additionalProps,
},
// @ts-ignore
...Object.values(config[actualType].props).map(
// @ts-ignore
(value) => value.type,
),
),
{override: true},
)
return
} else {
const sheetObject = sheet.object(
theatreKey,
Object.assign(
{
...additionalProps,
},
// @ts-ignore
...Object.values(config[actualType].props).map(
// @ts-ignore
(value) => value.type,
),
),
)
allRegisteredObjects.add(sheetObject)
setSheetObject(sheetObject)
if (objRef)
typeof objRef === 'function'
@ -117,7 +152,8 @@ Then you can use it in your JSX like any other editable component. Note the make
// @ts-ignore
objectConfig: config[actualType],
})
}, [sheet, storeKey])
}
}, [sheet, storeKey, additionalProps])
// store initial values of props
useLayoutEffect(() => {
@ -161,6 +197,9 @@ Then you can use it in your JSX like any other editable component. Note the make
return () => {
untap()
sheetObject.sheet.deleteObject(theatreKey)
allRegisteredObjects.delete(sheetObject)
editorStore.getState().removeEditable(storeKey)
}
}, [sheetObject])

View file

@ -45,6 +45,7 @@ export type EditorStore = {
init: (scene: Scene, gl: WebGLRenderer) => void
addEditable: (theatreKey: string, editable: Editable<any>) => void
removeEditable: (theatreKey: string) => void
createSnapshot: () => void
setSnapshotProxyObject: (
proxyObject: Object3D | null,
@ -85,6 +86,16 @@ const config: StateCreator<EditorStore> = (set, get) => {
}))
},
removeEditable: (theatreKey) => {
set((state) => {
const editables = {...state.editables}
delete editables[theatreKey]
return {
editables,
}
})
},
createSnapshot: () => {
set((state) => ({
sceneSnapshot: state.scene?.clone() ?? null,