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 {
/**
* @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
/**

View file

@ -33,6 +33,11 @@ const cachedSubPathPointersWeakMap = new WeakMap<
* A wrapper type for the type a `Pointer` points to.
*/
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
}
@ -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).
* 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> &
(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
: unknown extends O
? UnindexablePointer
@ -64,10 +77,9 @@ export type Pointer<O> = PointerType<O> &
? Pointer<T>[]
: O extends {}
? {
[K in keyof O]-?: Pointer<O[K]>
} /*&
{[K in string | number]: Pointer<K extends keyof O ? O[K] : undefined>}*/
: UnindexablePointer)
[K in keyof O]-?: Pointer<O[K] | Optional>
}
: UnindexablePointer
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 type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import coreTicker from './coreTicker'
import type {ProjectId} from '@theatre/shared/utils/ids'
export {types}
/**
@ -45,7 +46,7 @@ export {types}
*/
export function getProject(id: string, config: IProjectConfig = {}): IProject {
const {...restOfConfig} = config
const existingProject = projectsSingleton.get(id)
const existingProject = projectsSingleton.get(id as ProjectId)
if (existingProject) {
if (process.env.NODE_ENV !== 'production') {
if (!deepEqual(config, existingProject.config)) {
@ -67,9 +68,9 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject {
if (config.state) {
if (process.env.NODE_ENV !== 'production') {
shallowValidateOnDiskState(id, config.state)
shallowValidateOnDiskState(id as ProjectId, config.state)
} else {
deepValidateOnDiskState(id, config.state)
deepValidateOnDiskState(id as ProjectId, config.state)
}
}
@ -80,7 +81,7 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject {
* Lightweight validator that only makes sure the state's definitionVersion is correct.
* Does not do a thorough validation of the state.
*/
const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => {
const shallowValidateOnDiskState = (projectId: ProjectId, s: OnDiskState) => {
if (
Array.isArray(s) ||
s == null ||
@ -94,7 +95,7 @@ const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => {
}
}
const deepValidateOnDiskState = (projectId: string, s: OnDiskState) => {
const deepValidateOnDiskState = (projectId: ProjectId, s: OnDiskState) => {
shallowValidateOnDiskState(projectId, s)
// @TODO do a deep validation here
}

View file

@ -6,13 +6,12 @@ import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
import type Sheet from '@theatre/core/sheets/Sheet'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {UnknownShorthandCompoundProps} from './propTypes/internals'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
const publicAPIToPrivateAPIMap = new WeakMap()
export function privateAPI<
P extends IProject | ISheet | ISheetObject<$IntentionalAny> | ISequence,
>(
export function privateAPI<P extends {type: string}>(
pub: P,
): P extends IProject
? Project
@ -29,8 +28,8 @@ export function privateAPI<
export function setPrivateAPI(pub: IProject, priv: Project): void
export function setPrivateAPI(pub: ISheet, priv: Sheet): void
export function setPrivateAPI(pub: ISequence, priv: Sequence): void
export function setPrivateAPI(
pub: ISheetObject<$IntentionalAny>,
export function setPrivateAPI<Props extends UnknownShorthandCompoundProps>(
pub: ISheetObject<Props>,
priv: SheetObject,
): 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 {defer} from '@theatre/shared/utils/defer'
import globals from '@theatre/shared/globals'
import type {
ProjectId,
SheetId,
SheetInstanceId,
} from '@theatre/shared/utils/ids'
export type Conf = Partial<{
state: OnDiskState
@ -44,7 +49,7 @@ export default class Project {
type: 'Theatre_Project' = 'Theatre_Project'
constructor(
id: string,
id: ProjectId,
readonly config: Conf = {},
readonly publicApi: TheatreProject,
) {
@ -150,7 +155,10 @@ export default class Project {
return this._readyDeferred.status === 'resolved'
}
getOrCreateSheet(sheetId: string, instanceId: string = 'default'): Sheet {
getOrCreateSheet(
sheetId: SheetId,
instanceId: SheetInstanceId = 'default' as SheetInstanceId,
): Sheet {
let template = this._sheetTemplates.getState()[sheetId]
if (!template) {

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import type {SheetId} from '@theatre/shared/utils/ids'
import type {StrictRecord} from '@theatre/shared/utils/types'
import type {SheetState_Historic} from './types/SheetState_Historic'
@ -31,7 +32,7 @@ export interface ProjectEphemeralState {
* at {@link StudioHistoricState.coreByProject}
*/
export interface ProjectState_Historic {
sheetsById: StrictRecord<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 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 {SerializableMap, StrictRecord} from '@theatre/shared/utils/types'
import type {
KeyframeId,
ObjectAddressKey,
SequenceTrackId,
} from '@theatre/shared/utils/ids'
import type {
SerializableMap,
SerializableValue,
StrictRecord,
} from '@theatre/shared/utils/types'
export interface SheetState_Historic {
/**
@ -10,14 +18,13 @@ export interface SheetState_Historic {
* of another state, it will be able to inherit the overrides from ancestor states.
*/
staticOverrides: {
byObject: StrictRecord<string, SerializableMap>
byObject: StrictRecord<ObjectAddressKey, SerializableMap>
}
sequence?: Sequence
sequence?: HistoricPositionalSequence
}
type Sequence = PositionalSequence
type PositionalSequence = {
// Question: What is this? The timeline position of a sequence?
export type HistoricPositionalSequence = {
type: 'PositionalSequence'
length: number
/**
@ -27,7 +34,7 @@ type PositionalSequence = {
subUnitsPerUnit: number
tracksByObject: StrictRecord<
string,
ObjectAddressKey,
{
trackIdByPropPath: StrictRecord<string, SequenceTrackId>
trackData: StrictRecord<SequenceTrackId, TrackData>
@ -39,7 +46,10 @@ export type TrackData = BasicKeyframedTrack
export type Keyframe = {
id: KeyframeId
value: unknown
/** The `value` is the raw value type such as `Rgba` or `number`. See {@link SerializableValue} */
// Future: is there another layer that we may need to be able to store older values on the
// case of a prop config change? As keyframes can technically have their propConfig changed.
value: SerializableValue
position: number
handles: [leftX: number, leftY: number, rightX: number, rightY: number]
connectedRight: boolean
@ -47,5 +57,9 @@ export type Keyframe = {
export type BasicKeyframedTrack = {
type: 'BasicKeyframedTrack'
/**
* {@link Keyframe} is not provided an explicit generic value `T`, because
* a single track can technically have multiple different types for each keyframe.
*/
keyframes: Keyframe[]
}

View file

@ -10,8 +10,8 @@ import {
} from '@theatre/shared/utils/color'
import {clamp, mapValues} from 'lodash-es'
import type {
IShorthandCompoundProps,
IValidCompoundProps,
UnknownShorthandCompoundProps,
UnknownValidCompoundProps,
ShorthandCompoundPropsToLonghandCompoundProps,
} from './internals'
import {propTypeSymbol, sanitizeCompoundProps} from './internals'
@ -88,7 +88,7 @@ const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => {
* ```
*
*/
export const compound = <Props extends IShorthandCompoundProps>(
export const compound = <Props extends UnknownShorthandCompoundProps>(
props: Props,
opts: CommonOpts = {},
): PropTypeConfig_Compound<
@ -101,7 +101,7 @@ export const compound = <Props extends IShorthandCompoundProps>(
ShorthandCompoundPropsToLonghandCompoundProps<Props>
> = {
type: 'compound',
props: sanitizedProps,
props: sanitizedProps as $IntentionalAny,
valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType',
label: opts.label,
@ -690,7 +690,7 @@ export interface PropTypeConfig_StringLiteral<T extends string>
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]>
}
@ -701,13 +701,14 @@ type DeepPartial<Conf extends PropTypeConfig> =
? DeepPartialCompound<T>
: never
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps>
extends IBasePropType<
export interface PropTypeConfig_Compound<
Props extends UnknownValidCompoundProps,
> extends IBasePropType<
'compound',
{[K in keyof Props]: Props[K]['valueType']},
DeepPartialCompound<Props>
> {
props: Record<string, PropTypeConfig>
props: Record<keyof Props, PropTypeConfig>
}
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 type IValidCompoundProps = {
export type UnknownValidCompoundProps = {
[K in string]: PropTypeConfig
}
@ -27,18 +27,19 @@ export type IValidCompoundProps = {
* which would allow us to differentiate between values at runtime
* (e.g. `val.type = "Rgba"` vs `val.type = "Compound"` etc)
*/
type IShorthandProp =
type UnknownShorthandProp =
| string
| number
| boolean
| PropTypeConfig
| IShorthandCompoundProps
| UnknownShorthandCompoundProps
export type IShorthandCompoundProps = {
[K in string]: IShorthandProp
/** Given an object like this, we have enough info to predict the compound prop */
export type UnknownShorthandCompoundProps = {
[K in string]: UnknownShorthandProp
}
export type ShorthandPropToLonghandProp<P extends IShorthandProp> =
export type ShorthandPropToLonghandProp<P extends UnknownShorthandProp> =
P extends string
? PropTypeConfig_String
: P extends number
@ -47,12 +48,31 @@ export type ShorthandPropToLonghandProp<P extends IShorthandProp> =
? PropTypeConfig_Boolean
: P extends PropTypeConfig
? P
: P extends IShorthandCompoundProps
: P extends UnknownShorthandCompoundProps
? PropTypeConfig_Compound<ShorthandCompoundPropsToLonghandCompoundProps<P>>
: 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<
P extends IShorthandCompoundProps,
P extends UnknownShorthandCompoundProps,
> = {
[K in keyof P]: ShorthandPropToLonghandProp<P[K]>
}
@ -89,9 +109,9 @@ export function toLonghandProp(p: unknown): PropTypeConfig {
}
export function sanitizeCompoundProps(
props: IShorthandCompoundProps,
): IValidCompoundProps {
const sanitizedProps: IValidCompoundProps = {}
props: UnknownShorthandCompoundProps,
): UnknownValidCompoundProps {
const sanitizedProps: UnknownValidCompoundProps = {}
if (process.env.NODE_ENV !== 'production') {
if (typeof props !== 'object' || !props) {
throw new InvalidArgumentError(

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import SimpleCache from '@theatre/shared/utils/SimpleCache'
import type {
$FixMe,
$IntentionalAny,
DeepPartialOfSerializableValue,
SerializableMap,
SerializableValue,
} from '@theatre/shared/utils/types'
@ -25,14 +26,29 @@ import TheatreSheetObject from './TheatreSheetObject'
import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes'
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
/**
* Internally, the sheet's actual configured value is not a specific type, since we
* can change the prop config at will, as such this is an alias of {@link SerializableMap}.
*
* TODO: Incorporate this knowledge into SheetObject & TemplateSheetObject
*/
type SheetObjectPropsValue = SerializableMap
/**
* An object on a sheet consisting of zero or more properties which can
* be overridden statically or overridden by being sequenced.
*
* Note that this cannot be generic over `Props`, since the user is
* able to change prop configs for the sheet object's properties.
*/
export default class SheetObject implements IdentityDerivationProvider {
get type(): 'Theatre_SheetObject' {
return 'Theatre_SheetObject'
}
readonly $$isIdentityDerivationProvider: true = true
readonly address: SheetObjectAddress
readonly publicApi: TheatreSheetObject<$IntentionalAny>
private readonly _initialValue = new Atom<SerializableMap>({})
readonly publicApi: TheatreSheetObject
private readonly _initialValue = new Atom<SheetObjectPropsValue>({})
private readonly _cache = new SimpleCache()
constructor(
@ -48,7 +64,7 @@ export default class SheetObject implements IdentityDerivationProvider {
this.publicApi = new TheatreSheetObject(this)
}
getValues(): IDerivation<Pointer<SerializableMap>> {
getValues(): IDerivation<Pointer<SheetObjectPropsValue>> {
return this._cache.get('getValues()', () =>
prism(() => {
const defaults = val(this.template.getDefaultValues())
@ -101,7 +117,7 @@ export default class SheetObject implements IdentityDerivationProvider {
final = withSeqs
}
const a = valToAtom<SerializableMap>('finalAtom', final)
const a = valToAtom<SheetObjectPropsValue>('finalAtom', final)
return a.pointer
}),
@ -131,7 +147,7 @@ export default class SheetObject implements IdentityDerivationProvider {
/**
* Returns values of props that are sequenced.
*/
getSequencedValues(): IDerivation<Pointer<SerializableMap>> {
getSequencedValues(): IDerivation<Pointer<SheetObjectPropsValue>> {
return prism(() => {
const tracksToProcessD = prism.memo(
'tracksToProcess',
@ -140,7 +156,7 @@ export default class SheetObject implements IdentityDerivationProvider {
)
const tracksToProcess = val(tracksToProcessD)
const valsAtom = new Atom<SerializableMap>({})
const valsAtom = new Atom<SheetObjectPropsValue>({})
prism.effect(
'processTracks',
@ -216,17 +232,20 @@ export default class SheetObject implements IdentityDerivationProvider {
return interpolationTripleAtPosition(trackP, timeD)
}
get propsP(): Pointer<$FixMe> {
get propsP(): Pointer<SheetObjectPropsValue> {
return this._cache.get('propsP', () =>
pointer<{props: {}}>({root: this, path: []}),
) as $FixMe
}
validateValue(pointer: Pointer<$FixMe>, value: unknown) {
validateValue(
pointer: Pointer<SheetObjectPropsValue>,
value: DeepPartialOfSerializableValue<SheetObjectPropsValue>,
) {
// @todo
}
setInitialValue(val: SerializableMap) {
setInitialValue(val: DeepPartialOfSerializableValue<SheetObjectPropsValue>) {
this.validateValue(this.propsP, val)
this._initialValue.setState(val)
}

View file

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

View file

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

View file

@ -13,11 +13,13 @@ import type {IDerivation, Pointer} from '@theatre/dataverse'
import {prism, val} from '@theatre/dataverse'
import type SheetObject from './SheetObject'
import type {
IShorthandCompoundProps,
ShorthandPropToLonghandProp,
UnknownShorthandCompoundProps,
PropsValue,
} from '@theatre/core/propTypes/internals'
export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
export interface ISheetObject<
Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps,
> {
/**
* 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})
* 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.
@ -100,7 +108,7 @@ export interface ISheetObject<Props extends IShorthandCompoundProps = {}> {
}
export default class TheatreSheetObject<
Props extends IShorthandCompoundProps = {},
Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps,
> implements ISheetObject<Props>
{
get type(): 'Theatre_SheetObject_PublicAPI' {
@ -134,7 +142,7 @@ export default class TheatreSheetObject<
private _valuesDerivation(): IDerivation<this['value']> {
return this._cache.get('onValuesChangeDerivation', () => {
const sheetObject = privateAPI(this)
const d: IDerivation<Props> = prism(() => {
const d: IDerivation<PropsValue<Props>> = prism(() => {
return val(sheetObject.getValues().getValue()) as $FixMe
})
return d
@ -145,7 +153,7 @@ export default class TheatreSheetObject<
return this._valuesDerivation().tapImmediate(coreTicker, fn)
}
get value(): ShorthandPropToLonghandProp<Props>['valueType'] {
get value(): PropsValue<Props> {
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 {
$FixMe,
$IntentionalAny,
SerializableMap,
SerializableValue,
} 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.
*/
export default function getPropDefaultsOfSheetObject(
config: SheetObjectConfig<$IntentionalAny>,
config: SheetObjectPropTypeConfig,
): SerializableMap {
return getDefaultsOfPropTypeConfig(config) as $IntentionalAny
return getDefaultsOfPropTypeConfig(config) as SerializableMap // sheet objects result in non-primitive objects
}
function getDefaultsOfPropTypeConfig(

View file

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

View file

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

View file

@ -9,15 +9,18 @@ import type Sheet from '@theatre/core/sheets/Sheet'
import type {SheetAddress} from '@theatre/shared/utils/addresses'
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import deepEqual from 'fast-deep-equal'
import type {IShorthandCompoundProps} from '@theatre/core/propTypes/internals'
import type {
UnknownShorthandCompoundProps,
UnknownValidCompoundProps,
} from '@theatre/core/propTypes/internals'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {ObjectAddressKey} from '@theatre/shared/utils/ids'
export type SheetObjectConfig<
Props extends PropTypeConfig_Compound<$IntentionalAny>,
> = Props
export type SheetObjectPropTypeConfig =
PropTypeConfig_Compound<UnknownValidCompoundProps>
export interface ISheet {
/**
@ -62,7 +65,7 @@ export interface ISheet {
* obj.value.position // {x: 0, y: 0}
* ```
*/
object<Props extends IShorthandCompoundProps>(
object<Props extends UnknownShorthandCompoundProps>(
key: string,
props: Props,
): ISheetObject<Props>
@ -75,7 +78,7 @@ export interface ISheet {
const weakMapOfUnsanitizedProps = new WeakMap<
SheetObject,
IShorthandCompoundProps
UnknownShorthandCompoundProps
>()
export default class TheatreSheet implements ISheet {
@ -89,7 +92,7 @@ export default class TheatreSheet implements ISheet {
setPrivateAPI(this, sheet)
}
object<Props extends IShorthandCompoundProps>(
object<Props extends UnknownShorthandCompoundProps>(
key: string,
config: Props,
): ISheetObject<Props> {
@ -99,8 +102,15 @@ export default class TheatreSheet implements ISheet {
`sheet.object("${key}", ...)`,
)
const existingObject = internal.getObject(sanitizedPath)
const existingObject = internal.getObject(sanitizedPath as ObjectAddressKey)
/**
* Future: `nativeObject` Idea is to potentially allow the user to provide their own
* object in to the object call as a way to keep a handle to an underlying object via
* the {@link ISheetObject}.
*
* For example, a THREEjs object or an HTMLElement is passed in.
*/
const nativeObject = null
if (existingObject) {
@ -121,12 +131,12 @@ export default class TheatreSheet implements ISheet {
} else {
const sanitizedConfig = compound(config)
const object = internal.createObject(
sanitizedPath,
sanitizedPath as ObjectAddressKey,
nativeObject,
sanitizedConfig,
)
if (process.env.NODE_ENV !== 'production') {
weakMapOfUnsanitizedProps.set(object, config)
weakMapOfUnsanitizedProps.set(object as $FixMe, config)
}
return object.publicApi as $IntentionalAny
}

View file

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

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

View file

@ -1,9 +1,10 @@
import {nanoid as generateNonSecure} from 'nanoid/non-secure'
import type {$IntentionalAny, Nominal} from './types'
import type {Nominal} from './Nominal'
import type {$IntentionalAny} from './types'
export type KeyframeId = Nominal<string, 'KeyframeId'>
export type KeyframeId = Nominal<'KeyframeId'>
export function generateKeyframeId() {
export function generateKeyframeId(): KeyframeId {
return generateNonSecure(10) as KeyframeId
}
@ -11,10 +12,16 @@ export function asKeyframeId(s: string): KeyframeId {
return s as $IntentionalAny
}
// @todo make nominal
export type SequenceTrackId = string
export type ProjectId = Nominal<'ProjectId'>
export type SheetId = Nominal<'SheetId'>
export type SheetInstanceId = Nominal<'SheetInstanceId'>
export type PaneInstanceId = Nominal<'PaneInstanceId'>
export type SequenceTrackId = Nominal<'SequenceTrackId'>
export type ObjectAddressKey = Nominal<'ObjectAddressKey'>
/** UI panels can contain a {@link PaneInstanceId} or something else. */
export type UIPanelId = Nominal<'UIPanelId'>
export function generateSequenceTrackId() {
export function generateSequenceTrackId(): SequenceTrackId {
return generateNonSecure(10) as $IntentionalAny as SequenceTrackId
}

View file

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

View file

@ -44,6 +44,13 @@ export type SerializablePrimitive =
| boolean
| {r: number; g: number; b: number; a: number}
/**
* This type represents all values that can be safely serialized.
* Also, it's notable that this type is compatible for dataverse pointer traversal (everything
* is path accessible [e.g. `a.b.c`]).
*
* One example usage is for keyframe values or static overrides such as `Rgba`, `string`, `number`, and "compound values".
*/
export type SerializableValue<
Primitives extends SerializablePrimitive = SerializablePrimitive,
> = Primitives | SerializableMap
@ -57,15 +64,13 @@ export type DeepPartialOfSerializableValue<T extends SerializableValue> =
}
: 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
* doesn't allow generic index signatures, we're leaving it be.
* This is equivalent to `Partial<Record<Key, V>>` being used to describe a sort of Map
* where the keys might not have values.
*
* TODO fix this once https://github.com/microsoft/TypeScript/pull/26797 lands (likely typescript 4.4)
* We do not use `Map`s, because they add comlpexity with converting to `JSON.stringify` + pointer types
*/
export type Nominal<T, 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]`
@ -81,4 +86,4 @@ export type $IntentionalAny = any
* Represents the `x` or `y` value of getBoundingClientRect().
* In other words, represents a distance from 0,0 in screen space.
*/
export type PositionInScreenSpace = Nominal<number, 'ScreenSpaceDim'>
export type PositionInScreenSpace = number

View file

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

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

View file

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

View file

@ -16,6 +16,7 @@ import getStudio from './getStudio'
import type React from 'react'
import {debounce} from 'lodash-es'
import type Sheet from '@theatre/core/sheets/Sheet'
import type {PaneInstanceId, ProjectId} from '@theatre/shared/utils/ids'
export interface ITransactionAPI {
/**
@ -116,7 +117,7 @@ export interface IExtension {
export type PaneInstance<ClassName extends string> = {
extensionId: string
instanceId: string
instanceId: PaneInstanceId
definition: PaneClassDefinition
}
@ -471,10 +472,12 @@ export default class TheatreStudio implements IStudio {
}
destroyPane(paneId: string): void {
return getStudio().paneManager.destroyPane(paneId)
return getStudio().paneManager.destroyPane(paneId as PaneInstanceId)
}
createContentOfSaveFile(projectId: string): Record<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 {usePrism} from '@theatre/react'
import type {UIPanelId} from '@theatre/shared/utils/ids'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import getStudio from '@theatre/studio/getStudio'
import type {PanelPosition} from '@theatre/studio/store/types'
@ -8,7 +9,7 @@ import React, {useContext} from 'react'
import useWindowSize from 'react-use/esm/useWindowSize'
type PanelStuff = {
panelId: string
panelId: UIPanelId
dims: {
width: number
height: number
@ -64,7 +65,7 @@ const PanelContext = React.createContext<PanelStuff>(null as $IntentionalAny)
export const usePanel = () => useContext(PanelContext)
const BasePanel: React.FC<{
panelId: string
panelId: UIPanelId
defaultPosition: PanelPosition
minDims: {width: number; height: number}
}> = ({panelId, children, defaultPosition, minDims}) => {

View file

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

View file

@ -1,6 +1,8 @@
import React, {useMemo} from 'react'
import DeterminePropEditor from './propEditors/DeterminePropEditor'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {Pointer} from '@theatre/dataverse'
import type {$FixMe} from '@theatre/shared/utils/types'
const ObjectDetails: React.FC<{
objects: SheetObject[]
@ -13,9 +15,9 @@ const ObjectDetails: React.FC<{
<DeterminePropEditor
key={key}
obj={obj}
pointerToProp={obj.propsP}
pointerToProp={obj.propsP as Pointer<$FixMe>}
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 type {$FixMe} from '@theatre/shared/utils/types'
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
import type {ProjectId} from '@theatre/shared/utils/ids'
const Container = styled.div`
padding: 8px 10px;
@ -37,7 +38,7 @@ const ChooseStateRow = styled.div`
gap: 8px;
`
const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => {
const StateConflictRow: React.FC<{projectId: ProjectId}> = ({projectId}) => {
const loadingState = useVal(
getStudio().atomP.ephemeral.coreByProject[projectId].loadingState,
)
@ -52,7 +53,7 @@ const StateConflictRow: React.FC<{projectId: string}> = ({projectId}) => {
}
const InConflict: React.FC<{
projectId: string
projectId: ProjectId
loadingState: Extract<
ProjectEphemeralState['loadingState'],
{type: 'browserStateIsNotBasedOnDiskState'}

View file

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

View file

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

View file

@ -8,5 +8,5 @@ export type IPropEditorFC<TPropTypeConfig extends IBasePropType<string, any>> =
propConfig: TPropTypeConfig
pointerToProp: Pointer<TPropTypeConfig['valueType']>
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 getDeep from '@theatre/shared/utils/getDeep'
import {usePrism} from '@theatre/react'
import type {$FixMe, SerializablePrimitive} from '@theatre/shared/utils/types'
import type {SerializablePrimitive} from '@theatre/shared/utils/types'
import {getPointerParts, prism, val} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import get from 'lodash-es/get'
@ -15,6 +15,7 @@ import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
import type {PropTypeConfig} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
interface CommonStuff<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(
'lcr',
(): NearbyKeyframes => {
@ -155,7 +156,7 @@ export function useEditingToolsForPrimitiveProp<
obj.template.project.pointers.historic.sheetsById[
obj.address.sheetId
].sequence.tracksByObject[obj.address.objectKey].trackData[
sequenceTrcackId
sequenceTrackId
],
)
if (!track || track.keyframes.length === 0) return {}
@ -186,7 +187,7 @@ export function useEditingToolsForPrimitiveProp<
}
}
},
[sequenceTrcackId],
[sequenceTrackId],
)
let shade: Shade

View file

@ -26,7 +26,7 @@ const ObjectsList: React.FC<{
<ObjectItem
depth={depth}
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),
}
} else {
return {selectedKeyframeIds: {}, selection: undefined}
return {
selectedKeyframeIds: {},
selection: undefined,
}
}
}, [layoutP, leaf.trackId])

View file

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

View file

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

View file

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

View file

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

View file

@ -49,7 +49,7 @@ type Which = 'left' | 'right'
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 {index, trackData} = props

View file

@ -55,7 +55,7 @@ const HitZone = styled.circle`
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 {index, trackData} = props

View file

@ -55,7 +55,7 @@ const HitZone = styled.circle`
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 {index, trackData} = props

View file

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

View file

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

View file

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

View file

@ -14,6 +14,11 @@ import {Atom, prism, val} from '@theatre/dataverse'
import type {SequenceEditorTree} from './tree'
import {calculateSequenceEditorTree} from './tree'
import {clamp} from 'lodash-es'
import type {
KeyframeId,
ObjectAddressKey,
SequenceTrackId,
} from '@theatre/shared/utils/ids'
// A Side is either the left side of the panel or the right side
type DimsOfPanelPart = {
@ -41,20 +46,20 @@ export type PanelDims = {
export type DopeSheetSelection = {
type: 'DopeSheetSelection'
byObjectKey: StrictRecord<
string,
ObjectAddressKey,
{
byTrackId: StrictRecord<
string,
SequenceTrackId,
{
byKeyframeId: StrictRecord<string, true>
byKeyframeId: StrictRecord<KeyframeId, true>
}
>
}
>
getDragHandlers(
origin: PropAddress & {
trackId: string
keyframeId: string
trackId: SequenceTrackId
keyframeId: KeyframeId
positionAtStartOfDrag: number
domNode: Element
},
@ -156,8 +161,8 @@ export type SequenceEditorPanelLayout = {
selectionAtom: Atom<{current?: DopeSheetSelection}>
}
// type UnitSpaceProression = Nominal<number, 'unitSpaceProgression'>
// type ClippedSpaceProgression = Nominal<number, 'ClippedSpaceProgression'>
// type UnitSpaceProression = number
// type ClippedSpaceProgression = number
/**
* 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
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)
}
}
tree.heightIncludingChildren = topSoFar - tree.top
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 Sheet from '@theatre/core/sheets/Sheet'
import {val} from '@theatre/dataverse'
import type {$IntentionalAny} from '@theatre/dataverse/src/types'
import {isSheet, isSheetObject} from '@theatre/shared/instanceTypes'
import type {SheetId} from '@theatre/shared/utils/ids'
import {uniq} from 'lodash-es'
import getStudio from './getStudio'
import type {OutlineSelectable, OutlineSelection} from './store/types'
@ -46,7 +48,7 @@ export const getSelectedInstanceOfSheetId = (
]
const instanceId = val(
projectStateP.stateBySheetId[selectedSheetId].selectedInstanceId,
projectStateP.stateBySheetId[selectedSheetId as SheetId].selectedInstanceId,
)
const template = val(project.sheetTemplatesP[selectedSheetId])
@ -59,10 +61,14 @@ export const getSelectedInstanceOfSheetId = (
// @todo #perf this will update every time an instance is added/removed.
const allInstances = val(template.instancesP)
return allInstances[Object.keys(allInstances)[0]]
return allInstances[keys(allInstances)[0]]
}
}
function keys<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
* makes sure we don't cause re-renders

View file

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

View file

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

View file

@ -11,6 +11,14 @@ import type {StrictRecord} from '@theatre/shared/utils/types'
import type Project from '@theatre/core/projects/Project'
import type Sheet from '@theatre/core/sheets/Sheet'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {
ObjectAddressKey,
PaneInstanceId,
ProjectId,
SheetId,
SheetInstanceId,
UIPanelId,
} from '@theatre/shared/utils/ids'
export type PanelPosition = {
edges: {
@ -56,37 +64,55 @@ export type OutlineSelectionState =
export type OutlineSelectable = Project | Sheet | SheetObject
export type OutlineSelection = OutlineSelectable[]
export type PanelInstanceDescriptor = {
instanceId: string
export type PaneInstanceDescriptor = {
instanceId: PaneInstanceId
paneClass: string
}
export type StudioHistoricState = {
projects: {
stateByProjectId: StrictRecord<
string,
{
stateBySheetId: StrictRecord<
string,
{
selectedInstanceId: undefined | string
/**
* See parent {@link StudioHistoricStateProject}.
* See root {@link StudioHistoricState}
*/
export type StudioHistoricStateProjectSheet = {
selectedInstanceId: undefined | SheetInstanceId
sequenceEditor: {
selectedPropsByObject: StrictRecord<
string,
ObjectAddressKey,
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
panelPositions?: {[panelIdOrPaneId in string]?: PanelPosition}
panelInstanceDesceriptors: {
[instanceId in string]?: PanelInstanceDescriptor
}
/** Panels can contain panes */
panelPositions?: {[panelId in UIPanelId]?: PanelPosition}
// This is misspelled, but I think some users are dependent on the exact shape of this stored JSON
// So, we cannot easily change it without providing backwards compatibility.
panelInstanceDesceriptors: StrictRecord<
PaneInstanceId,
PaneInstanceDescriptor
>
autoKey: boolean
coreByProject: {[projectId in string]: ProjectState_Historic}
coreByProject: Record<ProjectId, ProjectState_Historic>
}