Standardize handling of non-compound types (#118)

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

View file

@ -297,10 +297,16 @@ function isIdentityChangeProvider(
* For pointers, the value is returned by first creating a derivation, so it is
* 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
}
}

View file

@ -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'

View file

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

View file

@ -1,4 +1,4 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import 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.
// Well 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

View file

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

View file

@ -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()
})
})
})

View file

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

View file

@ -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!})
}

View file

@ -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')
}

View file

@ -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)

View file

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

View file

@ -0,0 +1,248 @@
import type {Pointer} from '@theatre/dataverse'
import {isSheetObject} from '@theatre/shared/instanceTypes'
import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
import get from 'lodash-es/get'
import type {ITransactionPrivateApi} from './StudioStore'
import forEachDeep from '@theatre/shared/utils/forEachDeep'
import getDeep from '@theatre/shared/utils/getDeep'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import {getPointerParts} from '@theatre/dataverse'
import type {
PropTypeConfig,
PropTypeConfig_AllNonCompounds,
PropTypeConfig_Compound,
} from '@theatre/core/propTypes'
import type {PathToProp} from '@theatre/shared/src/utils/addresses'
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
import {isPlainObject} from 'lodash-es'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
function cloneDeepSerializable<T>(v: T): T | undefined {
if (
typeof v === 'boolean' ||
typeof v === 'string' ||
typeof v === 'number'
) {
return v
} else if (isPlainObject(v)) {
const cloned: $IntentionalAny = {}
let clonedAtLeastOneProp = false
for (const [key, val] of Object.entries(v)) {
const clonedVal = cloneDeepSerializable(val)
if (typeof clonedVal !== 'undefined') {
cloned[key] = val
clonedAtLeastOneProp = true
}
}
if (clonedAtLeastOneProp) {
return cloned
}
} else {
return undefined
}
}
function forEachDeepSimplePropOfCompoundProp(
propType: PropTypeConfig_Compound<$IntentionalAny>,
path: Array<string | number>,
callback: (
propType: PropTypeConfig_AllNonCompounds,
path: Array<string | number>,
) => void,
) {
for (const [key, subType] of Object.entries(propType.props)) {
if (subType.type === 'compound') {
forEachDeepSimplePropOfCompoundProp(subType, [...path, key], callback)
} else if (subType.type === 'enum') {
throw new Error(`Not yet implemented`)
} else {
callback(subType, [...path, key])
}
}
}
export default function createTransactionPrivateApi(
ensureRunning: () => void,
stateEditors: ITransactionPrivateApi['stateEditors'],
drafts: ITransactionPrivateApi['drafts'],
): ITransactionPrivateApi {
return {
set: (pointer, value) => {
ensureRunning()
const _value = cloneDeepSerializable(value)
if (typeof _value === 'undefined') return
const {root, path} = getPointerParts(pointer as Pointer<$FixMe>)
if (isSheetObject(root)) {
const sequenceTracksTree = root.template
.getMapOfValidSequenceTracks_forStudio()
.getValue()
const propConfig = getPropConfigByPath(root.template.config, path)
if (!propConfig) {
throw new Error(
`Object ${
root.address.objectKey
} does not have a prop at ${JSON.stringify(path)}`,
)
}
// if (isPropConfigComposite(propConfig)) {
// propConfig.validate(_value)
// } else {
// propConfig.validate(_value)
// }
const setStaticOrKeyframeProp = <T>(
value: T,
propConfig: PropTypeConfig_AllNonCompounds,
path: PathToProp,
) => {
if (typeof value === 'undefined' || value === null) {
return
}
const deserialized = cloneDeepSerializable(
propConfig.deserialize(value),
)
if (typeof deserialized === 'undefined') {
throw new Error(
`Invalid value ${userReadableTypeOfValue(
value,
)} for object.props${path
.map((key) => `[${JSON.stringify(key)}]`)
.join('')} is invalid`,
)
}
const propAddress = {...root.address, pathToProp: path}
const trackId = get(sequenceTracksTree, path) as $FixMe as
| SequenceTrackId
| undefined
if (typeof trackId === 'string') {
const seq = root.sheet.getSequence()
seq.position = seq.closestGridPosition(seq.position)
stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition(
{
...propAddress,
trackId,
position: seq.position,
value: value as $FixMe,
snappingFunction: seq.closestGridPosition,
},
)
} else {
stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.setValueOfPrimitiveProp(
{...propAddress, value: value as $FixMe},
)
}
}
if (propConfig.type === 'compound') {
const pathToTopPointer = getPointerParts(
pointer as $IntentionalAny,
).path
const lengthOfTopPointer = pathToTopPointer.length
// If we are dealing with a compound prop, we recurse through its
// nested properties.
forEachDeepSimplePropOfCompoundProp(
propConfig,
pathToTopPointer,
(primitivePropConfig, pathToProp) => {
const pathToPropInProvidedValue =
pathToProp.slice(lengthOfTopPointer)
const v = getDeep(_value, pathToPropInProvidedValue)
if (typeof v !== 'undefined') {
setStaticOrKeyframeProp(v, primitivePropConfig, pathToProp)
} else {
throw new Error(
`Property object.props${pathToProp
.map((key) => `[${JSON.stringify(key)}]`)
.join('')} is required but not provided`,
)
}
},
)
} else if (propConfig.type === 'enum') {
throw new Error(`Enums aren't implemented yet`)
} else {
setStaticOrKeyframeProp(_value, propConfig, path)
}
} else {
throw new Error(
'Only setting props of SheetObject-s is supported in a transaction so far',
)
}
},
unset: (pointer) => {
ensureRunning()
const {root, path} = getPointerParts(pointer as Pointer<$FixMe>)
if (isSheetObject(root)) {
const sequenceTracksTree = root.template
.getMapOfValidSequenceTracks_forStudio()
.getValue()
const defaultValue = getDeep(
root.template.getDefaultValues().getValue(),
path,
)
const propConfig = getPropConfigByPath(
root.template.config,
path,
) as PropTypeConfig
const unsetStaticOrKeyframeProp = <T>(value: T, path: PathToProp) => {
const propAddress = {...root.address, pathToProp: path}
const trackId = get(sequenceTracksTree, path) as $FixMe as
| SequenceTrackId
| undefined
if (typeof trackId === 'string') {
stateEditors.coreByProject.historic.sheetsById.sequence.unsetKeyframeAtPosition(
{
...propAddress,
trackId,
position: root.sheet.getSequence().positionSnappedToGrid,
},
)
} else if (propConfig !== undefined) {
stateEditors.coreByProject.historic.sheetsById.staticOverrides.byObject.unsetValueOfPrimitiveProp(
propAddress,
)
}
}
if (propConfig.type === 'compound') {
forEachDeep(
defaultValue,
(v, pathToProp) => {
unsetStaticOrKeyframeProp(v, pathToProp)
},
getPointerParts(pointer as Pointer<$IntentionalAny>).path,
)
} else {
unsetStaticOrKeyframeProp(defaultValue, path)
}
} else {
throw new Error(
'Only setting props of SheetObject-s is supported in a transaction so far',
)
}
},
get drafts() {
ensureRunning()
return drafts
},
get stateEditors() {
return stateEditors
},
}
}

View file

@ -13,6 +13,7 @@ import React from 'react'
import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import 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)
}

View file

@ -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)

View file

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

View file

@ -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]

View file

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