Renamed @theatre/plugin-r3f to @theatre/r3f

This commit is contained in:
Aria Minaei 2021-09-06 10:19:10 +02:00
parent 03a2f26686
commit 4f66d57cf8
42 changed files with 19 additions and 19 deletions

View file

@ -0,0 +1,11 @@
module.exports = {
rules: {
'no-restricted-syntax': [
'error',
{
selector: `ImportDeclaration[source.value=/@theatre\\u002F(studio|core)\\u002F/]`,
message: `Importing Theatre's submodules would not work in the production build.`,
},
],
},
}

View file

@ -0,0 +1,47 @@
import React, {
createContext,
useContext,
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 useWrapperContext = (): {sheet: ISheet | undefined} => {
const val = useContext(ctx)
if (!val) {
throw new Error(
`No sheet found. You need to add a <SheetProvider> higher up in the tree. https://docs.theatrejs.com/r3f.html#sheetprovider`,
)
}
return val
}
export const useCurrentSheet = (): ISheet | undefined => {
return useWrapperContext().sheet
}
const SheetProvider: React.FC<{
getSheet: () => ISheet
}> = (props) => {
const {scene, gl} = useThree((s) => ({scene: s.scene, gl: s.gl}))
const [sheet, setSheet] = useState<ISheet | undefined>(undefined)
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>
}
export default SheetProvider

View file

@ -0,0 +1,224 @@
import type {Object3D} from 'three'
import {
BoxHelper,
CameraHelper,
DirectionalLightHelper,
PointLightHelper,
SpotLightHelper,
} from 'three'
import type {ReactElement, VFC} from 'react'
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'
import {useHelper, Sphere, Html} from '@react-three/drei'
import type {EditableType} from '../store'
import {useEditorStore} from '../store'
import shallow from 'zustand/shallow'
import {
BiSun,
BsCameraVideoFill,
BsFillCollectionFill,
GiCube,
GiLightBulb,
GiLightProjector,
} from 'react-icons/all'
import type {IconType} from 'react-icons'
import studio from '@theatre/studio'
import {useSelected} from './useSelected'
import {useVal} from '@theatre/react'
import {getEditorSheetObject} from './editorStuff'
export interface EditableProxyProps {
editableName: string
editableType: EditableType
object: Object3D
onChange?: () => void
}
const EditableProxy: VFC<EditableProxyProps> = ({
editableName: uniqueName,
editableType,
object,
}) => {
const editorObject = getEditorSheetObject()
const setSnapshotProxyObject = useEditorStore(
(state) => state.setSnapshotProxyObject,
shallow,
)
const selected = useSelected()
const showOverlayIcons =
useVal(editorObject?.props.viewport.showOverlayIcons) ?? false
useEffect(() => {
setSnapshotProxyObject(object, uniqueName)
return () => setSnapshotProxyObject(null, uniqueName)
}, [uniqueName, object, setSnapshotProxyObject])
useLayoutEffect(() => {
const originalVisibility = object.visible
if (object.userData.__visibleOnlyInEditor) {
object.visible = true
}
return () => {
// this has absolutely no effect, __visibleOnlyInEditor of the snapshot never changes, I'm just doing it because it looks right 🤷‍️
object.visible = originalVisibility
}
}, [object.userData.__visibleOnlyInEditor, object.visible])
// set up helper
let Helper:
| typeof SpotLightHelper
| typeof DirectionalLightHelper
| typeof PointLightHelper
| typeof BoxHelper
| typeof CameraHelper
switch (editableType) {
case 'spotLight':
Helper = SpotLightHelper
break
case 'directionalLight':
Helper = DirectionalLightHelper
break
case 'pointLight':
Helper = PointLightHelper
break
case 'perspectiveCamera':
case 'orthographicCamera':
Helper = CameraHelper
break
case 'group':
case 'mesh':
Helper = BoxHelper
}
let helperArgs: [string] | [number, string] | []
const size = 1
const color = 'darkblue'
switch (editableType) {
case 'directionalLight':
case 'pointLight':
helperArgs = [size, color]
break
case 'group':
case 'mesh':
case 'spotLight':
helperArgs = [color]
break
case 'perspectiveCamera':
case 'orthographicCamera':
helperArgs = []
}
let icon: ReactElement<IconType>
switch (editableType) {
case 'group':
icon = <BsFillCollectionFill />
break
case 'mesh':
icon = <GiCube />
break
case 'pointLight':
icon = <GiLightBulb />
break
case 'spotLight':
icon = <GiLightProjector />
break
case 'directionalLight':
icon = <BiSun />
break
case 'perspectiveCamera':
case 'orthographicCamera':
icon = <BsCameraVideoFill />
}
const objectRef = useRef(object)
useLayoutEffect(() => {
objectRef.current = object
}, [object])
const dimensionless = [
'spotLight',
'pointLight',
'directionalLight',
'perspectiveCamera',
'orthographicCamera',
]
const [hovered, setHovered] = useState(false)
useHelper(
objectRef,
selected === uniqueName || dimensionless.includes(editableType) || hovered
? Helper
: null,
...helperArgs,
)
return (
<>
<group
onClick={(e) => {
if (e.delta < 2) {
e.stopPropagation()
const theatreObject =
useEditorStore.getState().sheetObjects[uniqueName]
if (!theatreObject) {
console.log('no theatre object for', uniqueName)
} else {
studio.setSelection([theatreObject])
}
}
}}
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
}}
onPointerOut={(e) => {
e.stopPropagation()
setHovered(false)
}}
>
<primitive object={object}>
{showOverlayIcons && (
<Html
center
className="pointer-events-none p-1 rounded bg-white bg-opacity-70 shadow text-gray-700"
>
{icon}
</Html>
)}
{dimensionless.includes(editableType) && (
<Sphere
args={[2, 4, 2]}
onClick={(e) => {
if (e.delta < 2) {
e.stopPropagation()
const theatreObject =
useEditorStore.getState().sheetObjects[uniqueName]
if (!theatreObject) {
console.log('no theatre object for', uniqueName)
} else {
studio.setSelection([theatreObject])
}
}
}}
userData={{helper: true}}
>
<meshBasicMaterial visible={false} />
</Sphere>
)}
</primitive>
</group>
</>
)
}
export default EditableProxy

