Standardize handling of non-compound types (#118)

This commit is contained in:
Aria 2022-04-09 15:02:39 +02:00 committed by GitHub
parent 77c7fc969f
commit 16c070b6e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 782 additions and 396 deletions

View file

@ -297,10 +297,16 @@ function isIdentityChangeProvider(
* For pointers, the value is returned by first creating a derivation, so it is * For pointers, the value is returned by first creating a derivation, so it is
* reactive e.g. when used in a `prism`. * 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 = <P>( export const val = <
pointerOrDerivationOrPlainValue: P, P extends
| PointerType<$IntentionalAny>
| IDerivation<$IntentionalAny>
| undefined
| null,
>(
input: P,
): P extends PointerType<infer T> ): P extends PointerType<infer T>
? T ? T
: P extends IDerivation<infer T> : P extends IDerivation<infer T>
@ -308,13 +314,11 @@ export const val = <P>(
: P extends undefined | null : P extends undefined | null
? P ? P
: unknown => { : unknown => {
if (isPointer(pointerOrDerivationOrPlainValue)) { if (isPointer(input)) {
return valueDerivation( return valueDerivation(input).getValue() as $IntentionalAny
pointerOrDerivationOrPlainValue, } else if (isDerivation(input)) {
).getValue() as $IntentionalAny return input.getValue() as $IntentionalAny
} else if (isDerivation(pointerOrDerivationOrPlainValue)) {
return pointerOrDerivationOrPlainValue.getValue() as $IntentionalAny
} else { } else {
return pointerOrDerivationOrPlainValue as $IntentionalAny return input as $IntentionalAny
} }
} }

View file

@ -17,6 +17,6 @@ export {default as iterateAndCountTicks} from './derivations/iterateAndCountTick
export {default as iterateOver} from './derivations/iterateOver' export {default as iterateOver} from './derivations/iterateOver'
export {default as prism} from './derivations/prism/prism' export {default as prism} from './derivations/prism/prism'
export {default as pointer, getPointerParts, isPointer} from './pointer' 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 Ticker} from './Ticker'
export {default as PointerProxy} from './PointerProxy' export {default as PointerProxy} from './PointerProxy'

View file

@ -7,6 +7,10 @@ type PointerMeta = {
path: (string | number)[] path: (string | number)[]
} }
const symbolForUnpointableTypes = Symbol()
export type OpaqueToPointers = {[symbolForUnpointableTypes]: true}
export type UnindexableTypesForPointer = export type UnindexableTypesForPointer =
| number | number
| string | string
@ -15,6 +19,7 @@ export type UnindexableTypesForPointer =
| void | void
| undefined | undefined
| Function // eslint-disable-line @typescript-eslint/ban-types | Function // eslint-disable-line @typescript-eslint/ban-types
| OpaqueToPointers
export type UnindexablePointer = { export type UnindexablePointer = {
[K in $IntentionalAny]: Pointer<undefined> [K in $IntentionalAny]: Pointer<undefined>
@ -34,6 +39,19 @@ export type PointerType<O> = {
* explanation of pointers. * explanation of pointers.
* *
* @see Atom * @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<any>): 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<O> = PointerType<O> & export type Pointer<O> = PointerType<O> &
(O extends UnindexableTypesForPointer (O extends UnindexableTypesForPointer

View file

@ -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 userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import type {Rgba} from '@theatre/shared/utils/color' import type {Rgba} from '@theatre/shared/utils/color'
import { import {
@ -8,7 +8,7 @@ import {
srgbToLinearSrgb, srgbToLinearSrgb,
linearSrgbToSrgb, linearSrgbToSrgb,
} from '@theatre/shared/utils/color' } from '@theatre/shared/utils/color'
import {mapValues} from 'lodash-es' import {clamp, mapValues} from 'lodash-es'
import type { import type {
IShorthandCompoundProps, IShorthandCompoundProps,
IValidCompoundProps, IValidCompoundProps,
@ -17,6 +17,12 @@ import type {
import {sanitizeCompoundProps} from './internals' import {sanitizeCompoundProps} from './internals'
import {propTypeSymbol} 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.
// Well provide a nice UX to manage keyframing of multiple sub-props.
const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => { const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (opts === undefined) return if (opts === undefined) return
@ -80,19 +86,49 @@ export const compound = <Props extends IShorthandCompoundProps>(
label?: string label?: string
}, },
): PropTypeConfig_Compound< ): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props>, ShorthandCompoundPropsToLonghandCompoundProps<Props>
Props
> => { > => {
validateCommonOpts('t.compound(props, opts)', opts) validateCommonOpts('t.compound(props, opts)', opts)
const sanitizedProps = sanitizeCompoundProps(props) const sanitizedProps = sanitizeCompoundProps(props)
return { const deserializationCache = new WeakMap<{}, unknown>()
const config: PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props>
> = {
type: 'compound', type: 'compound',
props: sanitizedProps, props: sanitizedProps,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
default: mapValues(sanitizedProps, (p) => p.default) as $IntentionalAny, 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'] range?: PropTypeConfig_Number['range']
nudgeMultiplier?: number nudgeMultiplier?: number
label?: string label?: string
sanitize?: Sanitizer<number>
interpolate?: Interpolator<number>
}, },
): PropTypeConfig_Number => { ): PropTypeConfig_Number => {
if (process.env.NODE_ENV !== 'production') { 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 { return {
type: 'number', type: 'number',
@ -225,13 +251,20 @@ export const number = (
nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn, nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn,
nudgeMultiplier: nudgeMultiplier:
typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1, typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1,
isScalar: true, interpolate: _interpolateNumber,
sanitize: sanitize, deserialize: numberDeserializer(opts?.range),
interpolate: opts?.interpolate ?? _interpolateNumber,
} }
} }
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 typeof value === 'number' && isFinite(value) ? value : undefined
const _interpolateNumber = ( const _interpolateNumber = (
@ -288,8 +321,8 @@ export const rgba = (
default: decorateRgba(sanitized as Rgba), default: decorateRgba(sanitized as Rgba),
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize: _sanitizeRgba,
interpolate: _interpolateRgba, 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 // Clamp defaultValue components between 0 and 1
const sanitized = {} const sanitized = {}
for (const c of ['r', 'g', 'b', 'a']) { 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 = ( const _interpolateRgba = (
@ -360,7 +395,6 @@ export const boolean = (
defaultValue: boolean, defaultValue: boolean,
opts?: { opts?: {
label?: string label?: string
sanitize?: Sanitizer<boolean>
interpolate?: Interpolator<boolean> interpolate?: Interpolator<boolean>
}, },
): PropTypeConfig_Boolean => { ): PropTypeConfig_Boolean => {
@ -381,12 +415,12 @@ export const boolean = (
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize: _sanitizeBoolean, interpolate: opts?.interpolate ?? leftInterpolate,
interpolate: leftInterpolate, deserialize: _ensureBoolean,
} }
} }
const _sanitizeBoolean = (val: unknown): boolean | undefined => { const _ensureBoolean = (val: unknown): boolean | undefined => {
return typeof val === 'boolean' ? val : undefined return typeof val === 'boolean' ? val : undefined
} }
@ -419,7 +453,6 @@ export const string = (
defaultValue: string, defaultValue: string,
opts?: { opts?: {
label?: string label?: string
sanitize?: Sanitizer<string>
interpolate?: Interpolator<string> interpolate?: Interpolator<string>
}, },
): PropTypeConfig_String => { ): PropTypeConfig_String => {
@ -439,14 +472,15 @@ export const string = (
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize(value: unknown) {
if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'string' ? value : undefined
},
interpolate: opts?.interpolate ?? leftInterpolate, 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. * A stringLiteral prop type, useful for building menus or radio buttons.
* *
@ -481,7 +515,11 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
/** /**
* opts.as Determines if editor is shown as a menu or a switch. Either 'menu' or 'switch'. Default: 'menu' * 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<Extract<keyof Opts, string>>
},
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> { ): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return { return {
type: 'stringLiteral', type: 'stringLiteral',
@ -491,34 +529,41 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
as: opts?.as ?? 'menu', as: opts?.as ?? 'menu',
label: opts?.label, label: opts?.label,
sanitize(value: unknown) { interpolate: opts?.interpolate ?? leftInterpolate,
if (typeof value !== 'string') return undefined deserialize(json: unknown): undefined | Extract<keyof Opts, string> {
if (Object.hasOwnProperty.call(options, value)) { if (typeof json !== 'string') return undefined
return value as $IntentionalAny if (Object.prototype.hasOwnProperty.call(options, json)) {
return json as $IntentionalAny
} else { } else {
return undefined return undefined
} }
}, },
interpolate: leftInterpolate,
} }
} }
export type Sanitizer<T> = (value: unknown) => T | undefined export type Sanitizer<T> = (value: unknown) => T | undefined
export type Interpolator<T> = (left: T, right: T, progression: number) => T export type Interpolator<T> = (left: T, right: T, progression: number) => T
export interface IBasePropType<ValueType, PropTypes = ValueType> { export interface IBasePropType<
LiteralIdentifier extends string,
ValueType,
DeserializeType = ValueType,
> {
type: LiteralIdentifier
valueType: ValueType valueType: ValueType
[propTypeSymbol]: 'TheatrePropType' [propTypeSymbol]: 'TheatrePropType'
label: string | undefined label: string | undefined
isScalar?: true
sanitize?: Sanitizer<PropTypes>
interpolate?: Interpolator<PropTypes>
default: ValueType default: ValueType
deserialize: (json: unknown) => undefined | DeserializeType
} }
export interface PropTypeConfig_Number extends IBasePropType<number> { interface ISimplePropType<LiteralIdentifier extends string, ValueType>
type: 'number' extends IBasePropType<LiteralIdentifier, ValueType, ValueType> {
default: number interpolate: Interpolator<ValueType>
}
export interface PropTypeConfig_Number
extends ISimplePropType<'number', number> {
range?: [min: number, max: number] range?: [min: number, max: number]
nudgeFn: NumberNudgeFn nudgeFn: NumberNudgeFn
nudgeMultiplier: number nudgeMultiplier: number
@ -547,54 +592,50 @@ const defaultNumberNudgeFn: NumberNudgeFn = ({
return deltaX * magnitude * config.nudgeMultiplier return deltaX * magnitude * config.nudgeMultiplier
} }
export interface PropTypeConfig_Boolean extends IBasePropType<boolean> { export interface PropTypeConfig_Boolean
type: 'boolean' extends ISimplePropType<'boolean', boolean> {}
default: boolean
}
interface CommonOpts { interface CommonOpts {
label?: string label?: string
} }
export interface PropTypeConfig_String extends IBasePropType<string> { export interface PropTypeConfig_String
type: 'string' extends ISimplePropType<'string', string> {}
default: string
}
export interface PropTypeConfig_StringLiteral<T extends string> export interface PropTypeConfig_StringLiteral<T extends string>
extends IBasePropType<T> { extends ISimplePropType<'stringLiteral', T> {
type: 'stringLiteral'
default: T
options: Record<T, string> options: Record<T, string>
as: 'menu' | 'switch' as: 'menu' | 'switch'
} }
export interface PropTypeConfig_Rgba extends IBasePropType<Rgba> { export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {}
type: 'rgba'
default: Rgba type DeepPartialCompound<Props extends IValidCompoundProps> = {
[K in keyof Props]?: DeepPartial<Props[K]>
} }
/** type DeepPartial<Conf extends PropTypeConfig> =
* Conf extends PropTypeConfig_AllNonCompounds
*/ ? Conf['valueType']
export interface PropTypeConfig_Compound< : Conf extends PropTypeConfig_Compound<infer T>
Props extends IValidCompoundProps, ? DeepPartialCompound<T>
PropTypes = Props, : never
> extends IBasePropType<
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps>
extends IBasePropType<
'compound',
{[K in keyof Props]: Props[K]['valueType']}, {[K in keyof Props]: Props[K]['valueType']},
PropTypes DeepPartialCompound<Props>
> { > {
type: 'compound'
props: Record<string, PropTypeConfig> props: Record<string, PropTypeConfig>
} }
export interface PropTypeConfig_Enum extends IBasePropType<{}> { export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> {
type: 'enum'
cases: Record<string, PropTypeConfig> cases: Record<string, PropTypeConfig>
defaultCase: string defaultCase: string
} }
export type PropTypeConfig_AllPrimitives = export type PropTypeConfig_AllNonCompounds =
| PropTypeConfig_Number | PropTypeConfig_Number
| PropTypeConfig_Boolean | PropTypeConfig_Boolean
| PropTypeConfig_String | PropTypeConfig_String
@ -602,6 +643,6 @@ export type PropTypeConfig_AllPrimitives =
| PropTypeConfig_Rgba | PropTypeConfig_Rgba
export type PropTypeConfig = export type PropTypeConfig =
| PropTypeConfig_AllPrimitives | PropTypeConfig_AllNonCompounds
| PropTypeConfig_Compound<$IntentionalAny> | PropTypeConfig_Compound<$IntentionalAny>
| PropTypeConfig_Enum | PropTypeConfig_Enum

View file

@ -14,12 +14,12 @@ export type InterpolationTriple = {
progression: number 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 // 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. // low-hanging fruit for perf optimization.
// It can be improved by: // It can be improved by:
// 1. Not creating a new InterpolationTriple object on every change // 1. Not creating a new InterpolationTriple object on every change
// 2. Caching propConfig.sanitize(value) // 2. Caching propConfig.deserialize(value)
export default function interpolationTripleAtPosition( export default function interpolationTripleAtPosition(
trackP: Pointer<TrackData | undefined>, trackP: Pointer<TrackData | undefined>,

View file

@ -4,128 +4,324 @@
import {setupTestSheet} from '@theatre/shared/testUtils' import {setupTestSheet} from '@theatre/shared/testUtils'
import {encodePathToProp} from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses'
import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids' import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids'
import {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`, () => { describe(`SheetObject`, () => {
test('it should support setting/unsetting static props', async () => { describe('static overrides', () => {
const {obj, studio} = await setupTestSheet({ const setup = async (
staticOverrides: { staticOverrides: SheetState_Historic['staticOverrides']['byObject'][string] = {},
byObject: { ) => {
obj: { const {studio, objPublicAPI} = await setupTestSheet({
position: { staticOverrides: {
x: 10, 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( test(`should be a deep merge of default values and static overrides`, async () => {
prism(() => { const {teardown, objValues} = await setup({position: {x: 10}})
return val(val(obj.getValues())) expect(objValues.next().value).toMatchObject({
}), position: {x: 10, y: 0, z: 0},
) })
teardown()
expect(objValues.next().value).toMatchObject({
position: {x: 10, y: 1, z: 2},
}) })
// setting a static test(`should allow introducing a static override to a simple prop`, async () => {
studio.transaction(({set}) => { const {teardown, objValues, studio, objPublicAPI} = await setup({
set(obj.propsP.position.y, 5) 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({ test(`should allow introducing a static override to a compound prop`, async () => {
position: {x: 10, y: 5, z: 2}, 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 test(`should allow removing a static override to a simple prop`, async () => {
studio.transaction(({unset}) => { const {teardown, objValues, studio, objPublicAPI} = await setup()
unset(obj.propsP.position.y) 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({ test(`should allow removing a static override to a compound prop`, async () => {
position: {x: 10, y: 1, z: 2}, 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 () => { describe(`sequenced overrides`, () => {
const {obj, sheet} = await setupTestSheet({ test('calculation of sequenced overrides', async () => {
staticOverrides: { const {objPublicAPI, sheet} = await setupTestSheet({
byObject: {}, staticOverrides: {
}, byObject: {},
sequence: { },
type: 'PositionalSequence', sequence: {
length: 20, type: 'PositionalSequence',
subUnitsPerUnit: 30, length: 20,
tracksByObject: { subUnitsPerUnit: 30,
obj: { tracksByObject: {
trackIdByPropPath: { obj: {
[encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'), trackIdByPropPath: {
}, [encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'),
trackData: { },
'1': { trackData: {
type: 'BasicKeyframedTrack', '1': {
keyframes: [ type: 'BasicKeyframedTrack',
{ keyframes: [
id: asKeyframeId('0'), {
position: 10, id: asKeyframeId('0'),
connectedRight: true, position: 10,
handles: [0.5, 0.5, 0.5, 0.5], connectedRight: true,
value: 3, handles: [0.5, 0.5, 0.5, 0.5],
}, value: 3,
{ },
id: asKeyframeId('1'), {
position: 20, id: asKeyframeId('1'),
connectedRight: false, position: 20,
handles: [0.5, 0.5, 0.5, 0.5], connectedRight: false,
value: 6, 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()
}) })
}) })

View file

@ -22,15 +22,8 @@ import type {
import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse' import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse'
import type SheetObjectTemplate from './SheetObjectTemplate' import type SheetObjectTemplate from './SheetObjectTemplate'
import TheatreSheetObject from './TheatreSheetObject' import TheatreSheetObject from './TheatreSheetObject'
import type {Interpolator} from '@theatre/core/propTypes' import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes'
import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils' import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
// type Everything = {
// final: SerializableMap
// statics: SerializableMap
// defaults: SerializableMap
// sequenced: SerializableMap
// }
export default class SheetObject implements IdentityDerivationProvider { export default class SheetObject implements IdentityDerivationProvider {
get type(): 'Theatre_SheetObject' { get type(): 'Theatre_SheetObject' {
@ -157,23 +150,32 @@ export default class SheetObject implements IdentityDerivationProvider {
const propConfig = getPropConfigByPath( const propConfig = getPropConfigByPath(
this.template.config, this.template.config,
pathToProp, pathToProp,
)! )! as Extract<PropTypeConfig, {interpolate: $IntentionalAny}>
const deserialize = propConfig.deserialize
const interpolate = const interpolate =
propConfig.interpolate! as Interpolator<$IntentionalAny> propConfig.interpolate! as Interpolator<$IntentionalAny>
const updateSequenceValueFromItsDerivation = () => { const updateSequenceValueFromItsDerivation = () => {
const triple = derivation.getValue() const triple = derivation.getValue()
if (!triple) if (!triple) return valsAtom.setIn(pathToProp, undefined)
return valsAtom.setIn(pathToProp, propConfig!.default)
const left = valueInProp(triple.left, propConfig) const leftDeserialized = deserialize(triple.left)
const left =
typeof leftDeserialized === 'undefined'
? propConfig.default
: leftDeserialized
if (triple.right === undefined) if (triple.right === undefined)
return valsAtom.setIn(pathToProp, left) 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( return valsAtom.setIn(
pathToProp, pathToProp,

View file

@ -24,7 +24,10 @@ import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject'
import SheetObject from './SheetObject' import SheetObject from './SheetObject'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import 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' import getOrderingOfPropTypeConfig from './getOrderingOfPropTypeConfig'
export type IPropPathToTrackIdTree = { export type IPropPathToTrackIdTree = {
@ -91,14 +94,16 @@ export default class SheetObjectTemplate {
this.address.sheetId this.address.sheetId
] ]
const value = const json =
val( val(
pointerToSheetState.staticOverrides.byObject[ pointerToSheetState.staticOverrides.byObject[
this.address.objectKey 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) const propConfig = getPropConfigByPath(this.config, pathToProp)
if (!propConfig || !propConfig?.sanitize || !propConfig.interpolate) if (!propConfig || !isPropConfSequencable(propConfig)) continue
continue
arrayOfIds.push({pathToProp, trackId: trackId!}) arrayOfIds.push({pathToProp, trackId: trackId!})
} }

View file

@ -1,6 +1,7 @@
import type { import type {
IBasePropType, IBasePropType,
PropTypeConfig, PropTypeConfig,
PropTypeConfig_AllNonCompounds,
PropTypeConfig_Compound, PropTypeConfig_Compound,
PropTypeConfig_Enum, PropTypeConfig_Enum,
} from '@theatre/core/propTypes' } 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 value - An arbitrary value. May be matching the prop's type or not
* @param propConfig - The configuration object for a prop * @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 * otherwise returns the default value for the prop
*/ */
export function valueInProp<PropValueType>( export function valueInProp<PropConfig extends PropTypeConfig_AllNonCompounds>(
value: unknown, value: unknown,
propConfig: IBasePropType<PropValueType>, propConfig: PropConfig,
): PropValueType | unknown { ): PropConfig extends IBasePropType<$IntentionalAny, $IntentionalAny, infer T>
const sanitize = propConfig.sanitize ? T
if (!sanitize) return value : never {
const deserialize = propConfig.deserialize
const sanitizedVal = sanitize(value) const sanitizedVal = deserialize(value)
if (typeof sanitizedVal === 'undefined') { if (typeof sanitizedVal === 'undefined') {
return propConfig.default return propConfig.default
} else { } else {
return sanitizedVal return sanitizedVal
} }
} }
export function isPropConfSequencable(
conf: PropTypeConfig,
): conf is Extract<PropTypeConfig, {interpolate: any}> {
return Object.prototype.hasOwnProperty.call(conf, 'interpolate')
}

View file

@ -10,6 +10,20 @@ import coreTicker from '@theatre/core/coreTicker'
import globals from './globals' import globals from './globals'
/* eslint-enable no-restricted-syntax */ /* eslint-enable no-restricted-syntax */
const defaultProps = {
position: {
x: 0,
y: 0,
z: 0,
},
color: t.rgba(),
deeply: {
nested: {
checkbox: true,
},
},
}
let lastProjectN = 0 let lastProjectN = 0
export async function setupTestSheet(sheetState: SheetState_Historic) { export async function setupTestSheet(sheetState: SheetState_Historic) {
const studio = getStudio()! const studio = getStudio()!
@ -31,13 +45,7 @@ export async function setupTestSheet(sheetState: SheetState_Historic) {
ticker.tick() ticker.tick()
await project.ready await project.ready
const sheetPublicAPI = project.sheet('Sheet') const sheetPublicAPI = project.sheet('Sheet')
const objPublicAPI = sheetPublicAPI.object('obj', { const objPublicAPI = sheetPublicAPI.object('obj', defaultProps)
position: {
x: 0,
y: t.number(1),
z: t.number(2),
},
})
const obj = privateAPI(objPublicAPI) const obj = privateAPI(objPublicAPI)

View file

@ -13,26 +13,18 @@ import type {
} from '@theatre/studio/store/types' } from '@theatre/studio/store/types'
import type {Deferred} from '@theatre/shared/utils/defer' import type {Deferred} from '@theatre/shared/utils/defer'
import {defer} from '@theatre/shared/utils/defer' import {defer} from '@theatre/shared/utils/defer'
import 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 atomFromReduxStore from '@theatre/studio/utils/redux/atomFromReduxStore'
import configureStore from '@theatre/studio/utils/redux/configureStore' 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 type {Atom, Pointer} from '@theatre/dataverse'
import {getPointerParts, val} from '@theatre/dataverse'
import type {Draft} from 'immer' import type {Draft} from 'immer'
import {createDraft, finishDraft} from 'immer' import {createDraft, finishDraft} from 'immer'
import get from 'lodash-es/get'
import type {Store} from 'redux' import type {Store} from 'redux'
import {persistStateOfStudio} from './persistStateOfStudio' import {persistStateOfStudio} from './persistStateOfStudio'
import {isSheetObject} from '@theatre/shared/instanceTypes'
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
import {generateDiskStateRevision} from './generateDiskStateRevision' import {generateDiskStateRevision} from './generateDiskStateRevision'
import type {PropTypeConfig} from '@theatre/core/propTypes'
import type {PathToProp} from '@theatre/shared/src/utils/addresses' import createTransactionPrivateApi from './createTransactionPrivateApi'
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
import {cloneDeep} from 'lodash-es'
export type Drafts = { export type Drafts = {
historic: Draft<StudioHistoricState> historic: Draft<StudioHistoricState>
@ -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 = <T>(
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 = <T>(
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 stateEditors = setDrafts__onlyMeantToBeCalledByTransaction(drafts)
const api: ITransactionPrivateApi = createTransactionPrivateApi(
ensureRunning,
stateEditors,
drafts,
)
try { try {
fn(api) fn(api)
running = false running = false

View file

@ -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<T>(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<string | number>,
callback: (
propType: PropTypeConfig_AllNonCompounds,
path: Array<string | number>,
) => 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 = <T>(
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 = <T>(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
},
}
}

View file

@ -13,6 +13,7 @@ import React from 'react'
import DefaultOrStaticValueIndicator from './DefaultValueIndicator' import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
import type {PropTypeConfig} from '@theatre/core/propTypes' import type {PropTypeConfig} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
interface CommonStuff<T> { interface CommonStuff<T> {
value: T value: T
@ -309,7 +310,3 @@ type Shade =
| 'Sequenced_OnKeyframe_BeingScrubbed' | 'Sequenced_OnKeyframe_BeingScrubbed'
| 'Sequenced_BeingInterpolated' | 'Sequenced_BeingInterpolated'
| 'Sequened_NotBeingInterpolated' | 'Sequened_NotBeingInterpolated'
function isPropConfSequencable(conf: PropTypeConfig): boolean {
return conf.type === 'number' || (!!conf.sanitize && !!conf.interpolate)
}

View file

@ -132,7 +132,7 @@ type IProps = {
const LengthIndicator: React.FC<IProps> = ({layoutP}) => { const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null) const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDraggingD] = useDragBulge(node, {layoutP}) const [isDragging] = useDragBulge(node, {layoutP})
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover( const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
{}, {},
() => { () => {
@ -177,7 +177,7 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
height: height + 'px', height: height + 'px',
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`, transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
}} }}
className={val(isDraggingD) ? 'dragging' : ''} className={isDragging ? 'dragging' : ''}
> >
<ThumbContainer> <ThumbContainer>
<Tumb <Tumb
@ -206,10 +206,13 @@ const LengthIndicator: React.FC<IProps> = ({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) const propsRef = useRef(props)
propsRef.current = props propsRef.current = props
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)

View file

@ -11,8 +11,12 @@ import React, {useMemo, useRef, useState} from 'react'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor' import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
import KeyframeEditor from './KeyframeEditor/KeyframeEditor' import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils' import {
import type {PropTypeConfig} from '@theatre/core/propTypes' getPropConfigByPath,
isPropConfigComposite,
valueInProp,
} from '@theatre/shared/propTypes/utils'
import type {PropTypeConfig_AllNonCompounds} from '@theatre/core/propTypes'
export type ExtremumSpace = { export type ExtremumSpace = {
fromValueSpace: (v: number) => number fromValueSpace: (v: number) => number
@ -33,7 +37,12 @@ const BasicKeyframedTrack: React.FC<{
const propConfig = getPropConfigByPath( const propConfig = getPropConfigByPath(
sheetObject.template.config, sheetObject.template.config,
pathToProp, pathToProp,
)! )! as PropTypeConfig_AllNonCompounds
if (isPropConfigComposite(propConfig)) {
console.error(`Composite prop types cannot be keyframed`)
return <></>
}
const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false) const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false)
const lockExtremums = useMemo(() => { const lockExtremums = useMemo(() => {
@ -55,7 +64,7 @@ const BasicKeyframedTrack: React.FC<{
const extremumSpace: ExtremumSpace = useMemo(() => { const extremumSpace: ExtremumSpace = useMemo(() => {
const extremums = const extremums =
propConfig.isScalar === true propConfig.type === 'number'
? calculateScalarExtremums(trackData.keyframes, propConfig) ? calculateScalarExtremums(trackData.keyframes, propConfig)
: calculateNonScalarExtremums(trackData.keyframes) : calculateNonScalarExtremums(trackData.keyframes)
@ -92,7 +101,7 @@ const BasicKeyframedTrack: React.FC<{
layoutP={layoutP} layoutP={layoutP}
sheetObject={sheetObject} sheetObject={sheetObject}
trackId={trackId} trackId={trackId}
isScalar={propConfig.isScalar === true} isScalar={propConfig.type === 'number'}
key={'keyframe-' + kf.id} key={'keyframe-' + kf.id}
extremumSpace={cachedExtremumSpace.current} extremumSpace={cachedExtremumSpace.current}
color={color} color={color}
@ -118,7 +127,7 @@ type Extremums = [min: number, max: number]
function calculateScalarExtremums( function calculateScalarExtremums(
keyframes: Keyframe[], keyframes: Keyframe[],
propConfig: PropTypeConfig, propConfig: PropTypeConfig_AllNonCompounds,
): Extremums { ): Extremums {
let min = Infinity, let min = Infinity,
max = -Infinity max = -Infinity

View file

@ -15,7 +15,7 @@ import CurveHandle from './CurveHandle'
import GraphEditorDotScalar from './GraphEditorDotScalar' import GraphEditorDotScalar from './GraphEditorDotScalar'
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar' import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash' import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
import type {PropTypeConfig} from '@theatre/core/propTypes' import type {PropTypeConfig_AllNonCompounds} from '@theatre/core/propTypes'
const Container = styled.g` const Container = styled.g`
/* position: absolute; */ /* position: absolute; */
@ -33,7 +33,7 @@ const KeyframeEditor: React.FC<{
extremumSpace: ExtremumSpace extremumSpace: ExtremumSpace
isScalar: boolean isScalar: boolean
color: keyof typeof graphEditorColors color: keyof typeof graphEditorColors
propConfig: PropTypeConfig propConfig: PropTypeConfig_AllNonCompounds
}> = (props) => { }> = (props) => {
const {index, trackData, isScalar} = props const {index, trackData, isScalar} = props
const cur = trackData.keyframes[index] const cur = trackData.keyframes[index]

View file

@ -1,7 +1,7 @@
import type {} from '@theatre/core/projects/store/types/SheetState_Historic' import type {} from '@theatre/core/projects/store/types/SheetState_Historic'
import type { import type {
PropTypeConfig, PropTypeConfig,
PropTypeConfig_AllPrimitives, PropTypeConfig_AllNonCompounds,
PropTypeConfig_Compound, PropTypeConfig_Compound,
} from '@theatre/core/propTypes' } from '@theatre/core/propTypes'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
@ -53,7 +53,7 @@ export type SequenceEditorTree_PrimitiveProp =
sheetObject: SheetObject sheetObject: SheetObject
pathToProp: PathToProp pathToProp: PathToProp
trackId: SequenceTrackId trackId: SequenceTrackId
propConf: PropTypeConfig_AllPrimitives propConf: PropTypeConfig_AllNonCompounds
} }
export type SequenceEditorTree_AllRowTypes = export type SequenceEditorTree_AllRowTypes =
@ -231,7 +231,7 @@ export const calculateSequenceEditorTree = (
sheetObject: SheetObject, sheetObject: SheetObject,
trackId: SequenceTrackId, trackId: SequenceTrackId,
pathToProp: PathToProp, pathToProp: PathToProp,
propConf: PropTypeConfig_AllPrimitives, propConf: PropTypeConfig_AllNonCompounds,
arrayOfChildren: Array< arrayOfChildren: Array<
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
>, >,