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:
Andrew Prifer 2022-05-16 12:43:45 +02:00 committed by GitHub
parent 10b4954ee2
commit fc9df7c346
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 195 additions and 88 deletions

View file

@ -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

View file

@ -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])
}

View file

@ -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[]

View file

@ -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,
}}
/>
)

View file

@ -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)
}
}

View file

@ -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}`