View file

@ -0,0 +1,23 @@
import type {ComponentProps, ElementType} from 'react'
import React from 'react'
import {useEditorStore} from '../store'
import {createPortal} from '@react-three/fiber'
export type EditorHelperProps<T extends ElementType> = {
component: T
} & ComponentProps<T>
const EditorHelper = <T extends ElementType>({
component: Component,
...props
}: EditorHelperProps<T>) => {
if (process.env.NODE_ENV === 'development') {
const helpersRoot = useEditorStore((state) => state.helpersRoot)
return <>{createPortal(<Component {...props} />, helpersRoot)}</>
} else {
return null
}
}
export default EditorHelper

View file

@ -0,0 +1,268 @@
import type {VFC} from 'react'
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import {useEditorStore} from '../store'
import {createPortal} from '@react-three/fiber'
import EditableProxy from './EditableProxy'
import type {OrbitControls} from 'three-stdlib'
import TransformControls from './TransformControls'
import shallow from 'zustand/shallow'
import type {Material, Mesh, Object3D} from 'three'
import {MeshBasicMaterial, MeshPhongMaterial} from 'three'
import studio from '@theatre/studio'
import type {ISheetObject} from '@theatre/core'
import type {$FixMe} from '../types'
import {useSelected} from './useSelected'
import {useVal} from '@theatre/react'
import useInvalidate from './useInvalidate'
import {getEditorSheetObject} from './editorStuff'
export interface ProxyManagerProps {
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
}
type IEditableProxy = {
portal: ReturnType<typeof createPortal>
object: Object3D
sheetObject: ISheetObject<$FixMe>
}
const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
const isBeingEdited = useRef(false)
const editorObject = getEditorSheetObject()
const [sceneSnapshot, sheetObjects] = useEditorStore(
(state) => [state.sceneSnapshot, state.sheetObjects],
shallow,
)
const transformControlsMode =
useVal(editorObject?.props.transformControls.mode) ?? 'translate'
const transformControlsSpace =
useVal(editorObject?.props.transformControls.space) ?? 'world'
const viewportShading =
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
const sceneProxy = useMemo(() => sceneSnapshot?.clone(), [sceneSnapshot])
const [editableProxies, setEditableProxies] = useState<
{
[name in string]?: IEditableProxy
}
>({})
const invalidate = useInvalidate()
// set up scene proxies
useLayoutEffect(() => {
if (!sceneProxy) {
return
}
const editableProxies: {[name: string]: IEditableProxy} = {}
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]) {
object.parent!.remove(object)
} else {
const uniqueName = object.userData.__editableName
editableProxies[uniqueName] = {
portal: createPortal(
<EditableProxy
editableName={object.userData.__editableName}
editableType={object.userData.__editableType}
object={object}
/>,
object.parent!,
),
object: object,
sheetObject: sheetObjects[uniqueName]!,
}
}
}
})
setEditableProxies(editableProxies)
}, [orbitControlsRef, sceneProxy])
const selected = useSelected()
const editableProxyOfSelected = selected && editableProxies[selected]
// subscribe to external changes
useEffect(() => {
if (!editableProxyOfSelected) return
const object = editableProxyOfSelected.object
const sheetObject = editableProxyOfSelected.sheetObject
const setFromTheatre = (newValues: any) => {
object.position.set(
newValues.position.x,
newValues.position.y,
newValues.position.z,
)
object.rotation.set(
newValues.rotation.x,
newValues.rotation.y,
newValues.rotation.z,
)
object.scale.set(newValues.scale.x, newValues.scale.y, newValues.scale.z)
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[]
}>({})
useLayoutEffect(() => {
if (!sceneProxy) {
return
}
const renderMaterials: {
[id: string]: Material | Material[]
} = {}
sceneProxy.traverse((object) => {
const mesh = object as Mesh
if (mesh.isMesh && !mesh.userData.helper) {
renderMaterials[mesh.id] = mesh.material
}
})
setRenderMaterials(renderMaterials)
return () => {
// @todo do we need this cleanup?
// Object.entries(renderMaterials).forEach(([id, material]) => {
// ;(sceneProxy.getObjectById(Number.parseInt(id)) as Mesh).material =
// material
// })
}
}, [sceneProxy])
useLayoutEffect(() => {
if (!sceneProxy) {
return
}
sceneProxy.traverse((object) => {
const mesh = object as Mesh
if (mesh.isMesh && !mesh.userData.helper) {
let material
switch (viewportShading) {
case 'wireframe':
mesh.material = new MeshBasicMaterial({
wireframe: true,
color: 'black',
})
break
case 'flat':
// it is possible that renderMaterials hasn't updated yet
if (!renderMaterials[mesh.id]) {
return
}
material = new MeshBasicMaterial()
if (renderMaterials[mesh.id].hasOwnProperty('color')) {
material.color = (renderMaterials[mesh.id] as any).color
}
if (renderMaterials[mesh.id].hasOwnProperty('map')) {
material.map = (renderMaterials[mesh.id] as any).map
}
if (renderMaterials[mesh.id].hasOwnProperty('vertexColors')) {
material.vertexColors = (
renderMaterials[mesh.id] as any
).vertexColors
}
mesh.material = material
break
case 'solid':
// it is possible that renderMaterials hasn't updated yet
if (!renderMaterials[mesh.id]) {
return
}
material = new MeshPhongMaterial()
if (renderMaterials[mesh.id].hasOwnProperty('color')) {
material.color = (renderMaterials[mesh.id] as any).color
}
if (renderMaterials[mesh.id].hasOwnProperty('map')) {
material.map = (renderMaterials[mesh.id] as any).map
}
if (renderMaterials[mesh.id].hasOwnProperty('vertexColors')) {
material.vertexColors = (
renderMaterials[mesh.id] as any
).vertexColors
}
mesh.material = material
break
case 'rendered':
mesh.material = renderMaterials[mesh.id]
}
}
})
}, [viewportShading, renderMaterials, sceneProxy])
if (!sceneProxy) {
return null
}
return (
<>
<primitive object={sceneProxy} />
{selected && editableProxyOfSelected && (
<TransformControls
mode={transformControlsMode}
space={transformControlsSpace}
orbitControlsRef={orbitControlsRef}
object={editableProxyOfSelected.object}
onObjectChange={() => {
const sheetObject = editableProxyOfSelected.sheetObject
const obj = editableProxyOfSelected.object
studio.transaction(({set}) => {
set(sheetObject.props, {
position: {
x: obj.position.x,
y: obj.position.y,
z: obj.position.z,
},
rotation: {
x: obj.rotation.x,
y: obj.rotation.y,
z: obj.rotation.z,
},
scale: {
x: obj.scale.x,
y: obj.scale.y,
z: obj.scale.z,
},
})
})
}}
onDraggingChange={(event) => (isBeingEdited.current = event.value)}
/>
)}
{Object.values(editableProxies).map(
(editableProxy) => editableProxy!.portal,
)}
</>
)
}
export default ProxyManager

View file

@ -0,0 +1,27 @@
import React, {useEffect} from 'react'
import useRefreshSnapshot from './useRefreshSnapshot'
/**
* Putting this element in a suspense tree makes sure the snapshot editor
* gets refreshed once the tree renders.
*
* Alternatively you can use
* @link useRefreshSnapshot()
*
* @example
* ```jsx
* <Suspense fallback={null}>
* <RefreshSnapshot />
* <Model url={sceneGLB} />
* </Suspense>
* ```
*/
const RefreshSnapshot: React.FC<{}> = (props) => {
const refreshSnapshot = useRefreshSnapshot()
useEffect(() => {
refreshSnapshot()
}, [])
return <></>
}
export default RefreshSnapshot

View file

@ -0,0 +1,188 @@
import {useCallback, useLayoutEffect} from 'react'
import React from 'react'
import {Canvas} from '@react-three/fiber'
import type {BaseSheetObjectType} from '../store'
import {allRegisteredObjects, useEditorStore} from '../store'
import shallow from 'zustand/shallow'
import root from 'react-shadow/styled-components'
import ProxyManager from './ProxyManager'
import studio, {ToolbarIconButton} from '@theatre/studio'
import {useVal} from '@theatre/react'
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
import {IoCameraReverseOutline} from 'react-icons/all'
import type {ISheet} from '@theatre/core'
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
import {getEditorSheet, getEditorSheetObject} from './editorStuff'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
const GlobalStyle = createGlobalStyle`
:host {
contain: strict;
all: initial;
color: white;
font: 11px -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe Editor,
HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif;
}
* {
padding: 0;
margin: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
list-style: none;
}
`
const EditorScene: React.FC<{snapshotEditorSheet: ISheet; paneId: string}> = ({
snapshotEditorSheet,
paneId,
}) => {
const [editorCamera, orbitControlsRef] = useSnapshotEditorCamera(
snapshotEditorSheet,
paneId,
)
const editorObject = getEditorSheetObject()
const helpersRoot = useEditorStore((state) => state.helpersRoot, shallow)
const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true
const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true
return (
<>
{showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
{showAxes && <axesHelper args={[500]} />}
{editorCamera}
<primitive object={helpersRoot}></primitive>
<ProxyManager orbitControlsRef={orbitControlsRef} />
<color attach="background" args={[0.24, 0.24, 0.24]} />
</>
)
}
const Wrapper = styled.div`
tab-size: 4;
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
margin: 0;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
`
const CanvasWrapper = styled.div`
display: relative;
z-index: 0;
height: 100%;
`
const Overlay = styled.div`
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
`
const Tools = styled.div`
position: absolute;
left: 8px;
top: 6px;
pointer-events: auto;
`
const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
const snapshotEditorSheet = getEditorSheet()
const paneId = props.paneId
const editorObject = getEditorSheetObject()
const [sceneSnapshot, createSnapshot] = useEditorStore(
(state) => [state.sceneSnapshot, state.createSnapshot],
shallow,
)
const editorOpen = true
useLayoutEffect(() => {
let timeout: NodeJS.Timeout | undefined
if (editorOpen) {
// a hack to make sure all the scene's props are
// applied before we take a snapshot
timeout = setTimeout(createSnapshot, 100)
}
return () => {
if (timeout !== undefined) {
clearTimeout(timeout)
}
}
}, [editorOpen])
const onPointerMissed = useCallback(() => {
// This callback runs when the user clicks in an empty space inside a SnapshotEditor.
// We'll try to set the current selection to the nearest sheet _if_ at least one object
// belonging to R3F was selected previously.
const obj: undefined | BaseSheetObjectType = studio.selection.find(
(sheetOrObject) =>
allRegisteredObjects.has(sheetOrObject as $IntentionalAny),
) as $IntentionalAny
if (obj) {
studio.setSelection([obj.sheet])
}
}, [])
if (!editorObject) return <></>
return (
<root.div>
<StyleSheetManager disableVendorPrefixes>
<>
<GlobalStyle />
<Wrapper>
<Overlay>
<Tools>
<ToolbarIconButton
title="Refresh Snapshot"
onClick={createSnapshot}
>
<IoCameraReverseOutline />
</ToolbarIconButton>
</Tools>
</Overlay>
{sceneSnapshot ? (
<>
<CanvasWrapper>
<Canvas
// @ts-ignore
colorManagement
onCreated={({gl}) => {
gl.setClearColor('white')
}}
shadowMap
dpr={[1, 2]}
fog={'red'}
frameloop="demand"
onPointerMissed={onPointerMissed}
>
<EditorScene
snapshotEditorSheet={snapshotEditorSheet}
paneId={paneId}
/>
</Canvas>
</CanvasWrapper>
</>
) : null}
</Wrapper>
{/* </PortalContext.Provider> */}
</>
</StyleSheetManager>
</root.div>
)
}
export default SnapshotEditor

View file

@ -0,0 +1,118 @@
import type {VFC} from 'react'
import React from 'react'
import {IoCameraOutline} from 'react-icons/all'
import studio, {ToolbarIconButton} from '@theatre/studio'
import {useVal} from '@theatre/react'
import TransformControlsModeSelect from './TransformControlsModeSelect'
import ViewportShadingSelect from './ViewportShadingSelect'
import TransformControlsSpaceSelect from './TransformControlsSpaceSelect'
import {getEditorSheetObject} from '../editorStuff'
const Toolbar: VFC = () => {
const editorObject = getEditorSheetObject()
const transformControlsMode =
useVal(editorObject?.props.transformControls.mode) ?? 'translate'
const transformControlsSpace =
useVal(editorObject?.props.transformControls.space) ?? 'world'
const viewportShading =
useVal(editorObject?.props.viewport.shading) ?? 'rendered'
if (!editorObject) return <></>
return (
<>
<ToolbarIconButton
onClick={() => {
studio.createPane('snapshot')
}}
title="Create snapshot"
>
<IoCameraOutline />
</ToolbarIconButton>
<TransformControlsModeSelect
value={transformControlsMode}
onChange={(value) =>
studio.transaction(({set}) =>
set(editorObject!.props.transformControls.mode, value),
)
}
/>
<TransformControlsSpaceSelect
value={transformControlsSpace}
onChange={(space) => {
studio.transaction(({set}) => {
set(editorObject.props.transformControls.space, space)
})
}}
/>
<ViewportShadingSelect
value={viewportShading}
onChange={(shading) => {
studio.transaction(({set}) => {
set(editorObject.props.viewport.shading, shading)
})
}}
/>
{/* <ToolbarIconButton
label="Focus on selected"
icon={<RiFocus3Line />}
onClick={() => {
const orbitControls =
useEditorStore.getState().orbitControlsRef?.current
const selected = getSelected()
let focusObject
if (selected) {
focusObject =
useEditorStore.getState().editablesSnapshot![selected].proxyObject
}
if (orbitControls && focusObject) {
focusObject.getWorldPosition(
// @ts-ignore TODO
orbitControls.target as Vector3,
)
}
}}
/> */}
{/* <ToolbarIconButton
label="Align object to view"
icon={<GiPocketBow />}
onClick={() => {
const camera = (
useEditorStore.getState().orbitControlsRef?.current as $FixMe
)?.object
const selected = getSelected()
let proxyObject
if (selected) {
proxyObject =
useEditorStore.getState().editablesSnapshot![selected].proxyObject
if (proxyObject && camera) {
const direction = new Vector3()
const position = camera.position.clone()
camera.getWorldDirection(direction)
proxyObject.position.set(0, 0, 0)
proxyObject.lookAt(direction)
proxyObject.parent!.worldToLocal(position)
proxyObject.position.copy(position)
proxyObject.updateMatrix()
}
}
}}
/> */}
</>
)
}
export default Toolbar

View file

@ -0,0 +1,41 @@
import {ToolbarSwitchSelect} from '@theatre/studio'
import type {VFC} from 'react'
import React from 'react'
import {GiClockwiseRotation, GiMove, GiResize} from 'react-icons/all'
import type {TransformControlsMode} from '../../store'
export interface TransformControlsModeSelectProps {
value: TransformControlsMode
onChange: (value: string) => void
}
const TransformControlsModeSelect: VFC<TransformControlsModeSelectProps> = ({
value,
onChange,
}) => {
return (
<ToolbarSwitchSelect
value={value}
onChange={onChange}
options={[
{
value: 'translate',
label: 'Tool: Translate',
icon: <GiMove />,
},
{
value: 'rotate',
label: 'Tool: Rotate',
icon: <GiClockwiseRotation />,
},
{
value: 'scale',
label: 'Tool: Scale',
icon: <GiResize />,
},
]}
/>
)
}
export default TransformControlsModeSelect

View file

@ -0,0 +1,34 @@
import type {VFC} from 'react'
import React from 'react'
import type {TransformControlsSpace} from '../../store'
import {BiCube, BiGlobe} from 'react-icons/all'
import {ToolbarSwitchSelect} from '@theatre/studio'
export interface TransformControlsSpaceSelectProps {
value: TransformControlsSpace
onChange: (value: TransformControlsSpace) => void
}
const TransformControlsSpaceSelect: VFC<TransformControlsSpaceSelectProps> = ({
value,
onChange,
}) => (
<ToolbarSwitchSelect
value={value}
onChange={onChange}
options={[
{
value: 'world',
label: 'Space: World',
icon: <BiGlobe />,
},
{
value: 'local',
label: 'Space: Local',
icon: <BiCube />,
},
]}
/>
)
export default TransformControlsSpaceSelect

View file

@ -0,0 +1,44 @@
import type {VFC} from 'react'
import React from 'react'
import type {ViewportShading} from '../../store'
import {FaCube, GiCube, GiIceCube, BiCube} from 'react-icons/all'
import {ToolbarSwitchSelect} from '@theatre/studio'
export interface ViewportShadingSelectProps {
value: ViewportShading
onChange: (value: ViewportShading) => void
}
const ViewportShadingSelect: VFC<ViewportShadingSelectProps> = ({
value,
onChange,
}) => (
<ToolbarSwitchSelect
value={value}
onChange={onChange}
options={[
{
value: 'wireframe',
label: 'Display: Wireframe',
icon: <BiCube />,
},
{
value: 'flat',
label: 'Display: Flat',
icon: <GiCube />,
},
{
value: 'solid',
label: 'Display: Solid',
icon: <FaCube />,
},
{
value: 'rendered',
label: 'Display: Rendered',
icon: <GiIceCube />,
},
]}
/>
)
export default ViewportShadingSelect

View file

@ -0,0 +1,97 @@
import type {Object3D, Event} from 'three'
import React, {forwardRef, useLayoutEffect, useEffect, useMemo} from 'react'
import type {ReactThreeFiber, Overwrite} from '@react-three/fiber'
import {useThree} from '@react-three/fiber'
import {TransformControls as TransformControlsImpl} from 'three/examples/jsm/controls/TransformControls'
import type {OrbitControls} from 'three-stdlib'
type R3fTransformControls = Overwrite<
ReactThreeFiber.Object3DNode<
TransformControlsImpl,
typeof TransformControlsImpl
>,
{target?: ReactThreeFiber.Vector3}
>
export interface TransformControlsProps extends R3fTransformControls {
object: Object3D
orbitControlsRef?: React.MutableRefObject<OrbitControls | null>
onObjectChange?: (event: Event) => void
onDraggingChange?: (event: Event) => void
}
const TransformControls = forwardRef(
(
{
children,
object,
orbitControlsRef,
onObjectChange,
onDraggingChange,
...props
}: TransformControlsProps,
ref,
) => {
const {camera, gl, invalidate} = useThree()
const controls = useMemo(
() => new TransformControlsImpl(camera, gl.domElement),
[camera, gl.domElement],
)
useLayoutEffect(() => {
controls.attach(object)
return () => void controls.detach()
}, [object, controls])
useEffect(() => {
controls?.addEventListener?.('change', invalidate)
return () => controls?.removeEventListener?.('change', invalidate)
}, [controls, invalidate])
useEffect(() => {
const callback = (event: Event) => {
if (orbitControlsRef && orbitControlsRef.current) {
// @ts-ignore TODO
orbitControlsRef.current.enabled = !event.value
}
}
if (controls) {
controls.addEventListener!('dragging-changed', callback)
}
return () => {
controls.removeEventListener!('dragging-changed', callback)
}
}, [controls, orbitControlsRef])
useEffect(() => {
if (onObjectChange) {
controls.addEventListener('objectChange', onObjectChange)
}
return () => {
if (onObjectChange) {
controls.removeEventListener('objectChange', onObjectChange)
}
}
}, [onObjectChange, controls])
useEffect(() => {
if (onDraggingChange) {
controls.addEventListener('dragging-changed', onDraggingChange)
}
return () => {
if (onDraggingChange) {
controls.removeEventListener('dragging-changed', onDraggingChange)
}
}
}, [controls, onDraggingChange])
return <primitive dispose={null} object={controls} ref={ref} {...props} />
},
)
export default TransformControls

View file

@ -0,0 +1,205 @@
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
import type {
DirectionalLight,
Group,
Mesh,
OrthographicCamera,
PerspectiveCamera,
PointLight,
SpotLight,
} from 'three'
import {Vector3} from 'three'
import type {EditableType} from '../store'
import {allRegisteredObjects} from '../store'
import {baseSheetObjectType} from '../store'
import {useEditorStore} from '../store'
import mergeRefs from 'react-merge-refs'
import type {$FixMe} from '@theatre/shared/utils/types'
import type {ISheetObject} from '@theatre/core'
import useInvalidate from './useInvalidate'
import {useCurrentSheet} from '../SheetProvider'
interface Elements {
group: Group
mesh: Mesh
spotLight: SpotLight
directionalLight: DirectionalLight
perspectiveCamera: PerspectiveCamera
orthographicCamera: OrthographicCamera
pointLight: PointLight
}
const editable = <
T extends ComponentType<any> | EditableType | 'primitive',
U extends T extends EditableType ? T : EditableType,
>(
Component: T,
type: T extends 'primitive' ? null : U,
) => {
type Props = Omit<ComponentProps<T>, 'visible'> & {
uniqueName: string
visible?: boolean | 'editor'
} & (T extends 'primitive'
? {
editableType: U
}
: {}) &
RefAttributes<Elements[U]>
return forwardRef(
({uniqueName, visible, editableType, ...props}: Props, ref) => {
const objectRef = useRef<Elements[U]>()
const sheet = useCurrentSheet()
const [sheetObject, setSheetObject] = useState<
undefined | ISheetObject<$FixMe>
>(undefined)
const invalidate = useInvalidate()
useLayoutEffect(() => {
if (!sheet) return
const sheetObject = sheet.object(uniqueName, baseSheetObjectType)
allRegisteredObjects.add(sheetObject)
setSheetObject(sheetObject)
useEditorStore
.getState()
.setSheetObject(uniqueName, sheetObject as $FixMe)
}, [sheet, uniqueName])
const transformDeps: string[] = []
;['x', 'y', 'z'].forEach((axis) => {
transformDeps.push(
props[`position-${axis}` as any],
props[`rotation-${axis}` as any],
props[`scale-${axis}` as any],
)
})
// store initial values of props
useLayoutEffect(() => {
if (!sheetObject) return
// calculate initial properties before adding the editable
const position: Vector3 = props.position
? Array.isArray(props.position)
? new Vector3(...(props.position as any))
: props.position
: new Vector3()
const rotation: Vector3 = props.rotation
? Array.isArray(props.rotation)
? new Vector3(...(props.rotation as any))
: props.rotation
: new Vector3()
const scale: Vector3 = props.scale
? Array.isArray(props.scale)
? new Vector3(...(props.scale as any))
: props.scale
: new Vector3(1, 1, 1)
;['x', 'y', 'z'].forEach((axis, index) => {
if (props[`position-${axis}` as any])
position.setComponent(index, props[`position-${axis}` as any])
if (props[`rotation-${axis}` as any])
rotation.setComponent(index, props[`rotation-${axis}` as any])
if (props[`scale-${axis}` as any])
scale.setComponent(index, props[`scale-${axis}` as any])
})
const initial = {
position: {
x: position.x,
y: position.y,
z: position.z,
},
rotation: {
x: rotation.x,
y: rotation.y,
z: rotation.z,
},
scale: {
x: scale.x,
y: scale.y,
z: scale.z,
},
}
sheetObject!.initialValue = initial
}, [
uniqueName,
sheetObject,
props.position,
props.rotation,
props.scale,
...transformDeps,
])
// subscribe to prop changes from theatre
useLayoutEffect(() => {
if (!sheetObject) return
const object = objectRef.current!
const setFromTheatre = (newValues: any) => {
object.position.set(
newValues.position.x,
newValues.position.y,
newValues.position.z,
)
object.rotation.set(
newValues.rotation.x,
newValues.rotation.y,
newValues.rotation.z,
)
object.scale.set(
newValues.scale.x,
newValues.scale.y,
newValues.scale.z,
)
invalidate()
}
setFromTheatre(sheetObject.value)
const untap = sheetObject.onValuesChange(setFromTheatre)
return () => {
untap()
}
}, [sheetObject])
return (
// @ts-ignore
<Component
ref={mergeRefs([objectRef, ref])}
{...props}
visible={visible !== 'editor' && visible}
userData={{
__editable: true,
__editableName: uniqueName,
__editableType: type ?? editableType,
__visibleOnlyInEditor: visible === 'editor',
}}
/>
)
},
)
}
const createEditable = <T extends EditableType>(type: T) =>
// @ts-ignore
editable(type, type)
editable.primitive = editable('primitive', null)
editable.group = createEditable('group')
editable.mesh = createEditable('mesh')
editable.spotLight = createEditable('spotLight')
editable.directionalLight = createEditable('directionalLight')
editable.pointLight = createEditable('pointLight')
editable.perspectiveCamera = createEditable('perspectiveCamera')
editable.orthographicCamera = createEditable('orthographicCamera')
export default editable

View file

@ -0,0 +1,67 @@
import type {ISheet, ISheetObject} from '@theatre/core'
import {types} from '@theatre/core'
import studio from '@theatre/studio'
let sheet: ISheet | undefined = undefined
let sheetObject: ISheetObject<typeof editorSheetObjectConfig> | undefined =
undefined
const editorSheetObjectConfig = types.compound({
viewport: types.compound(
{
showAxes: types.boolean(true, {label: 'Axes'}),
showGrid: types.boolean(true, {label: 'Grid'}),
showOverlayIcons: types.boolean(false, {label: 'Overlay Icons'}),
shading: types.stringLiteral(
'rendered',
{
flat: 'Flat',
rendered: 'Rendered',
solid: 'Solid',
wireframe: 'Wireframe',
},
{as: 'menu', label: 'Shading'},
),
},
{label: 'Viewport Config'},
),
transformControls: types.compound(
{
mode: types.stringLiteral(
'translate',
{
translate: 'Translate',
rotate: 'Rotate',
scale: 'Scale',
},
{as: 'switch', label: 'Mode'},
),
space: types.stringLiteral(
'world',
{
local: 'Local',
world: 'World',
},
{as: 'switch', label: 'Space'},
),
},
{label: 'Transform Controls'},
),
})
export function getEditorSheet(): ISheet {
if (!sheet) {
sheet = studio.getStudioProject().sheet('R3F UI')
}
return sheet
}
export function getEditorSheetObject(): ISheetObject<
typeof editorSheetObjectConfig
> | null {
if (!sheetObject) {
sheetObject =
getEditorSheet().object('Editor', editorSheetObjectConfig) || null
}
return sheetObject
}

View file

@ -0,0 +1,5 @@
import {useThree} from '@react-three/fiber'
export default function useInvalidate() {
return useThree(({invalidate}) => invalidate)
}

View file

@ -0,0 +1,37 @@
import type {MutableRefObject} from 'react'
import {useMemo, useState} from 'react'
/**
* Combines useRef() and useState().
*
* @example
* ```typescript
* const [ref, val] = useRefAndState<HTMLDivElement | null>(null)
*
* useEffect(() => {
* val.addEventListener(...)
* }, [val])
*
* return <div ref={ref}></div>
* ```
*/
export default function useRefAndState<T>(
initialValue: T,
): [ref: MutableRefObject<T>, state: T] {
const ref = useMemo(() => {
let current = initialValue
return {
get current() {
return current
},
set current(v: T) {
current = v
setState(v)
},
}
}, [])
const [state, setState] = useState<T>(() => initialValue)
return [ref, state]
}

View file

@ -0,0 +1,11 @@
import {useCallback} from 'react'
import {useEditorStore} from '../store'
/**
* Returns a function that can be called to refresh the snapshot in the snapshot editor.
*/
export default function useRefreshSnapshot() {
return useCallback(() => {
useEditorStore.getState().createSnapshot()
}, [])
}

View file

@ -0,0 +1,41 @@
import {useLayoutEffect, useRef, useState} from 'react'
import {allRegisteredObjects} from '../store'
import studio from '@theatre/studio'
import type {ISheetObject} from '@theatre/core'
export function useSelected(): undefined | string {
const [state, set] = useState<string | undefined>(undefined)
const stateRef = useRef(state)
stateRef.current = state
useLayoutEffect(() => {
const setFromStudio = (selection: typeof studio.selection) => {
const item = selection.find(
(s): s is ISheetObject =>
s.type === 'Theatre_SheetObject_PublicAPI' &&
allRegisteredObjects.has(s),
)
if (!item) {
set(undefined)
} else {
set(item.address.objectKey)
}
}
setFromStudio(studio.selection)
return studio.onSelectionChange(setFromStudio)
}, [])
return state
}
export function getSelected(): undefined | string {
const item = studio.selection.find(
(s): s is ISheetObject =>
s.type === 'Theatre_SheetObject_PublicAPI' && allRegisteredObjects.has(s),
)
if (!item) {
return undefined
} else {
return item.address.objectKey
}
}

View file

@ -0,0 +1,168 @@
import {OrbitControls, PerspectiveCamera} from '@react-three/drei'
import type {OrbitControls as OrbitControlsImpl} from 'three-stdlib'
import type {MutableRefObject} from 'react'
import {useLayoutEffect, useRef} from 'react'
import React from 'react'
import useRefAndState from './useRefAndState'
import studio from '@theatre/studio'
import type {PerspectiveCamera as PerspectiveCameraImpl} from 'three'
import type {ISheet} from '@theatre/core'
import {types} from '@theatre/core'
import type {ISheetObject} from '@theatre/core'
import {useThree} from '@react-three/fiber'
const camConf = types.compound({
transform: types.compound({
position: types.compound({
x: types.number(10),
y: types.number(10),
z: types.number(0),
}),
target: types.compound({
x: types.number(0),
y: types.number(0),
z: types.number(0),
}),
}),
lens: types.compound({
zoom: types.number(1, {range: [0.0001, 10]}),
fov: types.number(50, {range: [1, 1000]}),
near: types.number(0.1, {range: [0, Infinity]}),
far: types.number(2000, {range: [0, Infinity]}),
focus: types.number(10, {range: [0, Infinity]}),
filmGauge: types.number(35, {range: [0, Infinity]}),
filmOffset: types.number(0, {range: [0, Infinity]}),
}),
})
export default function useSnapshotEditorCamera(
snapshotEditorSheet: ISheet,
paneId: string,
): [
node: React.ReactNode,
orbitControlsRef: MutableRefObject<OrbitControlsImpl | null>,
] {
// OrbitControls and Cam might change later on, so we use useRefAndState()
// instead of useRef() to catch those changes.
const [orbitControlsRef, orbitControls] =
useRefAndState<OrbitControlsImpl | null>(null)
const [camRef, cam] = useRefAndState<PerspectiveCameraImpl | undefined>(
undefined,
)
const objRef = useRef<ISheetObject<typeof camConf> | null>(null)
useLayoutEffect(() => {
if (!objRef.current) {
objRef.current = snapshotEditorSheet.object(
`Editor Camera ${paneId}`,
camConf,
)
}
}, [paneId])
usePassValuesFromTheatreToCamera(cam, orbitControls, objRef)
usePassValuesFromOrbitControlsToTheatre(cam, orbitControls, objRef)
const node = (
<>
<PerspectiveCamera makeDefault ref={camRef} position={[0, 102, 0]} />
<OrbitControls
makeDefault
ref={orbitControlsRef}
camera={cam}
enableDamping={false}
/>
</>
)
return [node, orbitControlsRef]
}
function usePassValuesFromOrbitControlsToTheatre(
cam: PerspectiveCameraImpl | undefined,
orbitControls: OrbitControlsImpl | null,
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
) {
useLayoutEffect(() => {
if (!cam || orbitControls == null) return
let currentScrub: undefined | ReturnType<typeof studio['debouncedScrub']>
let started = false
const onStart = () => {
started = true
if (!currentScrub) {
currentScrub = studio.debouncedScrub(600)
}
}
const onEnd = () => {
started = false
}
const onChange = () => {
if (!started) return
const p = cam!.position
const position = {x: p.x, y: p.y, z: p.z}
const t = orbitControls!.target
const target = {x: t.x, y: t.y, z: t.z}
const transform = {
position,
target,
}
currentScrub!.capture(({set}) => {
set(objRef.current!.props.transform, transform)
})
}
orbitControls.addEventListener('start', onStart)
orbitControls.addEventListener('end', onEnd)
orbitControls.addEventListener('change', onChange)
return () => {
orbitControls.removeEventListener('start', onStart)
orbitControls.removeEventListener('end', onEnd)
orbitControls.removeEventListener('change', onChange)
}
}, [cam, orbitControls])
}
function usePassValuesFromTheatreToCamera(
cam: PerspectiveCameraImpl | undefined,
orbitControls: OrbitControlsImpl | null,
objRef: MutableRefObject<ISheetObject<typeof camConf> | null>,
) {
const invalidate = useThree(({invalidate}) => invalidate)
useLayoutEffect(() => {
if (!cam || orbitControls === null) return
const obj = objRef.current!
const setFromTheatre = (props: typeof camConf['valueType']): void => {
const {position, target} = props.transform
cam.zoom = props.lens.zoom
cam.fov = props.lens.fov
cam.near = props.lens.near
cam.far = props.lens.far
cam.focus = props.lens.focus
cam.filmGauge = props.lens.filmGauge
cam.filmOffset = props.lens.filmOffset
cam.position.set(position.x, position.y, position.z)
cam.updateProjectionMatrix()
orbitControls.target.set(target.x, target.y, target.z)
orbitControls.update()
invalidate()
}
const unsub = obj.onValuesChange(setFromTheatre)
setFromTheatre(obj.value)
return unsub
}, [cam, orbitControls, objRef, invalidate])
}

View file

@ -0,0 +1,18 @@
import SnapshotEditor from './components/SnapshotEditor'
import type {IExtension} from '@theatre/studio'
import Toolbar from './components/Toolbar/Toolbar'
const r3fExtension: IExtension = {
id: '@theatre/r3f',
globalToolbar: {
component: Toolbar,
},
panes: [
{
class: 'snapshot',
component: SnapshotEditor,
},
],
}
export default r3fExtension

3
packages/r3f/src/globals.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
declare module '*.txt' {
export default string
}

View file

@ -0,0 +1,8 @@
export {default as extension} from './extension'
export {default as EditorHelper} from './components/EditorHelper'
export type {EditorHelperProps} from './components/EditorHelper'
export {default as editable} from './components/editable'
export type {EditableState, BindFunction} from './store'
export {default as SheetProvider, useCurrentSheet} from './SheetProvider'
export {default as useRefreshSnapshot} from './components/useRefreshSnapshot'
export {default as RefreshSnapshot} from './components/RefreshSnapshot'

277
packages/r3f/src/store.ts Normal file
View file

@ -0,0 +1,277 @@
import type {StateCreator} from 'zustand'
import create from 'zustand'
import type {Object3D, Scene, WebGLRenderer} from 'three'
import {Group} from 'three'
import type {ISheetObject} from '@theatre/core'
import {types} from '@theatre/core'
export type EditableType =
| 'group'
| 'mesh'
| 'spotLight'
| 'directionalLight'
| 'pointLight'
| 'perspectiveCamera'
| 'orthographicCamera'
export type TransformControlsMode = 'translate' | 'rotate' | 'scale'
export type TransformControlsSpace = 'world' | 'local'
export type ViewportShading = 'wireframe' | 'flat' | 'solid' | 'rendered'
const positionComp = types.number(1, {nudgeMultiplier: 0.1})
const rotationComp = types.number(1, {nudgeMultiplier: 0.02})
const scaleComp = types.number(1, {nudgeMultiplier: 0.1})
export const baseSheetObjectType = types.compound({
position: types.compound({
x: positionComp,
y: positionComp,
z: positionComp,
}),
rotation: types.compound({
x: rotationComp,
y: rotationComp,
z: rotationComp,
}),
scale: types.compound({
x: scaleComp,
y: scaleComp,
z: scaleComp,
}),
})
export type BaseSheetObjectType = ISheetObject<typeof baseSheetObjectType>
export const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
export interface AbstractEditable<T extends EditableType> {
type: T
role: 'active' | 'removed'
sheetObject?: ISheetObject<any>
}
// all these identical types are to prepare for a future in which different object types have different properties
export interface EditableGroup extends AbstractEditable<'group'> {
sheetObject?: BaseSheetObjectType
}
export interface EditableMesh extends AbstractEditable<'mesh'> {
sheetObject?: BaseSheetObjectType
}
export interface EditableSpotLight extends AbstractEditable<'spotLight'> {
sheetObject?: BaseSheetObjectType
}
export interface EditableDirectionalLight
extends AbstractEditable<'directionalLight'> {
sheetObject?: BaseSheetObjectType
}
export interface EditablePointLight extends AbstractEditable<'pointLight'> {
sheetObject?: BaseSheetObjectType
}
export interface EditablePerspectiveCamera
extends AbstractEditable<'perspectiveCamera'> {
sheetObject?: BaseSheetObjectType
}
export interface EditableOrthographicCamera
extends AbstractEditable<'orthographicCamera'> {
sheetObject?: BaseSheetObjectType
}
export type Editable =
| EditableGroup
| EditableMesh
| EditableSpotLight
| EditableDirectionalLight
| EditablePointLight
| EditablePerspectiveCamera
| EditableOrthographicCamera
export type EditableSnapshot<T extends Editable = Editable> = {
proxyObject?: Object3D | null
} & T
export interface AbstractSerializedEditable<T extends EditableType> {
type: T
}
export interface SerializedEditableGroup
extends AbstractSerializedEditable<'group'> {}
export interface SerializedEditableMesh
extends AbstractSerializedEditable<'mesh'> {}
export interface SerializedEditableSpotLight
extends AbstractSerializedEditable<'spotLight'> {}
export interface SerializedEditableDirectionalLight
extends AbstractSerializedEditable<'directionalLight'> {}
export interface SerializedEditablePointLight
extends AbstractSerializedEditable<'pointLight'> {}
export interface SerializedEditablePerspectiveCamera
extends AbstractSerializedEditable<'perspectiveCamera'> {}
export interface SerializedEditableOrthographicCamera
extends AbstractSerializedEditable<'orthographicCamera'> {}
export type SerializedEditable =
| SerializedEditableGroup
| SerializedEditableMesh
| SerializedEditableSpotLight
| SerializedEditableDirectionalLight
| SerializedEditablePointLight
| SerializedEditablePerspectiveCamera
| SerializedEditableOrthographicCamera
export interface EditableState {
editables: Record<string, SerializedEditable>
}
export type EditorStore = {
sheetObjects: {[uniqueName in string]?: BaseSheetObjectType}
scene: Scene | null
gl: WebGLRenderer | null
allowImplicitInstancing: boolean
helpersRoot: Group
editables: Record<string, Editable>
// this will come in handy when we start supporting multiple canvases
canvasName: string
sceneSnapshot: Scene | null
editablesSnapshot: Record<string, EditableSnapshot> | null
init: (
scene: Scene,
gl: WebGLRenderer,
allowImplicitInstancing: boolean,
) => void
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
removeEditable: (uniqueName: string) => void
createSnapshot: () => void
setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void
setSnapshotProxyObject: (
proxyObject: Object3D | null,
uniqueName: string,
) => void
}
const config: StateCreator<EditorStore> = (set, get) => {
return {
sheet: null,
editorObject: null,
sheetObjects: {},
scene: null,
gl: null,
allowImplicitInstancing: false,
helpersRoot: new Group(),
editables: {},
canvasName: 'default',
sceneSnapshot: null,
editablesSnapshot: null,
initialEditorCamera: {},
init: (scene, gl, allowImplicitInstancing) => {
set({
scene,
gl,
allowImplicitInstancing,
})
},
addEditable: (type, uniqueName) =>
set((state) => {
if (state.editables[uniqueName]) {
if (
state.editables[uniqueName].type !== type &&
process.env.NODE_ENV === 'development'
) {
console.error(`Warning: There is a mismatch between the serialized type of ${uniqueName} and the one set when adding it to the scene.
Serialized: ${state.editables[uniqueName].type}.
Current: ${type}.
This might have happened either because you changed the type of an object, in which case a re-export will solve the issue, or because you re-used the uniqueName for an object of a different type, which is an error.`)
}
if (
state.editables[uniqueName].role === 'active' &&
!state.allowImplicitInstancing
) {
throw Error(
`Scene already has an editable object named ${uniqueName}.
If this is intentional, please set the allowImplicitInstancing prop of EditableManager to true.`,
)
} else {
}
}
return {
editables: {
...state.editables,
[uniqueName]: {
type: type as EditableType,
role: 'active',
},
},
}
}),
removeEditable: (name) =>
set((state) => {
const {[name]: removed, ...rest} = state.editables
return {
editables: {
...rest,
[name]: {...removed, role: 'removed'},
},
}
}),
setSheetObject: (uniqueName, sheetObject) => {
set((state) => ({
sheetObjects: {
...state.sheetObjects,
[uniqueName]: sheetObject,
},
}))
},
createSnapshot: () => {
set((state) => ({
sceneSnapshot: state.scene?.clone() ?? null,
editablesSnapshot: state.editables,
}))
},
setSnapshotProxyObject: (proxyObject, uniqueName) => {
set((state) => ({
editablesSnapshot: {
...state.editablesSnapshot,
[uniqueName]: {
...state.editablesSnapshot![uniqueName],
proxyObject,
},
},
}))
},
}
}
export const useEditorStore = create<EditorStore>(config)
export type BindFunction = (options: {
allowImplicitInstancing?: boolean
gl: WebGLRenderer
scene: Scene
}) => void
export const bindToCanvas: BindFunction = ({
allowImplicitInstancing = false,
gl,
scene,
}) => {
const init = useEditorStore.getState().init
init(scene, gl, allowImplicitInstancing)
}

View file

@ -0,0 +1,2 @@
export type $FixMe = any
export type $IntentionalAny = any