diff --git a/packages/dataverse/src/Atom.ts b/packages/dataverse/src/Atom.ts index a0acacd..1151523 100644 --- a/packages/dataverse/src/Atom.ts +++ b/packages/dataverse/src/Atom.ts @@ -297,10 +297,16 @@ function isIdentityChangeProvider( * For pointers, the value is returned by first creating a derivation, so it is * reactive e.g. when used in a `prism`. * - * @param pointerOrDerivationOrPlainValue - The argument to return a value from. + * @param input - The argument to return a value from. */ -export const val =

( - pointerOrDerivationOrPlainValue: P, +export const val = < + P extends + | PointerType<$IntentionalAny> + | IDerivation<$IntentionalAny> + | undefined + | null, +>( + input: P, ): P extends PointerType ? T : P extends IDerivation @@ -308,13 +314,11 @@ export const val =

( : P extends undefined | null ? P : unknown => { - if (isPointer(pointerOrDerivationOrPlainValue)) { - return valueDerivation( - pointerOrDerivationOrPlainValue, - ).getValue() as $IntentionalAny - } else if (isDerivation(pointerOrDerivationOrPlainValue)) { - return pointerOrDerivationOrPlainValue.getValue() as $IntentionalAny + if (isPointer(input)) { + return valueDerivation(input).getValue() as $IntentionalAny + } else if (isDerivation(input)) { + return input.getValue() as $IntentionalAny } else { - return pointerOrDerivationOrPlainValue as $IntentionalAny + return input as $IntentionalAny } } diff --git a/packages/dataverse/src/index.ts b/packages/dataverse/src/index.ts index 383a638..d3773f9 100644 --- a/packages/dataverse/src/index.ts +++ b/packages/dataverse/src/index.ts @@ -17,6 +17,6 @@ export {default as iterateAndCountTicks} from './derivations/iterateAndCountTick export {default as iterateOver} from './derivations/iterateOver' export {default as prism} from './derivations/prism/prism' export {default as pointer, getPointerParts, isPointer} from './pointer' -export type {Pointer, PointerType} from './pointer' +export type {Pointer, PointerType, OpaqueToPointers} from './pointer' export {default as Ticker} from './Ticker' export {default as PointerProxy} from './PointerProxy' diff --git a/packages/dataverse/src/pointer.ts b/packages/dataverse/src/pointer.ts index 243efbc..8b6bb45 100644 --- a/packages/dataverse/src/pointer.ts +++ b/packages/dataverse/src/pointer.ts @@ -7,6 +7,10 @@ type PointerMeta = { path: (string | number)[] } +const symbolForUnpointableTypes = Symbol() + +export type OpaqueToPointers = {[symbolForUnpointableTypes]: true} + export type UnindexableTypesForPointer = | number | string @@ -15,6 +19,7 @@ export type UnindexableTypesForPointer = | void | undefined | Function // eslint-disable-line @typescript-eslint/ban-types + | OpaqueToPointers export type UnindexablePointer = { [K in $IntentionalAny]: Pointer @@ -34,6 +39,19 @@ export type PointerType = { * explanation of pointers. * * @see Atom + * + * @remarks + * The Pointer type is quite tricky because it doesn't play well with `any` and other inexact types. + * Here is an example that one would expect to work, but currently doesn't: + * ```ts + * declare function expectAnyPointer(pointer: Pointer): void + * + * expectAnyPointer(null as Pointer<{}>) // doesn't work + * ``` + * + * 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 :) + * */ export type Pointer = PointerType & (O extends UnindexableTypesForPointer diff --git a/theatre/core/src/propTypes/index.ts b/theatre/core/src/propTypes/index.ts index 1f4b33a..d463c53 100644 --- a/theatre/core/src/propTypes/index.ts +++ b/theatre/core/src/propTypes/index.ts @@ -1,4 +1,4 @@ -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 type {Rgba} from '@theatre/shared/utils/color' import { @@ -8,7 +8,7 @@ import { srgbToLinearSrgb, linearSrgbToSrgb, } from '@theatre/shared/utils/color' -import {mapValues} from 'lodash-es' +import {clamp, mapValues} from 'lodash-es' import type { IShorthandCompoundProps, IValidCompoundProps, @@ -17,6 +17,12 @@ import type { import {sanitizeCompoundProps} from './internals' import {propTypeSymbol} from './internals' +// Notes on naming: +// As of now, prop types are either `simple` or `composite`. +// The compound type is a composite type. So is the upcoming enum type. +// Composite types are not directly sequenceable yet. Their simple sub/ancestor props are. +// We’ll provide a nice UX to manage keyframing of multiple sub-props. + const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => { if (process.env.NODE_ENV !== 'production') { if (opts === undefined) return @@ -80,19 +86,49 @@ export const compound = ( label?: string }, ): PropTypeConfig_Compound< - ShorthandCompoundPropsToLonghandCompoundProps, - Props + ShorthandCompoundPropsToLonghandCompoundProps > => { validateCommonOpts('t.compound(props, opts)', opts) const sanitizedProps = sanitizeCompoundProps(props) - return { + const deserializationCache = new WeakMap<{}, unknown>() + const config: PropTypeConfig_Compound< + ShorthandCompoundPropsToLonghandCompoundProps + > = { type: 'compound', props: sanitizedProps, valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', label: opts?.label, default: mapValues(sanitizedProps, (p) => p.default) as $IntentionalAny, + deserialize: (json: unknown) => { + if (typeof json !== 'object' || !json) return undefined + if (deserializationCache.has(json)) { + return deserializationCache.get(json) + } + + const deserialized: $FixMe = {} + let atLeastOnePropWasDeserialized = false + for (const [key, propConfig] of Object.entries(sanitizedProps)) { + if (Object.prototype.hasOwnProperty.call(json, key)) { + const deserializedSub = propConfig.deserialize( + (json as $IntentionalAny)[key] as unknown, + ) + if ( + typeof deserializedSub !== 'undefined' && + deserializedSub !== null + ) { + atLeastOnePropWasDeserialized = true + deserialized[key] = deserializedSub + } + } + } + deserializationCache.set(json, deserialized) + if (atLeastOnePropWasDeserialized) { + return deserialized + } + }, } + return config } /** @@ -143,8 +179,6 @@ export const number = ( range?: PropTypeConfig_Number['range'] nudgeMultiplier?: number label?: string - sanitize?: Sanitizer - interpolate?: Interpolator }, ): PropTypeConfig_Number => { if (process.env.NODE_ENV !== 'production') { @@ -206,14 +240,6 @@ export const number = ( } } } - const sanitize = !opts?.sanitize - ? _sanitizeNumber - : (val: unknown): number | undefined => { - const n = _sanitizeNumber(val) - if (typeof n === 'number') { - return opts.sanitize!(n) - } - } return { type: 'number', @@ -225,13 +251,20 @@ export const number = ( nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn, nudgeMultiplier: typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1, - isScalar: true, - sanitize: sanitize, - interpolate: opts?.interpolate ?? _interpolateNumber, + interpolate: _interpolateNumber, + deserialize: numberDeserializer(opts?.range), } } -const _sanitizeNumber = (value: unknown): undefined | number => +const numberDeserializer = (range?: PropTypeConfig_Number['range']) => + range + ? (json: unknown): undefined | number => { + if (!(typeof json === 'number' && isFinite(json))) return undefined + return clamp(json, range[0], range[1]) + } + : _ensureNumber + +const _ensureNumber = (value: unknown): undefined | number => typeof value === 'number' && isFinite(value) ? value : undefined const _interpolateNumber = ( @@ -288,8 +321,8 @@ export const rgba = ( default: decorateRgba(sanitized as Rgba), [propTypeSymbol]: 'TheatrePropType', label: opts?.label, - sanitize: _sanitizeRgba, interpolate: _interpolateRgba, + deserialize: _sanitizeRgba, } } @@ -304,6 +337,8 @@ const _sanitizeRgba = (val: unknown): Rgba | undefined => { } } + if (!valid) return undefined + // Clamp defaultValue components between 0 and 1 const sanitized = {} for (const c of ['r', 'g', 'b', 'a']) { @@ -313,7 +348,7 @@ const _sanitizeRgba = (val: unknown): Rgba | undefined => { ) } - return valid ? decorateRgba(sanitized as Rgba) : undefined + return decorateRgba(sanitized as Rgba) } const _interpolateRgba = ( @@ -360,7 +395,6 @@ export const boolean = ( defaultValue: boolean, opts?: { label?: string - sanitize?: Sanitizer interpolate?: Interpolator }, ): PropTypeConfig_Boolean => { @@ -381,12 +415,12 @@ export const boolean = ( valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', label: opts?.label, - sanitize: _sanitizeBoolean, - interpolate: leftInterpolate, + interpolate: opts?.interpolate ?? leftInterpolate, + deserialize: _ensureBoolean, } } -const _sanitizeBoolean = (val: unknown): boolean | undefined => { +const _ensureBoolean = (val: unknown): boolean | undefined => { return typeof val === 'boolean' ? val : undefined } @@ -419,7 +453,6 @@ export const string = ( defaultValue: string, opts?: { label?: string - sanitize?: Sanitizer interpolate?: Interpolator }, ): PropTypeConfig_String => { @@ -439,14 +472,15 @@ export const string = ( valueType: null as $IntentionalAny, [propTypeSymbol]: 'TheatrePropType', label: opts?.label, - sanitize(value: unknown) { - if (opts?.sanitize) return opts.sanitize(value) - return typeof value === 'string' ? value : undefined - }, interpolate: opts?.interpolate ?? leftInterpolate, + deserialize: _ensureString, } } +function _ensureString(s: unknown): string | undefined { + return typeof s === 'string' ? s : undefined +} + /** * A stringLiteral prop type, useful for building menus or radio buttons. * @@ -481,7 +515,11 @@ export function stringLiteral( /** * opts.as Determines if editor is shown as a menu or a switch. Either 'menu' or 'switch'. Default: 'menu' */ - opts?: {as?: 'menu' | 'switch'; label?: string}, + opts?: { + as?: 'menu' | 'switch' + label?: string + interpolate?: Interpolator> + }, ): PropTypeConfig_StringLiteral> { return { type: 'stringLiteral', @@ -491,34 +529,41 @@ export function stringLiteral( valueType: null as $IntentionalAny, as: opts?.as ?? 'menu', label: opts?.label, - sanitize(value: unknown) { - if (typeof value !== 'string') return undefined - if (Object.hasOwnProperty.call(options, value)) { - return value as $IntentionalAny + interpolate: opts?.interpolate ?? leftInterpolate, + deserialize(json: unknown): undefined | Extract { + if (typeof json !== 'string') return undefined + if (Object.prototype.hasOwnProperty.call(options, json)) { + return json as $IntentionalAny } else { return undefined } }, - interpolate: leftInterpolate, } } export type Sanitizer = (value: unknown) => T | undefined export type Interpolator = (left: T, right: T, progression: number) => T -export interface IBasePropType { +export interface IBasePropType< + LiteralIdentifier extends string, + ValueType, + DeserializeType = ValueType, +> { + type: LiteralIdentifier valueType: ValueType [propTypeSymbol]: 'TheatrePropType' label: string | undefined - isScalar?: true - sanitize?: Sanitizer - interpolate?: Interpolator default: ValueType + deserialize: (json: unknown) => undefined | DeserializeType } -export interface PropTypeConfig_Number extends IBasePropType { - type: 'number' - default: number +interface ISimplePropType + extends IBasePropType { + interpolate: Interpolator +} + +export interface PropTypeConfig_Number + extends ISimplePropType<'number', number> { range?: [min: number, max: number] nudgeFn: NumberNudgeFn nudgeMultiplier: number @@ -547,54 +592,50 @@ const defaultNumberNudgeFn: NumberNudgeFn = ({ return deltaX * magnitude * config.nudgeMultiplier } -export interface PropTypeConfig_Boolean extends IBasePropType { - type: 'boolean' - default: boolean -} +export interface PropTypeConfig_Boolean + extends ISimplePropType<'boolean', boolean> {} interface CommonOpts { label?: string } -export interface PropTypeConfig_String extends IBasePropType { - type: 'string' - default: string -} +export interface PropTypeConfig_String + extends ISimplePropType<'string', string> {} export interface PropTypeConfig_StringLiteral - extends IBasePropType { - type: 'stringLiteral' - default: T + extends ISimplePropType<'stringLiteral', T> { options: Record as: 'menu' | 'switch' } -export interface PropTypeConfig_Rgba extends IBasePropType { - type: 'rgba' - default: Rgba +export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {} + +type DeepPartialCompound = { + [K in keyof Props]?: DeepPartial } -/** - * - */ -export interface PropTypeConfig_Compound< - Props extends IValidCompoundProps, - PropTypes = Props, -> extends IBasePropType< +type DeepPartial = + Conf extends PropTypeConfig_AllNonCompounds + ? Conf['valueType'] + : Conf extends PropTypeConfig_Compound + ? DeepPartialCompound + : never + +export interface PropTypeConfig_Compound + extends IBasePropType< + 'compound', {[K in keyof Props]: Props[K]['valueType']}, - PropTypes + DeepPartialCompound > { - type: 'compound' props: Record } -export interface PropTypeConfig_Enum extends IBasePropType<{}> { - type: 'enum' +export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> { cases: Record defaultCase: string } -export type PropTypeConfig_AllPrimitives = +export type PropTypeConfig_AllNonCompounds = | PropTypeConfig_Number | PropTypeConfig_Boolean | PropTypeConfig_String @@ -602,6 +643,6 @@ export type PropTypeConfig_AllPrimitives = | PropTypeConfig_Rgba export type PropTypeConfig = - | PropTypeConfig_AllPrimitives + | PropTypeConfig_AllNonCompounds | PropTypeConfig_Compound<$IntentionalAny> | PropTypeConfig_Enum diff --git a/theatre/core/src/sequences/interpolationTripleAtPosition.ts b/theatre/core/src/sequences/interpolationTripleAtPosition.ts index 402dafa..109676b 100644 --- a/theatre/core/src/sequences/interpolationTripleAtPosition.ts +++ b/theatre/core/src/sequences/interpolationTripleAtPosition.ts @@ -14,12 +14,12 @@ export type InterpolationTriple = { progression: number } -// @remarks This new implementation supports sequencing non-scalars, but it's also heavier +// @remarks This new implementation supports sequencing non-numeric props, but it's also heavier // on the GC. This shouldn't be a problem for the vast majority of users, but it's also a // low-hanging fruit for perf optimization. // It can be improved by: // 1. Not creating a new InterpolationTriple object on every change -// 2. Caching propConfig.sanitize(value) +// 2. Caching propConfig.deserialize(value) export default function interpolationTripleAtPosition( trackP: Pointer, diff --git a/theatre/core/src/sheetObjects/SheetObject.test.ts b/theatre/core/src/sheetObjects/SheetObject.test.ts index 51e8281..59b6edc 100644 --- a/theatre/core/src/sheetObjects/SheetObject.test.ts +++ b/theatre/core/src/sheetObjects/SheetObject.test.ts @@ -4,128 +4,324 @@ import {setupTestSheet} from '@theatre/shared/testUtils' import {encodePathToProp} from '@theatre/shared/utils/addresses' import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids' -import {iterateOver, prism, val} from '@theatre/dataverse' +import {iterateOver, prism} from '@theatre/dataverse' +import type {SheetState_Historic} from '@theatre/core/projects/store/types/SheetState_Historic' describe(`SheetObject`, () => { - test('it should support setting/unsetting static props', async () => { - const {obj, studio} = await setupTestSheet({ - staticOverrides: { - byObject: { - obj: { - position: { - x: 10, - }, + describe('static overrides', () => { + const setup = async ( + staticOverrides: SheetState_Historic['staticOverrides']['byObject'][string] = {}, + ) => { + const {studio, objPublicAPI} = await setupTestSheet({ + staticOverrides: { + byObject: { + obj: staticOverrides, }, }, - }, + }) + + const objValues = iterateOver(prism(() => objPublicAPI.value)) + const teardown = () => objValues.return() + + return {studio, objPublicAPI, objValues, teardown} + } + + describe(`conformance`, () => { + test(`invalid static overrides should get ignored`, async () => { + const {teardown, objValues} = await setup({ + nonExistentProp: 1, + position: { + // valid + x: 10, + // invalid + y: '20', + }, + // invalid + color: 'ss', + deeply: { + nested: { + // invalid + checkbox: 0, + }, + }, + }) + const {value} = objValues.next() + expect(value).toMatchObject({ + position: {x: 10, y: 0, z: 0}, + color: {r: 0, g: 0, b: 0, a: 1}, + deeply: { + nested: { + checkbox: true, + }, + }, + }) + expect(value).not.toHaveProperty('nonExistentProp') + teardown() + }) + + test(`setting a compound prop should only work if all its sub-props are present`, async () => { + const {teardown, objValues, objPublicAPI, studio} = await setup({}) + expect(() => { + studio.transaction(({set}) => { + set(objPublicAPI.props.position, {x: 1, y: 2} as any as { + x: number + y: number + z: number + }) + }) + }).toThrow() + }) + + test(`setting a compound prop should only work if all its sub-props are valid`, async () => { + const {teardown, objValues, objPublicAPI, studio} = await setup({}) + expect(() => { + studio.transaction(({set}) => { + set(objPublicAPI.props.position, {x: 1, y: 2, z: 'bad'} as any as { + x: number + y: number + z: number + }) + }) + }).toThrow() + }) + + test(`setting a simple prop should only work if it is valid`, async () => { + const {teardown, objValues, objPublicAPI, studio} = await setup({}) + expect(() => { + studio.transaction(({set}) => { + set(objPublicAPI.props.position.x, 'bad' as any as number) + }) + }).toThrow() + }) }) - const objValues = iterateOver( - prism(() => { - return val(val(obj.getValues())) - }), - ) - - expect(objValues.next().value).toMatchObject({ - position: {x: 10, y: 1, z: 2}, + test(`should be a deep merge of default values and static overrides`, async () => { + const {teardown, objValues} = await setup({position: {x: 10}}) + expect(objValues.next().value).toMatchObject({ + position: {x: 10, y: 0, z: 0}, + }) + teardown() }) - // setting a static - studio.transaction(({set}) => { - set(obj.propsP.position.y, 5) + test(`should allow introducing a static override to a simple prop`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup({ + position: {x: 10}, + }) + studio.transaction(({set}) => { + set(objPublicAPI.props.position.y, 5) + }) + + expect(objValues.next().value).toMatchObject({ + position: {x: 10, y: 5, z: 0}, + }) + + teardown() }) - expect(objValues.next().value).toMatchObject({ - position: {x: 10, y: 5, z: 2}, + test(`should allow introducing a static override to a compound prop`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.position, {x: 1, y: 2, z: 3}) + }) + + expect(objValues.next().value).toMatchObject({ + position: {x: 1, y: 2, z: 3}, + }) + + teardown() }) - // unsetting a static - studio.transaction(({unset}) => { - unset(obj.propsP.position.y) + test(`should allow removing a static override to a simple prop`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.position, {x: 1, y: 2, z: 3}) + }) + + studio.transaction(({unset}) => { + unset(objPublicAPI.props.position.z) + }) + + expect(objValues.next().value).toMatchObject({ + position: {x: 1, y: 2, z: 0}, + }) + + teardown() }) - expect(objValues.next().value).toMatchObject({ - position: {x: 10, y: 1, z: 2}, + test(`should allow removing a static override to a compound prop`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.position, {x: 1, y: 2, z: 3}) + }) + + studio.transaction(({unset}) => { + unset(objPublicAPI.props.position) + }) + + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 0, z: 0}, + }) + + teardown() }) - objValues.return() + describe(`simple props as json objects`, () => { + test(`with no overrides`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + + expect(objValues.next().value).toMatchObject({ + color: {r: 0, g: 0, b: 0, a: 1}, + }) + + teardown() + }) + + describe(`setting overrides`, () => { + test(`should allow setting an override`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.color, {r: 0.1, g: 0.2, b: 0.3, a: 0.5}) + }) + + expect(objValues.next().value).toMatchObject({ + color: {r: 0.1, g: 0.2, b: 0.3, a: 0.5}, + }) + + teardown() + }) + + test(`should disallow setting an override on json sub-props`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + + // TODO also disallow in typescript + expect(() => { + studio.transaction(({set}) => { + set(objPublicAPI.props.color.r, 1) + }) + }).toThrow() + + expect(objValues.next().value).toMatchObject({ + color: {r: 0, g: 0, b: 0, a: 1}, + }) + + teardown() + }) + }) + + describe(`unsetting overrides`, () => { + test(`should allow unsetting an override`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.color, {r: 0.1, g: 0.2, b: 0.3, a: 0.5}) + }) + + studio.transaction(({unset}) => { + unset(objPublicAPI.props.color) + }) + + expect(objValues.next().value).toMatchObject({ + color: {r: 0, g: 0, b: 0, a: 1}, + }) + + teardown() + }) + + test(`should disallow unsetting an override on sub-props`, async () => { + const {teardown, objValues, studio, objPublicAPI} = await setup() + studio.transaction(({set}) => { + set(objPublicAPI.props.color, {r: 0.1, g: 0.2, b: 0.3, a: 0.5}) + }) + + // TODO: also disallow in types + expect(() => { + studio.transaction(({unset}) => { + unset(objPublicAPI.props.color.r) + }) + }).toThrow() + + expect(objValues.next().value).toMatchObject({ + color: {r: 0.1, g: 0.2, b: 0.3, a: 0.5}, + }) + + teardown() + }) + }) + }) }) - test('it should support sequenced props', async () => { - const {obj, sheet} = await setupTestSheet({ - staticOverrides: { - byObject: {}, - }, - sequence: { - type: 'PositionalSequence', - length: 20, - subUnitsPerUnit: 30, - tracksByObject: { - obj: { - trackIdByPropPath: { - [encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'), - }, - trackData: { - '1': { - type: 'BasicKeyframedTrack', - keyframes: [ - { - id: asKeyframeId('0'), - position: 10, - connectedRight: true, - handles: [0.5, 0.5, 0.5, 0.5], - value: 3, - }, - { - id: asKeyframeId('1'), - position: 20, - connectedRight: false, - handles: [0.5, 0.5, 0.5, 0.5], - value: 6, - }, - ], + describe(`sequenced overrides`, () => { + test('calculation of sequenced overrides', async () => { + const {objPublicAPI, sheet} = await setupTestSheet({ + staticOverrides: { + byObject: {}, + }, + sequence: { + type: 'PositionalSequence', + length: 20, + subUnitsPerUnit: 30, + tracksByObject: { + obj: { + trackIdByPropPath: { + [encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'), + }, + trackData: { + '1': { + type: 'BasicKeyframedTrack', + keyframes: [ + { + id: asKeyframeId('0'), + position: 10, + connectedRight: true, + handles: [0.5, 0.5, 0.5, 0.5], + value: 3, + }, + { + id: asKeyframeId('1'), + position: 20, + connectedRight: false, + handles: [0.5, 0.5, 0.5, 0.5], + value: 6, + }, + ], + }, }, }, }, }, - }, + }) + + const seq = sheet.publicApi.sequence + + const objValues = iterateOver(prism(() => objPublicAPI.value)) + + expect(seq.position).toEqual(0) + + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 3, z: 0}, + }) + + seq.position = 5 + expect(seq.position).toEqual(5) + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 3, z: 0}, + }) + + seq.position = 11 + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 3.29999747758308, z: 0}, + }) + + seq.position = 15 + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 4.5, z: 0}, + }) + + seq.position = 22 + expect(objValues.next().value).toMatchObject({ + position: {x: 0, y: 6, z: 0}, + }) + + objValues.return() }) - - const seq = sheet.publicApi.sequence - - const objValues = iterateOver( - prism(() => { - return val(val(obj.getValues())) - }), - ) - - expect(seq.position).toEqual(0) - - expect(objValues.next().value).toMatchObject({ - position: {x: 0, y: 3, z: 2}, - }) - - seq.position = 5 - expect(seq.position).toEqual(5) - expect(objValues.next().value).toMatchObject({ - position: {x: 0, y: 3, z: 2}, - }) - - seq.position = 11 - expect(objValues.next().value).toMatchObject({ - position: {x: 0, y: 3.29999747758308, z: 2}, - }) - - seq.position = 15 - expect(objValues.next().value).toMatchObject({ - position: {x: 0, y: 4.5, z: 2}, - }) - - seq.position = 22 - expect(objValues.next().value).toMatchObject({ - position: {x: 0, y: 6, z: 2}, - }) - - objValues.return() }) }) diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 23fa671..5bdd738 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -22,15 +22,8 @@ import type { import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse' import type SheetObjectTemplate from './SheetObjectTemplate' import TheatreSheetObject from './TheatreSheetObject' -import type {Interpolator} from '@theatre/core/propTypes' -import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils' - -// type Everything = { -// final: SerializableMap -// statics: SerializableMap -// defaults: SerializableMap -// sequenced: SerializableMap -// } +import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes' +import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' export default class SheetObject implements IdentityDerivationProvider { get type(): 'Theatre_SheetObject' { @@ -157,23 +150,32 @@ export default class SheetObject implements IdentityDerivationProvider { const propConfig = getPropConfigByPath( this.template.config, pathToProp, - )! + )! as Extract + const deserialize = propConfig.deserialize const interpolate = propConfig.interpolate! as Interpolator<$IntentionalAny> const updateSequenceValueFromItsDerivation = () => { const triple = derivation.getValue() - if (!triple) - return valsAtom.setIn(pathToProp, propConfig!.default) + if (!triple) return valsAtom.setIn(pathToProp, undefined) - const left = valueInProp(triple.left, propConfig) + const leftDeserialized = deserialize(triple.left) + + const left = + typeof leftDeserialized === 'undefined' + ? propConfig.default + : leftDeserialized if (triple.right === undefined) return valsAtom.setIn(pathToProp, left) - const right = valueInProp(triple.right, propConfig) + const rightDeserialized = deserialize(triple.right) + const right = + typeof rightDeserialized === 'undefined' + ? propConfig.default + : rightDeserialized return valsAtom.setIn( pathToProp, diff --git a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts index 06cd475..9261b0c 100644 --- a/theatre/core/src/sheetObjects/SheetObjectTemplate.ts +++ b/theatre/core/src/sheetObjects/SheetObjectTemplate.ts @@ -24,7 +24,10 @@ import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject' import SheetObject from './SheetObject' import logger from '@theatre/shared/logger' import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' -import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' +import { + getPropConfigByPath, + isPropConfSequencable, +} from '@theatre/shared/propTypes/utils' import getOrderingOfPropTypeConfig from './getOrderingOfPropTypeConfig' export type IPropPathToTrackIdTree = { @@ -91,14 +94,16 @@ export default class SheetObjectTemplate { this.address.sheetId ] - const value = + const json = val( pointerToSheetState.staticOverrides.byObject[ this.address.objectKey ], ) || {} - return value + const config = val(this._config.pointer) + const deserialized = config.deserialize(json) || {} + return deserialized }), ) } @@ -138,8 +143,7 @@ export default class SheetObjectTemplate { const propConfig = getPropConfigByPath(this.config, pathToProp) - if (!propConfig || !propConfig?.sanitize || !propConfig.interpolate) - continue + if (!propConfig || !isPropConfSequencable(propConfig)) continue arrayOfIds.push({pathToProp, trackId: trackId!}) } diff --git a/theatre/shared/src/propTypes/utils.ts b/theatre/shared/src/propTypes/utils.ts index 72a5545..26e4c03 100644 --- a/theatre/shared/src/propTypes/utils.ts +++ b/theatre/shared/src/propTypes/utils.ts @@ -1,6 +1,7 @@ import type { IBasePropType, PropTypeConfig, + PropTypeConfig_AllNonCompounds, PropTypeConfig_Compound, PropTypeConfig_Enum, } from '@theatre/core/propTypes' @@ -33,20 +34,27 @@ export function getPropConfigByPath( /** * @param value - An arbitrary value. May be matching the prop's type or not * @param propConfig - The configuration object for a prop - * @returns value if it matches the prop's type (or if the prop doesn't have a sanitizer), + * @returns value if it matches the prop's type * otherwise returns the default value for the prop */ -export function valueInProp( +export function valueInProp( value: unknown, - propConfig: IBasePropType, -): PropValueType | unknown { - const sanitize = propConfig.sanitize - if (!sanitize) return value + propConfig: PropConfig, +): PropConfig extends IBasePropType<$IntentionalAny, $IntentionalAny, infer T> + ? T + : never { + const deserialize = propConfig.deserialize - const sanitizedVal = sanitize(value) + const sanitizedVal = deserialize(value) if (typeof sanitizedVal === 'undefined') { return propConfig.default } else { return sanitizedVal } } + +export function isPropConfSequencable( + conf: PropTypeConfig, +): conf is Extract { + return Object.prototype.hasOwnProperty.call(conf, 'interpolate') +} diff --git a/theatre/shared/src/testUtils.ts b/theatre/shared/src/testUtils.ts index 3fa8817..89c1cd3 100644 --- a/theatre/shared/src/testUtils.ts +++ b/theatre/shared/src/testUtils.ts @@ -10,6 +10,20 @@ import coreTicker from '@theatre/core/coreTicker' import globals from './globals' /* eslint-enable no-restricted-syntax */ +const defaultProps = { + position: { + x: 0, + y: 0, + z: 0, + }, + color: t.rgba(), + deeply: { + nested: { + checkbox: true, + }, + }, +} + let lastProjectN = 0 export async function setupTestSheet(sheetState: SheetState_Historic) { const studio = getStudio()! @@ -31,13 +45,7 @@ export async function setupTestSheet(sheetState: SheetState_Historic) { ticker.tick() await project.ready const sheetPublicAPI = project.sheet('Sheet') - const objPublicAPI = sheetPublicAPI.object('obj', { - position: { - x: 0, - y: t.number(1), - z: t.number(2), - }, - }) + const objPublicAPI = sheetPublicAPI.object('obj', defaultProps) const obj = privateAPI(objPublicAPI) diff --git a/theatre/studio/src/StudioStore/StudioStore.ts b/theatre/studio/src/StudioStore/StudioStore.ts index 4b4dc17..6b7b311 100644 --- a/theatre/studio/src/StudioStore/StudioStore.ts +++ b/theatre/studio/src/StudioStore/StudioStore.ts @@ -13,26 +13,18 @@ import type { } from '@theatre/studio/store/types' import type {Deferred} from '@theatre/shared/utils/defer' import {defer} from '@theatre/shared/utils/defer' -import forEachDeep from '@theatre/shared/utils/forEachDeep' -import getDeep from '@theatre/shared/utils/getDeep' -import type {SequenceTrackId} from '@theatre/shared/utils/ids' import atomFromReduxStore from '@theatre/studio/utils/redux/atomFromReduxStore' import configureStore from '@theatre/studio/utils/redux/configureStore' -import type {$FixMe, $IntentionalAny, VoidFn} from '@theatre/shared/utils/types' +import type {VoidFn} from '@theatre/shared/utils/types' import type {Atom, Pointer} from '@theatre/dataverse' -import {getPointerParts, val} from '@theatre/dataverse' import type {Draft} from 'immer' import {createDraft, finishDraft} from 'immer' -import get from 'lodash-es/get' import type {Store} from 'redux' import {persistStateOfStudio} from './persistStateOfStudio' -import {isSheetObject} from '@theatre/shared/instanceTypes' import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import {generateDiskStateRevision} from './generateDiskStateRevision' -import type {PropTypeConfig} from '@theatre/core/propTypes' -import type {PathToProp} from '@theatre/shared/src/utils/addresses' -import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' -import {cloneDeep} from 'lodash-es' + +import createTransactionPrivateApi from './createTransactionPrivateApi' export type Drafts = { historic: Draft @@ -131,158 +123,14 @@ export default class StudioStore { } } - const api: ITransactionPrivateApi = { - set: (pointer, value) => { - ensureRunning() - const _value = cloneDeep(value) - const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) - if (isSheetObject(root)) { - root.validateValue(pointer as Pointer<$FixMe>, _value) - - const sequenceTracksTree = val( - root.template - .getMapOfValidSequenceTracks_forStudio() - .getValue(), - ) - - const propConfig = getPropConfigByPath( - root.template.config, - path, - ) as PropTypeConfig - - const setStaticOrKeyframeProp = ( - value: T, - path: PathToProp, - ) => { - if (typeof value === 'undefined' || value === null) { - return - } - - const propAddress = {...root.address, pathToProp: path} - - const trackId = get(sequenceTracksTree, path) as $FixMe as - | SequenceTrackId - | undefined - if (typeof trackId === 'string') { - const propConfig = getPropConfigByPath( - root.template.config, - path, - ) as PropTypeConfig | undefined - // TODO: Make sure this causes no problems wrt decorated - // or otherwise unserializable stuff that sanitize might return. - // value needs to be serializable. - if (propConfig?.sanitize) value = propConfig.sanitize(value) - - const seq = root.sheet.getSequence() - seq.position = seq.closestGridPosition(seq.position) - stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( - { - ...propAddress, - trackId, - position: seq.position, - value: value as $FixMe, - snappingFunction: seq.closestGridPosition, - }, - ) - } else if (propConfig !== undefined) { - stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.setValueOfPrimitiveProp( - {...propAddress, value: value as $FixMe}, - ) - } - } - - // If we are dealing with a compound prop, we recurse through its - // nested properties. - if (propConfig.type === 'compound') { - forEachDeep( - _value, - (v, pathToProp) => { - setStaticOrKeyframeProp(v, pathToProp) - }, - getPointerParts(pointer as Pointer<$IntentionalAny>).path, - ) - } else { - setStaticOrKeyframeProp(_value, path) - } - } else { - throw new Error( - 'Only setting props of SheetObject-s is supported in a transaction so far', - ) - } - }, - unset: (pointer) => { - ensureRunning() - const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) - if (isSheetObject(root)) { - const sequenceTracksTree = val( - root.template - .getMapOfValidSequenceTracks_forStudio() - .getValue(), - ) - - const defaultValue = getDeep( - root.template.getDefaultValues().getValue(), - path, - ) - - const propConfig = getPropConfigByPath( - root.template.config, - path, - ) as PropTypeConfig - - const unsetStaticOrKeyframeProp = ( - value: T, - path: PathToProp, - ) => { - const propAddress = {...root.address, pathToProp: path} - - const trackId = get(sequenceTracksTree, path) as $FixMe as - | SequenceTrackId - | undefined - - if (typeof trackId === 'string') { - stateEditors.coreByProject.historic.sheetsById.sequence.unsetKeyframeAtPosition( - { - ...propAddress, - trackId, - position: root.sheet.getSequence().positionSnappedToGrid, - }, - ) - } else if (propConfig !== undefined) { - stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.unsetValueOfPrimitiveProp( - propAddress, - ) - } - } - - if (propConfig.type === 'compound') { - forEachDeep( - defaultValue, - (v, pathToProp) => { - unsetStaticOrKeyframeProp(v, pathToProp) - }, - getPointerParts(pointer as Pointer<$IntentionalAny>).path, - ) - } else { - unsetStaticOrKeyframeProp(defaultValue, path) - } - } else { - throw new Error( - 'Only setting props of SheetObject-s is supported in a transaction so far', - ) - } - }, - get drafts() { - ensureRunning() - return drafts - }, - get stateEditors() { - return stateEditors - }, - } - const stateEditors = setDrafts__onlyMeantToBeCalledByTransaction(drafts) + const api: ITransactionPrivateApi = createTransactionPrivateApi( + ensureRunning, + stateEditors, + drafts, + ) + try { fn(api) running = false diff --git a/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts new file mode 100644 index 0000000..b41b519 --- /dev/null +++ b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts @@ -0,0 +1,248 @@ +import type {Pointer} from '@theatre/dataverse' +import {isSheetObject} from '@theatre/shared/instanceTypes' +import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types' +import get from 'lodash-es/get' +import type {ITransactionPrivateApi} from './StudioStore' +import forEachDeep from '@theatre/shared/utils/forEachDeep' +import getDeep from '@theatre/shared/utils/getDeep' +import type {SequenceTrackId} from '@theatre/shared/utils/ids' +import {getPointerParts} from '@theatre/dataverse' +import type { + PropTypeConfig, + PropTypeConfig_AllNonCompounds, + PropTypeConfig_Compound, +} from '@theatre/core/propTypes' +import type {PathToProp} from '@theatre/shared/src/utils/addresses' +import {getPropConfigByPath} from '@theatre/shared/propTypes/utils' +import {isPlainObject} from 'lodash-es' +import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' + +function cloneDeepSerializable(v: T): T | undefined { + if ( + typeof v === 'boolean' || + typeof v === 'string' || + typeof v === 'number' + ) { + return v + } else if (isPlainObject(v)) { + const cloned: $IntentionalAny = {} + let clonedAtLeastOneProp = false + for (const [key, val] of Object.entries(v)) { + const clonedVal = cloneDeepSerializable(val) + if (typeof clonedVal !== 'undefined') { + cloned[key] = val + clonedAtLeastOneProp = true + } + } + if (clonedAtLeastOneProp) { + return cloned + } + } else { + return undefined + } +} + +function forEachDeepSimplePropOfCompoundProp( + propType: PropTypeConfig_Compound<$IntentionalAny>, + path: Array, + callback: ( + propType: PropTypeConfig_AllNonCompounds, + path: Array, + ) => void, +) { + for (const [key, subType] of Object.entries(propType.props)) { + if (subType.type === 'compound') { + forEachDeepSimplePropOfCompoundProp(subType, [...path, key], callback) + } else if (subType.type === 'enum') { + throw new Error(`Not yet implemented`) + } else { + callback(subType, [...path, key]) + } + } +} + +export default function createTransactionPrivateApi( + ensureRunning: () => void, + stateEditors: ITransactionPrivateApi['stateEditors'], + drafts: ITransactionPrivateApi['drafts'], +): ITransactionPrivateApi { + return { + set: (pointer, value) => { + ensureRunning() + const _value = cloneDeepSerializable(value) + if (typeof _value === 'undefined') return + + const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) + if (isSheetObject(root)) { + const sequenceTracksTree = root.template + .getMapOfValidSequenceTracks_forStudio() + .getValue() + + const propConfig = getPropConfigByPath(root.template.config, path) + + if (!propConfig) { + throw new Error( + `Object ${ + root.address.objectKey + } does not have a prop at ${JSON.stringify(path)}`, + ) + } + + // if (isPropConfigComposite(propConfig)) { + // propConfig.validate(_value) + // } else { + + // propConfig.validate(_value) + // } + + const setStaticOrKeyframeProp = ( + value: T, + propConfig: PropTypeConfig_AllNonCompounds, + path: PathToProp, + ) => { + if (typeof value === 'undefined' || value === null) { + return + } + + const deserialized = cloneDeepSerializable( + propConfig.deserialize(value), + ) + if (typeof deserialized === 'undefined') { + throw new Error( + `Invalid value ${userReadableTypeOfValue( + value, + )} for object.props${path + .map((key) => `[${JSON.stringify(key)}]`) + .join('')} is invalid`, + ) + } + + const propAddress = {...root.address, pathToProp: path} + + const trackId = get(sequenceTracksTree, path) as $FixMe as + | SequenceTrackId + | undefined + + if (typeof trackId === 'string') { + const seq = root.sheet.getSequence() + seq.position = seq.closestGridPosition(seq.position) + stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( + { + ...propAddress, + trackId, + position: seq.position, + value: value as $FixMe, + snappingFunction: seq.closestGridPosition, + }, + ) + } else { + stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.setValueOfPrimitiveProp( + {...propAddress, value: value as $FixMe}, + ) + } + } + + if (propConfig.type === 'compound') { + const pathToTopPointer = getPointerParts( + pointer as $IntentionalAny, + ).path + + const lengthOfTopPointer = pathToTopPointer.length + // If we are dealing with a compound prop, we recurse through its + // nested properties. + forEachDeepSimplePropOfCompoundProp( + propConfig, + pathToTopPointer, + (primitivePropConfig, pathToProp) => { + const pathToPropInProvidedValue = + pathToProp.slice(lengthOfTopPointer) + + const v = getDeep(_value, pathToPropInProvidedValue) + if (typeof v !== 'undefined') { + setStaticOrKeyframeProp(v, primitivePropConfig, pathToProp) + } else { + throw new Error( + `Property object.props${pathToProp + .map((key) => `[${JSON.stringify(key)}]`) + .join('')} is required but not provided`, + ) + } + }, + ) + } else if (propConfig.type === 'enum') { + throw new Error(`Enums aren't implemented yet`) + } else { + setStaticOrKeyframeProp(_value, propConfig, path) + } + } else { + throw new Error( + 'Only setting props of SheetObject-s is supported in a transaction so far', + ) + } + }, + unset: (pointer) => { + ensureRunning() + const {root, path} = getPointerParts(pointer as Pointer<$FixMe>) + if (isSheetObject(root)) { + const sequenceTracksTree = root.template + .getMapOfValidSequenceTracks_forStudio() + .getValue() + + const defaultValue = getDeep( + root.template.getDefaultValues().getValue(), + path, + ) + + const propConfig = getPropConfigByPath( + root.template.config, + path, + ) as PropTypeConfig + + const unsetStaticOrKeyframeProp = (value: T, path: PathToProp) => { + const propAddress = {...root.address, pathToProp: path} + + const trackId = get(sequenceTracksTree, path) as $FixMe as + | SequenceTrackId + | undefined + + if (typeof trackId === 'string') { + stateEditors.coreByProject.historic.sheetsById.sequence.unsetKeyframeAtPosition( + { + ...propAddress, + trackId, + position: root.sheet.getSequence().positionSnappedToGrid, + }, + ) + } else if (propConfig !== undefined) { + stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.unsetValueOfPrimitiveProp( + propAddress, + ) + } + } + + if (propConfig.type === 'compound') { + forEachDeep( + defaultValue, + (v, pathToProp) => { + unsetStaticOrKeyframeProp(v, pathToProp) + }, + getPointerParts(pointer as Pointer<$IntentionalAny>).path, + ) + } else { + unsetStaticOrKeyframeProp(defaultValue, path) + } + } else { + throw new Error( + 'Only setting props of SheetObject-s is supported in a transaction so far', + ) + } + }, + get drafts() { + ensureRunning() + return drafts + }, + get stateEditors() { + return stateEditors + }, + } +} diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx index d262d46..30352ea 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx +++ b/theatre/studio/src/panels/DetailPanel/propEditors/utils/useEditingToolsForPrimitiveProp.tsx @@ -13,6 +13,7 @@ import React from 'react' import DefaultOrStaticValueIndicator from './DefaultValueIndicator' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' import type {PropTypeConfig} from '@theatre/core/propTypes' +import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' interface CommonStuff { value: T @@ -309,7 +310,3 @@ type Shade = | 'Sequenced_OnKeyframe_BeingScrubbed' | 'Sequenced_BeingInterpolated' | 'Sequened_NotBeingInterpolated' - -function isPropConfSequencable(conf: PropTypeConfig): boolean { - return conf.type === 'number' || (!!conf.sanitize && !!conf.interpolate) -} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx index 5780f94..7e9f99b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx @@ -132,7 +132,7 @@ type IProps = { const LengthIndicator: React.FC = ({layoutP}) => { const [nodeRef, node] = useRefAndState(null) - const [isDraggingD] = useDragBulge(node, {layoutP}) + const [isDragging] = useDragBulge(node, {layoutP}) const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( {}, () => { @@ -177,7 +177,7 @@ const LengthIndicator: React.FC = ({layoutP}) => { height: height + 'px', transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`, }} - className={val(isDraggingD) ? 'dragging' : ''} + className={isDragging ? 'dragging' : ''} > = ({layoutP}) => { /> ) - }, [layoutP, nodeRef, isDraggingD, popoverNode]) + }, [layoutP, nodeRef, isDragging, popoverNode]) } -function useDragBulge(node: HTMLDivElement | null, props: IProps) { +function useDragBulge( + node: HTMLDivElement | null, + props: IProps, +): [isDragging: boolean] { const propsRef = useRef(props) propsRef.current = props const [isDragging, setIsDragging] = useState(false) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx index 8304f2d..601e2a6 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -11,8 +11,12 @@ import React, {useMemo, useRef, useState} from 'react' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor' import KeyframeEditor from './KeyframeEditor/KeyframeEditor' -import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils' -import type {PropTypeConfig} from '@theatre/core/propTypes' +import { + getPropConfigByPath, + isPropConfigComposite, + valueInProp, +} from '@theatre/shared/propTypes/utils' +import type {PropTypeConfig_AllNonCompounds} from '@theatre/core/propTypes' export type ExtremumSpace = { fromValueSpace: (v: number) => number @@ -33,7 +37,12 @@ const BasicKeyframedTrack: React.FC<{ const propConfig = getPropConfigByPath( sheetObject.template.config, pathToProp, - )! + )! as PropTypeConfig_AllNonCompounds + + if (isPropConfigComposite(propConfig)) { + console.error(`Composite prop types cannot be keyframed`) + return <> + } const [areExtremumsLocked, setAreExtremumsLocked] = useState(false) const lockExtremums = useMemo(() => { @@ -55,7 +64,7 @@ const BasicKeyframedTrack: React.FC<{ const extremumSpace: ExtremumSpace = useMemo(() => { const extremums = - propConfig.isScalar === true + propConfig.type === 'number' ? calculateScalarExtremums(trackData.keyframes, propConfig) : calculateNonScalarExtremums(trackData.keyframes) @@ -92,7 +101,7 @@ const BasicKeyframedTrack: React.FC<{ layoutP={layoutP} sheetObject={sheetObject} trackId={trackId} - isScalar={propConfig.isScalar === true} + isScalar={propConfig.type === 'number'} key={'keyframe-' + kf.id} extremumSpace={cachedExtremumSpace.current} color={color} @@ -118,7 +127,7 @@ type Extremums = [min: number, max: number] function calculateScalarExtremums( keyframes: Keyframe[], - propConfig: PropTypeConfig, + propConfig: PropTypeConfig_AllNonCompounds, ): Extremums { let min = Infinity, max = -Infinity diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx index 2bc5379..1b3be1b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx @@ -15,7 +15,7 @@ import CurveHandle from './CurveHandle' import GraphEditorDotScalar from './GraphEditorDotScalar' import GraphEditorDotNonScalar from './GraphEditorDotNonScalar' import GraphEditorNonScalarDash from './GraphEditorNonScalarDash' -import type {PropTypeConfig} from '@theatre/core/propTypes' +import type {PropTypeConfig_AllNonCompounds} from '@theatre/core/propTypes' const Container = styled.g` /* position: absolute; */ @@ -33,7 +33,7 @@ const KeyframeEditor: React.FC<{ extremumSpace: ExtremumSpace isScalar: boolean color: keyof typeof graphEditorColors - propConfig: PropTypeConfig + propConfig: PropTypeConfig_AllNonCompounds }> = (props) => { const {index, trackData, isScalar} = props const cur = trackData.keyframes[index] diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts index b5c32f5..92a3918 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts @@ -1,7 +1,7 @@ import type {} from '@theatre/core/projects/store/types/SheetState_Historic' import type { PropTypeConfig, - PropTypeConfig_AllPrimitives, + PropTypeConfig_AllNonCompounds, PropTypeConfig_Compound, } from '@theatre/core/propTypes' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' @@ -53,7 +53,7 @@ export type SequenceEditorTree_PrimitiveProp = sheetObject: SheetObject pathToProp: PathToProp trackId: SequenceTrackId - propConf: PropTypeConfig_AllPrimitives + propConf: PropTypeConfig_AllNonCompounds } export type SequenceEditorTree_AllRowTypes = @@ -231,7 +231,7 @@ export const calculateSequenceEditorTree = ( sheetObject: SheetObject, trackId: SequenceTrackId, pathToProp: PathToProp, - propConf: PropTypeConfig_AllPrimitives, + propConf: PropTypeConfig_AllNonCompounds, arrayOfChildren: Array< SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren >,