Make @theatre/r3f
play well with different vs of react,three,r3f #177
This commit is contained in:
parent
e8c440f357
commit
9b4aa4b0e0
50 changed files with 796 additions and 145 deletions
11
packages/r3f/src/main/.eslintrc.js
Normal file
11
packages/r3f/src/main/.eslintrc.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: `ImportDeclaration[importKind!='type'][source.value=/\\u002Fextension\\u002F/]`,
|
||||
message: `The main bundle should not be able to import the internals of extension.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
28
packages/r3f/src/main/RefreshSnapshot.tsx
Normal file
28
packages/r3f/src/main/RefreshSnapshot.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import {refreshSnapshot} from './utils'
|
||||
|
||||
/**
|
||||
* Putting this element in a suspense tree makes sure the snapshot editor
|
||||
* gets refreshed once the tree renders.
|
||||
*
|
||||
* Alternatively you can use {@link refreshSnapshot}
|
||||
*
|
||||
* @example
|
||||
* Usage
|
||||
* ```jsx
|
||||
* <Suspense fallback={null}>
|
||||
* <RefreshSnapshot />
|
||||
* <Model url={sceneGLB} />
|
||||
* </Suspense>
|
||||
* ```
|
||||
*/
|
||||
const RefreshSnapshot: React.FC<{}> = () => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
refreshSnapshot()
|
||||
})
|
||||
}, [])
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default RefreshSnapshot
|
45
packages/r3f/src/main/SheetProvider.tsx
Normal file
45
packages/r3f/src/main/SheetProvider.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
} from 'react'
|
||||
import {useThree} from '@react-three/fiber'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import {bindToCanvas} from './store'
|
||||
|
||||
const ctx = createContext<{sheet: ISheet}>(undefined!)
|
||||
|
||||
const useWrapperContext = (): {sheet: ISheet} => {
|
||||
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<{
|
||||
sheet: ISheet
|
||||
}> = ({sheet, children}) => {
|
||||
const {scene, gl} = useThree((s) => ({scene: s.scene, gl: s.gl}))
|
||||
|
||||
useEffect(() => {
|
||||
if (!sheet || sheet.type !== 'Theatre_Sheet_PublicAPI') {
|
||||
throw new Error(`sheet in <Wrapper sheet={sheet}> has an invalid value`)
|
||||
}
|
||||
}, [sheet])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
bindToCanvas({gl, scene})
|
||||
}, [scene, gl])
|
||||
|
||||
return <ctx.Provider value={{sheet}}>{children}</ctx.Provider>
|
||||
}
|
||||
|
||||
export default SheetProvider
|
119
packages/r3f/src/main/defaultEditableFactoryConfig.ts
Normal file
119
packages/r3f/src/main/defaultEditableFactoryConfig.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import type {EditableFactoryConfig} from './editableFactoryConfigUtils'
|
||||
import {
|
||||
createColorPropConfig,
|
||||
createNumberPropConfig,
|
||||
createVector,
|
||||
createVectorPropConfig,
|
||||
extendObjectProps,
|
||||
} from './editableFactoryConfigUtils'
|
||||
import type {
|
||||
DirectionalLight,
|
||||
Object3D,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
PointLight,
|
||||
SpotLight,
|
||||
} from 'three'
|
||||
import {
|
||||
BoxHelper,
|
||||
CameraHelper,
|
||||
DirectionalLightHelper,
|
||||
PointLightHelper,
|
||||
SpotLightHelper,
|
||||
} from 'three'
|
||||
|
||||
const baseObjectConfig = {
|
||||
props: {
|
||||
position: createVectorPropConfig('position'),
|
||||
rotation: createVectorPropConfig('rotation'),
|
||||
scale: createVectorPropConfig('scale', createVector([1, 1, 1])),
|
||||
},
|
||||
useTransformControls: true,
|
||||
icon: 'cube' as const,
|
||||
createHelper: (object: Object3D) => new BoxHelper(object, selectionColor),
|
||||
}
|
||||
|
||||
const baseLightConfig = {
|
||||
...extendObjectProps(baseObjectConfig, {
|
||||
intensity: createNumberPropConfig('intensity', 1),
|
||||
distance: createNumberPropConfig('distance'),
|
||||
decay: createNumberPropConfig('decay'),
|
||||
}),
|
||||
dimensionless: true,
|
||||
}
|
||||
|
||||
const baseCameraConfig = {
|
||||
...extendObjectProps(baseObjectConfig, {
|
||||
near: createNumberPropConfig('near', 0.1, {nudgeMultiplier: 0.1}),
|
||||
far: createNumberPropConfig('far', 2000, {nudgeMultiplier: 0.1}),
|
||||
}),
|
||||
updateObject: (camera: PerspectiveCamera | OrthographicCamera) => {
|
||||
camera.updateProjectionMatrix()
|
||||
},
|
||||
icon: 'camera' as const,
|
||||
dimensionless: true,
|
||||
createHelper: (camera: PerspectiveCamera) => new CameraHelper(camera),
|
||||
}
|
||||
|
||||
const selectionColor = '#40AAA4'
|
||||
|
||||
const defaultEditableFactoryConfig = {
|
||||
group: {
|
||||
...baseObjectConfig,
|
||||
icon: 'collection' as const,
|
||||
createHelper: (object: Object3D) => new BoxHelper(object, selectionColor),
|
||||
},
|
||||
mesh: {
|
||||
...baseObjectConfig,
|
||||
icon: 'cube' as const,
|
||||
createHelper: (object: Object3D) => new BoxHelper(object, selectionColor),
|
||||
},
|
||||
spotLight: {
|
||||
...extendObjectProps(baseLightConfig, {
|
||||
angle: createNumberPropConfig('angle', 0, {nudgeMultiplier: 0.001}),
|
||||
penumbra: createNumberPropConfig('penumbra', 0, {nudgeMultiplier: 0.001}),
|
||||
}),
|
||||
icon: 'spotLight' as const,
|
||||
createHelper: (light: SpotLight) =>
|
||||
new SpotLightHelper(light, selectionColor),
|
||||
},
|
||||
directionalLight: {
|
||||
...extendObjectProps(baseObjectConfig, {
|
||||
intensity: createNumberPropConfig('intensity', 1),
|
||||
}),
|
||||
icon: 'sun' as const,
|
||||
dimensionless: true,
|
||||
createHelper: (light: DirectionalLight) =>
|
||||
new DirectionalLightHelper(light, 1, selectionColor),
|
||||
},
|
||||
pointLight: {
|
||||
...baseLightConfig,
|
||||
icon: 'lightBulb' as const,
|
||||
createHelper: (light: PointLight) =>
|
||||
new PointLightHelper(light, 1, selectionColor),
|
||||
},
|
||||
perspectiveCamera: extendObjectProps(baseCameraConfig, {
|
||||
fov: createNumberPropConfig('fov', 50, {nudgeMultiplier: 0.1}),
|
||||
zoom: createNumberPropConfig('zoom', 1),
|
||||
}),
|
||||
orthographicCamera: baseCameraConfig,
|
||||
points: baseObjectConfig,
|
||||
line: baseObjectConfig,
|
||||
lineLoop: baseObjectConfig,
|
||||
lineSegments: baseObjectConfig,
|
||||
fog: {
|
||||
props: {
|
||||
color: createColorPropConfig('color'),
|
||||
near: createNumberPropConfig('near', 1, {nudgeMultiplier: 0.1}),
|
||||
far: createNumberPropConfig('far', 1000, {nudgeMultiplier: 0.1}),
|
||||
},
|
||||
useTransformControls: false,
|
||||
icon: 'cloud' as const,
|
||||
},
|
||||
}
|
||||
|
||||
// Assert that the config is indeed of EditableFactoryConfig without actually
|
||||
// forcing it to that type so that we can pass the real type to the editable factory
|
||||
defaultEditableFactoryConfig as EditableFactoryConfig
|
||||
|
||||
export default defaultEditableFactoryConfig
|
180
packages/r3f/src/main/editable.tsx
Normal file
180
packages/r3f/src/main/editable.tsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
|
||||
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
|
||||
import {allRegisteredObjects, editorStore} from './store'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import useInvalidate from './useInvalidate'
|
||||
import {useCurrentSheet} from './SheetProvider'
|
||||
import defaultEditableFactoryConfig from './defaultEditableFactoryConfig'
|
||||
import type {EditableFactoryConfig} from './editableFactoryConfigUtils'
|
||||
import {makeStoreKey} from './utils'
|
||||
import type {$FixMe} from '../types'
|
||||
|
||||
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||
config: EditableFactoryConfig,
|
||||
) => {
|
||||
const editable = <
|
||||
T extends ComponentType<any> | Keys | 'primitive',
|
||||
U extends T extends Keys ? T : Keys,
|
||||
>(
|
||||
Component: T,
|
||||
type: T extends 'primitive' ? null : U,
|
||||
) => {
|
||||
type Props = Omit<ComponentProps<T>, 'visible'> & {
|
||||
uniqueName: string
|
||||
visible?: boolean | 'editor'
|
||||
additionalProps?: $FixMe
|
||||
objRef?: $FixMe
|
||||
} & (T extends 'primitive'
|
||||
? {
|
||||
editableType: U
|
||||
}
|
||||
: {}) &
|
||||
RefAttributes<JSX.IntrinsicElements[U]>
|
||||
|
||||
return forwardRef(
|
||||
(
|
||||
{
|
||||
uniqueName,
|
||||
visible,
|
||||
editableType,
|
||||
additionalProps,
|
||||
objRef,
|
||||
...props
|
||||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
const actualType = type ?? editableType
|
||||
|
||||
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||
|
||||
const sheet = useCurrentSheet()!
|
||||
|
||||
const storeKey = makeStoreKey(sheet, uniqueName)
|
||||
|
||||
const [sheetObject, setSheetObject] = useState<
|
||||
undefined | ISheetObject<$FixMe>
|
||||
>(undefined)
|
||||
|
||||
const invalidate = useInvalidate()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!sheet) return
|
||||
const sheetObject = sheet.object(
|
||||
uniqueName,
|
||||
Object.assign(
|
||||
{
|
||||
...additionalProps,
|
||||
},
|
||||
// @ts-ignore
|
||||
...Object.values(config[actualType].props).map(
|
||||
// @ts-ignore
|
||||
(value) => value.type,
|
||||
),
|
||||
),
|
||||
)
|
||||
allRegisteredObjects.add(sheetObject)
|
||||
setSheetObject(sheetObject)
|
||||
|
||||
if (objRef) objRef!.current = sheetObject
|
||||
|
||||
editorStore.getState().addEditable(storeKey, {
|
||||
type: actualType,
|
||||
sheetObject,
|
||||
visibleOnlyInEditor: visible === 'editor',
|
||||
// @ts-ignore
|
||||
objectConfig: config[actualType],
|
||||
})
|
||||
}, [sheet, storeKey])
|
||||
|
||||
// store initial values of props
|
||||
useLayoutEffect(() => {
|
||||
if (!sheetObject) return
|
||||
sheetObject!.initialValue = Object.fromEntries(
|
||||
// @ts-ignore
|
||||
Object.entries(config[actualType].props).map(
|
||||
// @ts-ignore
|
||||
([key, value]) => [key, value.parse(props)],
|
||||
),
|
||||
)
|
||||
}, [
|
||||
sheetObject,
|
||||
// @ts-ignore
|
||||
...Object.keys(config[actualType].props).map(
|
||||
// @ts-ignore
|
||||
(key) => props[key],
|
||||
),
|
||||
])
|
||||
|
||||
// subscribe to prop changes from theatre
|
||||
useLayoutEffect(() => {
|
||||
if (!sheetObject) return
|
||||
|
||||
const object = objectRef.current!
|
||||
|
||||
const setFromTheatre = (newValues: any) => {
|
||||
// @ts-ignore
|
||||
Object.entries(config[actualType].props).forEach(
|
||||
// @ts-ignore
|
||||
([key, value]) => value.apply(newValues[key], object),
|
||||
)
|
||||
// @ts-ignore
|
||||
config[actualType].updateObject?.(object)
|
||||
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,
|
||||
__storeKey: storeKey,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const extensions = {
|
||||
...Object.fromEntries(
|
||||
Object.keys(config).map((key) => [
|
||||
key,
|
||||
// @ts-ignore
|
||||
editable(key, key),
|
||||
]),
|
||||
),
|
||||
primitive: editable('primitive', null),
|
||||
} as unknown as {
|
||||
[Property in Keys]: React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<
|
||||
Omit<JSX.IntrinsicElements[Property], 'visible'> & {
|
||||
uniqueName: string
|
||||
visible?: boolean | 'editor'
|
||||
additionalProps?: $FixMe
|
||||
objRef?: $FixMe
|
||||
} & React.RefAttributes<JSX.IntrinsicElements[Property]>
|
||||
>
|
||||
>
|
||||
}
|
||||
|
||||
return Object.assign(editable, extensions)
|
||||
}
|
||||
|
||||
const editable = createEditable<keyof typeof defaultEditableFactoryConfig>(
|
||||
defaultEditableFactoryConfig,
|
||||
)
|
||||
|
||||
export default editable
|
121
packages/r3f/src/main/editableFactoryConfigUtils.ts
Normal file
121
packages/r3f/src/main/editableFactoryConfigUtils.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import type {UnknownShorthandCompoundProps} from '@theatre/core'
|
||||
import {types} from '@theatre/core'
|
||||
import type {Object3D} from 'three'
|
||||
import type {IconID} from '../extension/icons'
|
||||
import {Color} from 'three'
|
||||
|
||||
export type Helper = Object3D & {
|
||||
update?: () => void
|
||||
}
|
||||
type PropConfig<T> = {
|
||||
parse: (props: Record<string, any>) => T
|
||||
apply: (value: T, object: any) => void
|
||||
type: UnknownShorthandCompoundProps
|
||||
}
|
||||
type Props = Record<string, PropConfig<any>>
|
||||
type Meta<T> = {
|
||||
useTransformControls: boolean
|
||||
updateObject?: (object: T) => void
|
||||
icon: IconID
|
||||
dimensionless?: boolean
|
||||
createHelper?: (object: T) => Helper
|
||||
}
|
||||
export type ObjectConfig<T> = {props: Props} & Meta<T>
|
||||
export type EditableFactoryConfig = Partial<
|
||||
Record<keyof JSX.IntrinsicElements, ObjectConfig<any>>
|
||||
>
|
||||
|
||||
type Vector3 = {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export const createVector = (components?: [number, number, number]) => {
|
||||
return components
|
||||
? {x: components[0], y: components[1], z: components[2]}
|
||||
: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const createVectorPropConfig = (
|
||||
key: string,
|
||||
defaultValue = createVector(),
|
||||
{nudgeMultiplier = 0.01} = {},
|
||||
): PropConfig<Vector3> => ({
|
||||
parse: (props) => {
|
||||
const vector = props[key]
|
||||
? Array.isArray(props[key])
|
||||
? createVector(props[key] as any)
|
||||
: {
|
||||
x: props[key].x,
|
||||
y: props[key].y,
|
||||
z: props[key].z,
|
||||
}
|
||||
: defaultValue
|
||||
;(['x', 'y', 'z'] as const).forEach((axis) => {
|
||||
if (props[`${key}-${axis}` as any])
|
||||
vector[axis] = props[`${key}-${axis}` as any]
|
||||
})
|
||||
return vector
|
||||
},
|
||||
apply: (value, object) => {
|
||||
object[key].set(value.x, value.y, value.z)
|
||||
},
|
||||
type: {
|
||||
[key]: {
|
||||
x: types.number(defaultValue.x, {nudgeMultiplier}),
|
||||
y: types.number(defaultValue.y, {nudgeMultiplier}),
|
||||
z: types.number(defaultValue.z, {nudgeMultiplier}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const createNumberPropConfig = (
|
||||
key: string,
|
||||
defaultValue: number = 0,
|
||||
{nudgeMultiplier = 0.01} = {},
|
||||
): PropConfig<number> => ({
|
||||
parse: (props) => {
|
||||
return props[key] ?? defaultValue
|
||||
},
|
||||
apply: (value, object) => {
|
||||
object[key] = value
|
||||
},
|
||||
type: {
|
||||
[key]: types.number(defaultValue, {nudgeMultiplier}),
|
||||
},
|
||||
})
|
||||
|
||||
export type Rgba = {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
a: number
|
||||
}
|
||||
|
||||
export const createColorPropConfig = (
|
||||
key: string,
|
||||
defaultValue = new Color(0, 0, 0),
|
||||
): PropConfig<Rgba> => ({
|
||||
parse: (props) => {
|
||||
return {...(props[key] ?? defaultValue), a: 1}
|
||||
},
|
||||
apply: (value, object) => {
|
||||
object[key].setRGB(value.r, value.g, value.b)
|
||||
},
|
||||
type: {
|
||||
[key]: types.rgba({...defaultValue, a: 1}),
|
||||
},
|
||||
})
|
||||
|
||||
export const extendObjectProps = <T extends {props: {}}>(
|
||||
objectConfig: T,
|
||||
extension: Props,
|
||||
) => ({
|
||||
...objectConfig,
|
||||
props: {...objectConfig.props, ...extension},
|
||||
})
|
116
packages/r3f/src/main/store.ts
Normal file
116
packages/r3f/src/main/store.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import type {StateCreator} from 'zustand'
|
||||
import create from 'zustand/vanilla'
|
||||
import type {Object3D, Scene, WebGLRenderer} from 'three'
|
||||
import {Group} from 'three'
|
||||
import type {ISheetObject} from '@theatre/core'
|
||||
import type {ObjectConfig} from './editableFactoryConfigUtils'
|
||||
|
||||
export type TransformControlsMode = 'translate' | 'rotate' | 'scale'
|
||||
export type TransformControlsSpace = 'world' | 'local'
|
||||
export type ViewportShading = 'wireframe' | 'flat' | 'solid' | 'rendered'
|
||||
|
||||
export type BaseSheetObjectType = ISheetObject<any>
|
||||
|
||||
export const allRegisteredObjects = new WeakSet<BaseSheetObjectType>()
|
||||
|
||||
export interface Editable<T> {
|
||||
type: string
|
||||
sheetObject: ISheetObject<any>
|
||||
objectConfig: ObjectConfig<T>
|
||||
visibleOnlyInEditor: boolean
|
||||
}
|
||||
|
||||
export type EditableSnapshot<T extends Editable<any> = Editable<any>> = {
|
||||
proxyObject?: Object3D | null
|
||||
} & T
|
||||
|
||||
export interface SerializedEditable {
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface EditableState {
|
||||
editables: Record<string, SerializedEditable>
|
||||
}
|
||||
|
||||
export type EditorStore = {
|
||||
scene: Scene | null
|
||||
gl: WebGLRenderer | null
|
||||
helpersRoot: Group
|
||||
editables: Record<string, Editable<any>>
|
||||
// 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) => void
|
||||
|
||||
addEditable: (uniqueName: string, editable: Editable<any>) => void
|
||||
createSnapshot: () => void
|
||||
setSnapshotProxyObject: (
|
||||
proxyObject: Object3D | null,
|
||||
uniqueName: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
const config: StateCreator<EditorStore> = (set) => {
|
||||
return {
|
||||
sheet: null,
|
||||
editorObject: null,
|
||||
scene: null,
|
||||
gl: null,
|
||||
helpersRoot: new Group(),
|
||||
editables: {},
|
||||
canvasName: 'default',
|
||||
sceneSnapshot: null,
|
||||
editablesSnapshot: null,
|
||||
initialEditorCamera: {},
|
||||
|
||||
init: (scene, gl) => {
|
||||
set({
|
||||
scene,
|
||||
gl,
|
||||
})
|
||||
},
|
||||
|
||||
addEditable: (uniqueName, editable) => {
|
||||
set((state) => ({
|
||||
editables: {
|
||||
...state.editables,
|
||||
[uniqueName]: editable,
|
||||
},
|
||||
}))
|
||||
},
|
||||
|
||||
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 editorStore = create<EditorStore>(config)
|
||||
|
||||
export type BindFunction = (options: {
|
||||
allowImplicitInstancing?: boolean
|
||||
gl: WebGLRenderer
|
||||
scene: Scene
|
||||
}) => void
|
||||
|
||||
export const bindToCanvas: BindFunction = ({gl, scene}) => {
|
||||
const init = editorStore.getState().init
|
||||
init(scene, gl)
|
||||
}
|
5
packages/r3f/src/main/useInvalidate.ts
Normal file
5
packages/r3f/src/main/useInvalidate.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {useThree} from '@react-three/fiber'
|
||||
|
||||
export default function useInvalidate() {
|
||||
return useThree(({invalidate}) => invalidate)
|
||||
}
|
7
packages/r3f/src/main/utils.ts
Normal file
7
packages/r3f/src/main/utils.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {editorStore} from './store'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
|
||||
export const refreshSnapshot = editorStore.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