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 <aria.minaei@gmail.com>
This commit is contained in:
Cole Lawrence 2022-04-29 13:00:14 -04:00
parent 9d9fc1680e
commit 1387ce62d2
58 changed files with 647 additions and 299 deletions

View file

@ -25,6 +25,8 @@ enum ValueTypes {
export interface IdentityDerivationProvider { export interface IdentityDerivationProvider {
/** /**
* @internal * @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 readonly $$isIdentityDerivationProvider: true
/** /**

View file

@ -33,6 +33,11 @@ const cachedSubPathPointersWeakMap = new WeakMap<
* A wrapper type for the type a `Pointer` points to. * A wrapper type for the type a `Pointer` points to.
*/ */
export type PointerType<O> = { export type PointerType<O> = {
/**
* Only accessible via the type system.
* This is a helper for getting the underlying pointer type
* via the type space.
*/
$$__pointer_type: O $$__pointer_type: O
} }
@ -53,10 +58,18 @@ export type PointerType<O> = {
* *
* The current solution is to just avoid using `any` with pointer-related code (or type-test it well). * 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 :) * 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<O> = PointerType<O> & export type Pointer<O> = PointerType<O> &
(O extends UnindexableTypesForPointer // `Exclude<O, undefined>` will remove `undefined` from the first type
// `undefined extends O ? undefined : never` will give us `undefined` if `O` is `... | undefined`
PointerInner<Exclude<O, undefined>, 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, Optional> = O extends UnindexableTypesForPointer
? UnindexablePointer ? UnindexablePointer
: unknown extends O : unknown extends O
? UnindexablePointer ? UnindexablePointer
@ -64,10 +77,9 @@ export type Pointer<O> = PointerType<O> &
? Pointer<T>[] ? Pointer<T>[]
: O extends {} : O extends {}
? { ? {
[K in keyof O]-?: Pointer<O[K]> [K in keyof O]-?: Pointer<O[K] | Optional>
} /*& }
{[K in string | number]: Pointer<K extends keyof O ? O[K] : undefined>}*/ : UnindexablePointer
: UnindexablePointer)
const pointerMetaSymbol = Symbol('pointerMeta') const pointerMetaSymbol = Symbol('pointerMeta')

View file

@ -0,0 +1,106 @@
import type {Pointer, UnindexablePointer} from './pointer'
import type {$IntentionalAny} from './types'
const nominal = Symbol()
type Nominal<Name> = string & {[nominal]: Name}
type Key = Nominal<'key'>
type Id = Nominal<'id'>
type IdObject = {
inner: true
}
type KeyObject = {
inner: {
byIds: Partial<Record<Id, IdObject>>
}
}
type NestedNominalThing = {
optional?: true
byKeys: Partial<Record<Key, KeyObject>>
}
interface TypeError<M> {}
type Debug<T extends 0> = T
type IsTrue<T extends true> = T
type IsFalse<F extends false> = F
type IsExtends<F, R extends F> = F
type IsExactly<F, R extends F> = F extends R
? true
: TypeError<[F, 'does not extend', R]>
function test() {
const p = todo<Pointer<NestedNominalThing>>()
const key1 = todo<Key>()
const id1 = todo<Id>()
type A = UnindexablePointer[typeof key1]
type BaseChecks = [
IsExtends<any, any>,
IsExtends<undefined | 1, undefined>,
IsExtends<string, Key>,
IsTrue<IsExactly<UnindexablePointer[typeof key1], Pointer<undefined>>>,
IsTrue<
IsExactly<Pointer<undefined | true>['...']['...'], Pointer<undefined>>
>,
IsTrue<
IsExactly<
Pointer<Record<Key, true | undefined>>[Key],
Pointer<true | undefined>
>
>,
IsTrue<IsExactly<Pointer<undefined>[Key], Pointer<undefined>>>,
// Debug<Pointer<undefined | Record<string, true>>[Key]>,
IsTrue<IsExactly<Pointer<Record<string, true>>[string], Pointer<true>>>,
IsTrue<
IsExactly<
Pointer<undefined | Record<string, true>>[string],
Pointer<true | undefined>
>
>,
IsTrue<
IsExactly<
Pointer<undefined | Record<Key, true>>[Key],
Pointer<true | undefined>
>
>,
// Debug<Pointer<undefined | true>['...']['...']>,
// IsFalse<any extends Pointer<undefined | true> ? true : false>,
// what extends what
IsTrue<1 & undefined extends undefined ? true : false>,
IsFalse<1 | undefined extends undefined ? true : false>,
]
t<Pointer<undefined | true>>() //
.isExactly(p.optional).ok
t<Pointer<undefined | KeyObject>>() //
.isExactly(p.byKeys[key1]).ok
t<Pointer<undefined | KeyObject['inner']>>() //
.isExactly(p.byKeys[key1].inner).ok
t<Pointer<undefined | IdObject>>() //
.isExactly(p.byKeys[key1].inner.byIds[id1]).ok
p.byKeys[key1]
}
function todo<T>(hmm?: TemplateStringsArray): T {
return null as $IntentionalAny
}
function t<T>(): {
isExactly<R extends T>(
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}
}

View file

@ -16,6 +16,7 @@ import {isPointer} from '@theatre/dataverse'
import {isDerivation, valueDerivation} from '@theatre/dataverse' import {isDerivation, valueDerivation} from '@theatre/dataverse'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import coreTicker from './coreTicker' import coreTicker from './coreTicker'
import type {ProjectId} from '@theatre/shared/utils/ids'
export {types} export {types}
/** /**
@ -45,7 +46,7 @@ export {types}
*/ */
export function getProject(id: string, config: IProjectConfig = {}): IProject { export function getProject(id: string, config: IProjectConfig = {}): IProject {
const {...restOfConfig} = config const {...restOfConfig} = config
const existingProject = projectsSingleton.get(id) const existingProject = projectsSingleton.get(id as ProjectId)
if (existingProject) { if (existingProject) {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (!deepEqual(config, existingProject.config)) { if (!deepEqual(config, existingProject.config)) {
@ -67,9 +68,9 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject {
if (config.state) { if (config.state) {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
shallowValidateOnDiskState(id, config.state) shallowValidateOnDiskState(id as ProjectId, config.state)
} else { } 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. * Lightweight validator that only makes sure the state's definitionVersion is correct.
* Does not do a thorough validation of the state. * Does not do a thorough validation of the state.
*/ */
const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => { const shallowValidateOnDiskState = (projectId: ProjectId, s: OnDiskState) => {
if ( if (
Array.isArray(s) || Array.isArray(s) ||
s == null || 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) shallowValidateOnDiskState(projectId, s)
// @TODO do a deep validation here // @TODO do a deep validation here
} }

View file

@ -6,13 +6,12 @@ import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject' import type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {UnknownShorthandCompoundProps} from './propTypes/internals'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
const publicAPIToPrivateAPIMap = new WeakMap() const publicAPIToPrivateAPIMap = new WeakMap()
export function privateAPI< export function privateAPI<P extends {type: string}>(
P extends IProject | ISheet | ISheetObject<$IntentionalAny> | ISequence,
>(
pub: P, pub: P,
): P extends IProject ): P extends IProject
? Project ? Project
@ -29,8 +28,8 @@ export function privateAPI<
export function setPrivateAPI(pub: IProject, priv: Project): void export function setPrivateAPI(pub: IProject, priv: Project): void
export function setPrivateAPI(pub: ISheet, priv: Sheet): void export function setPrivateAPI(pub: ISheet, priv: Sheet): void
export function setPrivateAPI(pub: ISequence, priv: Sequence): void export function setPrivateAPI(pub: ISequence, priv: Sequence): void
export function setPrivateAPI( export function setPrivateAPI<Props extends UnknownShorthandCompoundProps>(
pub: ISheetObject<$IntentionalAny>, pub: ISheetObject<Props>,
priv: SheetObject, priv: SheetObject,
): void ): void
export function setPrivateAPI(pub: {}, priv: {}): void { export function setPrivateAPI(pub: {}, priv: {}): void {

View file

@ -13,6 +13,11 @@ import type {ProjectState} from './store/storeTypes'
import type {Deferred} from '@theatre/shared/utils/defer' import type {Deferred} from '@theatre/shared/utils/defer'
import {defer} from '@theatre/shared/utils/defer' import {defer} from '@theatre/shared/utils/defer'
import globals from '@theatre/shared/globals' import globals from '@theatre/shared/globals'
import type {
ProjectId,
SheetId,
SheetInstanceId,
} from '@theatre/shared/utils/ids'
export type Conf = Partial<{ export type Conf = Partial<{
state: OnDiskState state: OnDiskState
@ -44,7 +49,7 @@ export default class Project {
type: 'Theatre_Project' = 'Theatre_Project' type: 'Theatre_Project' = 'Theatre_Project'
constructor( constructor(
id: string, id: ProjectId,
readonly config: Conf = {}, readonly config: Conf = {},
readonly publicApi: TheatreProject, readonly publicApi: TheatreProject,
) { ) {
@ -150,7 +155,10 @@ export default class Project {
return this._readyDeferred.status === 'resolved' 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] let template = this._sheetTemplates.getState()[sheetId]
if (!template) { if (!template) {

View file

@ -2,6 +2,11 @@ import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs'
import Project from '@theatre/core/projects/Project' import Project from '@theatre/core/projects/Project'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {ProjectAddress} from '@theatre/shared/utils/addresses' 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 {validateInstanceId} from '@theatre/shared/utils/sanitizers'
import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths' import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
@ -58,7 +63,7 @@ export default class TheatreProject implements IProject {
* @internal * @internal
*/ */
constructor(id: string, config: IProjectConfig = {}) { constructor(id: string, config: IProjectConfig = {}) {
setPrivateAPI(this, new Project(id, config, this)) setPrivateAPI(this, new Project(id as ProjectId, config, this))
} }
get ready(): Promise<void> { get ready(): Promise<void> {
@ -87,7 +92,9 @@ export default class TheatreProject implements IProject {
) )
} }
return privateAPI(this).getOrCreateSheet(sanitizedPath, instanceId) return privateAPI(this).getOrCreateSheet(
.publicApi sanitizedPath as SheetId,
instanceId as SheetInstanceId,
).publicApi
} }
} }

View file

@ -1,8 +1,9 @@
import {Atom} from '@theatre/dataverse' import {Atom} from '@theatre/dataverse'
import type {ProjectId} from '@theatre/shared/utils/ids'
import type Project from './Project' import type Project from './Project'
interface State { interface State {
projects: Record<string, Project> projects: Record<ProjectId, Project>
} }
class ProjectsSingleton { class ProjectsSingleton {
@ -12,15 +13,15 @@ class ProjectsSingleton {
/** /**
* We're trusting here that each project id is unique * 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) this.atom.reduceState(['projects', id], () => project)
} }
get(id: string): Project | undefined { get(id: ProjectId): Project | undefined {
return this.atom.getState().projects[id] return this.atom.getState().projects[id]
} }
has(id: string) { has(id: ProjectId) {
return !!this.get(id) return !!this.get(id)
} }
} }

View file

@ -1,3 +1,4 @@
import type {SheetId} from '@theatre/shared/utils/ids'
import type {StrictRecord} from '@theatre/shared/utils/types' import type {StrictRecord} from '@theatre/shared/utils/types'
import type {SheetState_Historic} from './types/SheetState_Historic' import type {SheetState_Historic} from './types/SheetState_Historic'
@ -31,7 +32,7 @@ export interface ProjectEphemeralState {
* at {@link StudioHistoricState.coreByProject} * at {@link StudioHistoricState.coreByProject}
*/ */
export interface ProjectState_Historic { export interface ProjectState_Historic {
sheetsById: StrictRecord<string, SheetState_Historic> sheetsById: StrictRecord<SheetId, SheetState_Historic>
/** /**
* The last 50 revision IDs this state is based on, starting with the most recent one. * 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 * The most recent one is the revision ID of this state

View file

@ -1,5 +1,13 @@
import type {KeyframeId, SequenceTrackId} from '@theatre/shared/utils/ids' import type {
import type {SerializableMap, StrictRecord} from '@theatre/shared/utils/types' KeyframeId,
ObjectAddressKey,
SequenceTrackId,
} from '@theatre/shared/utils/ids'
import type {
SerializableMap,
SerializableValue,
StrictRecord,
} from '@theatre/shared/utils/types'
export interface SheetState_Historic { 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. * of another state, it will be able to inherit the overrides from ancestor states.
*/ */
staticOverrides: { staticOverrides: {
byObject: StrictRecord<string, SerializableMap> byObject: StrictRecord<ObjectAddressKey, SerializableMap>
} }
sequence?: Sequence sequence?: HistoricPositionalSequence
} }
type Sequence = PositionalSequence // Question: What is this? The timeline position of a sequence?
export type HistoricPositionalSequence = {
type PositionalSequence = {
type: 'PositionalSequence' type: 'PositionalSequence'
length: number length: number
/** /**
@ -27,7 +34,7 @@ type PositionalSequence = {
subUnitsPerUnit: number subUnitsPerUnit: number
tracksByObject: StrictRecord< tracksByObject: StrictRecord<
string, ObjectAddressKey,
{ {
trackIdByPropPath: StrictRecord<string, SequenceTrackId> trackIdByPropPath: StrictRecord<string, SequenceTrackId>
trackData: StrictRecord<SequenceTrackId, TrackData> trackData: StrictRecord<SequenceTrackId, TrackData>
@ -39,7 +46,10 @@ export type TrackData = BasicKeyframedTrack
export type Keyframe = { export type Keyframe = {
id: KeyframeId 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 position: number
handles: [leftX: number, leftY: number, rightX: number, rightY: number] handles: [leftX: number, leftY: number, rightX: number, rightY: number]
connectedRight: boolean connectedRight: boolean
@ -47,5 +57,9 @@ export type Keyframe = {
export type BasicKeyframedTrack = { export type BasicKeyframedTrack = {
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[] keyframes: Keyframe[]
} }

View file

@ -10,8 +10,8 @@ import {
} from '@theatre/shared/utils/color' } from '@theatre/shared/utils/color'
import {clamp, mapValues} from 'lodash-es' import {clamp, mapValues} from 'lodash-es'
import type { import type {
IShorthandCompoundProps, UnknownShorthandCompoundProps,
IValidCompoundProps, UnknownValidCompoundProps,
ShorthandCompoundPropsToLonghandCompoundProps, ShorthandCompoundPropsToLonghandCompoundProps,
} from './internals' } from './internals'
import {propTypeSymbol, sanitizeCompoundProps} from './internals' import {propTypeSymbol, sanitizeCompoundProps} from './internals'
@ -88,7 +88,7 @@ const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => {
* ``` * ```
* *
*/ */
export const compound = <Props extends IShorthandCompoundProps>( export const compound = <Props extends UnknownShorthandCompoundProps>(
props: Props, props: Props,
opts: CommonOpts = {}, opts: CommonOpts = {},
): PropTypeConfig_Compound< ): PropTypeConfig_Compound<
@ -101,7 +101,7 @@ export const compound = <Props extends IShorthandCompoundProps>(
ShorthandCompoundPropsToLonghandCompoundProps<Props> ShorthandCompoundPropsToLonghandCompoundProps<Props>
> = { > = {
type: 'compound', type: 'compound',
props: sanitizedProps, props: sanitizedProps as $IntentionalAny,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts.label, label: opts.label,
@ -690,7 +690,7 @@ export interface PropTypeConfig_StringLiteral<T extends string>
export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {} export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {}
type DeepPartialCompound<Props extends IValidCompoundProps> = { type DeepPartialCompound<Props extends UnknownValidCompoundProps> = {
[K in keyof Props]?: DeepPartial<Props[K]> [K in keyof Props]?: DeepPartial<Props[K]>
} }
@ -701,13 +701,14 @@ type DeepPartial<Conf extends PropTypeConfig> =
? DeepPartialCompound<T> ? DeepPartialCompound<T>
: never : never
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps> export interface PropTypeConfig_Compound<
extends IBasePropType< Props extends UnknownValidCompoundProps,
> extends IBasePropType<
'compound', 'compound',
{[K in keyof Props]: Props[K]['valueType']}, {[K in keyof Props]: Props[K]['valueType']},
DeepPartialCompound<Props> DeepPartialCompound<Props>
> { > {
props: Record<string, PropTypeConfig> props: Record<keyof Props, PropTypeConfig>
} }
export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> { export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> {

View file

@ -13,7 +13,7 @@ import * as t from './index'
export const propTypeSymbol = Symbol('TheatrePropType_Basic') export const propTypeSymbol = Symbol('TheatrePropType_Basic')
export type IValidCompoundProps = { export type UnknownValidCompoundProps = {
[K in string]: PropTypeConfig [K in string]: PropTypeConfig
} }
@ -27,18 +27,19 @@ export type IValidCompoundProps = {
* which would allow us to differentiate between values at runtime * which would allow us to differentiate between values at runtime
* (e.g. `val.type = "Rgba"` vs `val.type = "Compound"` etc) * (e.g. `val.type = "Rgba"` vs `val.type = "Compound"` etc)
*/ */
type IShorthandProp = type UnknownShorthandProp =
| string | string
| number | number
| boolean | boolean
| PropTypeConfig | PropTypeConfig
| IShorthandCompoundProps | UnknownShorthandCompoundProps
export type IShorthandCompoundProps = { /** Given an object like this, we have enough info to predict the compound prop */
[K in string]: IShorthandProp export type UnknownShorthandCompoundProps = {
[K in string]: UnknownShorthandProp
} }
export type ShorthandPropToLonghandProp<P extends IShorthandProp> = export type ShorthandPropToLonghandProp<P extends UnknownShorthandProp> =
P extends string P extends string
? PropTypeConfig_String ? PropTypeConfig_String
: P extends number : P extends number
@ -47,12 +48,31 @@ export type ShorthandPropToLonghandProp<P extends IShorthandProp> =
? PropTypeConfig_Boolean ? PropTypeConfig_Boolean
: P extends PropTypeConfig : P extends PropTypeConfig
? P ? P
: P extends IShorthandCompoundProps : P extends UnknownShorthandCompoundProps
? PropTypeConfig_Compound<ShorthandCompoundPropsToLonghandCompoundProps<P>> ? PropTypeConfig_Compound<ShorthandCompoundPropsToLonghandCompoundProps<P>>
: never : never
export type ShorthandCompoundPropsToInitialValue<
P extends UnknownShorthandCompoundProps,
> = LonghandCompoundPropsToInitialValue<
ShorthandCompoundPropsToLonghandCompoundProps<P>
>
type LonghandCompoundPropsToInitialValue<P extends UnknownValidCompoundProps> =
{
[K in keyof P]: P[K]['valueType']
}
export type PropsValue<P> = P extends UnknownValidCompoundProps
? LonghandCompoundPropsToInitialValue<P>
: P extends UnknownShorthandCompoundProps
? LonghandCompoundPropsToInitialValue<
ShorthandCompoundPropsToLonghandCompoundProps<P>
>
: never
export type ShorthandCompoundPropsToLonghandCompoundProps< export type ShorthandCompoundPropsToLonghandCompoundProps<
P extends IShorthandCompoundProps, P extends UnknownShorthandCompoundProps,
> = { > = {
[K in keyof P]: ShorthandPropToLonghandProp<P[K]> [K in keyof P]: ShorthandPropToLonghandProp<P[K]>
} }
@ -89,9 +109,9 @@ export function toLonghandProp(p: unknown): PropTypeConfig {
} }
export function sanitizeCompoundProps( export function sanitizeCompoundProps(
props: IShorthandCompoundProps, props: UnknownShorthandCompoundProps,
): IValidCompoundProps { ): UnknownValidCompoundProps {
const sanitizedProps: IValidCompoundProps = {} const sanitizedProps: UnknownValidCompoundProps = {}
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (typeof props !== 'object' || !props) { if (typeof props !== 'object' || !props) {
throw new InvalidArgumentError( throw new InvalidArgumentError(

View file

@ -6,11 +6,15 @@ import type {
import type {IDerivation, Pointer} from '@theatre/dataverse' import type {IDerivation, Pointer} from '@theatre/dataverse'
import {ConstantDerivation, prism, val} from '@theatre/dataverse' import {ConstantDerivation, prism, val} from '@theatre/dataverse'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
import type {SerializableValue} from '@theatre/shared/utils/types'
import UnitBezier from 'timing-function/lib/UnitBezier' import UnitBezier from 'timing-function/lib/UnitBezier'
/** `left` and `right` are not necessarily the same type. */
export type InterpolationTriple = { export type InterpolationTriple = {
left: unknown /** `left` and `right` are not necessarily the same type. */
right?: unknown left: SerializableValue
/** `left` and `right` are not necessarily the same type. */
right?: SerializableValue
progression: number progression: number
} }
@ -75,10 +79,10 @@ function _forKeyframedTrack(
const undefinedConstD = new ConstantDerivation(undefined) const undefinedConstD = new ConstantDerivation(undefined)
const updateState = ( function updateState(
progressionD: IDerivation<number>, progressionD: IDerivation<number>,
track: BasicKeyframedTrack, track: BasicKeyframedTrack,
): IStartedState => { ): IStartedState {
const progression = progressionD.getValue() const progression = progressionD.getValue()
if (track.keyframes.length === 0) { if (track.keyframes.length === 0) {
return { return {

View file

@ -4,18 +4,19 @@
import {setupTestSheet} from '@theatre/shared/testUtils' import {setupTestSheet} from '@theatre/shared/testUtils'
import {encodePathToProp} from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses'
import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids' import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids'
import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids'
import {iterateOver, prism} from '@theatre/dataverse' import {iterateOver, prism} from '@theatre/dataverse'
import type {SheetState_Historic} from '@theatre/core/projects/store/types/SheetState_Historic' import type {SheetState_Historic} from '@theatre/core/projects/store/types/SheetState_Historic'
describe(`SheetObject`, () => { describe(`SheetObject`, () => {
describe('static overrides', () => { describe('static overrides', () => {
const setup = async ( const setup = async (
staticOverrides: SheetState_Historic['staticOverrides']['byObject'][string] = {}, staticOverrides: SheetState_Historic['staticOverrides']['byObject'][ObjectAddressKey] = {},
) => { ) => {
const {studio, objPublicAPI} = await setupTestSheet({ const {studio, objPublicAPI} = await setupTestSheet({
staticOverrides: { staticOverrides: {
byObject: { byObject: {
obj: staticOverrides, ['obj' as ObjectAddressKey]: staticOverrides,
}, },
}, },
}) })
@ -260,12 +261,12 @@ describe(`SheetObject`, () => {
length: 20, length: 20,
subUnitsPerUnit: 30, subUnitsPerUnit: 30,
tracksByObject: { tracksByObject: {
obj: { ['obj' as ObjectAddressKey]: {
trackIdByPropPath: { trackIdByPropPath: {
[encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'), [encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'),
}, },
trackData: { trackData: {
'1': { ['1' as SequenceTrackId]: {
type: 'BasicKeyframedTrack', type: 'BasicKeyframedTrack',
keyframes: [ keyframes: [
{ {

View file

@ -9,6 +9,7 @@ import SimpleCache from '@theatre/shared/utils/SimpleCache'
import type { import type {
$FixMe, $FixMe,
$IntentionalAny, $IntentionalAny,
DeepPartialOfSerializableValue,
SerializableMap, SerializableMap,
SerializableValue, SerializableValue,
} from '@theatre/shared/utils/types' } from '@theatre/shared/utils/types'
@ -25,14 +26,29 @@ import TheatreSheetObject from './TheatreSheetObject'
import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes' import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes'
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' 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 { export default class SheetObject implements IdentityDerivationProvider {
get type(): 'Theatre_SheetObject' { get type(): 'Theatre_SheetObject' {
return 'Theatre_SheetObject' return 'Theatre_SheetObject'
} }
readonly $$isIdentityDerivationProvider: true = true readonly $$isIdentityDerivationProvider: true = true
readonly address: SheetObjectAddress readonly address: SheetObjectAddress
readonly publicApi: TheatreSheetObject<$IntentionalAny> readonly publicApi: TheatreSheetObject
private readonly _initialValue = new Atom<SerializableMap>({}) private readonly _initialValue = new Atom<SheetObjectPropsValue>({})
private readonly _cache = new SimpleCache() private readonly _cache = new SimpleCache()
constructor( constructor(
@ -48,7 +64,7 @@ export default class SheetObject implements IdentityDerivationProvider {
this.publicApi = new TheatreSheetObject(this) this.publicApi = new TheatreSheetObject(this)
} }
getValues(): IDerivation<Pointer<SerializableMap>> { getValues(): IDerivation<Pointer<SheetObjectPropsValue>> {
return this._cache.get('getValues()', () => return this._cache.get('getValues()', () =>
prism(() => { prism(() => {
const defaults = val(this.template.getDefaultValues()) const defaults = val(this.template.getDefaultValues())
@ -101,7 +117,7 @@ export default class SheetObject implements IdentityDerivationProvider {
final = withSeqs final = withSeqs
} }
const a = valToAtom<SerializableMap>('finalAtom', final) const a = valToAtom<SheetObjectPropsValue>('finalAtom', final)
return a.pointer return a.pointer
}), }),
@ -131,7 +147,7 @@ export default class SheetObject implements IdentityDerivationProvider {
/** /**
* Returns values of props that are sequenced. * Returns values of props that are sequenced.
*/ */
getSequencedValues(): IDerivation<Pointer<SerializableMap>> { getSequencedValues(): IDerivation<Pointer<SheetObjectPropsValue>> {
return prism(() => { return prism(() => {
const tracksToProcessD = prism.memo( const tracksToProcessD = prism.memo(
'tracksToProcess', 'tracksToProcess',
@ -140,7 +156,7 @@ export default class SheetObject implements IdentityDerivationProvider {
) )
const tracksToProcess = val(tracksToProcessD) const tracksToProcess = val(tracksToProcessD)
const valsAtom = new Atom<SerializableMap>({}) const valsAtom = new Atom<SheetObjectPropsValue>({})
prism.effect( prism.effect(
'processTracks', 'processTracks',
@ -216,17 +232,20 @@ export default class SheetObject implements IdentityDerivationProvider {
return interpolationTripleAtPosition(trackP, timeD) return interpolationTripleAtPosition(trackP, timeD)
} }
get propsP(): Pointer<$FixMe> { get propsP(): Pointer<SheetObjectPropsValue> {
return this._cache.get('propsP', () => return this._cache.get('propsP', () =>
pointer<{props: {}}>({root: this, path: []}), pointer<{props: {}}>({root: this, path: []}),
) as $FixMe ) as $FixMe
} }
validateValue(pointer: Pointer<$FixMe>, value: unknown) { validateValue(
pointer: Pointer<SheetObjectPropsValue>,
value: DeepPartialOfSerializableValue<SheetObjectPropsValue>,
) {
// @todo // @todo
} }
setInitialValue(val: SerializableMap) { setInitialValue(val: DeepPartialOfSerializableValue<SheetObjectPropsValue>) {
this.validateValue(this.propsP, val) this.validateValue(this.propsP, val)
this._initialValue.setState(val) this._initialValue.setState(val)
} }

View file

@ -4,6 +4,7 @@
import {setupTestSheet} from '@theatre/shared/testUtils' import {setupTestSheet} from '@theatre/shared/testUtils'
import {encodePathToProp} from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses'
import {asSequenceTrackId} from '@theatre/shared/utils/ids' 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 type {$IntentionalAny} from '@theatre/shared/utils/types'
import {iterateOver} from '@theatre/dataverse' import {iterateOver} from '@theatre/dataverse'
@ -19,15 +20,15 @@ describe(`SheetObjectTemplate`, () => {
subUnitsPerUnit: 30, subUnitsPerUnit: 30,
length: 10, length: 10,
tracksByObject: { tracksByObject: {
obj: { ['obj' as ObjectAddressKey]: {
trackIdByPropPath: { trackIdByPropPath: {
[encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'), [encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'),
[encodePathToProp(['position', 'invalid'])]: [encodePathToProp(['position', 'invalid'])]:
asSequenceTrackId('invalidTrack'), asSequenceTrackId('invalidTrack'),
}, },
trackData: { trackData: {
x: null as $IntentionalAny, ['x' as SequenceTrackId]: null as $IntentionalAny,
invalid: null as $IntentionalAny, ['invalid' as SequenceTrackId]: null as $IntentionalAny,
}, },
}, },
}, },
@ -74,15 +75,15 @@ describe(`SheetObjectTemplate`, () => {
subUnitsPerUnit: 30, subUnitsPerUnit: 30,
length: 10, length: 10,
tracksByObject: { tracksByObject: {
obj: { ['obj' as ObjectAddressKey]: {
trackIdByPropPath: { trackIdByPropPath: {
[encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'), [encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'),
[encodePathToProp(['position', 'invalid'])]: [encodePathToProp(['position', 'invalid'])]:
asSequenceTrackId('invalidTrack'), asSequenceTrackId('invalidTrack'),
}, },
trackData: { trackData: {
x: null as $IntentionalAny, ['x' as SequenceTrackId]: null as $IntentionalAny,
invalid: null as $IntentionalAny, ['invalid' as SequenceTrackId]: null as $IntentionalAny,
}, },
}, },
}, },

View file

@ -1,7 +1,7 @@
import type Project from '@theatre/core/projects/Project' import type Project from '@theatre/core/projects/Project'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate' 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 {emptyArray} from '@theatre/shared/utils'
import type { import type {
PathToProp, PathToProp,
@ -9,7 +9,7 @@ import type {
WithoutSheetInstance, WithoutSheetInstance,
} from '@theatre/shared/utils/addresses' } from '@theatre/shared/utils/addresses'
import getDeep from '@theatre/shared/utils/getDeep' 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 SimpleCache from '@theatre/shared/utils/SimpleCache'
import type { import type {
$FixMe, $FixMe,
@ -23,7 +23,6 @@ import set from 'lodash-es/set'
import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject' import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject'
import SheetObject from './SheetObject' import SheetObject from './SheetObject'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
import { import {
getPropConfigByPath, getPropConfigByPath,
isPropConfSequencable, isPropConfSequencable,
@ -34,12 +33,15 @@ export type IPropPathToTrackIdTree = {
[key in string]?: SequenceTrackId | 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 { export default class SheetObjectTemplate {
readonly address: WithoutSheetInstance<SheetObjectAddress> readonly address: WithoutSheetInstance<SheetObjectAddress>
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate' readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
protected _config: Atom< protected _config: Atom<SheetObjectPropTypeConfig>
SheetObjectConfig<PropTypeConfig_Compound<$IntentionalAny>>
>
readonly _cache = new SimpleCache() readonly _cache = new SimpleCache()
readonly project: Project readonly project: Project
@ -49,9 +51,9 @@ export default class SheetObjectTemplate {
constructor( constructor(
readonly sheetTemplate: SheetTemplate, readonly sheetTemplate: SheetTemplate,
objectKey: string, objectKey: ObjectAddressKey,
nativeObject: unknown, nativeObject: unknown,
config: SheetObjectConfig<$IntentionalAny>, config: SheetObjectPropTypeConfig,
) { ) {
this.address = {...sheetTemplate.address, objectKey} this.address = {...sheetTemplate.address, objectKey}
this._config = new Atom(config) this._config = new Atom(config)
@ -61,13 +63,13 @@ export default class SheetObjectTemplate {
createInstance( createInstance(
sheet: Sheet, sheet: Sheet,
nativeObject: unknown, nativeObject: unknown,
config: SheetObjectConfig<$IntentionalAny>, config: SheetObjectPropTypeConfig,
): SheetObject { ): SheetObject {
this._config.setState(config) this._config.setState(config)
return new SheetObject(sheet, this, nativeObject) return new SheetObject(sheet, this, nativeObject)
} }
overrideConfig(config: SheetObjectConfig<$IntentionalAny>) { overrideConfig(config: SheetObjectPropTypeConfig) {
this._config.setState(config) this._config.setState(config)
} }
@ -99,7 +101,7 @@ export default class SheetObjectTemplate {
pointerToSheetState.staticOverrides.byObject[ pointerToSheetState.staticOverrides.byObject[
this.address.objectKey this.address.objectKey
], ],
) || {} ) ?? {}
const config = val(this._config.pointer) const config = val(this._config.pointer)
const deserialized = config.deserializeAndSanitize(json) || {} const deserialized = config.deserializeAndSanitize(json) || {}

View file

@ -13,11 +13,13 @@ import type {IDerivation, Pointer} from '@theatre/dataverse'
import {prism, val} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse'
import type SheetObject from './SheetObject' import type SheetObject from './SheetObject'
import type { import type {
IShorthandCompoundProps, UnknownShorthandCompoundProps,
ShorthandPropToLonghandProp, PropsValue,
} from '@theatre/core/propTypes/internals' } from '@theatre/core/propTypes/internals'
export interface ISheetObject<Props extends IShorthandCompoundProps = {}> { export interface ISheetObject<
Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps,
> {
/** /**
* All Objects will have `object.type === 'Theatre_SheetObject_PublicAPI'` * All Objects will have `object.type === 'Theatre_SheetObject_PublicAPI'`
*/ */
@ -32,8 +34,14 @@ export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
* const obj = sheet.object("obj", {x: 0}) * const obj = sheet.object("obj", {x: 0})
* console.log(obj.value.x) // prints 0 or the current numeric value * 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<Props>['valueType'] readonly value: PropsValue<Props>
/** /**
* A Pointer to the props of the object. * A Pointer to the props of the object.
@ -100,7 +108,7 @@ export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
} }
export default class TheatreSheetObject< export default class TheatreSheetObject<
Props extends IShorthandCompoundProps = {}, Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps,
> implements ISheetObject<Props> > implements ISheetObject<Props>
{ {
get type(): 'Theatre_SheetObject_PublicAPI' { get type(): 'Theatre_SheetObject_PublicAPI' {
@ -134,7 +142,7 @@ export default class TheatreSheetObject<
private _valuesDerivation(): IDerivation<this['value']> { private _valuesDerivation(): IDerivation<this['value']> {
return this._cache.get('onValuesChangeDerivation', () => { return this._cache.get('onValuesChangeDerivation', () => {
const sheetObject = privateAPI(this) const sheetObject = privateAPI(this)
const d: IDerivation<Props> = prism(() => { const d: IDerivation<PropsValue<Props>> = prism(() => {
return val(sheetObject.getValues().getValue()) as $FixMe return val(sheetObject.getValues().getValue()) as $FixMe
}) })
return d return d
@ -145,7 +153,7 @@ export default class TheatreSheetObject<
return this._valuesDerivation().tapImmediate(coreTicker, fn) return this._valuesDerivation().tapImmediate(coreTicker, fn)
} }
get value(): ShorthandPropToLonghandProp<Props>['valueType'] { get value(): PropsValue<Props> {
return this._valuesDerivation().getValue() return this._valuesDerivation().getValue()
} }

View file

@ -1,7 +1,6 @@
import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet' import type {SheetObjectPropTypeConfig} from '@theatre/core/sheets/TheatreSheet'
import type { import type {
$FixMe, $FixMe,
$IntentionalAny,
SerializableMap, SerializableMap,
SerializableValue, SerializableValue,
} from '@theatre/shared/utils/types' } from '@theatre/shared/utils/types'
@ -17,9 +16,9 @@ const cachedDefaults = new WeakMap<PropTypeConfig, SerializableValue>()
* Generates and caches a default value for the config of a SheetObject. * Generates and caches a default value for the config of a SheetObject.
*/ */
export default function getPropDefaultsOfSheetObject( export default function getPropDefaultsOfSheetObject(
config: SheetObjectConfig<$IntentionalAny>, config: SheetObjectPropTypeConfig,
): SerializableMap { ): SerializableMap {
return getDefaultsOfPropTypeConfig(config) as $IntentionalAny return getDefaultsOfPropTypeConfig(config) as SerializableMap // sheet objects result in non-primitive objects
} }
function getDefaultsOfPropTypeConfig( function getDefaultsOfPropTypeConfig(

View file

@ -1,17 +1,28 @@
import type Project from '@theatre/core/projects/Project' import type Project from '@theatre/core/projects/Project'
import Sequence from '@theatre/core/sequences/Sequence' import Sequence from '@theatre/core/sequences/Sequence'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' 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 TheatreSheet from '@theatre/core/sheets/TheatreSheet'
import type {SheetAddress} from '@theatre/shared/utils/addresses' import type {SheetAddress} from '@theatre/shared/utils/addresses'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {Atom, valueDerivation} from '@theatre/dataverse' import {Atom, valueDerivation} from '@theatre/dataverse'
import type SheetTemplate from './SheetTemplate' 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<ObjectAddressKey, SheetObject>
/**
* 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 { export default class Sheet {
private readonly _objects: Atom<IObjects> = new Atom<IObjects>({}) private readonly _objects: Atom<SheetObjectMap> =
new Atom<SheetObjectMap>({})
private _sequence: undefined | Sequence private _sequence: undefined | Sequence
readonly address: SheetAddress readonly address: SheetAddress
readonly publicApi: TheatreSheet readonly publicApi: TheatreSheet
@ -21,7 +32,7 @@ export default class Sheet {
constructor( constructor(
readonly template: SheetTemplate, readonly template: SheetTemplate,
public readonly instanceId: string, public readonly instanceId: SheetInstanceId,
) { ) {
this.project = template.project this.project = template.project
this.address = { this.address = {
@ -37,24 +48,24 @@ export default class Sheet {
* with that of "an element." * with that of "an element."
*/ */
createObject( createObject(
key: string, objectKey: ObjectAddressKey,
nativeObject: unknown, nativeObject: ObjectNativeObject,
config: SheetObjectConfig<$IntentionalAny>, config: SheetObjectPropTypeConfig,
): SheetObject { ): SheetObject {
const objTemplate = this.template.getObjectTemplate( const objTemplate = this.template.getObjectTemplate(
key, objectKey,
nativeObject, nativeObject,
config, config,
) )
const object = objTemplate.createInstance(this, nativeObject, config) const object = objTemplate.createInstance(this, nativeObject, config)
this._objects.setIn([key], object) this._objects.setIn([objectKey], object)
return object return object
} }
getObject(key: string): SheetObject | undefined { getObject(key: ObjectAddressKey): SheetObject | undefined {
return this._objects.getState()[key] return this._objects.getState()[key]
} }

View file

@ -4,27 +4,38 @@ import type {
SheetAddress, SheetAddress,
WithoutSheetInstance, WithoutSheetInstance,
} from '@theatre/shared/utils/addresses' } from '@theatre/shared/utils/addresses'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {Atom} from '@theatre/dataverse' import {Atom} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import Sheet from './Sheet' 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 { export default class SheetTemplate {
readonly type: 'Theatre_SheetTemplate' = 'Theatre_SheetTemplate' readonly type: 'Theatre_SheetTemplate' = 'Theatre_SheetTemplate'
readonly address: WithoutSheetInstance<SheetAddress> readonly address: WithoutSheetInstance<SheetAddress>
private _instances = new Atom<{[instanceId: string]: Sheet}>({}) private _instances = new Atom<Record<SheetInstanceId, Sheet>>({})
readonly instancesP = this._instances.pointer readonly instancesP: Pointer<Record<SheetInstanceId, Sheet>> =
this._instances.pointer
private _objectTemplates = new Atom<{ private _objectTemplates = new Atom<SheetTemplateObjectTemplateMap>({})
[objectKey: string]: SheetObjectTemplate
}>({})
readonly objectTemplatesP = this._objectTemplates.pointer readonly objectTemplatesP = this._objectTemplates.pointer
constructor(readonly project: Project, sheetId: string) { constructor(readonly project: Project, sheetId: SheetId) {
this.address = {...project.address, sheetId} this.address = {...project.address, sheetId}
} }
getInstance(instanceId: string): Sheet { getInstance(instanceId: SheetInstanceId): Sheet {
let inst = this._instances.getState()[instanceId] let inst = this._instances.getState()[instanceId]
if (!inst) { if (!inst) {
@ -36,15 +47,15 @@ export default class SheetTemplate {
} }
getObjectTemplate( getObjectTemplate(
key: string, objectKey: ObjectAddressKey,
nativeObject: unknown, nativeObject: ObjectNativeObject,
config: SheetObjectConfig<$IntentionalAny>, config: SheetObjectPropTypeConfig,
): SheetObjectTemplate { ): SheetObjectTemplate {
let template = this._objectTemplates.getState()[key] let template = this._objectTemplates.getState()[objectKey]
if (!template) { if (!template) {
template = new SheetObjectTemplate(this, key, nativeObject, config) template = new SheetObjectTemplate(this, objectKey, nativeObject, config)
this._objectTemplates.setIn([key], template) this._objectTemplates.setIn([objectKey], template)
} }
return template return template

View file

@ -9,15 +9,18 @@ import type Sheet from '@theatre/core/sheets/Sheet'
import type {SheetAddress} from '@theatre/shared/utils/addresses' import type {SheetAddress} from '@theatre/shared/utils/addresses'
import {InvalidArgumentError} from '@theatre/shared/utils/errors' import {InvalidArgumentError} from '@theatre/shared/utils/errors'
import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths' 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 userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import deepEqual from 'fast-deep-equal' 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 SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {ObjectAddressKey} from '@theatre/shared/utils/ids'
export type SheetObjectConfig< export type SheetObjectPropTypeConfig =
Props extends PropTypeConfig_Compound<$IntentionalAny>, PropTypeConfig_Compound<UnknownValidCompoundProps>
> = Props
export interface ISheet { export interface ISheet {
/** /**
@ -62,7 +65,7 @@ export interface ISheet {
* obj.value.position // {x: 0, y: 0} * obj.value.position // {x: 0, y: 0}
* ``` * ```
*/ */
object<Props extends IShorthandCompoundProps>( object<Props extends UnknownShorthandCompoundProps>(
key: string, key: string,
props: Props, props: Props,
): ISheetObject<Props> ): ISheetObject<Props>
@ -75,7 +78,7 @@ export interface ISheet {
const weakMapOfUnsanitizedProps = new WeakMap< const weakMapOfUnsanitizedProps = new WeakMap<
SheetObject, SheetObject,
IShorthandCompoundProps UnknownShorthandCompoundProps
>() >()
export default class TheatreSheet implements ISheet { export default class TheatreSheet implements ISheet {
@ -89,7 +92,7 @@ export default class TheatreSheet implements ISheet {
setPrivateAPI(this, sheet) setPrivateAPI(this, sheet)
} }
object<Props extends IShorthandCompoundProps>( object<Props extends UnknownShorthandCompoundProps>(
key: string, key: string,
config: Props, config: Props,
): ISheetObject<Props> { ): ISheetObject<Props> {
@ -99,8 +102,15 @@ export default class TheatreSheet implements ISheet {
`sheet.object("${key}", ...)`, `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 const nativeObject = null
if (existingObject) { if (existingObject) {
@ -121,12 +131,12 @@ export default class TheatreSheet implements ISheet {
} else { } else {
const sanitizedConfig = compound(config) const sanitizedConfig = compound(config)
const object = internal.createObject( const object = internal.createObject(
sanitizedPath, sanitizedPath as ObjectAddressKey,
nativeObject, nativeObject,
sanitizedConfig, sanitizedConfig,
) )
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
weakMapOfUnsanitizedProps.set(object, config) weakMapOfUnsanitizedProps.set(object as $FixMe, config)
} }
return object.publicApi as $IntentionalAny return object.publicApi as $IntentionalAny
} }

View file

@ -8,6 +8,7 @@ import * as t from '@theatre/core/propTypes'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import coreTicker from '@theatre/core/coreTicker' import coreTicker from '@theatre/core/coreTicker'
import globals from './globals' import globals from './globals'
import type {SheetId} from './utils/ids'
/* eslint-enable no-restricted-syntax */ /* eslint-enable no-restricted-syntax */
const defaultProps = { const defaultProps = {
@ -32,7 +33,7 @@ export async function setupTestSheet(sheetState: SheetState_Historic) {
const projectState: ProjectState_Historic = { const projectState: ProjectState_Historic = {
definitionVersion: globals.currentProjectStateDefinitionVersion, definitionVersion: globals.currentProjectStateDefinitionVersion,
sheetsById: { sheetsById: {
Sheet: sheetState, ['Sheet' as SheetId]: sheetState,
}, },
revisionHistory: [], revisionHistory: [],
} }

View file

@ -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<N extends string> = 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<T extends Record<Nominal<string>, any>>(
obj: T,
): any extends T ? never[] : Extract<keyof T, string>[]
/** Nominal: Extension to the Object prototype definition to properly manage {@link Nominal} keyed records */
entries<T extends Record<Nominal<string>, any>>(
obj: T,
): any extends T
? [never, never][]
: Array<{[P in keyof T]: [P, T[P]]}[Extract<keyof T, string>]>
}
}

View file

@ -3,12 +3,13 @@ import type {
SerializableMap, SerializableMap,
SerializableValue, SerializableValue,
} from '@theatre/shared/utils/types' } from '@theatre/shared/utils/types'
import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids'
/** /**
* Represents the address to a project * Represents the address to a project
*/ */
export interface ProjectAddress { export interface ProjectAddress {
projectId: string projectId: ProjectId
} }
/** /**
@ -22,8 +23,8 @@ export interface ProjectAddress {
* ``` * ```
*/ */
export interface SheetAddress extends ProjectAddress { export interface SheetAddress extends ProjectAddress {
sheetId: string sheetId: SheetId
sheetInstanceId: string sheetInstanceId: SheetInstanceId
} }
/** /**
@ -36,7 +37,7 @@ export type WithoutSheetInstance<T extends SheetAddress> = Omit<
> >
export type SheetInstanceOptional<T extends SheetAddress> = export type SheetInstanceOptional<T extends SheetAddress> =
WithoutSheetInstance<T> & {sheetInstanceId?: string | undefined} WithoutSheetInstance<T> & {sheetInstanceId?: SheetInstanceId | undefined}
/** /**
* Represents the address to a Sheet's Object * Represents the address to a Sheet's Object
@ -51,7 +52,7 @@ export interface SheetObjectAddress extends SheetAddress {
* obj.address.objectKey === 'foo' * obj.address.objectKey === 'foo'
* ``` * ```
*/ */
objectKey: string objectKey: ObjectAddressKey
} }
export type PathToProp = Array<string | number> export type PathToProp = Array<string | number>

View file

@ -1,9 +1,10 @@
import {nanoid as generateNonSecure} from 'nanoid/non-secure' 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<string, 'KeyframeId'> export type KeyframeId = Nominal<'KeyframeId'>
export function generateKeyframeId() { export function generateKeyframeId(): KeyframeId {
return generateNonSecure(10) as KeyframeId return generateNonSecure(10) as KeyframeId
} }
@ -11,10 +12,16 @@ export function asKeyframeId(s: string): KeyframeId {
return s as $IntentionalAny return s as $IntentionalAny
} }
// @todo make nominal export type ProjectId = Nominal<'ProjectId'>
export type SequenceTrackId = string 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 return generateNonSecure(10) as $IntentionalAny as SequenceTrackId
} }

View file

@ -2,11 +2,11 @@ import type {Pointer} from '@theatre/dataverse'
import type {PathToProp} from './addresses' import type {PathToProp} from './addresses'
import type {$IntentionalAny} from './types' import type {$IntentionalAny} from './types'
export default function pointerDeep( export default function pointerDeep<T>(
base: Pointer<$IntentionalAny>, base: Pointer<T>,
toAppend: PathToProp, toAppend: PathToProp,
): Pointer<unknown> { ): Pointer<unknown> {
let p = base let p = base as $IntentionalAny
for (const k of toAppend) { for (const k of toAppend) {
p = p[k] p = p[k]
} }

View file

@ -44,6 +44,13 @@ export type SerializablePrimitive =
| boolean | boolean
| {r: number; g: number; b: number; a: number} | {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< export type SerializableValue<
Primitives extends SerializablePrimitive = SerializablePrimitive, Primitives extends SerializablePrimitive = SerializablePrimitive,
> = Primitives | SerializableMap > = Primitives | SerializableMap
@ -57,15 +64,13 @@ export type DeepPartialOfSerializableValue<T extends SerializableValue> =
} }
: T : T
export type StrictRecord<Key extends string, V> = {[K in Key]?: V}
/** /**
* This is supposed to create an "opaque" or "nominal" type, but since typescript * This is equivalent to `Partial<Record<Key, V>>` being used to describe a sort of Map
* doesn't allow generic index signatures, we're leaving it be. * 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, N extends string> = T export type StrictRecord<Key extends string, V> = {[K in Key]?: V}
/** /**
* TODO: We should deprecate this and just use `[start: number, end: number]` * 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(). * Represents the `x` or `y` value of getBoundingClientRect().
* In other words, represents a distance from 0,0 in screen space. * In other words, represents a distance from 0,0 in screen space.
*/ */
export type PositionInScreenSpace = Nominal<number, 'ScreenSpaceDim'> export type PositionInScreenSpace = number

View file

@ -1,7 +1,8 @@
import {prism, val} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse'
import {emptyArray} from '@theatre/shared/utils' import {emptyArray} from '@theatre/shared/utils'
import type {PaneInstanceId} from '@theatre/shared/utils/ids'
import SimpleCache from '@theatre/shared/utils/SimpleCache' 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 {Studio} from './Studio'
import type {PaneInstance} from './TheatreStudio' import type {PaneInstance} from './TheatreStudio'
@ -21,7 +22,7 @@ export default class PaneManager {
private _getAllPanes() { private _getAllPanes() {
return this._cache.get('_getAllPanels()', () => return this._cache.get('_getAllPanels()', () =>
prism((): {[instanceId in string]?: PaneInstance<string>} => { prism((): StrictRecord<PaneInstanceId, PaneInstance<string>> => {
const core = val(this._studio.coreP) const core = val(this._studio.coreP)
if (!core) return {} if (!core) return {}
const instanceDescriptors = val( const instanceDescriptors = val(
@ -31,17 +32,16 @@ export default class PaneManager {
this._studio.atomP.ephemeral.extensions.paneClasses, this._studio.atomP.ephemeral.extensions.paneClasses,
) )
const instances: {[instanceId in string]?: PaneInstance<string>} = {} const instances: StrictRecord<PaneInstanceId, PaneInstance<string>> = {}
for (const [, instanceDescriptor] of Object.entries( for (const instanceDescriptor of Object.values(instanceDescriptors)) {
instanceDescriptors, if (!instanceDescriptor) continue
)) { const panelClass = paneClasses[instanceDescriptor.paneClass]
const panelClass = paneClasses[instanceDescriptor!.paneClass]
if (!panelClass) continue if (!panelClass) continue
const {instanceId} = instanceDescriptor! const {instanceId} = instanceDescriptor
const {extensionId, classDefinition: definition} = panelClass const {extensionId, classDefinition: definition} = panelClass
const instance = prism.memo( const instance = prism.memo(
`instance-${instanceDescriptor!.instanceId}`, `instance-${instanceDescriptor.instanceId}`,
() => { () => {
const inst: PaneInstance<$IntentionalAny> = { const inst: PaneInstance<$IntentionalAny> = {
extensionId, extensionId,
@ -82,14 +82,14 @@ export default class PaneManager {
const allPaneInstances = val( const allPaneInstances = val(
this._studio.atomP.historic.panelInstanceDesceriptors, this._studio.atomP.historic.panelInstanceDesceriptors,
) )
let instanceId!: string let instanceId!: PaneInstanceId
for (let i = 1; i < 1000; i++) { for (let i = 1; i < 1000; i++) {
instanceId = `${paneClass} #${i}` instanceId = `${paneClass} #${i}` as PaneInstanceId
if (!allPaneInstances[instanceId]) break if (!allPaneInstances[instanceId]) break
} }
if (!extensionId) { if (!extensionId) {
throw new Error(`Pance class "${paneClass}" is not registered.`) throw new Error(`Pane class "${paneClass}" is not registered.`)
} }
this._studio.transaction(({drafts}) => { this._studio.transaction(({drafts}) => {
@ -102,7 +102,7 @@ export default class PaneManager {
return this._getAllPanes().getValue()[instanceId]! return this._getAllPanes().getValue()[instanceId]!
} }
destroyPane(instanceId: string): void { destroyPane(instanceId: PaneInstanceId): void {
const core = this._studio.core const core = this._studio.core
if (!core) { if (!core) {
throw new Error( throw new Error(

View file

@ -20,6 +20,7 @@ import type * as _coreExports from '@theatre/core/coreExports'
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
import type {Deferred} from '@theatre/shared/utils/defer' import type {Deferred} from '@theatre/shared/utils/defer'
import {defer} 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 export type CoreExports = typeof _coreExports
@ -27,10 +28,10 @@ export class Studio {
readonly ui!: UI readonly ui!: UI
readonly publicApi: IStudio readonly publicApi: IStudio
readonly address: {studioId: string} readonly address: {studioId: string}
readonly _projectsProxy: PointerProxy<Record<string, Project>> = readonly _projectsProxy: PointerProxy<Record<ProjectId, Project>> =
new PointerProxy(new Atom({}).pointer) new PointerProxy(new Atom({}).pointer)
readonly projectsP: Pointer<Record<string, Project>> = readonly projectsP: Pointer<Record<ProjectId, Project>> =
this._projectsProxy.pointer this._projectsProxy.pointer
private readonly _store = new StudioStore() private readonly _store = new StudioStore()
@ -124,7 +125,7 @@ export class Studio {
this._setProjectsP(coreBits.projectsP) this._setProjectsP(coreBits.projectsP)
} }
private _setProjectsP(projectsP: Pointer<Record<string, Project>>) { private _setProjectsP(projectsP: Pointer<Record<ProjectId, Project>>) {
this._projectsProxy.setPointer(projectsP) this._projectsProxy.setPointer(projectsP)
} }
@ -218,6 +219,6 @@ export class Studio {
} }
createContentOfSaveFile(projectId: string): OnDiskState { createContentOfSaveFile(projectId: string): OnDiskState {
return this._store.createContentOfSaveFile(projectId) return this._store.createContentOfSaveFile(projectId as ProjectId)
} }
} }

View file

@ -25,6 +25,7 @@ import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
import {generateDiskStateRevision} from './generateDiskStateRevision' import {generateDiskStateRevision} from './generateDiskStateRevision'
import createTransactionPrivateApi from './createTransactionPrivateApi' import createTransactionPrivateApi from './createTransactionPrivateApi'
import type {ProjectId} from '@theatre/shared/utils/ids'
export type Drafts = { export type Drafts = {
historic: Draft<StudioHistoricState> historic: Draft<StudioHistoricState>
@ -173,7 +174,7 @@ export default class StudioStore {
this._reduxStore.dispatch(studioActions.historic.redo()) this._reduxStore.dispatch(studioActions.historic.redo())
} }
createContentOfSaveFile(projectId: string): OnDiskState { createContentOfSaveFile(projectId: ProjectId): OnDiskState {
const projectState = const projectState =
this._reduxStore.getState().$persistent.historic.innerState.coreByProject[ this._reduxStore.getState().$persistent.historic.innerState.coreByProject[
projectId projectId

View file

@ -16,6 +16,7 @@ import getStudio from './getStudio'
import type React from 'react' import type React from 'react'
import {debounce} from 'lodash-es' import {debounce} from 'lodash-es'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import type {PaneInstanceId, ProjectId} from '@theatre/shared/utils/ids'
export interface ITransactionAPI { export interface ITransactionAPI {
/** /**
@ -116,7 +117,7 @@ export interface IExtension {
export type PaneInstance<ClassName extends string> = { export type PaneInstance<ClassName extends string> = {
extensionId: string extensionId: string
instanceId: string instanceId: PaneInstanceId
definition: PaneClassDefinition definition: PaneClassDefinition
} }
@ -471,10 +472,12 @@ export default class TheatreStudio implements IStudio {
} }
destroyPane(paneId: string): void { destroyPane(paneId: string): void {
return getStudio().paneManager.destroyPane(paneId) return getStudio().paneManager.destroyPane(paneId as PaneInstanceId)
} }
createContentOfSaveFile(projectId: string): Record<string, unknown> { createContentOfSaveFile(projectId: string): Record<string, unknown> {
return getStudio().createContentOfSaveFile(projectId) as $IntentionalAny return getStudio().createContentOfSaveFile(
projectId as ProjectId,
) as $IntentionalAny
} }
} }

View file

@ -1,5 +1,6 @@
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {UIPanelId} from '@theatre/shared/utils/ids'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {PanelPosition} from '@theatre/studio/store/types' import type {PanelPosition} from '@theatre/studio/store/types'
@ -8,7 +9,7 @@ import React, {useContext} from 'react'
import useWindowSize from 'react-use/esm/useWindowSize' import useWindowSize from 'react-use/esm/useWindowSize'
type PanelStuff = { type PanelStuff = {
panelId: string panelId: UIPanelId
dims: { dims: {
width: number width: number
height: number height: number
@ -64,7 +65,7 @@ const PanelContext = React.createContext<PanelStuff>(null as $IntentionalAny)
export const usePanel = () => useContext(PanelContext) export const usePanel = () => useContext(PanelContext)
const BasePanel: React.FC<{ const BasePanel: React.FC<{
panelId: string panelId: UIPanelId
defaultPosition: PanelPosition defaultPosition: PanelPosition
minDims: {width: number; height: number} minDims: {width: number; height: number}
}> = ({panelId, children, defaultPosition, minDims}) => { }> = ({panelId, children, defaultPosition, minDims}) => {

View file

@ -11,6 +11,7 @@ import {ErrorBoundary} from 'react-error-boundary'
import {IoClose} from 'react-icons/all' import {IoClose} from 'react-icons/all'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common' import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
import type {PaneInstanceId, UIPanelId} from '@theatre/shared/utils/ids'
const defaultPosition: PanelPosition = { const defaultPosition: PanelPosition = {
edges: { edges: {
@ -28,7 +29,7 @@ const ExtensionPaneWrapper: React.FC<{
}> = ({paneInstance}) => { }> = ({paneInstance}) => {
return ( return (
<BasePanel <BasePanel
panelId={`pane-${paneInstance.instanceId}`} panelId={`pane-${paneInstance.instanceId}` as UIPanelId}
defaultPosition={defaultPosition} defaultPosition={defaultPosition}
minDims={minDims} minDims={minDims}
> >
@ -137,7 +138,9 @@ const Content: React.FC<{paneInstance: PaneInstance<$FixMe>}> = ({
}) => { }) => {
const Comp = paneInstance.definition.component const Comp = paneInstance.definition.component
const closePane = useCallback(() => { const closePane = useCallback(() => {
getStudio().paneManager.destroyPane(paneInstance.instanceId) getStudio().paneManager.destroyPane(
paneInstance.instanceId as PaneInstanceId,
)
}, [paneInstance]) }, [paneInstance])
return ( return (

View file

@ -1,6 +1,8 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import DeterminePropEditor from './propEditors/DeterminePropEditor' import DeterminePropEditor from './propEditors/DeterminePropEditor'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' 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<{ const ObjectDetails: React.FC<{
objects: SheetObject[] objects: SheetObject[]
@ -13,9 +15,9 @@ const ObjectDetails: React.FC<{
<DeterminePropEditor <DeterminePropEditor
key={key} key={key}
obj={obj} obj={obj}
pointerToProp={obj.propsP} pointerToProp={obj.propsP as Pointer<$FixMe>}
propConfig={obj.template.config} propConfig={obj.template.config}
depth={1} visualIndentation={1}
/> />
) )
} }

View file

@ -8,6 +8,7 @@ import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip' import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip'
import type {$FixMe} from '@theatre/shared/utils/types' import type {$FixMe} from '@theatre/shared/utils/types'
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton' import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
import type {ProjectId} from '@theatre/shared/utils/ids'
const Container = styled.div` const Container = styled.div`
padding: 8px 10px; padding: 8px 10px;
@ -37,7 +38,7 @@ const ChooseStateRow = styled.div`
gap: 8px; gap: 8px;
` `
const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => { const StateConflictRow: React.FC<{projectId: ProjectId}> = ({projectId}) => {
const loadingState = useVal( const loadingState = useVal(
getStudio().atomP.ephemeral.coreByProject[projectId].loadingState, getStudio().atomP.ephemeral.coreByProject[projectId].loadingState,
) )
@ -52,7 +53,7 @@ const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => {
} }
const InConflict: React.FC<{ const InConflict: React.FC<{
projectId: string projectId: ProjectId
loadingState: Extract< loadingState: Extract<
ProjectEphemeralState['loadingState'], ProjectEphemeralState['loadingState'],
{type: 'browserStateIsNotBasedOnDiskState'} {type: 'browserStateIsNotBasedOnDiskState'}

View file

@ -61,7 +61,7 @@ const SubProps = styled.div<{depth: number; lastSubIsComposite: boolean}>`
const CompoundPropEditor: IPropEditorFC< const CompoundPropEditor: IPropEditorFC<
PropTypeConfig_Compound<$IntentionalAny> PropTypeConfig_Compound<$IntentionalAny>
> = ({pointerToProp, obj, propConfig, depth}) => { > = ({pointerToProp, obj, propConfig, visualIndentation: depth}) => {
const propName = propConfig.label ?? last(getPointerParts(pointerToProp).path) const propName = propConfig.label ?? last(getPointerParts(pointerToProp).path)
const allSubs = Object.entries(propConfig.props) const allSubs = Object.entries(propConfig.props)
@ -154,7 +154,7 @@ const CompoundPropEditor: IPropEditorFC<
propConfig={subPropConfig} propConfig={subPropConfig}
pointerToProp={pointerToProp[subPropKey]} pointerToProp={pointerToProp[subPropKey]}
obj={obj} obj={obj}
depth={depth + 1} visualIndentation={depth + 1}
/> />
) )
}, },

View file

@ -9,15 +9,15 @@ import NumberPropEditor from './NumberPropEditor'
import StringLiteralPropEditor from './StringLiteralPropEditor' import StringLiteralPropEditor from './StringLiteralPropEditor'
import StringPropEditor from './StringPropEditor' import StringPropEditor from './StringPropEditor'
import RgbaPropEditor from './RgbaPropEditor' 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 * Returns the PropTypeConfig by path. Assumes `path` is a valid prop path and that
* it exists in obj. * it exists in obj.
*/ */
export const getPropTypeByPointer = ( export function getPropTypeByPointer<
pointerToProp: SheetObject['propsP'], Props extends UnknownShorthandCompoundProps,
obj: SheetObject, >(pointerToProp: SheetObject['propsP'], obj: SheetObject): PropTypeConfig {
): PropTypeConfig => {
const rootConf = obj.template.config const rootConf = obj.template.config
const p = getPointerParts(pointerToProp).path const p = getPointerParts(pointerToProp).path
@ -67,7 +67,7 @@ type IPropEditorByPropType = {
obj: SheetObject obj: SheetObject
pointerToProp: Pointer<PropConfigByType<K>['valueType']> pointerToProp: Pointer<PropConfigByType<K>['valueType']>
propConfig: PropConfigByType<K> propConfig: PropConfigByType<K>
depth: number visualIndentation: number
}> }>
} }
@ -81,12 +81,20 @@ const propEditorByPropType: IPropEditorByPropType = {
rgba: RgbaPropEditor, rgba: RgbaPropEditor,
} }
const DeterminePropEditor: React.FC<{ export type IEditablePropertyProps<K extends PropTypeConfig['type']> = {
obj: SheetObject obj: SheetObject
pointerToProp: SheetObject['propsP'] pointerToProp: Pointer<PropConfigByType<K>['valueType']>
propConfig?: PropTypeConfig propConfig: PropConfigByType<K>
depth: number }
}> = (p) => {
type IDeterminePropEditorProps<K extends PropTypeConfig['type']> =
IEditablePropertyProps<K> & {
visualIndentation: number
}
const DeterminePropEditor: React.FC<
IDeterminePropEditorProps<PropTypeConfig['type']>
> = (p) => {
const propConfig = const propConfig =
p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj) p.propConfig ?? getPropTypeByPointer(p.pointerToProp, p.obj)
@ -95,7 +103,7 @@ const DeterminePropEditor: React.FC<{
return ( return (
<PropEditor <PropEditor
obj={p.obj} obj={p.obj}
depth={p.depth} visualIndentation={p.visualIndentation}
// @ts-expect-error This is fine // @ts-expect-error This is fine
pointerToProp={p.pointerToProp} pointerToProp={p.pointerToProp}
// @ts-expect-error This is fine // @ts-expect-error This is fine

View file

@ -8,5 +8,5 @@ export type IPropEditorFC<TPropTypeConfig extends IBasePropType<string, any>> =
propConfig: TPropTypeConfig propConfig: TPropTypeConfig
pointerToProp: Pointer<TPropTypeConfig['valueType']> pointerToProp: Pointer<TPropTypeConfig['valueType']>
obj: SheetObject obj: SheetObject
depth: number visualIndentation: number
}> }>

View file

@ -5,7 +5,7 @@ import type Scrub from '@theatre/studio/Scrub'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import getDeep from '@theatre/shared/utils/getDeep' import getDeep from '@theatre/shared/utils/getDeep'
import {usePrism} from '@theatre/react' 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 {getPointerParts, prism, val} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import get from 'lodash-es/get' import get from 'lodash-es/get'
@ -15,6 +15,7 @@ import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
import type {PropTypeConfig} from '@theatre/core/propTypes' import type {PropTypeConfig} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
interface CommonStuff<T> { interface CommonStuff<T> {
value: T 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( const nearbyKeyframes = prism.sub(
'lcr', 'lcr',
(): NearbyKeyframes => { (): NearbyKeyframes => {
@ -155,7 +156,7 @@ export function useEditingToolsForPrimitiveProp<
obj.template.project.pointers.historic.sheetsById[ obj.template.project.pointers.historic.sheetsById[
obj.address.sheetId obj.address.sheetId
].sequence.tracksByObject[obj.address.objectKey].trackData[ ].sequence.tracksByObject[obj.address.objectKey].trackData[
sequenceTrcackId sequenceTrackId
], ],
) )
if (!track || track.keyframes.length === 0) return {} if (!track || track.keyframes.length === 0) return {}
@ -186,7 +187,7 @@ export function useEditingToolsForPrimitiveProp<
} }
} }
}, },
[sequenceTrcackId], [sequenceTrackId],
) )
let shade: Shade let shade: Shade

View file

@ -26,7 +26,7 @@ const ObjectsList: React.FC<{
<ObjectItem <ObjectItem
depth={depth} depth={depth}
key={'objectPath(' + objectPath + ')'} key={'objectPath(' + objectPath + ')'}
sheetObject={object} sheetObject={object!}
/> />
) )
})} })}

View file

@ -44,7 +44,10 @@ const BasicKeyframedTrack: React.FC<BasicKeyframedTracksProps> = React.memo(
selection: val(selectionAtom.pointer.current), selection: val(selectionAtom.pointer.current),
} }
} else { } else {
return {selectedKeyframeIds: {}, selection: undefined} return {
selectedKeyframeIds: {},
selection: undefined,
}
} }
}, [layoutP, leaf.trackId]) }, [layoutP, leaf.trackId])

View file

@ -92,7 +92,7 @@ const HitZone = styled.div`
type IKeyframeDotProps = IKeyframeEditorProps type IKeyframeDotProps = IKeyframeEditorProps
/** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */ /** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
const KeyframeDot: React.FC<IKeyframeDotProps> = (props) => { const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null) const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragKeyframe(node, props) const [isDragging] = useDragKeyframe(node, props)

View file

@ -79,8 +79,8 @@ function useCaptureSelection(
val(layoutP.selectionAtom).setState({current: undefined}) val(layoutP.selectionAtom).setState({current: undefined})
}, },
onDrag(dx, dy, event) { onDrag(_dx, _dy, event) {
const state = ref.current! // const state = ref.current!
const rect = containerNode!.getBoundingClientRect() const rect = containerNode!.getBoundingClientRect()
const posInScaledSpace = event.clientX - rect.left const posInScaledSpace = event.clientX - rect.left
@ -97,25 +97,13 @@ function useCaptureSelection(
const selection = utils.boundsToSelection(layoutP, ref.current) const selection = utils.boundsToSelection(layoutP, ref.current)
val(layoutP.selectionAtom).setState({current: selection}) val(layoutP.selectionAtom).setState({current: selection})
}, },
onDragEnd(dragHappened) { onDragEnd(_dragHappened) {
ref.current = null ref.current = null
}, },
} }
}, [layoutP, containerNode, ref]), }, [layoutP, containerNode, ref]),
) )
// useEffect(() => {
// if (!containerNode) return
// const onClick = () => {
// }
// containerNode.addEventListener('click', onClick)
// return () => {
// containerNode.removeEventListener('click', onClick)
// }
// }, [containerNode])
return state return state
} }
@ -131,16 +119,11 @@ namespace utils {
primitiveProp(layoutP, leaf, bounds, selection) { primitiveProp(layoutP, leaf, bounds, selection) {
const {sheetObject, trackId} = leaf const {sheetObject, trackId} = leaf
const trackData = val( const trackData = val(
getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId] getStudio().atomP.historic.coreByProject[sheetObject.address.projectId]
.sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[ .sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[
sheetObject.address.objectKey sheetObject.address.objectKey
].trackData[trackId], ].trackData[trackId],
)! )!
const toCollect = trackData!.keyframes.filter(
(kf) =>
kf.position >= bounds.positions[0] &&
kf.position <= bounds.positions[1],
)
for (const kf of trackData.keyframes) { for (const kf of trackData.keyframes) {
if (kf.position <= bounds.positions[0]) continue if (kf.position <= bounds.positions[0]) continue

View file

@ -25,7 +25,7 @@ export type ExtremumSpace = {
lock(): VoidFn lock(): VoidFn
} }
const BasicKeyframedTrack: React.FC<{ const BasicKeyframedTrack: React.VFC<{
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
sheetObject: SheetObject sheetObject: SheetObject
pathToProp: PathToProp pathToProp: PathToProp
@ -102,7 +102,7 @@ const BasicKeyframedTrack: React.FC<{
sheetObject={sheetObject} sheetObject={sheetObject}
trackId={trackId} trackId={trackId}
isScalar={propConfig.type === 'number'} isScalar={propConfig.type === 'number'}
key={'keyframe-' + kf.id} key={kf.id}
extremumSpace={cachedExtremumSpace.current} extremumSpace={cachedExtremumSpace.current}
color={color} color={color}
/> />

View file

@ -15,7 +15,7 @@ const SVGPath = styled.path`
type IProps = Parameters<typeof KeyframeEditor>[0] type IProps = Parameters<typeof KeyframeEditor>[0]
const Curve: React.FC<IProps> = (props) => { const Curve: React.VFC<IProps> = (props) => {
const {index, trackData} = props const {index, trackData} = props
const cur = trackData.keyframes[index] const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]

View file

@ -49,7 +49,7 @@ type Which = 'left' | 'right'
type IProps = Parameters<typeof KeyframeEditor>[0] & {which: Which} type IProps = Parameters<typeof KeyframeEditor>[0] & {which: Which}
const CurveHandle: React.FC<IProps> = (props) => { const CurveHandle: React.VFC<IProps> = (props) => {
const [ref, node] = useRefAndState<SVGCircleElement | null>(null) const [ref, node] = useRefAndState<SVGCircleElement | null>(null)
const {index, trackData} = props const {index, trackData} = props

View file

@ -55,7 +55,7 @@ const HitZone = styled.circle`
type IProps = Parameters<typeof KeyframeEditor>[0] & {which: 'left' | 'right'} type IProps = Parameters<typeof KeyframeEditor>[0] & {which: 'left' | 'right'}
const GraphEditorDotNonScalar: React.FC<IProps> = (props) => { const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
const [ref, node] = useRefAndState<SVGCircleElement | null>(null) const [ref, node] = useRefAndState<SVGCircleElement | null>(null)
const {index, trackData} = props const {index, trackData} = props

View file

@ -55,7 +55,7 @@ const HitZone = styled.circle`
type IProps = Parameters<typeof KeyframeEditor>[0] type IProps = Parameters<typeof KeyframeEditor>[0]
const GraphEditorDotScalar: React.FC<IProps> = (props) => { const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
const [ref, node] = useRefAndState<SVGCircleElement | null>(null) const [ref, node] = useRefAndState<SVGCircleElement | null>(null)
const {index, trackData} = props const {index, trackData} = props

View file

@ -17,7 +17,7 @@ const SVGPath = styled.path`
type IProps = Parameters<typeof KeyframeEditor>[0] type IProps = Parameters<typeof KeyframeEditor>[0]
const GraphEditorNonScalarDash: React.FC<IProps> = (props) => { const GraphEditorNonScalarDash: React.VFC<IProps> = (props) => {
const {index, trackData} = props const {index, trackData} = props
const pathD = `M 0 0 L 1 1` const pathD = `M 0 0 L 1 1`

View file

@ -23,7 +23,7 @@ const Container = styled.g`
const noConnector = <></> const noConnector = <></>
const KeyframeEditor: React.FC<{ type IKeyframeEditorProps = {
index: number index: number
keyframe: Keyframe keyframe: Keyframe
trackData: TrackData trackData: TrackData
@ -34,7 +34,9 @@ const KeyframeEditor: React.FC<{
isScalar: boolean isScalar: boolean
color: keyof typeof graphEditorColors color: keyof typeof graphEditorColors
propConfig: PropTypeConfig_AllSimples propConfig: PropTypeConfig_AllSimples
}> = (props) => { }
const KeyframeEditor: React.VFC<IKeyframeEditorProps> = (props) => {
const {index, trackData, isScalar} = props const {index, trackData, isScalar} = props
const cur = trackData.keyframes[index] const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]

View file

@ -27,6 +27,7 @@ import {
TitleBar_Piece, TitleBar_Piece,
TitleBar_Punctuation, TitleBar_Punctuation,
} from '@theatre/studio/panels/BasePanel/common' } from '@theatre/studio/panels/BasePanel/common'
import type {UIPanelId} from '@theatre/shared/utils/ids'
const Container = styled(PanelWrapper)` const Container = styled(PanelWrapper)`
z-index: ${panelZIndexes.sequenceEditorPanel}; z-index: ${panelZIndexes.sequenceEditorPanel};
@ -58,7 +59,7 @@ export const zIndexes = (() => {
// sort the z-indexes // sort the z-indexes
let i = -1 let i = -1
for (const key of Object.keys(s)) { for (const key of Object.keys(s)) {
s[key as unknown as keyof typeof s] = i s[key] = i
i++ i++
} }
@ -83,10 +84,10 @@ const defaultPosition: PanelPosition = {
const minDims = {width: 800, height: 200} const minDims = {width: 800, height: 200}
const SequenceEditorPanel: React.FC<{}> = (props) => { const SequenceEditorPanel: React.VFC<{}> = (props) => {
return ( return (
<BasePanel <BasePanel
panelId="sequenceEditor" panelId={'sequenceEditor' as UIPanelId}
defaultPosition={defaultPosition} defaultPosition={defaultPosition}
minDims={minDims} minDims={minDims}
> >
@ -95,7 +96,7 @@ const SequenceEditorPanel: React.FC<{}> = (props) => {
) )
} }
const Content: React.FC<{}> = () => { const Content: React.VFC<{}> = () => {
const {dims} = usePanel() const {dims} = usePanel()
return usePrism(() => { return usePrism(() => {

View file

@ -14,6 +14,11 @@ import {Atom, prism, val} from '@theatre/dataverse'
import type {SequenceEditorTree} from './tree' import type {SequenceEditorTree} from './tree'
import {calculateSequenceEditorTree} from './tree' import {calculateSequenceEditorTree} from './tree'
import {clamp} from 'lodash-es' 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 // A Side is either the left side of the panel or the right side
type DimsOfPanelPart = { type DimsOfPanelPart = {
@ -41,20 +46,20 @@ export type PanelDims = {
export type DopeSheetSelection = { export type DopeSheetSelection = {
type: 'DopeSheetSelection' type: 'DopeSheetSelection'
byObjectKey: StrictRecord< byObjectKey: StrictRecord<
string, ObjectAddressKey,
{ {
byTrackId: StrictRecord< byTrackId: StrictRecord<
string, SequenceTrackId,
{ {
byKeyframeId: StrictRecord<string, true> byKeyframeId: StrictRecord<KeyframeId, true>
} }
> >
} }
> >
getDragHandlers( getDragHandlers(
origin: PropAddress & { origin: PropAddress & {
trackId: string trackId: SequenceTrackId
keyframeId: string keyframeId: KeyframeId
positionAtStartOfDrag: number positionAtStartOfDrag: number
domNode: Element domNode: Element
}, },
@ -156,8 +161,8 @@ export type SequenceEditorPanelLayout = {
selectionAtom: Atom<{current?: DopeSheetSelection}> selectionAtom: Atom<{current?: DopeSheetSelection}>
} }
// type UnitSpaceProression = Nominal<number, 'unitSpaceProgression'> // type UnitSpaceProression = number
// type ClippedSpaceProgression = Nominal<number, 'ClippedSpaceProgression'> // type ClippedSpaceProgression = number
/** /**
* This means the left side of the panel is 20% of its width, and the * This means the left side of the panel is 20% of its width, and the

View file

@ -87,9 +87,11 @@ export const calculateSequenceEditorTree = (
topSoFar += tree.nodeHeight topSoFar += tree.nodeHeight
nSoFar += 1 nSoFar += 1
for (const [_, sheetObject] of Object.entries(val(sheet.objectsP))) { for (const sheetObject of Object.values(val(sheet.objectsP))) {
if (sheetObject) {
addObject(sheetObject, tree.children, tree.depth + 1) addObject(sheetObject, tree.children, tree.depth + 1)
} }
}
tree.heightIncludingChildren = topSoFar - tree.top tree.heightIncludingChildren = topSoFar - tree.top
function addObject( function addObject(

View file

@ -3,7 +3,9 @@ import type Sequence from '@theatre/core/sequences/Sequence'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import type {$IntentionalAny} from '@theatre/dataverse/src/types'
import {isSheet, isSheetObject} from '@theatre/shared/instanceTypes' import {isSheet, isSheetObject} from '@theatre/shared/instanceTypes'
import type {SheetId} from '@theatre/shared/utils/ids'
import {uniq} from 'lodash-es' import {uniq} from 'lodash-es'
import getStudio from './getStudio' import getStudio from './getStudio'
import type {OutlineSelectable, OutlineSelection} from './store/types' import type {OutlineSelectable, OutlineSelection} from './store/types'
@ -46,7 +48,7 @@ export const getSelectedInstanceOfSheetId = (
] ]
const instanceId = val( const instanceId = val(
projectStateP.stateBySheetId[selectedSheetId].selectedInstanceId, projectStateP.stateBySheetId[selectedSheetId as SheetId].selectedInstanceId,
) )
const template = val(project.sheetTemplatesP[selectedSheetId]) 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. // @todo #perf this will update every time an instance is added/removed.
const allInstances = val(template.instancesP) const allInstances = val(template.instancesP)
return allInstances[Object.keys(allInstances)[0]] return allInstances[keys(allInstances)[0]]
} }
} }
function keys<T extends object>(obj: T): Exclude<keyof T, symbol | number>[] {
return Object.keys(obj) as $IntentionalAny
}
/** /**
* component instances could come and go all the time. This hook * component instances could come and go all the time. This hook
* makes sure we don't cause re-renders * makes sure we don't cause re-renders

View file

@ -1,4 +1,5 @@
import type { import type {
HistoricPositionalSequence,
Keyframe, Keyframe,
SheetState_Historic, SheetState_Historic,
} from '@theatre/core/projects/store/types/SheetState_Historic' } from '@theatre/core/projects/store/types/SheetState_Historic'
@ -11,7 +12,11 @@ import type {
WithoutSheetInstance, WithoutSheetInstance,
} from '@theatre/shared/utils/addresses' } from '@theatre/shared/utils/addresses'
import {encodePathToProp} 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 { import {
generateKeyframeId, generateKeyframeId,
generateSequenceTrackId, generateSequenceTrackId,
@ -72,7 +77,7 @@ namespace stateEditors {
export namespace historic { export namespace historic {
export namespace panelPositions { export namespace panelPositions {
export function setPanelPosition(p: { export function setPanelPosition(p: {
panelId: string panelId: UIPanelId
position: PanelPosition position: PanelPosition
}) { }) {
const h = drafts().historic const h = drafts().historic
@ -429,7 +434,9 @@ namespace stateEditors {
} }
export namespace sequence { export namespace sequence {
export function _ensure(p: WithoutSheetInstance<SheetAddress>) { export function _ensure(
p: WithoutSheetInstance<SheetAddress>,
): HistoricPositionalSequence {
const s = stateEditors.coreByProject.historic.sheetsById._ensure(p) const s = stateEditors.coreByProject.historic.sheetsById._ensure(p)
s.sequence ??= { s.sequence ??= {
subUnitsPerUnit: 30, subUnitsPerUnit: 30,
@ -529,7 +536,9 @@ namespace stateEditors {
} }
function _getTrack( function _getTrack(
p: WithoutSheetInstance<SheetObjectAddress> & {trackId: string}, p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: SequenceTrackId
},
) { ) {
return _ensureTracksOfObject(p).trackData[p.trackId] return _ensureTracksOfObject(p).trackData[p.trackId]
} }
@ -540,7 +549,7 @@ namespace stateEditors {
*/ */
export function setKeyframeAtPosition<T>( export function setKeyframeAtPosition<T>(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: SequenceTrackId
position: number position: number
handles?: [number, number, number, number] handles?: [number, number, number, number]
value: T value: T
@ -585,7 +594,7 @@ namespace stateEditors {
export function unsetKeyframeAtPosition( export function unsetKeyframeAtPosition(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: SequenceTrackId
position: number position: number
}, },
) { ) {
@ -604,7 +613,7 @@ namespace stateEditors {
export function transformKeyframes( export function transformKeyframes(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: SequenceTrackId
keyframeIds: KeyframeId[] keyframeIds: KeyframeId[]
translate: number translate: number
scale: number scale: number
@ -633,7 +642,7 @@ namespace stateEditors {
export function deleteKeyframes( export function deleteKeyframes(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: SequenceTrackId
keyframeIds: KeyframeId[] keyframeIds: KeyframeId[]
}, },
) { ) {
@ -645,9 +654,9 @@ namespace stateEditors {
) )
} }
export function replaceKeyframes<T>( export function replaceKeyframes(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: SequenceTrackId
keyframes: Array<Keyframe> keyframes: Array<Keyframe>
snappingFunction: SnappingFunction snappingFunction: SnappingFunction
}, },

View file

@ -1,5 +1,6 @@
import type {ProjectState} from '@theatre/core/projects/store/storeTypes' import type {ProjectState} from '@theatre/core/projects/store/storeTypes'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' 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' import type {IRange, StrictRecord} from '@theatre/shared/utils/types'
export type StudioAhistoricState = { export type StudioAhistoricState = {
@ -50,5 +51,5 @@ export type StudioAhistoricState = {
} }
> >
} }
coreByProject: {[projectId in string]: ProjectState['ahistoric']} coreByProject: {[projectId in ProjectId]: ProjectState['ahistoric']}
} }

View file

@ -11,6 +11,14 @@ import type {StrictRecord} from '@theatre/shared/utils/types'
import type Project from '@theatre/core/projects/Project' import type Project from '@theatre/core/projects/Project'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {
ObjectAddressKey,
PaneInstanceId,
ProjectId,
SheetId,
SheetInstanceId,
UIPanelId,
} from '@theatre/shared/utils/ids'
export type PanelPosition = { export type PanelPosition = {
edges: { edges: {
@ -56,37 +64,55 @@ export type OutlineSelectionState =
export type OutlineSelectable = Project | Sheet | SheetObject export type OutlineSelectable = Project | Sheet | SheetObject
export type OutlineSelection = OutlineSelectable[] export type OutlineSelection = OutlineSelectable[]
export type PanelInstanceDescriptor = { export type PaneInstanceDescriptor = {
instanceId: string instanceId: PaneInstanceId
paneClass: string paneClass: string
} }
export type StudioHistoricState = { /**
projects: { * See parent {@link StudioHistoricStateProject}.
stateByProjectId: StrictRecord< * See root {@link StudioHistoricState}
string, */
{ export type StudioHistoricStateProjectSheet = {
stateBySheetId: StrictRecord< selectedInstanceId: undefined | SheetInstanceId
string,
{
selectedInstanceId: undefined | string
sequenceEditor: { sequenceEditor: {
selectedPropsByObject: StrictRecord< selectedPropsByObject: StrictRecord<
string, ObjectAddressKey,
StrictRecord<PathToProp_Encoded, keyof typeof graphEditorColors> StrictRecord<PathToProp_Encoded, keyof typeof graphEditorColors>
> >
} }
} }
>
} /** See {@link StudioHistoricState} */
> export type StudioHistoricStateProject = {
stateBySheetId: StrictRecord<SheetId, StudioHistoricStateProjectSheet>
}
/**
* {@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<ProjectId, StudioHistoricStateProject>
} }
/** Panels can contain panes */
panels?: Panels panels?: Panels
panelPositions?: {[panelIdOrPaneId in string]?: PanelPosition} /** Panels can contain panes */
panelInstanceDesceriptors: { panelPositions?: {[panelId in UIPanelId]?: PanelPosition}
[instanceId in string]?: PanelInstanceDescriptor // 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 autoKey: boolean
coreByProject: {[projectId in string]: ProjectState_Historic} coreByProject: Record<ProjectId, ProjectState_Historic>
} }