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

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

View 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

View 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'),
)

Binary file not shown.

View file

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

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