WIP: refactor sequencing non-scalars

This commit is contained in:
Aria Minaei 2021-11-14 20:03:16 +01:00
parent 67b8a708dc
commit 4ec6dd1181
6 changed files with 208 additions and 167 deletions

View file

@ -1,5 +1,6 @@
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import {mapValues} from 'lodash-es'
import type { import type {
IShorthandCompoundProps, IShorthandCompoundProps,
IValidCompoundProps, IValidCompoundProps,
@ -8,10 +9,7 @@ import type {
import {sanitizeCompoundProps} from './internals' import {sanitizeCompoundProps} from './internals'
import {propTypeSymbol} from './internals' import {propTypeSymbol} from './internals'
const validateCommonOpts = <T>( const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => {
fnCallSignature: string,
opts?: PropTypeConfigOpts<T>,
) => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (opts === undefined) return if (opts === undefined) return
if (typeof opts !== 'object' || opts === null) { if (typeof opts !== 'object' || opts === null) {
@ -70,20 +68,22 @@ const validateCommonOpts = <T>(
*/ */
export const compound = <Props extends IShorthandCompoundProps>( export const compound = <Props extends IShorthandCompoundProps>(
props: Props, props: Props,
opts?: PropTypeConfigOpts<Props>, opts?: {
label?: string
},
): PropTypeConfig_Compound< ): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props>, ShorthandCompoundPropsToLonghandCompoundProps<Props>,
Props Props
> => { > => {
validateCommonOpts('t.compound(props, opts)', opts) validateCommonOpts('t.compound(props, opts)', opts)
const sanitizedProps = sanitizeCompoundProps(props)
return { return {
type: 'compound', type: 'compound',
props: sanitizeCompoundProps(props), props: sanitizedProps,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize: opts?.sanitize, default: mapValues(sanitizedProps, (p) => p.default) as $IntentionalAny,
interpolate: opts?.interpolate,
} }
} }
@ -134,7 +134,10 @@ export const number = (
nudgeFn?: PropTypeConfig_Number['nudgeFn'] nudgeFn?: PropTypeConfig_Number['nudgeFn']
range?: PropTypeConfig_Number['range'] range?: PropTypeConfig_Number['range']
nudgeMultiplier?: number nudgeMultiplier?: number
} & PropTypeConfigOpts<number>, label?: string
sanitize?: Sanitizer<number>
interpolate?: Interpolator<number>
},
): PropTypeConfig_Number => { ): PropTypeConfig_Number => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.number(defaultValue, opts)', opts) validateCommonOpts('t.number(defaultValue, opts)', opts)
@ -195,6 +198,15 @@ 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',
valueType: 0, valueType: 0,
@ -206,17 +218,22 @@ export const number = (
nudgeMultiplier: nudgeMultiplier:
typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1, typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1,
isScalar: true, isScalar: true,
sanitize(value) { sanitize: sanitize,
if (opts?.sanitize) return opts.sanitize(value) interpolate: opts?.interpolate ?? _interpolateNumber,
return typeof value === 'number' ? value : undefined
},
interpolate(left, right, progression) {
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left + progression * (right - left)
},
} }
} }
const _sanitizeNumber = (value: unknown): undefined | number =>
typeof value === 'number' && isFinite(value) ? value : undefined
const _interpolateNumber = (
left: number,
right: number,
progression: number,
): number => {
return left + progression * (right - left)
}
/** /**
* A boolean prop type * A boolean prop type
* *
@ -239,7 +256,11 @@ export const number = (
*/ */
export const boolean = ( export const boolean = (
defaultValue: boolean, defaultValue: boolean,
opts?: PropTypeConfigOpts<boolean>, opts?: {
label?: string
sanitize?: Sanitizer<boolean>
interpolate?: Interpolator<boolean>
},
): PropTypeConfig_Boolean => { ): PropTypeConfig_Boolean => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.boolean(defaultValue, opts)', opts) validateCommonOpts('t.boolean(defaultValue, opts)', opts)
@ -251,23 +272,26 @@ export const boolean = (
) )
} }
} }
return { return {
type: 'boolean', type: 'boolean',
default: defaultValue, default: defaultValue,
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize(value: unknown) { sanitize: _sanitizeBoolean,
if (opts?.sanitize) return opts.sanitize(value) interpolate: leftInterpolate,
return typeof value === 'boolean' ? value : undefined
},
interpolate(left, right, progression) {
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left
},
} }
} }
const _sanitizeBoolean = (val: unknown): boolean | undefined => {
return typeof val === 'boolean' ? val : undefined
}
function leftInterpolate<T>(left: T): T {
return left
}
/** /**
* A string prop type * A string prop type
* *
@ -291,7 +315,11 @@ export const boolean = (
*/ */
export const string = ( export const string = (
defaultValue: string, defaultValue: string,
opts?: PropTypeConfigOpts<string>, opts?: {
label?: string
sanitize?: Sanitizer<string>
interpolate?: Interpolator<string>
},
): PropTypeConfig_String => { ): PropTypeConfig_String => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.string(defaultValue, opts)', opts) validateCommonOpts('t.string(defaultValue, opts)', opts)
@ -313,7 +341,7 @@ export const string = (
if (opts?.sanitize) return opts.sanitize(value) if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'string' ? value : undefined return typeof value === 'string' ? value : undefined
}, },
interpolate: opts?.interpolate, interpolate: opts?.interpolate ?? leftInterpolate,
} }
} }
@ -351,9 +379,7 @@ 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'} & PropTypeConfigOpts< opts?: {as?: 'menu' | 'switch'; label?: string},
Extract<keyof Opts, string>
>,
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> { ): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return { return {
type: 'stringLiteral', type: 'stringLiteral',
@ -364,15 +390,14 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
as: opts?.as ?? 'menu', as: opts?.as ?? 'menu',
label: opts?.label, label: opts?.label,
sanitize(value: unknown) { sanitize(value: unknown) {
if (opts?.sanitize) return opts.sanitize(value) if (typeof value !== 'string') return undefined
return typeof value === 'string' && Object.keys(options).includes(value) if (Object.hasOwnProperty.call(options, value)) {
? (value as Extract<keyof Opts, string>) return value as $IntentionalAny
: undefined } else {
}, return undefined
interpolate(left, right, progression) { }
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left
}, },
interpolate: leftInterpolate,
} }
} }
@ -386,6 +411,7 @@ interface IBasePropType<ValueType, PropTypes = ValueType> {
isScalar?: true isScalar?: true
sanitize?: Sanitizer<PropTypes> sanitize?: Sanitizer<PropTypes>
interpolate?: Interpolator<PropTypes> interpolate?: Interpolator<PropTypes>
default: ValueType
} }
export interface PropTypeConfig_Number extends IBasePropType<number> { export interface PropTypeConfig_Number extends IBasePropType<number> {
@ -424,16 +450,8 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
default: boolean default: boolean
} }
export interface PropTypeConfig_Color<ColorObject> interface CommonOpts {
extends IBasePropType<ColorObject> {
type: 'color'
default: ColorObject
}
export interface PropTypeConfigOpts<ValueType> {
label?: string label?: string
sanitize?: Sanitizer<ValueType>
interpolate?: Interpolator<ValueType>
} }
export interface PropTypeConfig_String extends IBasePropType<string> { export interface PropTypeConfig_String extends IBasePropType<string> {

View file

@ -8,14 +8,16 @@ import {ConstantDerivation, prism, val} from '@theatre/dataverse'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
import UnitBezier from 'timing-function/lib/UnitBezier' import UnitBezier from 'timing-function/lib/UnitBezier'
export type KeyframeValueAtTime = export type InterpolationTriple = {
| {left: unknown; right?: unknown; progression: number} left: unknown
| undefined right?: unknown
progression: number
}
export default function trackValueAtTime( export default function interpolationTripleAtPosition(
trackP: Pointer<TrackData | undefined>, trackP: Pointer<TrackData | undefined>,
timeD: IDerivation<number>, timeD: IDerivation<number>,
): IDerivation<KeyframeValueAtTime> { ): IDerivation<InterpolationTriple | undefined> {
return prism(() => { return prism(() => {
const track = val(trackP) const track = val(trackP)
const driverD = prism.memo( const driverD = prism.memo(
@ -24,7 +26,7 @@ export default function trackValueAtTime(
if (!track) { if (!track) {
return new ConstantDerivation(undefined) return new ConstantDerivation(undefined)
} else if (track.type === 'BasicKeyframedTrack') { } else if (track.type === 'BasicKeyframedTrack') {
return trackValueAtTime_keyframedTrack(track, timeD) return _forKeyframedTrack(track, timeD)
} else { } else {
logger.error(`Track type not yet supported.`) logger.error(`Track type not yet supported.`)
return new ConstantDerivation(undefined) return new ConstantDerivation(undefined)
@ -41,15 +43,15 @@ type IStartedState = {
started: true started: true
validFrom: number validFrom: number
validTo: number validTo: number
der: IDerivation<KeyframeValueAtTime> der: IDerivation<InterpolationTriple | undefined>
} }
type IState = {started: false} | IStartedState type IState = {started: false} | IStartedState
function trackValueAtTime_keyframedTrack( function _forKeyframedTrack(
track: BasicKeyframedTrack, track: BasicKeyframedTrack,
timeD: IDerivation<number>, timeD: IDerivation<number>,
): IDerivation<KeyframeValueAtTime> { ): IDerivation<InterpolationTriple | undefined> {
return prism(() => { return prism(() => {
let stateRef = prism.ref<IState>('state', {started: false}) let stateRef = prism.ref<IState>('state', {started: false})
let state = stateRef.current let state = stateRef.current

View file

@ -1,5 +1,5 @@
import type {KeyframeValueAtTime} from '@theatre/core/sequences/trackValueAtTime' import type {InterpolationTriple} from '@theatre/core/sequences/interpolationTripleAtPosition'
import trackValueAtTime from '@theatre/core/sequences/trackValueAtTime' import interpolationTripleAtPosition from '@theatre/core/sequences/interpolationTripleAtPosition'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses' import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
import deepMergeWithCache from '@theatre/shared/utils/deepMergeWithCache' import deepMergeWithCache from '@theatre/shared/utils/deepMergeWithCache'
@ -22,8 +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 {get} from 'lodash-es' import type {Interpolator} from '@theatre/core/propTypes'
import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes' import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
// type Everything = { // type Everything = {
// final: SerializableMap // final: SerializableMap
@ -154,35 +154,33 @@ export default class SheetObject implements IdentityDerivationProvider {
for (const {trackId, pathToProp} of tracksToProcess) { for (const {trackId, pathToProp} of tracksToProcess) {
const derivation = this._trackIdToDerivation(trackId) const derivation = this._trackIdToDerivation(trackId)
const propConfig = getPropConfigByPath(
this.template.config,
pathToProp,
)!
const sanitize = propConfig.sanitize!
const interpolate =
propConfig.interpolate! as Interpolator<$IntentionalAny>
const updateSequenceValueFromItsDerivation = () => { const updateSequenceValueFromItsDerivation = () => {
const propConfig = get(this.template.config.props, pathToProp) as const triple = derivation.getValue()
| PropTypeConfig
| undefined if (!triple)
const value: KeyframeValueAtTime = derivation.getValue() return valsAtom.setIn(pathToProp, propConfig!.default)
if (!value) return valsAtom.setIn(pathToProp, value)
if (value.right === undefined) const left = sanitize(triple.left) || propConfig.default
return valsAtom.setIn(pathToProp, value.left)
if (propConfig?.interpolate) { if (triple.right === undefined)
const interpolate = return valsAtom.setIn(pathToProp, left)
propConfig.interpolate as Interpolator<unknown>
const right = sanitize(triple.right) || propConfig.default
return valsAtom.setIn( return valsAtom.setIn(
pathToProp, pathToProp,
interpolate(value.left, value.right, value.progression), interpolate(left, right, triple.progression),
) )
} }
if (
typeof value.left === 'number' &&
typeof value.right === 'number'
) {
//@Because tests don't provide prop config, and fail if this is omitted.
return valsAtom.setIn(
pathToProp,
value.left + value.progression * (value.right - value.left),
)
}
valsAtom.setIn(pathToProp, value.left)
}
const untap = derivation const untap = derivation
.changesWithoutValues() .changesWithoutValues()
.tap(updateSequenceValueFromItsDerivation) .tap(updateSequenceValueFromItsDerivation)
@ -205,12 +203,14 @@ export default class SheetObject implements IdentityDerivationProvider {
protected _trackIdToDerivation( protected _trackIdToDerivation(
trackId: SequenceTrackId, trackId: SequenceTrackId,
): IDerivation<KeyframeValueAtTime | undefined> { ): IDerivation<InterpolationTriple | undefined> {
const trackP = const trackP =
this.template.project.pointers.historic.sheetsById[this.address.sheetId] this.template.project.pointers.historic.sheetsById[this.address.sheetId]
.sequence.tracksByObject[this.address.objectKey].trackData[trackId] .sequence.tracksByObject[this.address.objectKey].trackData[trackId]
const timeD = this.sheet.getSequence().positionDerivation const timeD = this.sheet.getSequence().positionDerivation
return trackValueAtTime(trackP, timeD)
return interpolationTripleAtPosition(trackP, timeD)
} }
get propsP(): Pointer<$FixMe> { get propsP(): Pointer<$FixMe> {

View file

@ -129,8 +129,7 @@ const PrimitivePropRow: React.FC<{
}, [leaf]) }, [leaf])
const label = leaf.pathToProp[leaf.pathToProp.length - 1] const label = leaf.pathToProp[leaf.pathToProp.length - 1]
const isSelectable = const isSelectable = true
leaf.propConf.type !== 'boolean' && leaf.propConf.type !== 'stringLiteral'
return ( return (
<Container depth={leaf.depth}> <Container depth={leaf.depth}>

View file

@ -8,12 +8,10 @@ import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components'
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} from '@theatre/shared/propTypes/utils'
const Container = styled.div``
export type ExtremumSpace = { export type ExtremumSpace = {
fromValueSpace: (v: number) => number fromValueSpace: (v: number) => number
@ -29,7 +27,13 @@ const BasicKeyframedTrack: React.FC<{
trackId: SequenceTrackId trackId: SequenceTrackId
trackData: TrackData trackData: TrackData
color: keyof typeof graphEditorColors color: keyof typeof graphEditorColors
}> = React.memo(({layoutP, trackData, sheetObject, trackId, color}) => { }> = React.memo(
({layoutP, trackData, sheetObject, trackId, color, pathToProp}) => {
const propConfig = getPropConfigByPath(
sheetObject.template.config,
pathToProp,
)!
const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false) const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false)
const lockExtremums = useMemo(() => { const lockExtremums = useMemo(() => {
const locks = new Set<VoidFn>() const locks = new Set<VoidFn>()
@ -49,7 +53,10 @@ const BasicKeyframedTrack: React.FC<{
}, []) }, [])
const extremumSpace: ExtremumSpace = useMemo(() => { const extremumSpace: ExtremumSpace = useMemo(() => {
const extremums = calculateExtremums(trackData.keyframes) const extremums =
propConfig.isScalar === true
? calculateScalarExtremums(trackData.keyframes)
: calculateNonScalarExtremums(trackData.keyframes)
const fromValueSpace = (val: number): number => const fromValueSpace = (val: number): number =>
(val - extremums[0]) / (extremums[1] - extremums[0]) (val - extremums[0]) / (extremums[1] - extremums[0])
@ -99,13 +106,14 @@ const BasicKeyframedTrack: React.FC<{
{keyframeEditors} {keyframeEditors}
</g> </g>
) )
}) },
)
export default BasicKeyframedTrack export default BasicKeyframedTrack
type Extremums = [min: number, max: number] type Extremums = [min: number, max: number]
function calculateExtremums(keyframes: Keyframe[]): Extremums { function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
let min = Infinity, let min = Infinity,
max = -Infinity max = -Infinity
@ -127,3 +135,23 @@ function calculateExtremums(keyframes: Keyframe[]): Extremums {
return [min, max] return [min, max]
} }
function calculateNonScalarExtremums(keyframes: Keyframe[]): Extremums {
let min = 0,
max = 1
function check(n: number): void {
min = Math.min(n, min)
max = Math.max(n, max)
}
keyframes.forEach((cur, i) => {
if (!cur.connectedRight) return
const next = keyframes[i + 1]
if (!next) return
check(cur.handles[3])
check(next.handles[1])
})
return [min, max]
}

View file

@ -6,13 +6,9 @@ import {usePrism} from '@theatre/react'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import styled from 'styled-components'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack' import BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
import type {graphEditorColors} from './GraphEditor' import type {graphEditorColors} from './GraphEditor'
import type {TrackData} from '@theatre/core/projects/store/types/SheetState_Historic'
const Container = styled.div``
const PrimitivePropGraph: React.FC<{ const PrimitivePropGraph: React.FC<{
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
@ -36,9 +32,7 @@ const PrimitivePropGraph: React.FC<{
) )
return <></> return <></>
} else { } else {
return ( return <BasicKeyframedTrack {...props} trackData={trackData} />
<BasicKeyframedTrack {...props} trackData={trackData as TrackData} />
)
} }
}, [props.trackId, props.layoutP]) }, [props.trackId, props.layoutP])
} }