Put SnapshotEditor inside a Pane

This commit is contained in:
Aria Minaei 2021-07-14 18:37:32 +02:00
parent 921bc44270
commit 64273366ed
17 changed files with 395 additions and 50 deletions

View file

@ -172,7 +172,7 @@ type IEffect = {
const memosWeakMap = new WeakMap<PrismScope, Record<string, IMemo>>()
type IMemo = {
deps: undefined | unknown[]
deps: undefined | unknown[] | ReadonlyArray<unknown>
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<unknown>,
newDeps: undefined | unknown[] | ReadonlyArray<unknown>,
): boolean {
if (oldDeps === undefined || newDeps === undefined) {
return true
@ -244,7 +244,7 @@ function depsHaveChanged(
function memo<T>(
key: string,
fn: () => T,
deps: undefined | $IntentionalAny[],
deps: undefined | $IntentionalAny[] | ReadonlyArray<$IntentionalAny>,
): T {
const scope = hookScopeStack.peek()
if (!scope) {

View file

@ -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 = () => {
<StyleSheetManager disableVendorPrefixes>
<>
<GlobalStyle />
<Wrapper id="theatre-plugin-r3f-root" visible={true}>
{/* <Toolbar /> */}
<Wrapper>
{sceneSnapshot ? (
<>
<CanvasWrapper>
@ -140,4 +147,4 @@ const Editor: VFC = () => {
)
}
export default Editor
export default SnapshotEditor

View file

@ -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 (
<>
<ToolGroup>
<button
onClick={() => {
studio.createPane('snapshotEditor')
}}
>
Create snapshot
</button>
</ToolGroup>
<ToolGroup>
<TransformControlsModeSelect
value={transformControlsMode}

View file

@ -1,6 +1,4 @@
import React from 'react'
import {render} from 'react-dom'
import Editor from './components/Editor'
import SnapshotEditor from './components/SnapshotEditor'
export {default as EditorHelper} from './components/EditorHelper'
export type {EditorHelperProps} from './components/EditorHelper'
@ -10,6 +8,7 @@ export {bindToCanvas} from './store'
export type {EditableState, BindFunction} from './store'
import studio from '@theatre/studio'
import Toolbar from './components/Toolbar/Toolbar'
import {types} from '@theatre/core'
if (process.env.NODE_ENV === 'development') {
studio.extend({
@ -17,9 +16,14 @@ if (process.env.NODE_ENV === 'development') {
globalToolbar: {
component: Toolbar,
},
panes: [
{
class: 'snapshotEditor',
dataType: types.compound({
grosse: types.number(20),
}),
component: SnapshotEditor,
},
],
})
const editorRoot = document.createElement('div')
document.body.appendChild(editorRoot)
render(<Editor />, editorRoot)
}

View file

@ -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)

View file

@ -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<string>} => {
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<string>} = {}
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 extends string>(
paneClass: PaneClass,
): PaneInstance<PaneClass>[] {
return []
}
createPane<PaneClass extends string>(
paneClass: PaneClass,
): PaneInstance<PaneClass> {
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]!
}
}

View file

@ -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<FullStudioState>
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<string, IExtension>}> = 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),
)
}
}

View file

@ -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

View file

@ -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<V>(pointer: Pointer<V>): void
}
export interface IPanelType<DataType extends PropTypeConfig_Compound<{}>> {
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<string, IPanelType<$IntentionalAny>>
panes?: Array<PaneClassDefinition<$FixMe>>
}
export type PaneInstance<ClassName extends string> = {
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<ISheetObject>
extend(extension: IExtension): void
getPanesOfType<PaneClass extends string>(
paneClass: PaneClass,
): Array<PaneInstance<PaneClass>>
createPane<PaneClass extends string>(
paneClass: PaneClass,
): PaneInstance<PaneClass>
}
export default class TheatreStudio implements IStudio {
@ -138,4 +157,15 @@ export default class TheatreStudio implements IStudio {
scrub(): IScrub {
return getStudio().scrub()
}
getPanesOfType<PaneClass extends string>(
paneClass: PaneClass,
): PaneInstance<PaneClass>[] {
return getStudio().paneManager.getPanesOfType(paneClass)
}
createPane<PaneClass extends string>(
paneClass: PaneClass,
): PaneInstance<PaneClass> {
return getStudio().paneManager.createPane(paneClass)
}
}

View file

@ -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 (
<PaneWrapper key={`pane-${instanceId}`} paneInstance={paneInstance!} />
)
})
return (
<>
{paneEls}
<OutlinePanel />
<ObjectEditorPanel />
<SequenceEditorPanel />

View file

@ -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<PanelStuff>(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}) => {

View file

@ -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 (
<BasePanel
panelId={`pane-${paneInstance.instanceId}`}
defaultPosition={defaultPosition}
minDims={minDims}
>
<Content paneInstance={paneInstance} />
</BasePanel>
)
}
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 (
<Container>
<PanelDragZone>
<F1>
<Title>{paneInstance.instanceId}</Title>
</F1>
</PanelDragZone>
<F2>
<Comp id={paneInstance.instanceId} object={paneInstance.object} />
</F2>
</Container>
)
}
export default PaneWrapper

View file

@ -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: {},
},
},
}

View file

@ -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

View file

@ -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>
}
}
}
}

View file

@ -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}
}

View file

@ -17,9 +17,10 @@ const Container = styled.div`
const GlobalToolbar: React.FC<{}> = (props) => {
const groups: Array<React.ReactNode> = []
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(
<extension.globalToolbar.component