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 return (): ReturnType => { if (!base) { base = createBaseObjectConfig() } return base! } })() export type BaseSheetObjectType = ISheetObject< ReturnType['props'] > export interface AbstractEditable { type: T role: 'active' | 'removed' sheetObject?: ISheetObject } // 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 = { proxyObject?: Object3D | null } & T export interface AbstractSerializedEditable { 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 } export type EditorStore = { sheet: ISheet | null sheetObjects: {[uniqueName in string]?: BaseSheetObjectType} scene: Scene | null gl: WebGLRenderer | null allowImplicitInstancing: boolean orbitControlsRef: MutableRefObject | null helpersRoot: Group editables: Record // 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 | 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, ) => void addEditable: (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 = (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 = 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(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, ) } } }