Standardize handling of non-compound types (#118)
This commit is contained in:
parent
77c7fc969f
commit
16c070b6e9
17 changed files with 782 additions and 396 deletions
|
@ -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 = <P>(
|
||||
pointerOrDerivationOrPlainValue: P,
|
||||
export const val = <
|
||||
P extends
|
||||
| PointerType<$IntentionalAny>
|
||||
| IDerivation<$IntentionalAny>
|
||||
| undefined
|
||||
| null,
|
||||
>(
|
||||
input: P,
|
||||
): P extends PointerType<infer T>
|
||||
? T
|
||||
: P extends IDerivation<infer T>
|
||||
|
@ -308,13 +314,11 @@ export const val = <P>(
|
|||
: 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<undefined>
|
||||
|
@ -34,6 +39,19 @@ export type PointerType<O> = {
|
|||
* 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<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> &
|
||||
(O extends UnindexableTypesForPointer
|
||||
|
|
|
@ -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 = <Props extends IShorthandCompoundProps>(
|
|||
label?: string
|
||||
},
|
||||
): PropTypeConfig_Compound<
|
||||
ShorthandCompoundPropsToLonghandCompoundProps<Props>,
|
||||
Props
|
||||
ShorthandCompoundPropsToLonghandCompoundProps<Props>
|
||||
> => {
|
||||
validateCommonOpts('t.compound(props, opts)', opts)
|
||||
const sanitizedProps = sanitizeCompoundProps(props)
|
||||
return {
|
||||
const deserializationCache = new WeakMap<{}, unknown>()
|
||||
const config: PropTypeConfig_Compound<
|
||||
ShorthandCompoundPropsToLonghandCompoundProps<Props>
|
||||
> = {
|
||||
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<number>
|
||||
interpolate?: Interpolator<number>
|
||||
},
|
||||
): 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<boolean>
|
||||
interpolate?: Interpolator<boolean>
|
||||
},
|
||||
): 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<string>
|
||||
interpolate?: Interpolator<string>
|
||||
},
|
||||
): 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 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?: 'menu' | 'switch'; label?: string},
|
||||
opts?: {
|
||||
as?: 'menu' | 'switch'
|
||||
label?: string
|
||||
interpolate?: Interpolator<Extract<keyof Opts, string>>
|
||||
},
|
||||
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
|
||||
return {
|
||||
type: 'stringLiteral',
|
||||
|
@ -491,34 +529,41 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
|
|||
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<keyof Opts, string> {
|
||||
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<T> = (value: unknown) => T | undefined
|
||||
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
|
||||
[propTypeSymbol]: 'TheatrePropType'
|
||||
label: string | undefined
|
||||
isScalar?: true
|
||||
sanitize?: Sanitizer<PropTypes>
|
||||
interpolate?: Interpolator<PropTypes>
|
||||
default: ValueType
|
||||
deserialize: (json: unknown) => undefined | DeserializeType
|
||||
}
|
||||
|
||||
export interface PropTypeConfig_Number extends IBasePropType<number> {
|
||||
type: 'number'
|
||||
default: number
|
||||
interface ISimplePropType<LiteralIdentifier extends string, ValueType>
|
||||
extends IBasePropType<LiteralIdentifier, ValueType, ValueType> {
|
||||
interpolate: Interpolator<ValueType>
|
||||
}
|
||||
|
||||
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<boolean> {
|
||||
type: 'boolean'
|
||||
default: boolean
|
||||
}
|
||||
export interface PropTypeConfig_Boolean
|
||||
extends ISimplePropType<'boolean', boolean> {}
|
||||
|
||||
interface CommonOpts {
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface PropTypeConfig_String extends IBasePropType<string> {
|
||||
type: 'string'
|
||||
default: string
|
||||
}
|
||||
export interface PropTypeConfig_String
|
||||
extends ISimplePropType<'string', string> {}
|
||||
|
||||
export interface PropTypeConfig_StringLiteral<T extends string>
|
||||
extends IBasePropType<T> {
|
||||
type: 'stringLiteral'
|
||||
default: T
|
||||
extends ISimplePropType<'stringLiteral', T> {
|
||||
options: Record<T, string>
|
||||
as: 'menu' | 'switch'
|
||||
}
|
||||
|
||||
export interface PropTypeConfig_Rgba extends IBasePropType<Rgba> {
|
||||
type: 'rgba'
|
||||
default: Rgba
|
||||
export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {}
|
||||
|
||||
type DeepPartialCompound<Props extends IValidCompoundProps> = {
|
||||
[K in keyof Props]?: DeepPartial<Props[K]>
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface PropTypeConfig_Compound<
|
||||
Props extends IValidCompoundProps,
|
||||
PropTypes = Props,
|
||||
> extends IBasePropType<
|
||||
type DeepPartial<Conf extends PropTypeConfig> =
|
||||
Conf extends PropTypeConfig_AllNonCompounds
|
||||
? Conf['valueType']
|
||||
: Conf extends PropTypeConfig_Compound<infer T>
|
||||
? DeepPartialCompound<T>
|
||||
: never
|
||||
|
||||
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps>
|
||||
extends IBasePropType<
|
||||
'compound',
|
||||
{[K in keyof Props]: Props[K]['valueType']},
|
||||
PropTypes
|
||||
DeepPartialCompound<Props>
|
||||
> {
|
||||
type: 'compound'
|
||||
props: Record<string, PropTypeConfig>
|
||||
}
|
||||
|
||||
export interface PropTypeConfig_Enum extends IBasePropType<{}> {
|
||||
type: 'enum'
|
||||
export interface PropTypeConfig_Enum extends IBasePropType<'enum', {}> {
|
||||
cases: Record<string, PropTypeConfig>
|
||||
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
|
||||
|
|
|
@ -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<TrackData | undefined>,
|
||||
|
|
|
@ -4,55 +4,254 @@
|
|||
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({
|
||||
describe('static overrides', () => {
|
||||
const setup = async (
|
||||
staticOverrides: SheetState_Historic['staticOverrides']['byObject'][string] = {},
|
||||
) => {
|
||||
const {studio, objPublicAPI} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {
|
||||
obj: {
|
||||
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 objValues = iterateOver(
|
||||
prism(() => {
|
||||
return val(val(obj.getValues()))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 10, y: 1, z: 2},
|
||||
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()
|
||||
})
|
||||
|
||||
// setting a static
|
||||
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(obj.propsP.position.y, 5)
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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: 2},
|
||||
position: {x: 10, y: 5, z: 0},
|
||||
})
|
||||
|
||||
teardown()
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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})
|
||||
})
|
||||
|
||||
// unsetting a static
|
||||
studio.transaction(({unset}) => {
|
||||
unset(obj.propsP.position.y)
|
||||
unset(objPublicAPI.props.position.z)
|
||||
})
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 10, y: 1, z: 2},
|
||||
position: {x: 1, y: 2, z: 0},
|
||||
})
|
||||
|
||||
objValues.return()
|
||||
teardown()
|
||||
})
|
||||
|
||||
test('it should support sequenced props', async () => {
|
||||
const {obj, sheet} = await setupTestSheet({
|
||||
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()
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`sequenced overrides`, () => {
|
||||
test('calculation of sequenced overrides', async () => {
|
||||
const {objPublicAPI, sheet} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {},
|
||||
},
|
||||
|
@ -93,39 +292,36 @@ describe(`SheetObject`, () => {
|
|||
|
||||
const seq = sheet.publicApi.sequence
|
||||
|
||||
const objValues = iterateOver(
|
||||
prism(() => {
|
||||
return val(val(obj.getValues()))
|
||||
}),
|
||||
)
|
||||
const objValues = iterateOver(prism(() => objPublicAPI.value))
|
||||
|
||||
expect(seq.position).toEqual(0)
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 3, z: 2},
|
||||
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: 2},
|
||||
position: {x: 0, y: 3, z: 0},
|
||||
})
|
||||
|
||||
seq.position = 11
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 3.29999747758308, z: 2},
|
||||
position: {x: 0, y: 3.29999747758308, z: 0},
|
||||
})
|
||||
|
||||
seq.position = 15
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 4.5, z: 2},
|
||||
position: {x: 0, y: 4.5, z: 0},
|
||||
})
|
||||
|
||||
seq.position = 22
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 6, z: 2},
|
||||
position: {x: 0, y: 6, z: 0},
|
||||
})
|
||||
|
||||
objValues.return()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<PropTypeConfig, {interpolate: $IntentionalAny}>
|
||||
|
||||
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,
|
||||
|
|
|
@ -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!})
|
||||
}
|
||||
|
|
|
@ -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<PropValueType>(
|
||||
export function valueInProp<PropConfig extends PropTypeConfig_AllNonCompounds>(
|
||||
value: unknown,
|
||||
propConfig: IBasePropType<PropValueType>,
|
||||
): 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<PropTypeConfig, {interpolate: any}> {
|
||||
return Object.prototype.hasOwnProperty.call(conf, 'interpolate')
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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<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 api: ITransactionPrivateApi = createTransactionPrivateApi(
|
||||
ensureRunning,
|
||||
stateEditors,
|
||||
drafts,
|
||||
)
|
||||
|
||||
try {
|
||||
fn(api)
|
||||
running = false
|
||||
|
|
248
theatre/studio/src/StudioStore/createTransactionPrivateApi.ts
Normal file
248
theatre/studio/src/StudioStore/createTransactionPrivateApi.ts
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ type IProps = {
|
|||
|
||||
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(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<IProps> = ({layoutP}) => {
|
|||
height: height + 'px',
|
||||
transform: `translateX(${translateX === 0 ? -1000 : translateX}px)`,
|
||||
}}
|
||||
className={val(isDraggingD) ? 'dragging' : ''}
|
||||
className={isDragging ? 'dragging' : ''}
|
||||
>
|
||||
<ThumbContainer>
|
||||
<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)
|
||||
propsRef.current = props
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
|
|
@ -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<boolean>(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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
>,
|
||||
|
|
Loading…
Reference in a new issue