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
|
@ -92,7 +92,7 @@ function App() {
|
|||
shadowMap
|
||||
>
|
||||
<SheetProvider
|
||||
getSheet={() => getProject('Playground - R3F').sheet('R3F-Canvas')}
|
||||
sheet={getProject('Playground - R3F').sheet('R3F-Canvas')}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<e.perspectiveCamera makeDefault uniqueName="Camera" />
|
||||
|
|
109
packages/playground/src/shared/instances/App.tsx
Normal file
109
packages/playground/src/shared/instances/App.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {editable as e, RefreshSnapshot, SheetProvider} from '@theatre/r3f'
|
||||
import {Stars} from '@react-three/drei'
|
||||
import {getProject} from '@theatre/core'
|
||||
import React, {Suspense, useState} from 'react'
|
||||
import {Canvas} from '@react-three/fiber'
|
||||
import {useGLTF, PerspectiveCamera} from '@react-three/drei'
|
||||
import sceneGLB from './scene.glb'
|
||||
|
||||
document.body.style.backgroundColor = '#171717'
|
||||
|
||||
const EditableCamera = e(PerspectiveCamera, 'perspectiveCamera')
|
||||
|
||||
function Model({
|
||||
url,
|
||||
instance,
|
||||
...props
|
||||
}: {url: string; instance?: string} & JSX.IntrinsicElements['group']) {
|
||||
const {nodes} = useGLTF(url) as any
|
||||
|
||||
return (
|
||||
<e.group
|
||||
uniqueName={`Transforms for Rocket: ${instance ?? 'default'}`}
|
||||
{...props}
|
||||
>
|
||||
<SheetProvider sheet={getProject('Space').sheet('Rocket', instance)}>
|
||||
<group rotation={[-Math.PI / 2, 0, 0]} position={[0, -7, 0]} scale={7}>
|
||||
<group rotation={[Math.PI / 13.5, -Math.PI / 5.8, Math.PI / 5.6]}>
|
||||
<e.mesh
|
||||
uniqueName="Thingy"
|
||||
receiveShadow
|
||||
castShadow
|
||||
geometry={nodes.planet001.geometry}
|
||||
material={nodes.planet001.material}
|
||||
/>
|
||||
<e.mesh
|
||||
uniqueName="Debris 2"
|
||||
receiveShadow
|
||||
castShadow
|
||||
geometry={nodes.planet002.geometry}
|
||||
material={nodes.planet002.material}
|
||||
/>
|
||||
<e.mesh
|
||||
uniqueName="Debris 1"
|
||||
geometry={nodes.planet003.geometry}
|
||||
material={nodes.planet003.material}
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</SheetProvider>
|
||||
</e.group>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const bgs = ['#272730', '#b7c5d1']
|
||||
const [bgIndex, setBgIndex] = useState(0)
|
||||
const bg = bgs[bgIndex]
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
// return setBgIndex((bgIndex) => (bgIndex + 1) % bgs.length)
|
||||
}}
|
||||
style={{
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Canvas dpr={[1.5, 2]} linear shadows frameloop="demand">
|
||||
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
|
||||
<fog attach="fog" args={[bg, 16, 70]} />
|
||||
<color attach="background" args={[bg]} />
|
||||
<ambientLight intensity={0.75} />
|
||||
<EditableCamera
|
||||
uniqueName="Camera"
|
||||
makeDefault
|
||||
position={[0, 0, 0]}
|
||||
fov={75}
|
||||
near={20}
|
||||
far={70}
|
||||
>
|
||||
<e.pointLight
|
||||
uniqueName="Light 1"
|
||||
intensity={1}
|
||||
position={[-10, -25, -10]}
|
||||
/>
|
||||
<e.spotLight
|
||||
uniqueName="Light 2"
|
||||
castShadow
|
||||
intensity={2.25}
|
||||
angle={0.2}
|
||||
penumbra={1}
|
||||
position={[-25, 20, -15]}
|
||||
shadow-mapSize={[1024, 1024]}
|
||||
shadow-bias={-0.0001}
|
||||
/>
|
||||
<e.directionalLight uniqueName="Light 3" />
|
||||
</EditableCamera>
|
||||
<Suspense fallback={null}>
|
||||
<RefreshSnapshot />
|
||||
<Model url={sceneGLB} instance="Apollo" position={[18, 5, -42]} />
|
||||
<Model url={sceneGLB} instance="Sputnik" position={[-18, 5, -42]} />
|
||||
</Suspense>
|
||||
<Stars radius={500} depth={50} count={1000} factor={10} />
|
||||
</SheetProvider>
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
15
packages/playground/src/shared/instances/index.tsx
Normal file
15
packages/playground/src/shared/instances/index.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import studio from '@theatre/studio'
|
||||
import {extension} from '@theatre/r3f'
|
||||
|
||||
studio.extend(extension)
|
||||
studio.initialize()
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
)
|
BIN
packages/playground/src/shared/instances/scene.glb
Normal file
BIN
packages/playground/src/shared/instances/scene.glb
Normal file
Binary file not shown.
|
@ -1,11 +1,10 @@
|
|||
import {editable as e, RefreshSnapshot, SheetProvider} from '@theatre/r3f'
|
||||
import {Stars} from '@react-three/drei'
|
||||
import {getProject} from '@theatre/core'
|
||||
import React, {Suspense, useMemo, useState} from 'react'
|
||||
import React, {Suspense, useState} from 'react'
|
||||
import {Canvas} from '@react-three/fiber'
|
||||
import {useGLTF, PerspectiveCamera} from '@react-three/drei'
|
||||
import sceneGLB from './scene.glb'
|
||||
import {Color} from 'three'
|
||||
|
||||
document.body.style.backgroundColor = '#171717'
|
||||
|
||||
|
@ -55,14 +54,8 @@ function App() {
|
|||
}}
|
||||
>
|
||||
<Canvas dpr={[1.5, 2]} linear shadows frameloop="demand">
|
||||
<SheetProvider getSheet={() => getProject('Space').sheet('Scene')}>
|
||||
<e.fog
|
||||
attach="fog"
|
||||
color={useMemo(() => new Color(bg), [bg])}
|
||||
near={16}
|
||||
far={30}
|
||||
uniqueName="Fog"
|
||||
/>
|
||||
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
|
||||
<fog attach="fog" args={[bg, 16, 30]} />
|
||||
<color attach="background" args={[bg]} />
|
||||
<ambientLight intensity={0.75} />
|
||||
<EditableCamera
|
||||
|
|
|
@ -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…
Reference in a new issue