Support multiple sheet instances (#153)
* Support multiple/nested sheets and sheet instances * Add playground for instances * Fix playground and example * Change r3f's objectKey to storeKey to avoid confusion * Update all editable/uniqueName variables used as store key to storeKey too * Fix lint warnings
This commit is contained in:
parent
10b4954ee2
commit
fc9df7c346
11 changed files with 195 additions and 88 deletions
|
@ -1,16 +1,16 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {useThree} from '@react-three/fiber'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import {bindToCanvas} from './store'
|
||||
|
||||
const ctx = createContext<{sheet: ISheet | undefined} | undefined>(undefined)
|
||||
const ctx = createContext<{sheet: ISheet}>(undefined!)
|
||||
|
||||
const useWrapperContext = (): {sheet: ISheet | undefined} => {
|
||||
const useWrapperContext = (): {sheet: ISheet} => {
|
||||
const val = useContext(ctx)
|
||||
if (!val) {
|
||||
throw new Error(
|
||||
|
@ -25,23 +25,21 @@ export const useCurrentSheet = (): ISheet | undefined => {
|
|||
}
|
||||
|
||||
const SheetProvider: React.FC<{
|
||||
getSheet: () => ISheet
|
||||
}> = (props) => {
|
||||
sheet: ISheet
|
||||
}> = ({sheet, children}) => {
|
||||
const {scene, gl} = useThree((s) => ({scene: s.scene, gl: s.gl}))
|
||||
const [sheet, setSheet] = useState<ISheet | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!sheet || sheet.type !== 'Theatre_Sheet_PublicAPI') {
|
||||
throw new Error(`sheet in <Wrapper sheet={sheet}> has an invalid value`)
|
||||
}
|
||||
}, [sheet])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const sheet = props.getSheet()
|
||||
if (!sheet || sheet.type !== 'Theatre_Sheet_PublicAPI') {
|
||||
throw new Error(
|
||||
`getSheet() in <Wrapper getSheet={getSheet}> has returned an invalid value`,
|
||||
)
|
||||
}
|
||||
setSheet(sheet)
|
||||
bindToCanvas({gl, scene})
|
||||
}, [scene, gl])
|
||||
|
||||
return <ctx.Provider value={{sheet}}>{props.children}</ctx.Provider>
|
||||
return <ctx.Provider value={{sheet}}>{children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export default SheetProvider
|
||||
|
|
|
@ -15,14 +15,11 @@ import {invalidate, useFrame, useThree} from '@react-three/fiber'
|
|||
import {useDragDetector} from './DragDetector'
|
||||
|
||||
export interface EditableProxyProps {
|
||||
editableName: string
|
||||
storeKey: string
|
||||
object: Object3D
|
||||
}
|
||||
|
||||
const EditableProxy: VFC<EditableProxyProps> = ({
|
||||
editableName: uniqueName,
|
||||
object,
|
||||
}) => {
|
||||
const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
|
||||
const editorObject = getEditorSheetObject()
|
||||
const [setSnapshotProxyObject, editables] = useEditorStore(
|
||||
(state) => [state.setSnapshotProxyObject, state.editables],
|
||||
|
@ -31,17 +28,17 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
|
||||
const dragging = useDragDetector()
|
||||
|
||||
const editable = editables[uniqueName]
|
||||
const editable = editables[storeKey]
|
||||
|
||||
const selected = useSelected()
|
||||
const showOverlayIcons =
|
||||
useVal(editorObject?.props.viewport.showOverlayIcons) ?? false
|
||||
|
||||
useEffect(() => {
|
||||
setSnapshotProxyObject(object, uniqueName)
|
||||
setSnapshotProxyObject(object, storeKey)
|
||||
|
||||
return () => setSnapshotProxyObject(null, uniqueName)
|
||||
}, [uniqueName, object, setSnapshotProxyObject])
|
||||
return () => setSnapshotProxyObject(null, storeKey)
|
||||
}, [storeKey, object, setSnapshotProxyObject])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const originalVisibility = object.visible
|
||||
|
@ -68,7 +65,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
return
|
||||
}
|
||||
|
||||
if (selected === uniqueName || hovered) {
|
||||
if (selected === storeKey || hovered) {
|
||||
scene.add(helper)
|
||||
invalidate()
|
||||
}
|
||||
|
@ -93,6 +90,30 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
}
|
||||
}, [dragging])
|
||||
|
||||
// subscribe to external changes
|
||||
useEffect(() => {
|
||||
const sheetObject = editable.sheetObject
|
||||
const objectConfig = editable.objectConfig
|
||||
|
||||
const setFromTheatre = (newValues: any) => {
|
||||
// @ts-ignore
|
||||
Object.entries(objectConfig.props).forEach(([key, value]) => {
|
||||
// @ts-ignore
|
||||
return value.apply(newValues[key], object)
|
||||
})
|
||||
objectConfig.updateObject?.(object)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
setFromTheatre(sheetObject.value)
|
||||
|
||||
const untap = sheetObject.onValuesChange(setFromTheatre)
|
||||
|
||||
return () => {
|
||||
untap()
|
||||
}
|
||||
}, [editable])
|
||||
|
||||
return (
|
||||
<>
|
||||
<group
|
||||
|
@ -101,10 +122,10 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
e.stopPropagation()
|
||||
|
||||
const theatreObject =
|
||||
useEditorStore.getState().editables[uniqueName].sheetObject
|
||||
useEditorStore.getState().editables[storeKey].sheetObject
|
||||
|
||||
if (!theatreObject) {
|
||||
console.log('no theatre object for', uniqueName)
|
||||
console.log('no theatre object for', storeKey)
|
||||
} else {
|
||||
studio.setSelection([theatreObject])
|
||||
}
|
||||
|
@ -129,8 +150,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
>
|
||||
<primitive object={object}>
|
||||
{(showOverlayIcons ||
|
||||
(editable.objectConfig.dimensionless &&
|
||||
selected !== uniqueName)) && (
|
||||
(editable.objectConfig.dimensionless && selected !== storeKey)) && (
|
||||
<Html
|
||||
center
|
||||
style={{
|
||||
|
@ -149,10 +169,10 @@ const EditableProxy: VFC<EditableProxyProps> = ({
|
|||
if (e.delta < 2) {
|
||||
e.stopPropagation()
|
||||
const theatreObject =
|
||||
useEditorStore.getState().editables[uniqueName].sheetObject
|
||||
useEditorStore.getState().editables[storeKey].sheetObject
|
||||
|
||||
if (!theatreObject) {
|
||||
console.log('no theatre object for', uniqueName)
|
||||
console.log('no theatre object for', storeKey)
|
||||
} else {
|
||||
studio.setSelection([theatreObject])
|
||||
}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import type {VFC} from 'react'
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'
|
||||
import type {Editable} from '../store'
|
||||
import {useEditorStore} from '../store'
|
||||
import {createPortal} from '@react-three/fiber'
|
||||
|
@ -15,11 +9,10 @@ import TransformControls from './TransformControls'
|
|||
import shallow from 'zustand/shallow'
|
||||
import type {Material, Mesh, Object3D} from 'three'
|
||||
import {MeshBasicMaterial, MeshPhongMaterial} from 'three'
|
||||
import type {IScrub} from '@theatre/studio';
|
||||
import type {IScrub} from '@theatre/studio'
|
||||
import studio from '@theatre/studio'
|
||||
import {useSelected} from './useSelected'
|
||||
import {useVal} from '@theatre/react'
|
||||
import useInvalidate from './useInvalidate'
|
||||
import {getEditorSheetObject} from './editorStuff'
|
||||
|
||||
export interface ProxyManagerProps {
|
||||
|
@ -55,8 +48,6 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
}
|
||||
>({})
|
||||
|
||||
const invalidate = useInvalidate()
|
||||
|
||||
// set up scene proxies
|
||||
useLayoutEffect(() => {
|
||||
if (!sceneProxy) {
|
||||
|
@ -68,15 +59,15 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
sceneProxy.traverse((object) => {
|
||||
if (object.userData.__editable) {
|
||||
// there are duplicate uniqueNames in the scene, only display one instance in the editor
|
||||
if (editableProxies[object.userData.__editableName]) {
|
||||
if (editableProxies[object.userData.__storeKey]) {
|
||||
object.parent!.remove(object)
|
||||
} else {
|
||||
const uniqueName = object.userData.__editableName
|
||||
const uniqueName = object.userData.__storeKey
|
||||
|
||||
editableProxies[uniqueName] = {
|
||||
portal: createPortal(
|
||||
<EditableProxy
|
||||
editableName={object.userData.__editableName}
|
||||
storeKey={object.userData.__storeKey}
|
||||
object={object}
|
||||
/>,
|
||||
object.parent!,
|
||||
|
@ -95,32 +86,6 @@ const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
|
|||
const editableProxyOfSelected = selected && editableProxies[selected]
|
||||
const editable = selected ? editables[selected] : undefined
|
||||
|
||||
// subscribe to external changes
|
||||
useEffect(() => {
|
||||
if (!editableProxyOfSelected || !editable) return
|
||||
const object = editableProxyOfSelected.object
|
||||
const sheetObject = editableProxyOfSelected.editable.sheetObject
|
||||
const objectConfig = editable.objectConfig
|
||||
|
||||
const setFromTheatre = (newValues: any) => {
|
||||
// @ts-ignore
|
||||
Object.entries(objectConfig.props).forEach(([key, value]) => {
|
||||
// @ts-ignore
|
||||
return value.apply(newValues[key], object)
|
||||
})
|
||||
objectConfig.updateObject?.(object)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
setFromTheatre(sheetObject.value)
|
||||
|
||||
const untap = sheetObject.onValuesChange(setFromTheatre)
|
||||
|
||||
return () => {
|
||||
untap()
|
||||
}
|
||||
}, [editableProxyOfSelected, selected])
|
||||
|
||||
// set up viewport shading modes
|
||||
const [renderMaterials, setRenderMaterials] = useState<{
|
||||
[id: string]: Material | Material[]
|
||||
|
|
|
@ -8,6 +8,7 @@ import useInvalidate from './useInvalidate'
|
|||
import {useCurrentSheet} from '../SheetProvider'
|
||||
import defaultEditableFactoryConfig from '../defaultEditableFactoryConfig'
|
||||
import type {EditableFactoryConfig} from '../editableFactoryConfigUtils'
|
||||
import {makeStoreKey} from '../utils'
|
||||
|
||||
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||
config: EditableFactoryConfig,
|
||||
|
@ -47,7 +48,9 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
|||
|
||||
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||
|
||||
const sheet = useCurrentSheet()
|
||||
const sheet = useCurrentSheet()!
|
||||
|
||||
const storeKey = makeStoreKey(sheet, uniqueName)
|
||||
|
||||
const [sheetObject, setSheetObject] = useState<
|
||||
undefined | ISheetObject<$FixMe>
|
||||
|
@ -75,14 +78,14 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
|||
|
||||
if (objRef) objRef!.current = sheetObject
|
||||
|
||||
useEditorStore.getState().addEditable(uniqueName, {
|
||||
useEditorStore.getState().addEditable(storeKey, {
|
||||
type: actualType,
|
||||
sheetObject,
|
||||
visibleOnlyInEditor: visible === 'editor',
|
||||
// @ts-ignore
|
||||
objectConfig: config[actualType],
|
||||
})
|
||||
}, [sheet, uniqueName])
|
||||
}, [sheet, storeKey])
|
||||
|
||||
// store initial values of props
|
||||
useLayoutEffect(() => {
|
||||
|
@ -95,7 +98,6 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
|||
),
|
||||
)
|
||||
}, [
|
||||
uniqueName,
|
||||
sheetObject,
|
||||
// @ts-ignore
|
||||
...Object.keys(config[actualType].props).map(
|
||||
|
@ -138,7 +140,7 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
|||
visible={visible !== 'editor' && visible}
|
||||
userData={{
|
||||
__editable: true,
|
||||
__editableName: uniqueName,
|
||||
__storeKey: storeKey,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ import {allRegisteredObjects} from '../store'
|
|||
import studio from '@theatre/studio'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import type {$IntentionalAny} from '../types'
|
||||
import {makeStoreKey} from '../utils'
|
||||
|
||||
export function useSelected(): undefined | string {
|
||||
const [state, set] = useState<string | undefined>(undefined)
|
||||
|
@ -19,7 +20,7 @@ export function useSelected(): undefined | string {
|
|||
if (!item) {
|
||||
set(undefined)
|
||||
} else {
|
||||
set(item.address.objectKey)
|
||||
set(makeStoreKey(item.sheet, item.address.objectKey))
|
||||
}
|
||||
}
|
||||
setFromStudio(studio.selection)
|
||||
|
@ -38,6 +39,6 @@ export function getSelected(): undefined | string {
|
|||
if (!item) {
|
||||
return undefined
|
||||
} else {
|
||||
return item.address.objectKey
|
||||
return makeStoreKey(item.sheet, item.address.objectKey)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import {useEditorStore} from './store'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
|
||||
export const refreshSnapshot = useEditorStore.getState().createSnapshot
|
||||
|
||||
export const makeStoreKey = (sheet: ISheet, name: string) =>
|
||||
`${sheet.address.sheetId}:${sheet.address.sheetInstanceId}:${name}`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue