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