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
|
* 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
// We’ll 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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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!})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
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 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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
>,
|
>,
|
||||||
|
|
Loading…
Reference in a new issue