diff --git a/packages/dataverse/src/derivations/prism/prism.ts b/packages/dataverse/src/derivations/prism/prism.ts index 962aab0..04d762b 100644 --- a/packages/dataverse/src/derivations/prism/prism.ts +++ b/packages/dataverse/src/derivations/prism/prism.ts @@ -172,7 +172,7 @@ type IEffect = { const memosWeakMap = new WeakMap>() type IMemo = { - deps: undefined | unknown[] + deps: undefined | unknown[] | ReadonlyArray cachedValue: unknown } @@ -229,8 +229,8 @@ function effect(key: string, cb: () => () => void, deps?: unknown[]): void { } function depsHaveChanged( - oldDeps: undefined | unknown[], - newDeps: undefined | unknown[], + oldDeps: undefined | unknown[] | ReadonlyArray, + newDeps: undefined | unknown[] | ReadonlyArray, ): boolean { if (oldDeps === undefined || newDeps === undefined) { return true @@ -244,7 +244,7 @@ function depsHaveChanged( function memo( key: string, fn: () => T, - deps: undefined | $IntentionalAny[], + deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>, ): T { const scope = hookScopeStack.peek() if (!scope) { diff --git a/packages/plugin-r3f/src/components/Editor.tsx b/packages/plugin-r3f/src/components/SnapshotEditor.tsx similarity index 87% rename from packages/plugin-r3f/src/components/Editor.tsx rename to packages/plugin-r3f/src/components/SnapshotEditor.tsx index 2d431e5..692be6d 100644 --- a/packages/plugin-r3f/src/components/Editor.tsx +++ b/packages/plugin-r3f/src/components/SnapshotEditor.tsx @@ -62,19 +62,17 @@ const EditorScene = () => { ) } -const Wrapper = styled.div<{visible: boolean}>` +const Wrapper = styled.div` tab-size: 4; line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ margin: 0; - position: fixed; + position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; - z-index: 50; - display: ${(props) => (props.visible ? 'block' : 'none')}; ` const CanvasWrapper = styled.div` @@ -83,7 +81,9 @@ const CanvasWrapper = styled.div` height: 100%; ` -const Editor: VFC = () => { +const SnapshotEditor: VFC = () => { + console.log('Snapshot editor!!') + const [editorObject, sceneSnapshot, initialEditorCamera, createSnapshot] = useEditorStore( (state) => [ @@ -95,10 +95,18 @@ const Editor: VFC = () => { shallow, ) - const editorOpen = !!useVal(editorObject?.props.isOpen) + const editorOpen = true useLayoutEffect(() => { + let timeout: NodeJS.Timeout | undefined if (editorOpen) { - createSnapshot() + // 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]) @@ -109,8 +117,7 @@ const Editor: VFC = () => { <> - - {/* */} + {sceneSnapshot ? ( <> @@ -140,4 +147,4 @@ const Editor: VFC = () => { ) } -export default Editor +export default SnapshotEditor diff --git a/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx b/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx index 7162563..caefaf0 100644 --- a/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx +++ b/packages/plugin-r3f/src/components/Toolbar/Toolbar.tsx @@ -10,7 +10,7 @@ import {Vector3} from 'three' import type {$FixMe} from '@theatre/shared/utils/types' import studio from '@theatre/studio' import {getSelected} from '../useSelected' -import {useVal} from '@theatre/dataverse-react' +import {usePrism, useVal} from '@theatre/dataverse-react' import IconButton from './utils/IconButton' import styled from 'styled-components' @@ -19,6 +19,10 @@ const ToolGroup = styled.div` ` const Toolbar: VFC = () => { + usePrism(() => { + const panes = studio.getPanesOfType('snapshotEditor') + }, []) + const [editorObject] = useEditorStore( (state) => [state.editorObject], shallow, @@ -35,6 +39,15 @@ const Toolbar: VFC = () => { return ( <> + + + , editorRoot) } diff --git a/theatre/core/src/CoreBundle.ts b/theatre/core/src/CoreBundle.ts index 6b34b6a..47cd948 100644 --- a/theatre/core/src/CoreBundle.ts +++ b/theatre/core/src/CoreBundle.ts @@ -1,10 +1,12 @@ import type {Studio} from '@theatre/studio/Studio' import projectsSingleton from './projects/projectsSingleton' import {privateAPI} from './privateAPIs' +import * as coreExports from './coreExports' export type CoreBits = { projectsP: typeof projectsSingleton.atom.pointer.projects privateAPI: typeof privateAPI + coreExports: typeof coreExports } export default class CoreBundle { @@ -27,6 +29,7 @@ export default class CoreBundle { const bits: CoreBits = { projectsP: projectsSingleton.atom.pointer.projects, privateAPI: privateAPI, + coreExports, } callback(bits) diff --git a/theatre/studio/src/PaneManager.ts b/theatre/studio/src/PaneManager.ts new file mode 100644 index 0000000..a1df535 --- /dev/null +++ b/theatre/studio/src/PaneManager.ts @@ -0,0 +1,121 @@ +import {prism, val} from '@theatre/dataverse' +import {emptyArray} from '@theatre/shared/utils' +import SimpleCache from '@theatre/shared/utils/SimpleCache' +import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types' +import type {Studio} from './Studio' +import type {PaneInstance} from './TheatreStudio' + +export default class PaneManager { + private readonly _cache = new SimpleCache() + + constructor(private readonly _studio: Studio) { + this._instantiatePanesAsTheyComeIn() + } + + private _instantiatePanesAsTheyComeIn() { + const allPanesD = this._getAllPanes() + allPanesD.changesWithoutValues().tap(() => { + allPanesD.getValue() + }) + } + + private _getAllPanes() { + return this._cache.get('_getAllPanels()', () => + prism((): {[instanceId in string]?: PaneInstance} => { + const core = val(this._studio.coreP) + if (!core) return {} + const instanceDescriptors = val( + this._studio.atomP.historic.panelInstanceDesceriptors, + ) + const paneClasses = val( + this._studio.atomP.ephemeral.extensions.paneClasses, + ) + + const instances: {[instanceId in string]?: PaneInstance} = {} + for (const [, instanceDescriptor] of Object.entries( + instanceDescriptors, + )) { + const panelClass = paneClasses[instanceDescriptor!.paneClass] + if (!panelClass) continue + const {instanceId} = instanceDescriptor! + const {extensionId, classDefinition: definition} = panelClass + + const instance = prism.memo( + `instance-${instanceDescriptor!.instanceId}`, + () => { + const object = this._studio + .getExtensionSheet(extensionId, core) + .object( + 'Pane: ' + instanceId, + null, + core.types.compound({ + panelThingy: core.types.boolean(false), + }), + ) as $FixMe + + const inst: PaneInstance<$IntentionalAny> = { + extensionId, + instanceId, + object, + definition, + } + return inst + }, + emptyArray, + ) + + instances[instanceId] = instance + } + return instances + }), + ) + } + + get allPanesD() { + return this._getAllPanes() + } + + getPanesOfType( + paneClass: PaneClass, + ): PaneInstance[] { + return [] + } + + createPane( + paneClass: PaneClass, + ): PaneInstance { + const core = this._studio.core + if (!core) { + throw new Error( + `Can't create a pane because @theatre/core is not yet loaded`, + ) + } + + const extensionId = val( + this._studio.atomP.ephemeral.extensions.paneClasses[paneClass] + .extensionId, + ) + + const allPaneInstances = val( + this._studio.atomP.historic.panelInstanceDesceriptors, + ) + let instanceId!: string + for (let i = 1; i < 1000; i++) { + instanceId = `${paneClass} #${i}` + if (!allPaneInstances[instanceId]) break + } + + if (!extensionId) { + throw new Error(`Pance class "${paneClass}" is not registered.`) + } + + this._studio.transaction(({drafts}) => { + drafts.historic.panelInstanceDesceriptors[instanceId] = { + instanceId, + paneClass, + } + }) + + return this._getAllPanes().getValue()[instanceId]! + } +} diff --git a/theatre/studio/src/Studio.ts b/theatre/studio/src/Studio.ts index 3adc4fb..bb21e3f 100644 --- a/theatre/studio/src/Studio.ts +++ b/theatre/studio/src/Studio.ts @@ -1,5 +1,4 @@ import Scrub from '@theatre/studio/Scrub' -import type {FullStudioState} from '@theatre/studio/store' import type {StudioHistoricState} from '@theatre/studio/store/types/historic' import UI from '@theatre/studio/UI' import type {Pointer} from '@theatre/dataverse' @@ -14,10 +13,14 @@ import TheatreStudio from './TheatreStudio' import {nanoid} from 'nanoid/non-secure' import type Project from '@theatre/core/projects/Project' import type {CoreBits} from '@theatre/core/CoreBundle' -import type {privateAPI} from '@theatre/core/privateAPIs' +import SimpleCache from '@theatre/shared/utils/SimpleCache' +import type {IProject, ISheet} from '@theatre/core' +import PaneManager from './PaneManager' +import type * as _coreExports from '@theatre/core/coreExports' + +export type CoreExports = typeof _coreExports export class Studio { - readonly atomP: Pointer readonly ui!: UI readonly publicApi: IStudio readonly address: {studioId: string} @@ -28,23 +31,27 @@ export class Studio { this._projectsProxy.pointer private readonly _store = new StudioStore() - private _corePrivateApi: typeof privateAPI | undefined + private _corePrivateApi: CoreBits['privateAPI'] | undefined - private _extensions: Atom<{byId: Record}> = new Atom({ - byId: {}, - }) - readonly extensionsP = this._extensions.pointer.byId + private readonly _cache = new SimpleCache() + readonly paneManager: PaneManager + + private _coreAtom = new Atom<{core?: CoreExports}>({}) + + get atomP() { + return this._store.atomP + } constructor() { this.address = {studioId: nanoid(10)} this.publicApi = new TheatreStudio(this) - this.atomP = this._store.atomP if (process.env.NODE_ENV !== 'test') { this.ui = new UI(this) } this._attachToIncomingProjects() + this.paneManager = new PaneManager(this) } get initialized() { @@ -69,6 +76,7 @@ export class Studio { setCoreBits(coreBits: CoreBits) { this._corePrivateApi = coreBits.privateAPI + this._coreAtom.setIn(['core'], coreBits.coreExports) this._setProjectsP(coreBits.projectsP) } @@ -96,6 +104,14 @@ export class Studio { return this._corePrivateApi } + get core() { + return this._coreAtom.getState().core + } + + get coreP() { + return this._coreAtom.pointer.core + } + extend(extension: IExtension) { if (!extension || typeof extension !== 'object') { throw new Error(`Extensions must be JS objects`) @@ -105,12 +121,47 @@ export class Studio { throw new Error(`extension.id must be a string`) } - if (this._extensions.getState().byId[extension.id]) { - throw new Error( - `An extension with the id of ${extension.id} already exists`, - ) - } + this.transaction(({drafts}) => { + if (drafts.ephemeral.extensions.byId[extension.id]) { + throw new Error(`Extension id "${extension.id}" is already defined`) + } + drafts.ephemeral.extensions.byId[extension.id] = extension - this._extensions.setIn(['byId', extension.id], extension) + const allPaneClasses = drafts.ephemeral.extensions.paneClasses + + extension.panes?.forEach((classDefinition) => { + if (typeof classDefinition.class !== 'string') { + throw new Error(`pane.class must be a string`) + } + + if (classDefinition.class.length < 3) { + throw new Error( + `pane.class should be a string with 3 or more characters`, + ) + } + + const existing = allPaneClasses[classDefinition.class] + if (existing) { + throw new Error( + `Pane class "${classDefinition.class}" already exists and is supplied by extension ${existing}`, + ) + } + + allPaneClasses[classDefinition.class] = { + extensionId: extension.id, + classDefinition: classDefinition, + } + }) + }) + } + + getStudioProject(core: CoreExports): IProject { + return this._cache.get('getStudioProject', () => core.getProject('Studio')) + } + + getExtensionSheet(extensionId: string, core: CoreExports): ISheet { + return this._cache.get('extensionSheet-' + extensionId, () => + this.getStudioProject(core)!.sheet('Extension ' + extensionId), + ) } } diff --git a/theatre/studio/src/StudioStore/StudioStore.ts b/theatre/studio/src/StudioStore/StudioStore.ts index 4d6daf6..33bced4 100644 --- a/theatre/studio/src/StudioStore/StudioStore.ts +++ b/theatre/studio/src/StudioStore/StudioStore.ts @@ -78,6 +78,10 @@ export default class StudioStore { } } + getState(): FullStudioState { + return this._reduxStore.getState() + } + /** * This method causes the store to start the history from scratch. This is useful * for testing and development where you want to explicitly provide a state to the diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts index b13001a..8637774 100644 --- a/theatre/studio/src/TheatreStudio.ts +++ b/theatre/studio/src/TheatreStudio.ts @@ -3,7 +3,7 @@ import studioTicker from '@theatre/studio/studioTicker' import type {IDerivation, Pointer} from '@theatre/dataverse' import {prism} from '@theatre/dataverse' import SimpleCache from '@theatre/shared/utils/SimpleCache' -import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' +import type {$FixMe, VoidFn} from '@theatre/shared/utils/types' import type {IScrub} from '@theatre/studio/Scrub' import type {Studio} from '@theatre/studio/Studio' @@ -22,8 +22,10 @@ export interface ITransactionAPI { unset(pointer: Pointer): void } -export interface IPanelType> { - sheetName: string +export interface PaneClassDefinition< + DataType extends PropTypeConfig_Compound<{}>, +> { + class: string dataType: DataType component: React.ComponentType<{ id: string @@ -41,7 +43,16 @@ export type IExtension = { globalToolbar?: { component: React.ComponentType<{}> } - panes?: Record> + panes?: Array> +} + +export type PaneInstance = { + extensionId: string + instanceId: string + object: ISheetObject< + PropTypeConfig_Compound<{data: $FixMe; visible: PropTypeConfig_Boolean}> + > + definition: PaneClassDefinition<$FixMe> } export interface IStudio { @@ -63,6 +74,14 @@ export interface IStudio { readonly selection: Array extend(extension: IExtension): void + + getPanesOfType( + paneClass: PaneClass, + ): Array> + + createPane( + paneClass: PaneClass, + ): PaneInstance } export default class TheatreStudio implements IStudio { @@ -138,4 +157,15 @@ export default class TheatreStudio implements IStudio { scrub(): IScrub { return getStudio().scrub() } + + getPanesOfType( + paneClass: PaneClass, + ): PaneInstance[] { + return getStudio().paneManager.getPanesOfType(paneClass) + } + createPane( + paneClass: PaneClass, + ): PaneInstance { + return getStudio().paneManager.createPane(paneClass) + } } diff --git a/theatre/studio/src/UIRoot/PanelsRoot.tsx b/theatre/studio/src/UIRoot/PanelsRoot.tsx index 581b42d..d47cacc 100644 --- a/theatre/studio/src/UIRoot/PanelsRoot.tsx +++ b/theatre/studio/src/UIRoot/PanelsRoot.tsx @@ -2,10 +2,21 @@ import OutlinePanel from '@theatre/studio/panels/OutlinePanel/OutlinePanel' import ObjectEditorPanel from '@theatre/studio/panels/ObjectEditorPanel/ObjectEditorPanel' import React from 'react' import SequenceEditorPanel from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' +import getStudio from '@theatre/studio/getStudio' +import {useVal} from '@theatre/dataverse-react' +import PaneWrapper from '@theatre/studio/panels/BasePanel/PaneWrapper' const PanelsRoot: React.FC = () => { + const panes = useVal(getStudio().paneManager.allPanesD) + const paneEls = Object.entries(panes).map(([instanceId, paneInstance]) => { + return ( + + ) + }) + return ( <> + {paneEls} diff --git a/theatre/studio/src/panels/BasePanel/BasePanel.tsx b/theatre/studio/src/panels/BasePanel/BasePanel.tsx index cd4fa60..8ccb3d6 100644 --- a/theatre/studio/src/panels/BasePanel/BasePanel.tsx +++ b/theatre/studio/src/panels/BasePanel/BasePanel.tsx @@ -2,7 +2,7 @@ import {val} from '@theatre/dataverse' import {usePrism} from '@theatre/dataverse-react' import type {$IntentionalAny} from '@theatre/shared/utils/types' import getStudio from '@theatre/studio/getStudio' -import type {PanelId, PanelPosition} from '@theatre/studio/store/types' +import type {PanelPosition} from '@theatre/studio/store/types' import React, {useContext} from 'react' import useWindowSize from 'react-use/esm/useWindowSize' import styled from 'styled-components' @@ -15,7 +15,7 @@ const Container = styled.div` ` type PanelStuff = { - panelId: PanelId + panelId: string dims: { width: number height: number @@ -69,7 +69,7 @@ const PanelContext = React.createContext(null as $IntentionalAny) export const usePanel = () => useContext(PanelContext) const BasePanel: React.FC<{ - panelId: PanelId + panelId: string defaultPosition: PanelPosition minDims: {width: number; height: number} }> = ({panelId, children, defaultPosition, minDims}) => { diff --git a/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx b/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx new file mode 100644 index 0000000..6fe602d --- /dev/null +++ b/theatre/studio/src/panels/BasePanel/PaneWrapper.tsx @@ -0,0 +1,71 @@ +import type {$FixMe} from '@theatre/shared/utils/types' +import type {PanelPosition} from '@theatre/studio/store/types' +import type {PaneInstance} from '@theatre/studio/TheatreStudio' +import React from 'react' +import styled from 'styled-components' +import { + F1, + F2 as F2Impl, +} from '@theatre/studio/panels/ObjectEditorPanel/ObjectEditorPanel' +import BasePanel from './BasePanel' +import PanelDragZone from './PanelDragZone' +import PanelWrapper from './PanelWrapper' + +const defaultPosition: PanelPosition = { + edges: { + left: {from: 'screenLeft', distance: 0.3}, + right: {from: 'screenRight', distance: 0.3}, + top: {from: 'screenTop', distance: 0.3}, + bottom: {from: 'screenBottom', distance: 0.3}, + }, +} + +const minDims = {width: 300, height: 300} + +const PaneWrapper: React.FC<{ + paneInstance: PaneInstance<$FixMe> +}> = ({paneInstance}) => { + return ( + + + + ) +} + +const Container = styled(PanelWrapper)` + overflow-y: hidden; + display: flex; + flex-direction: column; +` + +const Title = styled.div` + width: 100%; +` + +const F2 = styled(F2Impl)` + position: relative; +` + +const Content: React.FC<{paneInstance: PaneInstance<$FixMe>}> = ({ + paneInstance, +}) => { + const Comp = paneInstance.definition.component + return ( + + + + {paneInstance.instanceId} + + + + + + + ) +} + +export default PaneWrapper diff --git a/theatre/studio/src/store/index.ts b/theatre/studio/src/store/index.ts index f125d59..d35ba8c 100644 --- a/theatre/studio/src/store/index.ts +++ b/theatre/studio/src/store/index.ts @@ -29,6 +29,7 @@ const initialState: StudioState = { }, autoKey: true, coreByProject: {}, + panelInstanceDesceriptors: {}, }, ephemeral: { initialised: false, @@ -36,6 +37,10 @@ const initialState: StudioState = { projects: { stateByProjectId: {}, }, + extensions: { + byId: {}, + paneClasses: {}, + }, }, } diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index 21e0f65..51909a4 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -29,7 +29,6 @@ import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/Grap import type { OutlineSelectable, OutlineSelectionState, - PanelId, PanelPosition, } from './types' import {uniq} from 'lodash-es' @@ -67,7 +66,7 @@ namespace stateEditors { export namespace historic { export namespace panelPositions { export function setPanelPosition(p: { - panelId: PanelId + panelId: string position: PanelPosition }) { const h = drafts().historic diff --git a/theatre/studio/src/store/types/ephemeral.ts b/theatre/studio/src/store/types/ephemeral.ts index 2fe0373..1e59237 100644 --- a/theatre/studio/src/store/types/ephemeral.ts +++ b/theatre/studio/src/store/types/ephemeral.ts @@ -1,5 +1,13 @@ import type {ProjectState} from '@theatre/core/projects/store/storeTypes' -import type {SerializableMap, StrictRecord} from '@theatre/shared/utils/types' +import type { + $IntentionalAny, + SerializableMap, + StrictRecord, +} from '@theatre/shared/utils/types' +import type { + IExtension, + PaneClassDefinition, +} from '@theatre/studio/TheatreStudio' export type StudioEphemeralState = { initialised: boolean @@ -22,4 +30,13 @@ export type StudioEphemeralState = { } > } + extensions: { + byId: {[extensionId in string]?: IExtension} + paneClasses: { + [paneClassName in string]?: { + extensionId: string + classDefinition: PaneClassDefinition<$IntentionalAny> + } + } + } } diff --git a/theatre/studio/src/store/types/historic.ts b/theatre/studio/src/store/types/historic.ts index 4095689..f402497 100644 --- a/theatre/studio/src/store/types/historic.ts +++ b/theatre/studio/src/store/types/historic.ts @@ -56,6 +56,11 @@ export type OutlineSelectionState = export type OutlineSelectable = Project | Sheet | SheetObject export type OutlineSelection = OutlineSelectable[] +export type PanelInstanceDescriptor = { + instanceId: string + paneClass: string +} + export type StudioHistoricState = { projects: { stateByProjectId: StrictRecord< @@ -77,7 +82,10 @@ export type StudioHistoricState = { > } panels?: Panels - panelPositions?: {[panelId in PanelId]?: PanelPosition} + panelPositions?: {[panelIdOrPaneId in string]?: PanelPosition} + panelInstanceDesceriptors: { + [instanceId in string]?: PanelInstanceDescriptor + } autoKey: boolean coreByProject: {[projectId in string]: ProjectState_Historic} } diff --git a/theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx b/theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx index d2447df..d5935bc 100644 --- a/theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx +++ b/theatre/studio/src/toolbars/GlobalToolbar/GlobalToolbar.tsx @@ -17,9 +17,10 @@ const Container = styled.div` const GlobalToolbar: React.FC<{}> = (props) => { const groups: Array = [] - const extensions = useVal(getStudio().extensionsP) + const extensionsById = useVal(getStudio().atomP.ephemeral.extensions.byId) - for (const [, extension] of Object.entries(extensions)) { + for (const [, extension] of Object.entries(extensionsById)) { + if (!extension) continue if (extension.globalToolbar) { groups.push(