From 1387ce62d285659763edfa74f9337da1deed141b Mon Sep 17 00:00:00 2001 From: Cole Lawrence Date: Fri, 29 Apr 2022 13:00:14 -0400 Subject: [PATCH] refactor: Add working Nominal types, clarify identifiers * Use more Nominal types to help with internal code id usage consistency * Broke apart StudioHistoricState type Co-authored-by: Aria --- packages/dataverse/src/Atom.ts | 2 + packages/dataverse/src/pointer.ts | 38 ++++--- packages/dataverse/src/pointer.typeTest.ts | 106 ++++++++++++++++++ theatre/core/src/coreExports.ts | 11 +- theatre/core/src/privateAPIs.ts | 9 +- theatre/core/src/projects/Project.ts | 12 +- theatre/core/src/projects/TheatreProject.ts | 13 ++- .../core/src/projects/projectsSingleton.ts | 9 +- theatre/core/src/projects/store/storeTypes.ts | 3 +- .../store/types/SheetState_Historic.ts | 32 ++++-- theatre/core/src/propTypes/index.ts | 17 +-- theatre/core/src/propTypes/internals.ts | 42 +++++-- .../interpolationTripleAtPosition.ts | 12 +- .../core/src/sheetObjects/SheetObject.test.ts | 9 +- theatre/core/src/sheetObjects/SheetObject.ts | 37 ++++-- .../sheetObjects/SheetObjectTemplate.test.ts | 13 ++- .../src/sheetObjects/SheetObjectTemplate.ts | 24 ++-- .../src/sheetObjects/TheatreSheetObject.ts | 22 ++-- .../getPropDefaultsOfSheetObject.ts | 7 +- theatre/core/src/sheets/Sheet.ts | 33 ++++-- theatre/core/src/sheets/SheetTemplate.ts | 41 ++++--- theatre/core/src/sheets/TheatreSheet.ts | 32 ++++-- theatre/shared/src/testUtils.ts | 3 +- theatre/shared/src/utils/Nominal.ts | 37 ++++++ theatre/shared/src/utils/addresses.ts | 11 +- theatre/shared/src/utils/ids.ts | 19 +++- theatre/shared/src/utils/pointerDeep.ts | 6 +- theatre/shared/src/utils/types.ts | 19 ++-- theatre/studio/src/PaneManager.ts | 26 ++--- theatre/studio/src/Studio.ts | 9 +- theatre/studio/src/StudioStore/StudioStore.ts | 3 +- theatre/studio/src/TheatreStudio.ts | 9 +- .../studio/src/panels/BasePanel/BasePanel.tsx | 5 +- .../panels/BasePanel/ExtensionPaneWrapper.tsx | 7 +- .../src/panels/DetailPanel/ObjectDetails.tsx | 6 +- .../ProjectDetails/StateConflictRow.tsx | 5 +- .../propEditors/CompoundPropEditor.tsx | 4 +- .../propEditors/DeterminePropEditor.tsx | 30 +++-- .../propEditors/utils/IPropEditorFC.ts | 2 +- .../utils/useEditingToolsForPrimitiveProp.tsx | 9 +- .../OutlinePanel/ObjectsList/ObjectsList.tsx | 2 +- .../BasicKeyframedTrack.tsx | 5 +- .../KeyframeEditor/KeyframeDot.tsx | 2 +- .../Right/DopeSheetSelectionView.tsx | 25 +---- .../BasicKeyframedTrack.tsx | 4 +- .../KeyframeEditor/Curve.tsx | 2 +- .../KeyframeEditor/CurveHandle.tsx | 2 +- .../GraphEditorDotNonScalar.tsx | 2 +- .../KeyframeEditor/GraphEditorDotScalar.tsx | 2 +- .../GraphEditorNonScalarDash.tsx | 2 +- .../KeyframeEditor/KeyframeEditor.tsx | 6 +- .../SequenceEditorPanel.tsx | 9 +- .../SequenceEditorPanel/layout/layout.ts | 19 ++-- .../panels/SequenceEditorPanel/layout/tree.ts | 6 +- theatre/studio/src/selectors.ts | 10 +- theatre/studio/src/store/stateEditors.ts | 29 +++-- theatre/studio/src/store/types/ahistoric.ts | 3 +- theatre/studio/src/store/types/historic.ts | 82 +++++++++----- 58 files changed, 647 insertions(+), 299 deletions(-) create mode 100644 packages/dataverse/src/pointer.typeTest.ts create mode 100644 theatre/shared/src/utils/Nominal.ts diff --git a/packages/dataverse/src/Atom.ts b/packages/dataverse/src/Atom.ts index 31b6b28..394d016 100644 --- a/packages/dataverse/src/Atom.ts +++ b/packages/dataverse/src/Atom.ts @@ -25,6 +25,8 @@ enum ValueTypes { export interface IdentityDerivationProvider { /** * @internal + * Future: We could consider using a `Symbol.for("dataverse/IdentityDerivationProvider")` as a key here, similar to + * how {@link Iterable} works for `of`. */ readonly $$isIdentityDerivationProvider: true /** diff --git a/packages/dataverse/src/pointer.ts b/packages/dataverse/src/pointer.ts index d914bb3..62aaed1 100644 --- a/packages/dataverse/src/pointer.ts +++ b/packages/dataverse/src/pointer.ts @@ -33,6 +33,11 @@ const cachedSubPathPointersWeakMap = new WeakMap< * A wrapper type for the type a `Pointer` points to. */ export type PointerType = { + /** + * Only accessible via the type system. + * This is a helper for getting the underlying pointer type + * via the type space. + */ $$__pointer_type: O } @@ -53,21 +58,28 @@ export type PointerType = { * * The current solution is to just avoid using `any` with pointer-related code (or type-test it well). * But if you enjoy solving typescript puzzles, consider fixing this :) - * + * Potentially, [TypeScript variance annotations in 4.7+](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#optional-variance-annotations-for-type-parameters) + * might be able to help us. */ export type Pointer = PointerType & - (O extends UnindexableTypesForPointer - ? UnindexablePointer - : unknown extends O - ? UnindexablePointer - : O extends (infer T)[] - ? Pointer[] - : O extends {} - ? { - [K in keyof O]-?: Pointer - } /*& - {[K in string | number]: Pointer}*/ - : UnindexablePointer) + // `Exclude` will remove `undefined` from the first type + // `undefined extends O ? undefined : never` will give us `undefined` if `O` is `... | undefined` + PointerInner, undefined extends O ? undefined : never> + +// By separating the `O` (non-undefined) from the `undefined` or `never`, we +// can properly use `O extends ...` to determine the kind of potential value +// without actually discarding optionality information. +type PointerInner = O extends UnindexableTypesForPointer + ? UnindexablePointer + : unknown extends O + ? UnindexablePointer + : O extends (infer T)[] + ? Pointer[] + : O extends {} + ? { + [K in keyof O]-?: Pointer + } + : UnindexablePointer const pointerMetaSymbol = Symbol('pointerMeta') diff --git a/packages/dataverse/src/pointer.typeTest.ts b/packages/dataverse/src/pointer.typeTest.ts new file mode 100644 index 0000000..9a75f9c --- /dev/null +++ b/packages/dataverse/src/pointer.typeTest.ts @@ -0,0 +1,106 @@ +import type {Pointer, UnindexablePointer} from './pointer' +import type {$IntentionalAny} from './types' + +const nominal = Symbol() +type Nominal = string & {[nominal]: Name} + +type Key = Nominal<'key'> +type Id = Nominal<'id'> + +type IdObject = { + inner: true +} + +type KeyObject = { + inner: { + byIds: Partial> + } +} + +type NestedNominalThing = { + optional?: true + byKeys: Partial> +} + +interface TypeError {} + +type Debug = T +type IsTrue = T +type IsFalse = F +type IsExtends = F +type IsExactly = F extends R + ? true + : TypeError<[F, 'does not extend', R]> + +function test() { + const p = todo>() + const key1 = todo() + const id1 = todo() + + type A = UnindexablePointer[typeof key1] + type BaseChecks = [ + IsExtends, + IsExtends, + IsExtends, + IsTrue>>, + IsTrue< + IsExactly['...']['...'], Pointer> + >, + IsTrue< + IsExactly< + Pointer>[Key], + Pointer + > + >, + IsTrue[Key], Pointer>>, + // Debug>[Key]>, + IsTrue>[string], Pointer>>, + IsTrue< + IsExactly< + Pointer>[string], + Pointer + > + >, + IsTrue< + IsExactly< + Pointer>[Key], + Pointer + > + >, + // Debug['...']['...']>, + // IsFalse ? true : false>, + // what extends what + IsTrue<1 & undefined extends undefined ? true : false>, + IsFalse<1 | undefined extends undefined ? true : false>, + ] + + t>() // + .isExactly(p.optional).ok + + t>() // + .isExactly(p.byKeys[key1]).ok + + t>() // + .isExactly(p.byKeys[key1].inner).ok + + t>() // + .isExactly(p.byKeys[key1].inner.byIds[id1]).ok + + p.byKeys[key1] +} + +function todo(hmm?: TemplateStringsArray): T { + return null as $IntentionalAny +} +function t(): { + isExactly( + hmm: R, + ): T extends R + ? // any extends R + // ? TypeError<[R, 'is any']> + // : + {ok: true} + : TypeError<[T, 'does not extend', R]> +} { + return {isExactly: (hmm) => hmm as $IntentionalAny} +} diff --git a/theatre/core/src/coreExports.ts b/theatre/core/src/coreExports.ts index e957e1c..8e2489b 100644 --- a/theatre/core/src/coreExports.ts +++ b/theatre/core/src/coreExports.ts @@ -16,6 +16,7 @@ import {isPointer} from '@theatre/dataverse' import {isDerivation, valueDerivation} from '@theatre/dataverse' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import coreTicker from './coreTicker' +import type {ProjectId} from '@theatre/shared/utils/ids' export {types} /** @@ -45,7 +46,7 @@ export {types} */ export function getProject(id: string, config: IProjectConfig = {}): IProject { const {...restOfConfig} = config - const existingProject = projectsSingleton.get(id) + const existingProject = projectsSingleton.get(id as ProjectId) if (existingProject) { if (process.env.NODE_ENV !== 'production') { if (!deepEqual(config, existingProject.config)) { @@ -67,9 +68,9 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject { if (config.state) { if (process.env.NODE_ENV !== 'production') { - shallowValidateOnDiskState(id, config.state) + shallowValidateOnDiskState(id as ProjectId, config.state) } else { - deepValidateOnDiskState(id, config.state) + deepValidateOnDiskState(id as ProjectId, config.state) } } @@ -80,7 +81,7 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject { * Lightweight validator that only makes sure the state's definitionVersion is correct. * Does not do a thorough validation of the state. */ -const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => { +const shallowValidateOnDiskState = (projectId: ProjectId, s: OnDiskState) => { if ( Array.isArray(s) || s == null || @@ -94,7 +95,7 @@ const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => { } } -const deepValidateOnDiskState = (projectId: string, s: OnDiskState) => { +const deepValidateOnDiskState = (projectId: ProjectId, s: OnDiskState) => { shallowValidateOnDiskState(projectId, s) // @TODO do a deep validation here } diff --git a/theatre/core/src/privateAPIs.ts b/theatre/core/src/privateAPIs.ts index a524183..3ed0bcf 100644 --- a/theatre/core/src/privateAPIs.ts +++ b/theatre/core/src/privateAPIs.ts @@ -6,13 +6,12 @@ import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject' import type Sheet from '@theatre/core/sheets/Sheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet' +import type {UnknownShorthandCompoundProps} from './propTypes/internals' import type {$IntentionalAny} from '@theatre/shared/utils/types' const publicAPIToPrivateAPIMap = new WeakMap() -export function privateAPI< - P extends IProject | ISheet | ISheetObject<$IntentionalAny> | ISequence, ->( +export function privateAPI

( pub: P, ): P extends IProject ? Project @@ -29,8 +28,8 @@ export function privateAPI< export function setPrivateAPI(pub: IProject, priv: Project): void export function setPrivateAPI(pub: ISheet, priv: Sheet): void export function setPrivateAPI(pub: ISequence, priv: Sequence): void -export function setPrivateAPI( - pub: ISheetObject<$IntentionalAny>, +export function setPrivateAPI( + pub: ISheetObject, priv: SheetObject, ): void export function setPrivateAPI(pub: {}, priv: {}): void { diff --git a/theatre/core/src/projects/Project.ts b/theatre/core/src/projects/Project.ts index 6c61f4d..86d094d 100644 --- a/theatre/core/src/projects/Project.ts +++ b/theatre/core/src/projects/Project.ts @@ -13,6 +13,11 @@ import type {ProjectState} from './store/storeTypes' import type {Deferred} from '@theatre/shared/utils/defer' import {defer} from '@theatre/shared/utils/defer' import globals from '@theatre/shared/globals' +import type { + ProjectId, + SheetId, + SheetInstanceId, +} from '@theatre/shared/utils/ids' export type Conf = Partial<{ state: OnDiskState @@ -44,7 +49,7 @@ export default class Project { type: 'Theatre_Project' = 'Theatre_Project' constructor( - id: string, + id: ProjectId, readonly config: Conf = {}, readonly publicApi: TheatreProject, ) { @@ -150,7 +155,10 @@ export default class Project { return this._readyDeferred.status === 'resolved' } - getOrCreateSheet(sheetId: string, instanceId: string = 'default'): Sheet { + getOrCreateSheet( + sheetId: SheetId, + instanceId: SheetInstanceId = 'default' as SheetInstanceId, + ): Sheet { let template = this._sheetTemplates.getState()[sheetId] if (!template) { diff --git a/theatre/core/src/projects/TheatreProject.ts b/theatre/core/src/projects/TheatreProject.ts index 3119724..5809953 100644 --- a/theatre/core/src/projects/TheatreProject.ts +++ b/theatre/core/src/projects/TheatreProject.ts @@ -2,6 +2,11 @@ import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs' import Project from '@theatre/core/projects/Project' import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {ProjectAddress} from '@theatre/shared/utils/addresses' +import type { + ProjectId, + SheetId, + SheetInstanceId, +} from '@theatre/shared/utils/ids' import {validateInstanceId} from '@theatre/shared/utils/sanitizers' import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths' import type {$IntentionalAny} from '@theatre/shared/utils/types' @@ -58,7 +63,7 @@ export default class TheatreProject implements IProject { * @internal */ constructor(id: string, config: IProjectConfig = {}) { - setPrivateAPI(this, new Project(id, config, this)) + setPrivateAPI(this, new Project(id as ProjectId, config, this)) } get ready(): Promise { @@ -87,7 +92,9 @@ export default class TheatreProject implements IProject { ) } - return privateAPI(this).getOrCreateSheet(sanitizedPath, instanceId) - .publicApi + return privateAPI(this).getOrCreateSheet( + sanitizedPath as SheetId, + instanceId as SheetInstanceId, + ).publicApi } } diff --git a/theatre/core/src/projects/projectsSingleton.ts b/theatre/core/src/projects/projectsSingleton.ts index 57cae75..8999632 100644 --- a/theatre/core/src/projects/projectsSingleton.ts +++ b/theatre/core/src/projects/projectsSingleton.ts @@ -1,8 +1,9 @@ import {Atom} from '@theatre/dataverse' +import type {ProjectId} from '@theatre/shared/utils/ids' import type Project from './Project' interface State { - projects: Record + projects: Record } class ProjectsSingleton { @@ -12,15 +13,15 @@ class ProjectsSingleton { /** * We're trusting here that each project id is unique */ - add(id: string, project: Project) { + add(id: ProjectId, project: Project) { this.atom.reduceState(['projects', id], () => project) } - get(id: string): Project | undefined { + get(id: ProjectId): Project | undefined { return this.atom.getState().projects[id] } - has(id: string) { + has(id: ProjectId) { return !!this.get(id) } } diff --git a/theatre/core/src/projects/store/storeTypes.ts b/theatre/core/src/projects/store/storeTypes.ts index fea7699..8596159 100644 --- a/theatre/core/src/projects/store/storeTypes.ts +++ b/theatre/core/src/projects/store/storeTypes.ts @@ -1,3 +1,4 @@ +import type {SheetId} from '@theatre/shared/utils/ids' import type {StrictRecord} from '@theatre/shared/utils/types' import type {SheetState_Historic} from './types/SheetState_Historic' @@ -31,7 +32,7 @@ export interface ProjectEphemeralState { * at {@link StudioHistoricState.coreByProject} */ export interface ProjectState_Historic { - sheetsById: StrictRecord + sheetsById: StrictRecord /** * The last 50 revision IDs this state is based on, starting with the most recent one. * The most recent one is the revision ID of this state diff --git a/theatre/core/src/projects/store/types/SheetState_Historic.ts b/theatre/core/src/projects/store/types/SheetState_Historic.ts index f436d18..d6c4159 100644 --- a/theatre/core/src/projects/store/types/SheetState_Historic.ts +++ b/theatre/core/src/projects/store/types/SheetState_Historic.ts @@ -1,5 +1,13 @@ -import type {KeyframeId, SequenceTrackId} from '@theatre/shared/utils/ids' -import type {SerializableMap, StrictRecord} from '@theatre/shared/utils/types' +import type { + KeyframeId, + ObjectAddressKey, + SequenceTrackId, +} from '@theatre/shared/utils/ids' +import type { + SerializableMap, + SerializableValue, + StrictRecord, +} from '@theatre/shared/utils/types' export interface SheetState_Historic { /** @@ -10,14 +18,13 @@ export interface SheetState_Historic { * of another state, it will be able to inherit the overrides from ancestor states. */ staticOverrides: { - byObject: StrictRecord + byObject: StrictRecord } - sequence?: Sequence + sequence?: HistoricPositionalSequence } -type Sequence = PositionalSequence - -type PositionalSequence = { +// Question: What is this? The timeline position of a sequence? +export type HistoricPositionalSequence = { type: 'PositionalSequence' length: number /** @@ -27,7 +34,7 @@ type PositionalSequence = { subUnitsPerUnit: number tracksByObject: StrictRecord< - string, + ObjectAddressKey, { trackIdByPropPath: StrictRecord trackData: StrictRecord @@ -39,7 +46,10 @@ export type TrackData = BasicKeyframedTrack export type Keyframe = { id: KeyframeId - value: unknown + /** The `value` is the raw value type such as `Rgba` or `number`. See {@link SerializableValue} */ + // Future: is there another layer that we may need to be able to store older values on the + // case of a prop config change? As keyframes can technically have their propConfig changed. + value: SerializableValue position: number handles: [leftX: number, leftY: number, rightX: number, rightY: number] connectedRight: boolean @@ -47,5 +57,9 @@ export type Keyframe = { export type BasicKeyframedTrack = { type: 'BasicKeyframedTrack' + /** + * {@link Keyframe} is not provided an explicit generic value `T`, because + * a single track can technically have multiple different types for each keyframe. + */ keyframes: Keyframe[] } diff --git a/theatre/core/src/propTypes/index.ts b/theatre/core/src/propTypes/index.ts index b3a1cdf..cb36c64 100644 --- a/theatre/core/src/propTypes/index.ts +++ b/theatre/core/src/propTypes/index.ts @@ -10,8 +10,8 @@ import { } from '@theatre/shared/utils/color' import {clamp, mapValues} from 'lodash-es' import type { - IShorthandCompoundProps, - IValidCompoundProps, + UnknownShorthandCompoundProps, + UnknownValidCompoundProps, ShorthandCompoundPropsToLonghandCompoundProps, } from './internals' import {propTypeSymbol, sanitizeCompoundProps} from './internals' @@ -88,7 +88,7 @@ const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => { * ``` * */ -export const compound = ( +export const compound = ( props: Props, opts: CommonOpts = {}, ): PropTypeConfig_Compound< @@ -101,7 +101,7 @@ export const compound = ( ShorthandCompoundPropsToLonghandCompoundProps > = { type: 'compound', - props: sanitizedProps, + props: sanitizedProps as $IntentionalAny, valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', label: opts.label, @@ -690,7 +690,7 @@ export interface PropTypeConfig_StringLiteral export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {} -type DeepPartialCompound = { +type DeepPartialCompound = { [K in keyof Props]?: DeepPartial } @@ -701,13 +701,14 @@ type DeepPartial = ? DeepPartialCompound : never -export interface PropTypeConfig_Compound - extends IBasePropType< +export interface PropTypeConfig_Compound< + Props extends UnknownValidCompoundProps, +> extends IBasePropType< 'compound', {[K in keyof Props]: Props[K]['valueType']}, DeepPartialCompound > { - props: Record + props: Record } export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> { diff --git a/theatre/core/src/propTypes/internals.ts b/theatre/core/src/propTypes/internals.ts index 1c5c674..32d0457 100644 --- a/theatre/core/src/propTypes/internals.ts +++ b/theatre/core/src/propTypes/internals.ts @@ -13,7 +13,7 @@ import * as t from './index' export const propTypeSymbol = Symbol('TheatrePropType_Basic') -export type IValidCompoundProps = { +export type UnknownValidCompoundProps = { [K in string]: PropTypeConfig } @@ -27,18 +27,19 @@ export type IValidCompoundProps = { * which would allow us to differentiate between values at runtime * (e.g. `val.type = "Rgba"` vs `val.type = "Compound"` etc) */ -type IShorthandProp = +type UnknownShorthandProp = | string | number | boolean | PropTypeConfig - | IShorthandCompoundProps + | UnknownShorthandCompoundProps -export type IShorthandCompoundProps = { - [K in string]: IShorthandProp +/** Given an object like this, we have enough info to predict the compound prop */ +export type UnknownShorthandCompoundProps = { + [K in string]: UnknownShorthandProp } -export type ShorthandPropToLonghandProp

= +export type ShorthandPropToLonghandProp

= P extends string ? PropTypeConfig_String : P extends number @@ -47,12 +48,31 @@ export type ShorthandPropToLonghandProp

= ? PropTypeConfig_Boolean : P extends PropTypeConfig ? P - : P extends IShorthandCompoundProps + : P extends UnknownShorthandCompoundProps ? PropTypeConfig_Compound> : never +export type ShorthandCompoundPropsToInitialValue< + P extends UnknownShorthandCompoundProps, +> = LonghandCompoundPropsToInitialValue< + ShorthandCompoundPropsToLonghandCompoundProps

+> + +type LonghandCompoundPropsToInitialValue

= + { + [K in keyof P]: P[K]['valueType'] + } + +export type PropsValue

= P extends UnknownValidCompoundProps + ? LonghandCompoundPropsToInitialValue

+ : P extends UnknownShorthandCompoundProps + ? LonghandCompoundPropsToInitialValue< + ShorthandCompoundPropsToLonghandCompoundProps

+ > + : never + export type ShorthandCompoundPropsToLonghandCompoundProps< - P extends IShorthandCompoundProps, + P extends UnknownShorthandCompoundProps, > = { [K in keyof P]: ShorthandPropToLonghandProp } @@ -89,9 +109,9 @@ export function toLonghandProp(p: unknown): PropTypeConfig { } export function sanitizeCompoundProps( - props: IShorthandCompoundProps, -): IValidCompoundProps { - const sanitizedProps: IValidCompoundProps = {} + props: UnknownShorthandCompoundProps, +): UnknownValidCompoundProps { + const sanitizedProps: UnknownValidCompoundProps = {} if (process.env.NODE_ENV !== 'production') { if (typeof props !== 'object' || !props) { throw new InvalidArgumentError( diff --git a/theatre/core/src/sequences/interpolationTripleAtPosition.ts b/theatre/core/src/sequences/interpolationTripleAtPosition.ts index dd663c4..55f9627 100644 --- a/theatre/core/src/sequences/interpolationTripleAtPosition.ts +++ b/theatre/core/src/sequences/interpolationTripleAtPosition.ts @@ -6,11 +6,15 @@ import type { import type {IDerivation, Pointer} from '@theatre/dataverse' import {ConstantDerivation, prism, val} from '@theatre/dataverse' import logger from '@theatre/shared/logger' +import type {SerializableValue} from '@theatre/shared/utils/types' import UnitBezier from 'timing-function/lib/UnitBezier' +/** `left` and `right` are not necessarily the same type. */ export type InterpolationTriple = { - left: unknown - right?: unknown + /** `left` and `right` are not necessarily the same type. */ + left: SerializableValue + /** `left` and `right` are not necessarily the same type. */ + right?: SerializableValue progression: number } @@ -75,10 +79,10 @@ function _forKeyframedTrack( const undefinedConstD = new ConstantDerivation(undefined) -const updateState = ( +function updateState( progressionD: IDerivation, track: BasicKeyframedTrack, -): IStartedState => { +): IStartedState { const progression = progressionD.getValue() if (track.keyframes.length === 0) { return { diff --git a/theatre/core/src/sheetObjects/SheetObject.test.ts b/theatre/core/src/sheetObjects/SheetObject.test.ts index 59b6edc..4d9d214 100644 --- a/theatre/core/src/sheetObjects/SheetObject.test.ts +++ b/theatre/core/src/sheetObjects/SheetObject.test.ts @@ -4,18 +4,19 @@ import {setupTestSheet} from '@theatre/shared/testUtils' import {encodePathToProp} from '@theatre/shared/utils/addresses' import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids' +import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids' import {iterateOver, prism} from '@theatre/dataverse' import type {SheetState_Historic} from '@theatre/core/projects/store/types/SheetState_Historic' describe(`SheetObject`, () => { describe('static overrides', () => { const setup = async ( - staticOverrides: SheetState_Historic['staticOverrides']['byObject'][string] = {}, + staticOverrides: SheetState_Historic['staticOverrides']['byObject'][ObjectAddressKey] = {}, ) => { const {studio, objPublicAPI} = await setupTestSheet({ staticOverrides: { byObject: { - obj: staticOverrides, + ['obj' as ObjectAddressKey]: staticOverrides, }, }, }) @@ -260,12 +261,12 @@ describe(`SheetObject`, () => { length: 20, subUnitsPerUnit: 30, tracksByObject: { - obj: { + ['obj' as ObjectAddressKey]: { trackIdByPropPath: { [encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'), }, trackData: { - '1': { + ['1' as SequenceTrackId]: { type: 'BasicKeyframedTrack', keyframes: [ { diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 22b37ef..319de7a 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -9,6 +9,7 @@ import SimpleCache from '@theatre/shared/utils/SimpleCache' import type { $FixMe, $IntentionalAny, + DeepPartialOfSerializableValue, SerializableMap, SerializableValue, } from '@theatre/shared/utils/types' @@ -25,14 +26,29 @@ import TheatreSheetObject from './TheatreSheetObject' import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes' import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' +/** + * Internally, the sheet's actual configured value is not a specific type, since we + * can change the prop config at will, as such this is an alias of {@link SerializableMap}. + * + * TODO: Incorporate this knowledge into SheetObject & TemplateSheetObject + */ +type SheetObjectPropsValue = SerializableMap + +/** + * An object on a sheet consisting of zero or more properties which can + * be overridden statically or overridden by being sequenced. + * + * Note that this cannot be generic over `Props`, since the user is + * able to change prop configs for the sheet object's properties. + */ export default class SheetObject implements IdentityDerivationProvider { get type(): 'Theatre_SheetObject' { return 'Theatre_SheetObject' } readonly $$isIdentityDerivationProvider: true = true readonly address: SheetObjectAddress - readonly publicApi: TheatreSheetObject<$IntentionalAny> - private readonly _initialValue = new Atom({}) + readonly publicApi: TheatreSheetObject + private readonly _initialValue = new Atom({}) private readonly _cache = new SimpleCache() constructor( @@ -48,7 +64,7 @@ export default class SheetObject implements IdentityDerivationProvider { this.publicApi = new TheatreSheetObject(this) } - getValues(): IDerivation> { + getValues(): IDerivation> { return this._cache.get('getValues()', () => prism(() => { const defaults = val(this.template.getDefaultValues()) @@ -101,7 +117,7 @@ export default class SheetObject implements IdentityDerivationProvider { final = withSeqs } - const a = valToAtom('finalAtom', final) + const a = valToAtom('finalAtom', final) return a.pointer }), @@ -131,7 +147,7 @@ export default class SheetObject implements IdentityDerivationProvider { /** * Returns values of props that are sequenced. */ - getSequencedValues(): IDerivation> { + getSequencedValues(): IDerivation> { return prism(() => { const tracksToProcessD = prism.memo( 'tracksToProcess', @@ -140,7 +156,7 @@ export default class SheetObject implements IdentityDerivationProvider { ) const tracksToProcess = val(tracksToProcessD) - const valsAtom = new Atom({}) + const valsAtom = new Atom({}) prism.effect( 'processTracks', @@ -216,17 +232,20 @@ export default class SheetObject implements IdentityDerivationProvider { return interpolationTripleAtPosition(trackP, timeD) } - get propsP(): Pointer<$FixMe> { + get propsP(): Pointer { return this._cache.get('propsP', () => pointer<{props: {}}>({root: this, path: []}), ) as $FixMe } - validateValue(pointer: Pointer<$FixMe>, value: unknown) { + validateValue( + pointer: Pointer, + value: DeepPartialOfSerializableValue, + ) { // @todo } - setInitialValue(val: SerializableMap) { + setInitialValue(val: DeepPartialOfSerializableValue) { this.validateValue(this.propsP, val) this._initialValue.setState(val) } diff --git a/theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts b/theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts index 550b2ed..e28a61d 100644 --- a/theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts +++ b/theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts @@ -4,6 +4,7 @@ import {setupTestSheet} from '@theatre/shared/testUtils' import {encodePathToProp} from '@theatre/shared/utils/addresses' import {asSequenceTrackId} from '@theatre/shared/utils/ids' +import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids' import type {$IntentionalAny} from '@theatre/shared/utils/types' import {iterateOver} from '@theatre/dataverse' @@ -19,15 +20,15 @@ describe(`SheetObjectTemplate`, () => { subUnitsPerUnit: 30, length: 10, tracksByObject: { - obj: { + ['obj' as ObjectAddressKey]: { trackIdByPropPath: { [encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'), [encodePathToProp(['position', 'invalid'])]: asSequenceTrackId('invalidTrack'), }, trackData: { - x: null as $IntentionalAny, - invalid: null as $IntentionalAny, + ['x' as SequenceTrackId]: null as $IntentionalAny, + ['invalid' as SequenceTrackId]: null as $IntentionalAny, }, }, }, @@ -74,15 +75,15 @@ describe(`SheetObjectTemplate`, () => { subUnitsPerUnit: 30, length: 10, tracksByObject: { - obj: { + ['obj' as ObjectAddressKey]: { trackIdByPropPath: { [encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'), [encodePathToProp(['position', 'invalid'])]: asSequenceTrackId('invalidTrack'), }, trackData: { - x: null as $IntentionalAny, - invalid: null as $IntentionalAny, + ['x' as SequenceTrackId]: null as $IntentionalAny, + ['invalid' as SequenceTrackId]: null as $IntentionalAny, }, }, }, diff --git a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts index dee21fd..64f7307 100644 --- a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts +++ b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts @@ -1,7 +1,7 @@ import type Project from '@theatre/core/projects/Project' import type Sheet from '@theatre/core/sheets/Sheet' import type SheetTemplate from '@theatre/core/sheets/SheetTemplate' -import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet' +import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet' import {emptyArray} from '@theatre/shared/utils' import type { PathToProp, @@ -9,7 +9,7 @@ import type { WithoutSheetInstance, } from '@theatre/shared/utils/addresses' import getDeep from '@theatre/shared/utils/getDeep' -import type {SequenceTrackId} from '@theatre/shared/utils/ids' +import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids' import SimpleCache from '@theatre/shared/utils/SimpleCache' import type { $FixMe, @@ -23,7 +23,6 @@ import set from 'lodash-es/set' import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject' import SheetObject from './SheetObject' import logger from '@theatre/shared/logger' -import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import { getPropConfigByPath, isPropConfSequencable, @@ -34,12 +33,15 @@ export type IPropPathToTrackIdTree = { [key in string]?: SequenceTrackId | IPropPathToTrackIdTree } +/** + * TODO: Add documentation, and share examples of sheet objects. + * + * See {@link SheetObject} for more information. + */ export default class SheetObjectTemplate { readonly address: WithoutSheetInstance readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate' - protected _config: Atom< - SheetObjectConfig> - > + protected _config: Atom readonly _cache = new SimpleCache() readonly project: Project @@ -49,9 +51,9 @@ export default class SheetObjectTemplate { constructor( readonly sheetTemplate: SheetTemplate, - objectKey: string, + objectKey: ObjectAddressKey, nativeObject: unknown, - config: SheetObjectConfig<$IntentionalAny>, + config: SheetObjectPropTypeConfig, ) { this.address = {...sheetTemplate.address, objectKey} this._config = new Atom(config) @@ -61,13 +63,13 @@ export default class SheetObjectTemplate { createInstance( sheet: Sheet, nativeObject: unknown, - config: SheetObjectConfig<$IntentionalAny>, + config: SheetObjectPropTypeConfig, ): SheetObject { this._config.setState(config) return new SheetObject(sheet, this, nativeObject) } - overrideConfig(config: SheetObjectConfig<$IntentionalAny>) { + overrideConfig(config: SheetObjectPropTypeConfig) { this._config.setState(config) } @@ -99,7 +101,7 @@ export default class SheetObjectTemplate { pointerToSheetState.staticOverrides.byObject[ this.address.objectKey ], - ) || {} + ) ?? {} const config = val(this._config.pointer) const deserialized = config.deserializeAndSanitize(json) || {} diff --git a/theatre/core/src/sheetObjects/TheatreSheetObject.ts b/theatre/core/src/sheetObjects/TheatreSheetObject.ts index 1c2e1ed..0e6dd56 100644 --- a/theatre/core/src/sheetObjects/TheatreSheetObject.ts +++ b/theatre/core/src/sheetObjects/TheatreSheetObject.ts @@ -13,11 +13,13 @@ import type {IDerivation, Pointer} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse' import type SheetObject from './SheetObject' import type { - IShorthandCompoundProps, - ShorthandPropToLonghandProp, + UnknownShorthandCompoundProps, + PropsValue, } from '@theatre/core/propTypes/internals' -export interface ISheetObject { +export interface ISheetObject< + Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps, +> { /** * All Objects will have `object.type === 'Theatre_SheetObject_PublicAPI'` */ @@ -32,8 +34,14 @@ export interface ISheetObject { * const obj = sheet.object("obj", {x: 0}) * console.log(obj.value.x) // prints 0 or the current numeric value * ``` + * + * Future: Notice that if the user actually changes the Props config for one of the + * properties, then this type can't be guaranteed accurrate. + * * Right now the user can't change prop configs, but we'll probably enable that + * functionality later via (`object.overrideConfig()`). We need to educate the + * user that they can't rely on static types to know the type of object.value. */ - readonly value: ShorthandPropToLonghandProp['valueType'] + readonly value: PropsValue /** * A Pointer to the props of the object. @@ -100,7 +108,7 @@ export interface ISheetObject { } export default class TheatreSheetObject< - Props extends IShorthandCompoundProps = {}, + Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps, > implements ISheetObject { get type(): 'Theatre_SheetObject_PublicAPI' { @@ -134,7 +142,7 @@ export default class TheatreSheetObject< private _valuesDerivation(): IDerivation { return this._cache.get('onValuesChangeDerivation', () => { const sheetObject = privateAPI(this) - const d: IDerivation = prism(() => { + const d: IDerivation> = prism(() => { return val(sheetObject.getValues().getValue()) as $FixMe }) return d @@ -145,7 +153,7 @@ export default class TheatreSheetObject< return this._valuesDerivation().tapImmediate(coreTicker, fn) } - get value(): ShorthandPropToLonghandProp['valueType'] { + get value(): PropsValue { return this._valuesDerivation().getValue() } diff --git a/theatre/core/src/sheetObjects/getPropDefaultsOfSheetObject.ts b/theatre/core/src/sheetObjects/getPropDefaultsOfSheetObject.ts index 10ac177..6abad0d 100644 --- a/theatre/core/src/sheetObjects/getPropDefaultsOfSheetObject.ts +++ b/theatre/core/src/sheetObjects/getPropDefaultsOfSheetObject.ts @@ -1,7 +1,6 @@ -import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet' +import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet' import type { $FixMe, - $IntentionalAny, SerializableMap, SerializableValue, } from '@theatre/shared/utils/types' @@ -17,9 +16,9 @@ const cachedDefaults = new WeakMap() * Generates and caches a default value for the config of a SheetObject. */ export default function getPropDefaultsOfSheetObject( - config: SheetObjectConfig<$IntentionalAny>, + config: SheetObjectPropTypeConfig, ): SerializableMap { - return getDefaultsOfPropTypeConfig(config) as $IntentionalAny + return getDefaultsOfPropTypeConfig(config) as SerializableMap // sheet objects result in non-primitive objects } function getDefaultsOfPropTypeConfig( diff --git a/theatre/core/src/sheets/Sheet.ts b/theatre/core/src/sheets/Sheet.ts index 199357f..c6a0c6f 100644 --- a/theatre/core/src/sheets/Sheet.ts +++ b/theatre/core/src/sheets/Sheet.ts @@ -1,17 +1,28 @@ import type Project from '@theatre/core/projects/Project' import Sequence from '@theatre/core/sequences/Sequence' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' -import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet' +import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet' import TheatreSheet from '@theatre/core/sheets/TheatreSheet' import type {SheetAddress} from '@theatre/shared/utils/addresses' -import type {$IntentionalAny} from '@theatre/shared/utils/types' import {Atom, valueDerivation} from '@theatre/dataverse' import type SheetTemplate from './SheetTemplate' +import type {ObjectAddressKey, SheetInstanceId} from '@theatre/shared/utils/ids' +import type {StrictRecord} from '@theatre/shared/utils/types' -type IObjects = {[key: string]: SheetObject} +type SheetObjectMap = StrictRecord + +/** + * Future: `nativeObject` Idea is to potentially allow the user to provide their own + * object in to the object call as a way to keep a handle to an underlying object via + * the {@link ISheetObject}. + * + * For example, a THREEjs object or an HTMLElement is passed in. + */ +export type ObjectNativeObject = unknown export default class Sheet { - private readonly _objects: Atom = new Atom({}) + private readonly _objects: Atom = + new Atom({}) private _sequence: undefined | Sequence readonly address: SheetAddress readonly publicApi: TheatreSheet @@ -21,7 +32,7 @@ export default class Sheet { constructor( readonly template: SheetTemplate, - public readonly instanceId: string, + public readonly instanceId: SheetInstanceId, ) { this.project = template.project this.address = { @@ -37,24 +48,24 @@ export default class Sheet { * with that of "an element." */ createObject( - key: string, - nativeObject: unknown, - config: SheetObjectConfig<$IntentionalAny>, + objectKey: ObjectAddressKey, + nativeObject: ObjectNativeObject, + config: SheetObjectPropTypeConfig, ): SheetObject { const objTemplate = this.template.getObjectTemplate( - key, + objectKey, nativeObject, config, ) const object = objTemplate.createInstance(this, nativeObject, config) - this._objects.setIn([key], object) + this._objects.setIn([objectKey], object) return object } - getObject(key: string): SheetObject | undefined { + getObject(key: ObjectAddressKey): SheetObject | undefined { return this._objects.getState()[key] } diff --git a/theatre/core/src/sheets/SheetTemplate.ts b/theatre/core/src/sheets/SheetTemplate.ts index 502c7ff..cae1cda 100644 --- a/theatre/core/src/sheets/SheetTemplate.ts +++ b/theatre/core/src/sheets/SheetTemplate.ts @@ -4,27 +4,38 @@ import type { SheetAddress, WithoutSheetInstance, } from '@theatre/shared/utils/addresses' -import type {$IntentionalAny} from '@theatre/shared/utils/types' import {Atom} from '@theatre/dataverse' +import type {Pointer} from '@theatre/dataverse' import Sheet from './Sheet' -import type {SheetObjectConfig} from './TheatreSheet' +import type {ObjectNativeObject} from './Sheet' +import type {SheetObjectPropTypeConfig} from './TheatreSheet' +import type { + ObjectAddressKey, + SheetId, + SheetInstanceId, +} from '@theatre/shared/utils/ids' +import type {StrictRecord} from '@theatre/shared/utils/types' + +type SheetTemplateObjectTemplateMap = StrictRecord< + ObjectAddressKey, + SheetObjectTemplate +> export default class SheetTemplate { readonly type: 'Theatre_SheetTemplate' = 'Theatre_SheetTemplate' readonly address: WithoutSheetInstance - private _instances = new Atom<{[instanceId: string]: Sheet}>({}) - readonly instancesP = this._instances.pointer + private _instances = new Atom>({}) + readonly instancesP: Pointer> = + this._instances.pointer - private _objectTemplates = new Atom<{ - [objectKey: string]: SheetObjectTemplate - }>({}) + private _objectTemplates = new Atom({}) readonly objectTemplatesP = this._objectTemplates.pointer - constructor(readonly project: Project, sheetId: string) { + constructor(readonly project: Project, sheetId: SheetId) { this.address = {...project.address, sheetId} } - getInstance(instanceId: string): Sheet { + getInstance(instanceId: SheetInstanceId): Sheet { let inst = this._instances.getState()[instanceId] if (!inst) { @@ -36,15 +47,15 @@ export default class SheetTemplate { } getObjectTemplate( - key: string, - nativeObject: unknown, - config: SheetObjectConfig<$IntentionalAny>, + objectKey: ObjectAddressKey, + nativeObject: ObjectNativeObject, + config: SheetObjectPropTypeConfig, ): SheetObjectTemplate { - let template = this._objectTemplates.getState()[key] + let template = this._objectTemplates.getState()[objectKey] if (!template) { - template = new SheetObjectTemplate(this, key, nativeObject, config) - this._objectTemplates.setIn([key], template) + template = new SheetObjectTemplate(this, objectKey, nativeObject, config) + this._objectTemplates.setIn([objectKey], template) } return template diff --git a/theatre/core/src/sheets/TheatreSheet.ts b/theatre/core/src/sheets/TheatreSheet.ts index f83288b..f0940e4 100644 --- a/theatre/core/src/sheets/TheatreSheet.ts +++ b/theatre/core/src/sheets/TheatreSheet.ts @@ -9,15 +9,18 @@ import type Sheet from '@theatre/core/sheets/Sheet' import type {SheetAddress} from '@theatre/shared/utils/addresses' import {InvalidArgumentError} from '@theatre/shared/utils/errors' import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths' -import type {$IntentionalAny} from '@theatre/shared/utils/types' +import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types' import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' import deepEqual from 'fast-deep-equal' -import type {IShorthandCompoundProps} from '@theatre/core/propTypes/internals' +import type { + UnknownShorthandCompoundProps, + UnknownValidCompoundProps, +} from '@theatre/core/propTypes/internals' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import type {ObjectAddressKey} from '@theatre/shared/utils/ids' -export type SheetObjectConfig< - Props extends PropTypeConfig_Compound<$IntentionalAny>, -> = Props +export type SheetObjectPropTypeConfig = + PropTypeConfig_Compound export interface ISheet { /** @@ -62,7 +65,7 @@ export interface ISheet { * obj.value.position // {x: 0, y: 0} * ``` */ - object( + object( key: string, props: Props, ): ISheetObject @@ -75,7 +78,7 @@ export interface ISheet { const weakMapOfUnsanitizedProps = new WeakMap< SheetObject, - IShorthandCompoundProps + UnknownShorthandCompoundProps >() export default class TheatreSheet implements ISheet { @@ -89,7 +92,7 @@ export default class TheatreSheet implements ISheet { setPrivateAPI(this, sheet) } - object( + object( key: string, config: Props, ): ISheetObject { @@ -99,8 +102,15 @@ export default class TheatreSheet implements ISheet { `sheet.object("${key}", ...)`, ) - const existingObject = internal.getObject(sanitizedPath) + const existingObject = internal.getObject(sanitizedPath as ObjectAddressKey) + /** + * Future: `nativeObject` Idea is to potentially allow the user to provide their own + * object in to the object call as a way to keep a handle to an underlying object via + * the {@link ISheetObject}. + * + * For example, a THREEjs object or an HTMLElement is passed in. + */ const nativeObject = null if (existingObject) { @@ -121,12 +131,12 @@ export default class TheatreSheet implements ISheet { } else { const sanitizedConfig = compound(config) const object = internal.createObject( - sanitizedPath, + sanitizedPath as ObjectAddressKey, nativeObject, sanitizedConfig, ) if (process.env.NODE_ENV !== 'production') { - weakMapOfUnsanitizedProps.set(object, config) + weakMapOfUnsanitizedProps.set(object as $FixMe, config) } return object.publicApi as $IntentionalAny } diff --git a/theatre/shared/src/testUtils.ts b/theatre/shared/src/testUtils.ts index 89c1cd3..cbf01c1 100644 --- a/theatre/shared/src/testUtils.ts +++ b/theatre/shared/src/testUtils.ts @@ -8,6 +8,7 @@ import * as t from '@theatre/core/propTypes' import getStudio from '@theatre/studio/getStudio' import coreTicker from '@theatre/core/coreTicker' import globals from './globals' +import type {SheetId} from './utils/ids' /* eslint-enable no-restricted-syntax */ const defaultProps = { @@ -32,7 +33,7 @@ export async function setupTestSheet(sheetState: SheetState_Historic) { const projectState: ProjectState_Historic = { definitionVersion: globals.currentProjectStateDefinitionVersion, sheetsById: { - Sheet: sheetState, + ['Sheet' as SheetId]: sheetState, }, revisionHistory: [], } diff --git a/theatre/shared/src/utils/Nominal.ts b/theatre/shared/src/utils/Nominal.ts new file mode 100644 index 0000000..bc476ce --- /dev/null +++ b/theatre/shared/src/utils/Nominal.ts @@ -0,0 +1,37 @@ +/** + * Using a symbol, we can sort of add unique properties to arbitrary other types. + * So, we use this to our advantage to add a "marker" of information to strings using + * the {@link Nominal} type. + * + * Can be used with keys in pointers. + * This identifier shows in the expanded {@link Nominal} as `string & {[nominal]:"SequenceTrackId"}`, + * So, we're opting to keeping the identifier short. + */ +const nominal = Symbol() + +/** + * This creates an "opaque"/"nominal" type. + * + * Our primary use case is to be able to use with keys in pointers. + * + * Numbers cannot be added together if they are "nominal" + * + * See {@link nominal} for more details. + */ +export type Nominal = string & {[nominal]: N} + +declare global { + // Fix Object.entries and Object.keys definitions for Nominal strict records + interface ObjectConstructor { + /** Nominal: Extension to the Object prototype definition to properly manage {@link Nominal} keyed records */ + keys, any>>( + obj: T, + ): any extends T ? never[] : Extract[] + /** Nominal: Extension to the Object prototype definition to properly manage {@link Nominal} keyed records */ + entries, any>>( + obj: T, + ): any extends T + ? [never, never][] + : Array<{[P in keyof T]: [P, T[P]]}[Extract]> + } +} diff --git a/theatre/shared/src/utils/addresses.ts b/theatre/shared/src/utils/addresses.ts index 8951036..584bcca 100644 --- a/theatre/shared/src/utils/addresses.ts +++ b/theatre/shared/src/utils/addresses.ts @@ -3,12 +3,13 @@ import type { SerializableMap, SerializableValue, } from '@theatre/shared/utils/types' +import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids' /** * Represents the address to a project */ export interface ProjectAddress { - projectId: string + projectId: ProjectId } /** @@ -22,8 +23,8 @@ export interface ProjectAddress { * ``` */ export interface SheetAddress extends ProjectAddress { - sheetId: string - sheetInstanceId: string + sheetId: SheetId + sheetInstanceId: SheetInstanceId } /** @@ -36,7 +37,7 @@ export type WithoutSheetInstance = Omit< > export type SheetInstanceOptional = - WithoutSheetInstance & {sheetInstanceId?: string | undefined} + WithoutSheetInstance & {sheetInstanceId?: SheetInstanceId | undefined} /** * Represents the address to a Sheet's Object @@ -51,7 +52,7 @@ export interface SheetObjectAddress extends SheetAddress { * obj.address.objectKey === 'foo' * ``` */ - objectKey: string + objectKey: ObjectAddressKey } export type PathToProp = Array diff --git a/theatre/shared/src/utils/ids.ts b/theatre/shared/src/utils/ids.ts index 33442f2..280612c 100644 --- a/theatre/shared/src/utils/ids.ts +++ b/theatre/shared/src/utils/ids.ts @@ -1,9 +1,10 @@ import {nanoid as generateNonSecure} from 'nanoid/non-secure' -import type {$IntentionalAny, Nominal} from './types' +import type {Nominal} from './Nominal' +import type {$IntentionalAny} from './types' -export type KeyframeId = Nominal +export type KeyframeId = Nominal<'KeyframeId'> -export function generateKeyframeId() { +export function generateKeyframeId(): KeyframeId { return generateNonSecure(10) as KeyframeId } @@ -11,10 +12,16 @@ export function asKeyframeId(s: string): KeyframeId { return s as $IntentionalAny } -// @todo make nominal -export type SequenceTrackId = string +export type ProjectId = Nominal<'ProjectId'> +export type SheetId = Nominal<'SheetId'> +export type SheetInstanceId = Nominal<'SheetInstanceId'> +export type PaneInstanceId = Nominal<'PaneInstanceId'> +export type SequenceTrackId = Nominal<'SequenceTrackId'> +export type ObjectAddressKey = Nominal<'ObjectAddressKey'> +/** UI panels can contain a {@link PaneInstanceId} or something else. */ +export type UIPanelId = Nominal<'UIPanelId'> -export function generateSequenceTrackId() { +export function generateSequenceTrackId(): SequenceTrackId { return generateNonSecure(10) as $IntentionalAny as SequenceTrackId } diff --git a/theatre/shared/src/utils/pointerDeep.ts b/theatre/shared/src/utils/pointerDeep.ts index 55a0db9..4369b9d 100644 --- a/theatre/shared/src/utils/pointerDeep.ts +++ b/theatre/shared/src/utils/pointerDeep.ts @@ -2,11 +2,11 @@ import type {Pointer} from '@theatre/dataverse' import type {PathToProp} from './addresses' import type {$IntentionalAny} from './types' -export default function pointerDeep( - base: Pointer<$IntentionalAny>, +export default function pointerDeep( + base: Pointer, toAppend: PathToProp, ): Pointer { - let p = base + let p = base as $IntentionalAny for (const k of toAppend) { p = p[k] } diff --git a/theatre/shared/src/utils/types.ts b/theatre/shared/src/utils/types.ts index 0432660..fc2711b 100644 --- a/theatre/shared/src/utils/types.ts +++ b/theatre/shared/src/utils/types.ts @@ -44,6 +44,13 @@ export type SerializablePrimitive = | boolean | {r: number; g: number; b: number; a: number} +/** + * This type represents all values that can be safely serialized. + * Also, it's notable that this type is compatible for dataverse pointer traversal (everything + * is path accessible [e.g. `a.b.c`]). + * + * One example usage is for keyframe values or static overrides such as `Rgba`, `string`, `number`, and "compound values". + */ export type SerializableValue< Primitives extends SerializablePrimitive = SerializablePrimitive, > = Primitives | SerializableMap @@ -57,15 +64,13 @@ export type DeepPartialOfSerializableValue = } : T -export type StrictRecord = {[K in Key]?: V} - /** - * This is supposed to create an "opaque" or "nominal" type, but since typescript - * doesn't allow generic index signatures, we're leaving it be. + * This is equivalent to `Partial>` being used to describe a sort of Map + * where the keys might not have values. * - * TODO fix this once https://github.com/microsoft/TypeScript/pull/26797 lands (likely typescript 4.4) + * We do not use `Map`s, because they add comlpexity with converting to `JSON.stringify` + pointer types */ -export type Nominal = T +export type StrictRecord = {[K in Key]?: V} /** * TODO: We should deprecate this and just use `[start: number, end: number]` @@ -81,4 +86,4 @@ export type $IntentionalAny = any * Represents the `x` or `y` value of getBoundingClientRect(). * In other words, represents a distance from 0,0 in screen space. */ -export type PositionInScreenSpace = Nominal +export type PositionInScreenSpace = number diff --git a/theatre/studio/src/PaneManager.ts b/theatre/studio/src/PaneManager.ts index 94c2a20..af7071f 100644 --- a/theatre/studio/src/PaneManager.ts +++ b/theatre/studio/src/PaneManager.ts @@ -1,7 +1,8 @@ import {prism, val} from '@theatre/dataverse' import {emptyArray} from '@theatre/shared/utils' +import type {PaneInstanceId} from '@theatre/shared/utils/ids' import SimpleCache from '@theatre/shared/utils/SimpleCache' -import type {$IntentionalAny} from '@theatre/shared/utils/types' +import type {$IntentionalAny, StrictRecord} from '@theatre/shared/utils/types' import type {Studio} from './Studio' import type {PaneInstance} from './TheatreStudio' @@ -21,7 +22,7 @@ export default class PaneManager { private _getAllPanes() { return this._cache.get('_getAllPanels()', () => - prism((): {[instanceId in string]?: PaneInstance} => { + prism((): StrictRecord> => { const core = val(this._studio.coreP) if (!core) return {} const instanceDescriptors = val( @@ -31,17 +32,16 @@ export default class PaneManager { this._studio.atomP.ephemeral.extensions.paneClasses, ) - const instances: {[instanceId in string]?: PaneInstance} = {} - for (const [, instanceDescriptor] of Object.entries( - instanceDescriptors, - )) { - const panelClass = paneClasses[instanceDescriptor!.paneClass] + const instances: StrictRecord> = {} + for (const instanceDescriptor of Object.values(instanceDescriptors)) { + if (!instanceDescriptor) continue + const panelClass = paneClasses[instanceDescriptor.paneClass] if (!panelClass) continue - const {instanceId} = instanceDescriptor! + const {instanceId} = instanceDescriptor const {extensionId, classDefinition: definition} = panelClass const instance = prism.memo( - `instance-${instanceDescriptor!.instanceId}`, + `instance-${instanceDescriptor.instanceId}`, () => { const inst: PaneInstance<$IntentionalAny> = { extensionId, @@ -82,14 +82,14 @@ export default class PaneManager { const allPaneInstances = val( this._studio.atomP.historic.panelInstanceDesceriptors, ) - let instanceId!: string + let instanceId!: PaneInstanceId for (let i = 1; i < 1000; i++) { - instanceId = `${paneClass} #${i}` + instanceId = `${paneClass} #${i}` as PaneInstanceId if (!allPaneInstances[instanceId]) break } if (!extensionId) { - throw new Error(`Pance class "${paneClass}" is not registered.`) + throw new Error(`Pane class "${paneClass}" is not registered.`) } this._studio.transaction(({drafts}) => { @@ -102,7 +102,7 @@ export default class PaneManager { return this._getAllPanes().getValue()[instanceId]! } - destroyPane(instanceId: string): void { + destroyPane(instanceId: PaneInstanceId): void { const core = this._studio.core if (!core) { throw new Error( diff --git a/theatre/studio/src/Studio.ts b/theatre/studio/src/Studio.ts index b30923f..0108c74 100644 --- a/theatre/studio/src/Studio.ts +++ b/theatre/studio/src/Studio.ts @@ -20,6 +20,7 @@ import type * as _coreExports from '@theatre/core/coreExports' import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import type {Deferred} from '@theatre/shared/utils/defer' import {defer} from '@theatre/shared/utils/defer' +import type {ProjectId} from '@theatre/shared/utils/ids' export type CoreExports = typeof _coreExports @@ -27,10 +28,10 @@ export class Studio { readonly ui!: UI readonly publicApi: IStudio readonly address: {studioId: string} - readonly _projectsProxy: PointerProxy> = + readonly _projectsProxy: PointerProxy> = new PointerProxy(new Atom({}).pointer) - readonly projectsP: Pointer> = + readonly projectsP: Pointer> = this._projectsProxy.pointer private readonly _store = new StudioStore() @@ -124,7 +125,7 @@ export class Studio { this._setProjectsP(coreBits.projectsP) } - private _setProjectsP(projectsP: Pointer>) { + private _setProjectsP(projectsP: Pointer>) { this._projectsProxy.setPointer(projectsP) } @@ -218,6 +219,6 @@ export class Studio { } createContentOfSaveFile(projectId: string): OnDiskState { - return this._store.createContentOfSaveFile(projectId) + return this._store.createContentOfSaveFile(projectId as ProjectId) } } diff --git a/theatre/studio/src/StudioStore/StudioStore.ts b/theatre/studio/src/StudioStore/StudioStore.ts index 6b7b311..f8ba364 100644 --- a/theatre/studio/src/StudioStore/StudioStore.ts +++ b/theatre/studio/src/StudioStore/StudioStore.ts @@ -25,6 +25,7 @@ import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import {generateDiskStateRevision} from './generateDiskStateRevision' import createTransactionPrivateApi from './createTransactionPrivateApi' +import type {ProjectId} from '@theatre/shared/utils/ids' export type Drafts = { historic: Draft @@ -173,7 +174,7 @@ export default class StudioStore { this._reduxStore.dispatch(studioActions.historic.redo()) } - createContentOfSaveFile(projectId: string): OnDiskState { + createContentOfSaveFile(projectId: ProjectId): OnDiskState { const projectState = this._reduxStore.getState().$persistent.historic.innerState.coreByProject[ projectId diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts index 71d370f..8c2624c 100644 --- a/theatre/studio/src/TheatreStudio.ts +++ b/theatre/studio/src/TheatreStudio.ts @@ -16,6 +16,7 @@ import getStudio from './getStudio' import type React from 'react' import {debounce} from 'lodash-es' import type Sheet from '@theatre/core/sheets/Sheet' +import type {PaneInstanceId, ProjectId} from '@theatre/shared/utils/ids' export interface ITransactionAPI { /** @@ -116,7 +117,7 @@ export interface IExtension { export type PaneInstance = { extensionId: string - instanceId: string + instanceId: PaneInstanceId definition: PaneClassDefinition } @@ -471,10 +472,12 @@ export default class TheatreStudio implements IStudio { } destroyPane(paneId: string): void { - return getStudio().paneManager.destroyPane(paneId) + return getStudio().paneManager.destroyPane(paneId as PaneInstanceId) } createContentOfSaveFile(projectId: string): Record { - return getStudio().createContentOfSaveFile(projectId) as $IntentionalAny + return getStudio().createContentOfSaveFile( + projectId as ProjectId, + ) as $IntentionalAny } } diff --git a/theatre/studio/src/panels/BasePanel/BasePanel.tsx b/theatre/studio/src/panels/BasePanel/BasePanel.tsx index 9a76ad4..6a69980 100644 --- a/theatre/studio/src/panels/BasePanel/BasePanel.tsx +++ b/theatre/studio/src/panels/BasePanel/BasePanel.tsx @@ -1,5 +1,6 @@ import {val} from '@theatre/dataverse' import {usePrism} from '@theatre/react' +import type {UIPanelId} from '@theatre/shared/utils/ids' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import getStudio from '@theatre/studio/getStudio' import type {PanelPosition} from '@theatre/studio/store/types' @@ -8,7 +9,7 @@ import React, {useContext} from 'react' import useWindowSize from 'react-use/esm/useWindowSize' type PanelStuff = { - panelId: string + panelId: UIPanelId dims: { width: number height: number @@ -64,7 +65,7 @@ const PanelContext = React.createContext(null as $IntentionalAny) export const usePanel = () => useContext(PanelContext) const BasePanel: React.FC<{ - panelId: string + panelId: UIPanelId defaultPosition: PanelPosition minDims: {width: number; height: number} }> = ({panelId, children, defaultPosition, minDims}) => { diff --git a/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx b/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx index d22d55e..7918a0d 100644 --- a/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx +++ b/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx @@ -11,6 +11,7 @@ import {ErrorBoundary} from 'react-error-boundary' import {IoClose} from 'react-icons/all' import getStudio from '@theatre/studio/getStudio' import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common' +import type {PaneInstanceId, UIPanelId} from '@theatre/shared/utils/ids' const defaultPosition: PanelPosition = { edges: { @@ -28,7 +29,7 @@ const ExtensionPaneWrapper: React.FC<{ }> = ({paneInstance}) => { return ( @@ -137,7 +138,9 @@ const Content: React.FC<{paneInstance: PaneInstance<$FixMe>}> = ({ }) => { const Comp = paneInstance.definition.component const closePane = useCallback(() => { - getStudio().paneManager.destroyPane(paneInstance.instanceId) + getStudio().paneManager.destroyPane( + paneInstance.instanceId as PaneInstanceId, + ) }, [paneInstance]) return ( diff --git a/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx b/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx index b8a1136..6f93e0f 100644 --- a/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx +++ b/theatre/studio/src/panels/DetailPanel/ObjectDetails.tsx @@ -1,6 +1,8 @@ import React, {useMemo} from 'react' import DeterminePropEditor from './propEditors/DeterminePropEditor' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import type {Pointer} from '@theatre/dataverse' +import type {$FixMe} from '@theatre/shared/utils/types' const ObjectDetails: React.FC<{ objects: SheetObject[] @@ -13,9 +15,9 @@ const ObjectDetails: React.FC<{ } propConfig={obj.template.config} - depth={1} + visualIndentation={1} /> ) } diff --git a/theatre/studio/src/panels/DetailPanel/ProjectDetails/StateConflictRow.tsx b/theatre/studio/src/panels/DetailPanel/ProjectDetails/StateConflictRow.tsx index 46dca87..aa29798 100644 --- a/theatre/studio/src/panels/DetailPanel/ProjectDetails/StateConflictRow.tsx +++ b/theatre/studio/src/panels/DetailPanel/ProjectDetails/StateConflictRow.tsx @@ -8,6 +8,7 @@ import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip' import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip' import type {$FixMe} from '@theatre/shared/utils/types' import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton' +import type {ProjectId} from '@theatre/shared/utils/ids' const Container = styled.div` padding: 8px 10px; @@ -37,7 +38,7 @@ const ChooseStateRow = styled.div` gap: 8px; ` -const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => { +const StateConflictRow: React.FC<{projectId: ProjectId}> = ({projectId}) => { const loadingState = useVal( getStudio().atomP.ephemeral.coreByProject[projectId].loadingState, ) @@ -52,7 +53,7 @@ const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => { } const InConflict: React.FC<{ - projectId: string + projectId: ProjectId loadingState: Extract< ProjectEphemeralState['loadingState'], {type: 'browserStateIsNotBasedOnDiskState'} diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx index 6513f9b..c2e6eec 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/propEditors/CompoundPropEditor.tsx @@ -61,7 +61,7 @@ const SubProps = styled.div<{depth: number; lastSubIsComposite: boolean}>` const CompoundPropEditor: IPropEditorFC< PropTypeConfig_Compound<$IntentionalAny> -> = ({pointerToProp, obj, propConfig, depth}) => { +> = ({pointerToProp, obj, propConfig, visualIndentation: depth}) => { const propName = propConfig.label ?? last(getPointerParts(pointerToProp).path) const allSubs = Object.entries(propConfig.props) @@ -154,7 +154,7 @@ const CompoundPropEditor: IPropEditorFC< propConfig={subPropConfig} pointerToProp={pointerToProp[subPropKey]} obj={obj} - depth={depth + 1} + visualIndentation={depth + 1} /> ) }, diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx index 30e9fa7..f9019e1 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/propEditors/DeterminePropEditor.tsx @@ -9,15 +9,15 @@ import NumberPropEditor from './NumberPropEditor' import StringLiteralPropEditor from './StringLiteralPropEditor' import StringPropEditor from './StringPropEditor' import RgbaPropEditor from './RgbaPropEditor' +import type {UnknownShorthandCompoundProps} from '@theatre/core/propTypes/internals' /** * Returns the PropTypeConfig by path. Assumes `path` is a valid prop path and that * it exists in obj. */ -export const getPropTypeByPointer = ( - pointerToProp: SheetObject['propsP'], - obj: SheetObject, -): PropTypeConfig => { +export function getPropTypeByPointer< + Props extends UnknownShorthandCompoundProps, +>(pointerToProp: SheetObject['propsP'], obj: SheetObject): PropTypeConfig { const rootConf = obj.template.config const p = getPointerParts(pointerToProp).path @@ -67,7 +67,7 @@ type IPropEditorByPropType = { obj: SheetObject pointerToProp: Pointer['valueType']> propConfig: PropConfigByType - depth: number + visualIndentation: number }> } @@ -81,12 +81,20 @@ const propEditorByPropType: IPropEditorByPropType = { rgba: RgbaPropEditor, } -const DeterminePropEditor: React.FC<{ +export type IEditablePropertyProps = { obj: SheetObject - pointerToProp: SheetObject['propsP'] - propConfig?: PropTypeConfig - depth: number -}> = (p) => { + pointerToProp: Pointer['valueType']> + propConfig: PropConfigByType +} + +type IDeterminePropEditorProps = + IEditablePropertyProps & { + visualIndentation: number + } + +const DeterminePropEditor: React.FC< + IDeterminePropEditorProps +> = (p) => { const propConfig = p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj) @@ -95,7 +103,7 @@ const DeterminePropEditor: React.FC<{ return ( > = propConfig: TPropTypeConfig pointerToProp: Pointer obj: SheetObject - depth: number + visualIndentation: number }> diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx index 3fc6861..f94b067 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx +++ b/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx @@ -5,7 +5,7 @@ import type Scrub from '@theatre/studio/Scrub' import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import getDeep from '@theatre/shared/utils/getDeep' import {usePrism} from '@theatre/react' -import type {$FixMe, SerializablePrimitive} from '@theatre/shared/utils/types' +import type {SerializablePrimitive} from '@theatre/shared/utils/types' import {getPointerParts, prism, val} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse' import get from 'lodash-es/get' @@ -15,6 +15,7 @@ import DefaultOrStaticValueIndicator from './DefaultValueIndicator' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' import type {PropTypeConfig} from '@theatre/core/propTypes' import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' +import type {SequenceTrackId} from '@theatre/shared/utils/ids' interface CommonStuff { value: T @@ -147,7 +148,7 @@ export function useEditingToolsForPrimitiveProp< }, }) - const sequenceTrcackId = possibleSequenceTrackId as $FixMe as string + const sequenceTrackId = possibleSequenceTrackId as SequenceTrackId const nearbyKeyframes = prism.sub( 'lcr', (): NearbyKeyframes => { @@ -155,7 +156,7 @@ export function useEditingToolsForPrimitiveProp< obj.template.project.pointers.historic.sheetsById[ obj.address.sheetId ].sequence.tracksByObject[obj.address.objectKey].trackData[ - sequenceTrcackId + sequenceTrackId ], ) if (!track || track.keyframes.length === 0) return {} @@ -186,7 +187,7 @@ export function useEditingToolsForPrimitiveProp< } } }, - [sequenceTrcackId], + [sequenceTrackId], ) let shade: Shade diff --git a/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx b/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx index 02cb7bb..092a506 100644 --- a/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx +++ b/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx @@ -26,7 +26,7 @@ const ObjectsList: React.FC<{ ) })} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx index e843d6d..29f32f4 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -44,7 +44,10 @@ const BasicKeyframedTrack: React.FC = React.memo( selection: val(selectionAtom.pointer.current), } } else { - return {selectedKeyframeIds: {}, selection: undefined} + return { + selectedKeyframeIds: {}, + selection: undefined, + } } }, [layoutP, leaf.trackId]) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx index 1188ebe..4c0baf0 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx @@ -92,7 +92,7 @@ const HitZone = styled.div` type IKeyframeDotProps = IKeyframeEditorProps /** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */ -const KeyframeDot: React.FC = (props) => { +const KeyframeDot: React.VFC = (props) => { const [ref, node] = useRefAndState(null) const [isDragging] = useDragKeyframe(node, props) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx index a1521fe..d396195 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx @@ -79,8 +79,8 @@ function useCaptureSelection( val(layoutP.selectionAtom).setState({current: undefined}) }, - onDrag(dx, dy, event) { - const state = ref.current! + onDrag(_dx, _dy, event) { + // const state = ref.current! const rect = containerNode!.getBoundingClientRect() const posInScaledSpace = event.clientX - rect.left @@ -97,25 +97,13 @@ function useCaptureSelection( const selection = utils.boundsToSelection(layoutP, ref.current) val(layoutP.selectionAtom).setState({current: selection}) }, - onDragEnd(dragHappened) { + onDragEnd(_dragHappened) { ref.current = null }, } }, [layoutP, containerNode, ref]), ) - // useEffect(() => { - // if (!containerNode) return - // const onClick = () => { - - // } - // containerNode.addEventListener('click', onClick) - - // return () => { - // containerNode.removeEventListener('click', onClick) - // } - // }, [containerNode]) - return state } @@ -131,16 +119,11 @@ namespace utils { primitiveProp(layoutP, leaf, bounds, selection) { const {sheetObject, trackId} = leaf const trackData = val( - getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId] + getStudio().atomP.historic.coreByProject[sheetObject.address.projectId] .sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[ sheetObject.address.objectKey ].trackData[trackId], )! - const toCollect = trackData!.keyframes.filter( - (kf) => - kf.position >= bounds.positions[0] && - kf.position <= bounds.positions[1], - ) for (const kf of trackData.keyframes) { if (kf.position <= bounds.positions[0]) continue diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx index 73c9aa3..6158a13 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -25,7 +25,7 @@ export type ExtremumSpace = { lock(): VoidFn } -const BasicKeyframedTrack: React.FC<{ +const BasicKeyframedTrack: React.VFC<{ layoutP: Pointer sheetObject: SheetObject pathToProp: PathToProp @@ -102,7 +102,7 @@ const BasicKeyframedTrack: React.FC<{ sheetObject={sheetObject} trackId={trackId} isScalar={propConfig.type === 'number'} - key={'keyframe-' + kf.id} + key={kf.id} extremumSpace={cachedExtremumSpace.current} color={color} /> diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx index f313995..313c0be 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/Curve.tsx @@ -15,7 +15,7 @@ const SVGPath = styled.path` type IProps = Parameters[0] -const Curve: React.FC = (props) => { +const Curve: React.VFC = (props) => { const {index, trackData} = props const cur = trackData.keyframes[index] const next = trackData.keyframes[index + 1] diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx index 4d7f9ea..fafd827 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/CurveHandle.tsx @@ -49,7 +49,7 @@ type Which = 'left' | 'right' type IProps = Parameters[0] & {which: Which} -const CurveHandle: React.FC = (props) => { +const CurveHandle: React.VFC = (props) => { const [ref, node] = useRefAndState(null) const {index, trackData} = props diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx index 972bdb3..e97d37e 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx @@ -55,7 +55,7 @@ const HitZone = styled.circle` type IProps = Parameters[0] & {which: 'left' | 'right'} -const GraphEditorDotNonScalar: React.FC = (props) => { +const GraphEditorDotNonScalar: React.VFC = (props) => { const [ref, node] = useRefAndState(null) const {index, trackData} = props diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx index b1b04d8..841e16a 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx @@ -55,7 +55,7 @@ const HitZone = styled.circle` type IProps = Parameters[0] -const GraphEditorDotScalar: React.FC = (props) => { +const GraphEditorDotScalar: React.VFC = (props) => { const [ref, node] = useRefAndState(null) const {index, trackData} = props diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorNonScalarDash.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorNonScalarDash.tsx index 7c2645d..90a99a1 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorNonScalarDash.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorNonScalarDash.tsx @@ -17,7 +17,7 @@ const SVGPath = styled.path` type IProps = Parameters[0] -const GraphEditorNonScalarDash: React.FC = (props) => { +const GraphEditorNonScalarDash: React.VFC = (props) => { const {index, trackData} = props const pathD = `M 0 0 L 1 1` diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx index 0374d98..f018fd3 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx @@ -23,7 +23,7 @@ const Container = styled.g` const noConnector = <> -const KeyframeEditor: React.FC<{ +type IKeyframeEditorProps = { index: number keyframe: Keyframe trackData: TrackData @@ -34,7 +34,9 @@ const KeyframeEditor: React.FC<{ isScalar: boolean color: keyof typeof graphEditorColors propConfig: PropTypeConfig_AllSimples -}> = (props) => { +} + +const KeyframeEditor: React.VFC = (props) => { const {index, trackData, isScalar} = props const cur = trackData.keyframes[index] const next = trackData.keyframes[index + 1] diff --git a/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx b/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx index 871053d..40b88e1 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/SequenceEditorPanel.tsx @@ -27,6 +27,7 @@ import { TitleBar_Piece, TitleBar_Punctuation, } from '@theatre/studio/panels/BasePanel/common' +import type {UIPanelId} from '@theatre/shared/utils/ids' const Container = styled(PanelWrapper)` z-index: ${panelZIndexes.sequenceEditorPanel}; @@ -58,7 +59,7 @@ export const zIndexes = (() => { // sort the z-indexes let i = -1 for (const key of Object.keys(s)) { - s[key as unknown as keyof typeof s] = i + s[key] = i i++ } @@ -83,10 +84,10 @@ const defaultPosition: PanelPosition = { const minDims = {width: 800, height: 200} -const SequenceEditorPanel: React.FC<{}> = (props) => { +const SequenceEditorPanel: React.VFC<{}> = (props) => { return ( @@ -95,7 +96,7 @@ const SequenceEditorPanel: React.FC<{}> = (props) => { ) } -const Content: React.FC<{}> = () => { +const Content: React.VFC<{}> = () => { const {dims} = usePanel() return usePrism(() => { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts index 6692478..65ed5bc 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts @@ -14,6 +14,11 @@ import {Atom, prism, val} from '@theatre/dataverse' import type {SequenceEditorTree} from './tree' import {calculateSequenceEditorTree} from './tree' import {clamp} from 'lodash-es' +import type { + KeyframeId, + ObjectAddressKey, + SequenceTrackId, +} from '@theatre/shared/utils/ids' // A Side is either the left side of the panel or the right side type DimsOfPanelPart = { @@ -41,20 +46,20 @@ export type PanelDims = { export type DopeSheetSelection = { type: 'DopeSheetSelection' byObjectKey: StrictRecord< - string, + ObjectAddressKey, { byTrackId: StrictRecord< - string, + SequenceTrackId, { - byKeyframeId: StrictRecord + byKeyframeId: StrictRecord } > } > getDragHandlers( origin: PropAddress & { - trackId: string - keyframeId: string + trackId: SequenceTrackId + keyframeId: KeyframeId positionAtStartOfDrag: number domNode: Element }, @@ -156,8 +161,8 @@ export type SequenceEditorPanelLayout = { selectionAtom: Atom<{current?: DopeSheetSelection}> } -// type UnitSpaceProression = Nominal -// type ClippedSpaceProgression = Nominal +// type UnitSpaceProression = number +// type ClippedSpaceProgression = number /** * This means the left side of the panel is 20% of its width, and the diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts index b1fc298..8879c09 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts @@ -87,8 +87,10 @@ export const calculateSequenceEditorTree = ( topSoFar += tree.nodeHeight nSoFar += 1 - for (const [_, sheetObject] of Object.entries(val(sheet.objectsP))) { - addObject(sheetObject, tree.children, tree.depth + 1) + for (const sheetObject of Object.values(val(sheet.objectsP))) { + if (sheetObject) { + addObject(sheetObject, tree.children, tree.depth + 1) + } } tree.heightIncludingChildren = topSoFar - tree.top diff --git a/theatre/studio/src/selectors.ts b/theatre/studio/src/selectors.ts index 845b3eb..94bf809 100644 --- a/theatre/studio/src/selectors.ts +++ b/theatre/studio/src/selectors.ts @@ -3,7 +3,9 @@ import type Sequence from '@theatre/core/sequences/Sequence' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type Sheet from '@theatre/core/sheets/Sheet' import {val} from '@theatre/dataverse' +import type {$IntentionalAny} from '@theatre/dataverse/src/types' import {isSheet, isSheetObject} from '@theatre/shared/instanceTypes' +import type {SheetId} from '@theatre/shared/utils/ids' import {uniq} from 'lodash-es' import getStudio from './getStudio' import type {OutlineSelectable, OutlineSelection} from './store/types' @@ -46,7 +48,7 @@ export const getSelectedInstanceOfSheetId = ( ] const instanceId = val( - projectStateP.stateBySheetId[selectedSheetId].selectedInstanceId, + projectStateP.stateBySheetId[selectedSheetId as SheetId].selectedInstanceId, ) const template = val(project.sheetTemplatesP[selectedSheetId]) @@ -59,10 +61,14 @@ export const getSelectedInstanceOfSheetId = ( // @todo #perf this will update every time an instance is added/removed. const allInstances = val(template.instancesP) - return allInstances[Object.keys(allInstances)[0]] + return allInstances[keys(allInstances)[0]] } } +function keys(obj: T): Exclude[] { + return Object.keys(obj) as $IntentionalAny +} + /** * component instances could come and go all the time. This hook * makes sure we don't cause re-renders diff --git a/theatre/studio/src/store/stateEditors.ts b/theatre/studio/src/store/stateEditors.ts index d401009..1fc8988 100644 --- a/theatre/studio/src/store/stateEditors.ts +++ b/theatre/studio/src/store/stateEditors.ts @@ -1,4 +1,5 @@ import type { + HistoricPositionalSequence, Keyframe, SheetState_Historic, } from '@theatre/core/projects/store/types/SheetState_Historic' @@ -11,7 +12,11 @@ import type { WithoutSheetInstance, } from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses' -import type {KeyframeId} from '@theatre/shared/utils/ids' +import type { + KeyframeId, + SequenceTrackId, + UIPanelId, +} from '@theatre/shared/utils/ids' import { generateKeyframeId, generateSequenceTrackId, @@ -72,7 +77,7 @@ namespace stateEditors { export namespace historic { export namespace panelPositions { export function setPanelPosition(p: { - panelId: string + panelId: UIPanelId position: PanelPosition }) { const h = drafts().historic @@ -429,7 +434,9 @@ namespace stateEditors { } export namespace sequence { - export function _ensure(p: WithoutSheetInstance) { + export function _ensure( + p: WithoutSheetInstance, + ): HistoricPositionalSequence { const s = stateEditors.coreByProject.historic.sheetsById._ensure(p) s.sequence ??= { subUnitsPerUnit: 30, @@ -529,7 +536,9 @@ namespace stateEditors { } function _getTrack( - p: WithoutSheetInstance & {trackId: string}, + p: WithoutSheetInstance & { + trackId: SequenceTrackId + }, ) { return _ensureTracksOfObject(p).trackData[p.trackId] } @@ -540,7 +549,7 @@ namespace stateEditors { */ export function setKeyframeAtPosition( p: WithoutSheetInstance & { - trackId: string + trackId: SequenceTrackId position: number handles?: [number, number, number, number] value: T @@ -585,7 +594,7 @@ namespace stateEditors { export function unsetKeyframeAtPosition( p: WithoutSheetInstance & { - trackId: string + trackId: SequenceTrackId position: number }, ) { @@ -604,7 +613,7 @@ namespace stateEditors { export function transformKeyframes( p: WithoutSheetInstance & { - trackId: string + trackId: SequenceTrackId keyframeIds: KeyframeId[] translate: number scale: number @@ -633,7 +642,7 @@ namespace stateEditors { export function deleteKeyframes( p: WithoutSheetInstance & { - trackId: string + trackId: SequenceTrackId keyframeIds: KeyframeId[] }, ) { @@ -645,9 +654,9 @@ namespace stateEditors { ) } - export function replaceKeyframes( + export function replaceKeyframes( p: WithoutSheetInstance & { - trackId: string + trackId: SequenceTrackId keyframes: Array snappingFunction: SnappingFunction }, diff --git a/theatre/studio/src/store/types/ahistoric.ts b/theatre/studio/src/store/types/ahistoric.ts index 3a101dd..c3c5768 100644 --- a/theatre/studio/src/store/types/ahistoric.ts +++ b/theatre/studio/src/store/types/ahistoric.ts @@ -1,5 +1,6 @@ import type {ProjectState} from '@theatre/core/projects/store/storeTypes' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' +import type {ProjectId} from '@theatre/shared/utils/ids' import type {IRange, StrictRecord} from '@theatre/shared/utils/types' export type StudioAhistoricState = { @@ -50,5 +51,5 @@ export type StudioAhistoricState = { } > } - coreByProject: {[projectId in string]: ProjectState['ahistoric']} + coreByProject: {[projectId in ProjectId]: ProjectState['ahistoric']} } diff --git a/theatre/studio/src/store/types/historic.ts b/theatre/studio/src/store/types/historic.ts index 7f88afd..c0d3823 100644 --- a/theatre/studio/src/store/types/historic.ts +++ b/theatre/studio/src/store/types/historic.ts @@ -11,6 +11,14 @@ import type {StrictRecord} from '@theatre/shared/utils/types' import type Project from '@theatre/core/projects/Project' import type Sheet from '@theatre/core/sheets/Sheet' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import type { + ObjectAddressKey, + PaneInstanceId, + ProjectId, + SheetId, + SheetInstanceId, + UIPanelId, +} from '@theatre/shared/utils/ids' export type PanelPosition = { edges: { @@ -56,37 +64,55 @@ export type OutlineSelectionState = export type OutlineSelectable = Project | Sheet | SheetObject export type OutlineSelection = OutlineSelectable[] -export type PanelInstanceDescriptor = { - instanceId: string +export type PaneInstanceDescriptor = { + instanceId: PaneInstanceId paneClass: string } -export type StudioHistoricState = { - projects: { - stateByProjectId: StrictRecord< - string, - { - stateBySheetId: StrictRecord< - string, - { - selectedInstanceId: undefined | string - sequenceEditor: { - selectedPropsByObject: StrictRecord< - string, - StrictRecord - > - } - } - > - } +/** + * See parent {@link StudioHistoricStateProject}. + * See root {@link StudioHistoricState} + */ +export type StudioHistoricStateProjectSheet = { + selectedInstanceId: undefined | SheetInstanceId + sequenceEditor: { + selectedPropsByObject: StrictRecord< + ObjectAddressKey, + StrictRecord > } - - panels?: Panels - panelPositions?: {[panelIdOrPaneId in string]?: PanelPosition} - panelInstanceDesceriptors: { - [instanceId in string]?: PanelInstanceDescriptor - } - autoKey: boolean - coreByProject: {[projectId in string]: ProjectState_Historic} +} + +/** See {@link StudioHistoricState} */ +export type StudioHistoricStateProject = { + stateBySheetId: StrictRecord +} + +/** + * {@link StudioHistoricState} includes both studio and project data, and + * contains data changed for an undo/redo history. + * + * ## Internally + * + * We use immer `Draft`s to encapsulate this whole state to then be operated + * on by each transaction. The derived values from the store will also include + * the application of the "temp transactions" stack. + */ +export type StudioHistoricState = { + projects: { + stateByProjectId: StrictRecord + } + + /** Panels can contain panes */ + panels?: Panels + /** Panels can contain panes */ + panelPositions?: {[panelId in UIPanelId]?: PanelPosition} + // This is misspelled, but I think some users are dependent on the exact shape of this stored JSON + // So, we cannot easily change it without providing backwards compatibility. + panelInstanceDesceriptors: StrictRecord< + PaneInstanceId, + PaneInstanceDescriptor + > + autoKey: boolean + coreByProject: Record }