theatre/packages/plugin-r3f/src/store.ts
2021-06-18 13:05:06 +02:00

558 lines
15 KiB
TypeScript

import type {StateCreator} from 'zustand'
import create from 'zustand'
import type {Matrix4, Object3D, Scene, WebGLRenderer} from 'three'
import {DefaultLoadingManager, Group} from 'three'
import type {MutableRefObject} from 'react'
import type {OrbitControls} from '@react-three/drei'
import deepEqual from 'fast-deep-equal'
// @ts-ignore TODO
import type {ContainerProps} from '@react-three/fiber'
import type {ISheet, 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'
export const createBaseObjectConfig = () => {
const {compound, number} = types
return {
props: compound({
position: compound({
x: number(0),
y: number(0),
z: number(0),
}),
rotation: compound({
x: number(0),
y: number(0),
z: number(0),
}),
scale: compound({
x: number(1),
y: number(1),
z: number(1),
}),
}),
}
}
export const getBaseObjectConfig = (() => {
let base: undefined | ReturnType<typeof createBaseObjectConfig>
return (): ReturnType<typeof createBaseObjectConfig> => {
if (!base) {
base = createBaseObjectConfig()
}
return base!
}
})()
export type BaseSheetObjectType = ISheetObject<
ReturnType<typeof getBaseObjectConfig>['props']
>
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'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditableMesh
extends AbstractSerializedEditable<'mesh'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditableSpotLight
extends AbstractSerializedEditable<'spotLight'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditableDirectionalLight
extends AbstractSerializedEditable<'directionalLight'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditablePointLight
extends AbstractSerializedEditable<'pointLight'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditablePerspectiveCamera
extends AbstractSerializedEditable<'perspectiveCamera'> {
_properties: {
transform: number[]
}
}
export interface SerializedEditableOrthographicCamera
extends AbstractSerializedEditable<'orthographicCamera'> {
_properties: {
transform: number[]
}
}
export type SerializedEditable =
| SerializedEditableGroup
| SerializedEditableMesh
| SerializedEditableSpotLight
| SerializedEditableDirectionalLight
| SerializedEditablePointLight
| SerializedEditablePerspectiveCamera
| SerializedEditableOrthographicCamera
export interface EditableState {
editables: Record<string, SerializedEditable>
}
export type EditorStore = {
sheet: ISheet | null
sheetObjects: {[uniqueName in string]?: BaseSheetObjectType}
scene: Scene | null
gl: WebGLRenderer | null
allowImplicitInstancing: boolean
orbitControlsRef: MutableRefObject<typeof OrbitControls | undefined> | null
helpersRoot: Group
editables: Record<string, Editable>
// this will come in handy when we start supporting multiple canvases
canvasName: string
initialState: EditableState | null
selected: string | null
transformControlsMode: TransformControlsMode
transformControlsSpace: TransformControlsSpace
viewportShading: ViewportShading
editorOpen: boolean
sceneSnapshot: Scene | null
editablesSnapshot: Record<string, EditableSnapshot> | null
hdrPaths: string[]
selectedHdr: string | null
showOverlayIcons: boolean
useHdrAsBackground: boolean
showGrid: boolean
showAxes: boolean
referenceWindowSize: number
initialEditorCamera: ContainerProps['camera']
init: (
scene: Scene,
gl: WebGLRenderer,
allowImplicitInstancing: boolean,
editorCamera: ContainerProps['camera'],
sheet: ISheet,
initialState?: EditableState,
) => void
setOrbitControlsRef: (
orbitControlsRef: MutableRefObject<typeof OrbitControls | undefined>,
) => void
addEditable: <T extends EditableType>(type: T, uniqueName: string) => void
removeEditable: (uniqueName: string) => void
setEditableTransform: (uniqueName: string, transform: Matrix4) => void
setSelected: (name: string | null) => void
setSelectedHdr: (hdr: string | null) => void
setTransformControlsMode: (mode: TransformControlsMode) => void
setTransformControlsSpace: (mode: TransformControlsSpace) => void
setViewportShading: (mode: ViewportShading) => void
setShowOverlayIcons: (show: boolean) => void
setUseHdrAsBackground: (use: boolean) => void
setShowGrid: (show: boolean) => void
setShowAxes: (show: boolean) => void
setReferenceWindowSize: (size: number) => void
setEditorOpen: (open: boolean) => void
createSnapshot: () => void
setSheetObject: (uniqueName: string, sheetObject: BaseSheetObjectType) => void
setSnapshotProxyObject: (
proxyObject: Object3D | null,
uniqueName: string,
) => void
serialize: () => {}
isPersistedStateDifferentThanInitial: () => boolean
applyPersistedState: () => void
}
interface PersistedState {
canvases: {
[name: string]: EditableState
}
}
const config: StateCreator<EditorStore> = (set, get) => {
setTimeout(() => {
const existingHandler = DefaultLoadingManager.onProgress
DefaultLoadingManager.onProgress = (url, loaded, total) => {
existingHandler(url, loaded, total)
if (url.match(/\.hdr$/)) {
set((state) => {
const newPaths = new Set(state.hdrPaths)
newPaths.add(url)
const selectedHdr = newPaths.size === 1 ? url : state.selectedHdr
return {hdrPaths: Array.from(newPaths), selectedHdr}
})
}
}
})
return {
sheet: null,
sheetObjects: {},
scene: null,
gl: null,
allowImplicitInstancing: false,
orbitControlsRef: null,
helpersRoot: new Group(),
editables: {},
canvasName: 'default',
initialState: null,
selected: null,
transformControlsMode: 'translate',
transformControlsSpace: 'world',
viewportShading: 'rendered',
editorOpen: false,
sceneSnapshot: null,
editablesSnapshot: null,
hdrPaths: [],
selectedHdr: null,
showOverlayIcons: false,
useHdrAsBackground: false,
showGrid: true,
showAxes: true,
referenceWindowSize: 120,
initialEditorCamera: {},
init: (
scene,
gl,
allowImplicitInstancing,
editorCamera,
sheet,
initialState,
) => {
const editables = get().editables
const newEditables: Record<string, Editable> = initialState
? Object.fromEntries(
Object.entries(initialState.editables).map(
([name, initialEditable]) => {
const originalEditable = editables[name]
return [
name,
{
type: initialEditable.type,
role: originalEditable?.role ?? 'removed',
},
]
},
),
)
: editables
set({
scene,
gl,
allowImplicitInstancing,
editables: newEditables,
initialEditorCamera: editorCamera,
initialState: initialState ?? null,
sheet,
})
},
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',
},
},
}
}),
setOrbitControlsRef: (camera) => {
set({orbitControlsRef: camera})
},
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,
},
}))
},
setEditableTransform: (uniqueName, transform) => {
set((state) => ({
editables: {
...state.editables,
[uniqueName]: {
...state.editables[uniqueName],
properties: {transform},
},
},
}))
},
setSelected: (name) => {
set({selected: name})
},
setSelectedHdr: (hdr) => {
set({selectedHdr: hdr})
},
setTransformControlsMode: (mode) => {
set({transformControlsMode: mode})
},
setTransformControlsSpace: (mode) => {
set({transformControlsSpace: mode})
},
setViewportShading: (mode) => {
set({viewportShading: mode})
},
setShowOverlayIcons: (show) => {
set({showOverlayIcons: show})
},
setUseHdrAsBackground: (use) => {
set({useHdrAsBackground: use})
},
setShowGrid: (show) => {
set({showGrid: show})
},
setShowAxes: (show) => {
set({showAxes: show})
},
setReferenceWindowSize: (size) => set({referenceWindowSize: size}),
setEditorOpen: (open) => {
set({editorOpen: open})
},
createSnapshot: () => {
set((state) => ({
sceneSnapshot: state.scene?.clone() ?? null,
editablesSnapshot: state.editables,
}))
},
setSnapshotProxyObject: (proxyObject, uniqueName) => {
set((state) => ({
editablesSnapshot: {
...state.editablesSnapshot,
[uniqueName]: {
...state.editablesSnapshot![uniqueName],
proxyObject,
},
},
}))
},
serialize: () => ({}),
isPersistedStateDifferentThanInitial: () => {
const initialState = get().initialState
const canvasName = get().canvasName!
if (!initialState || !initialPersistedState) {
return false
}
return !deepEqual(
initialPersistedState.canvases[canvasName],
initialState,
)
},
applyPersistedState: () => {
const editables = get().editables
const canvasName = get().canvasName!
if (!initialPersistedState) {
return
}
},
}
}
export const useEditorStore = create<EditorStore>(config)
const initPersistence = (
key: string,
): [PersistedState | null, (() => void) | undefined] => {
let initialPersistedState: PersistedState | null = null
let unsub
if (process.env.NODE_ENV === 'development') {
try {
const rawPersistedState = localStorage.getItem(key)
if (rawPersistedState) {
initialPersistedState = JSON.parse(rawPersistedState)
}
} catch (e) {}
unsub = useEditorStore.subscribe(
() => {
const canvasName = useEditorStore.getState().canvasName
const serialize = useEditorStore.getState().serialize
if (canvasName) {
const editables = serialize()
localStorage.setItem(
key,
JSON.stringify({
canvases: {
[canvasName]: editables,
},
}),
)
}
},
(state) => state.editables,
)
}
return [initialPersistedState, unsub]
}
let [initialPersistedState, unsub] = initPersistence('react-three-editable_')
export type BindFunction = (options: {
allowImplicitInstancing?: boolean
state?: EditableState
editorCamera?: ContainerProps['camera']
sheet: ISheet
}) => (options: {gl: WebGLRenderer; scene: Scene}) => void
export const configure = ({
localStorageNamespace = '',
enablePersistence = true,
} = {}): BindFunction => {
if (unsub) {
unsub()
}
if (enablePersistence) {
const persistence = initPersistence(
`react-three-editable_${localStorageNamespace}`,
)
initialPersistedState = persistence[0]
unsub = persistence[1]
} else {
initialPersistedState = null
unsub = undefined
}
return ({
allowImplicitInstancing = false,
state,
editorCamera = {},
sheet,
}) => {
return ({gl, scene}) => {
const init = useEditorStore.getState().init
init(
scene,
gl,
allowImplicitInstancing,
{...{position: [20, 20, 20]}, ...editorCamera},
sheet,
state,
)
}
}
}