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 userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import {mapValues} from 'lodash-es'
import type {
IShorthandCompoundProps,
IValidCompoundProps,
@ -8,10 +9,7 @@ import type {
import {sanitizeCompoundProps} from './internals'
import {propTypeSymbol} from './internals'
const validateCommonOpts = <T>(
fnCallSignature: string,
opts?: PropTypeConfigOpts<T>,
) => {
const validateCommonOpts = (fnCallSignature: string, opts?: CommonOpts) => {
if (process.env.NODE_ENV !== 'production') {
if (opts === undefined) return
if (typeof opts !== 'object' || opts === null) {
@ -70,20 +68,22 @@ const validateCommonOpts = <T>(
*/
export const compound = <Props extends IShorthandCompoundProps>(
props: Props,
opts?: PropTypeConfigOpts<Props>,
opts?: {
label?: string
},
): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props>,
Props
> => {
validateCommonOpts('t.compound(props, opts)', opts)
const sanitizedProps = sanitizeCompoundProps(props)
return {
type: 'compound',
props: sanitizeCompoundProps(props),
props: sanitizedProps,
valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType',
label: opts?.label,
sanitize: opts?.sanitize,
interpolate: opts?.interpolate,
default: mapValues(sanitizedProps, (p) => p.default) as $IntentionalAny,
}
}
@ -134,7 +134,10 @@ export const number = (
nudgeFn?: PropTypeConfig_Number['nudgeFn']
range?: PropTypeConfig_Number['range']
nudgeMultiplier?: number
} & PropTypeConfigOpts<number>,
label?: string
sanitize?: Sanitizer<number>
interpolate?: Interpolator<number>
},
): PropTypeConfig_Number => {
if (process.env.NODE_ENV !== 'production') {
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 {
type: 'number',
valueType: 0,
@ -206,17 +218,22 @@ export const number = (
nudgeMultiplier:
typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1,
isScalar: true,
sanitize(value) {
if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'number' ? value : undefined
},
interpolate(left, right, progression) {
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left + progression * (right - left)
},
sanitize: sanitize,
interpolate: opts?.interpolate ?? _interpolateNumber,
}
}
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
*
@ -239,7 +256,11 @@ export const number = (
*/
export const boolean = (
defaultValue: boolean,
opts?: PropTypeConfigOpts<boolean>,
opts?: {
label?: string
sanitize?: Sanitizer<boolean>
interpolate?: Interpolator<boolean>
},
): PropTypeConfig_Boolean => {
if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.boolean(defaultValue, opts)', opts)
@ -251,23 +272,26 @@ export const boolean = (
)
}
}
return {
type: 'boolean',
default: defaultValue,
valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType',
label: opts?.label,
sanitize(value: unknown) {
if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'boolean' ? value : undefined
},
interpolate(left, right, progression) {
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left
},
sanitize: _sanitizeBoolean,
interpolate: leftInterpolate,
}
}
const _sanitizeBoolean = (val: unknown): boolean | undefined => {
return typeof val === 'boolean' ? val : undefined
}
function leftInterpolate<T>(left: T): T {
return left
}
/**
* A string prop type
*
@ -291,7 +315,11 @@ export const boolean = (
*/
export const string = (
defaultValue: string,
opts?: PropTypeConfigOpts<string>,
opts?: {
label?: string
sanitize?: Sanitizer<string>
interpolate?: Interpolator<string>
},
): PropTypeConfig_String => {
if (process.env.NODE_ENV !== 'production') {
validateCommonOpts('t.string(defaultValue, opts)', opts)
@ -313,7 +341,7 @@ export const string = (
if (opts?.sanitize) return opts.sanitize(value)
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?: 'menu' | 'switch'} & PropTypeConfigOpts<
Extract<keyof Opts, string>
>,
opts?: {as?: 'menu' | 'switch'; label?: string},
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return {
type: 'stringLiteral',
@ -364,15 +390,14 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
as: opts?.as ?? 'menu',
label: opts?.label,
sanitize(value: unknown) {
if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'string' && Object.keys(options).includes(value)
? (value as Extract<keyof Opts, string>)
: undefined
},
interpolate(left, right, progression) {
if (opts?.interpolate) return opts.interpolate(left, right, progression)
return left
if (typeof value !== 'string') return undefined
if (Object.hasOwnProperty.call(options, value)) {
return value as $IntentionalAny
} else {
return undefined
}
},
interpolate: leftInterpolate,
}
}
@ -386,6 +411,7 @@ interface IBasePropType<ValueType, PropTypes = ValueType> {
isScalar?: true
sanitize?: Sanitizer<PropTypes>
interpolate?: Interpolator<PropTypes>
default: ValueType
}
export interface PropTypeConfig_Number extends IBasePropType<number> {
@ -424,16 +450,8 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
default: boolean
}
export interface PropTypeConfig_Color<ColorObject>
extends IBasePropType<ColorObject> {
type: 'color'
default: ColorObject
}
export interface PropTypeConfigOpts<ValueType> {
interface CommonOpts {
label?: string
sanitize?: Sanitizer<ValueType>
interpolate?: Interpolator<ValueType>
}
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 UnitBezier from 'timing-function/lib/UnitBezier'
export type KeyframeValueAtTime =
| {left: unknown; right?: unknown; progression: number}
| undefined
export type InterpolationTriple = {
left: unknown
right?: unknown
progression: number
}
export default function trackValueAtTime(
export default function interpolationTripleAtPosition(
trackP: Pointer<TrackData | undefined>,
timeD: IDerivation<number>,
): IDerivation<KeyframeValueAtTime> {
): IDerivation<InterpolationTriple | undefined> {
return prism(() => {
const track = val(trackP)
const driverD = prism.memo(
@ -24,7 +26,7 @@ export default function trackValueAtTime(
if (!track) {
return new ConstantDerivation(undefined)
} else if (track.type === 'BasicKeyframedTrack') {
return trackValueAtTime_keyframedTrack(track, timeD)
return _forKeyframedTrack(track, timeD)
} else {
logger.error(`Track type not yet supported.`)
return new ConstantDerivation(undefined)
@ -41,15 +43,15 @@ type IStartedState = {
started: true
validFrom: number
validTo: number
der: IDerivation<KeyframeValueAtTime>
der: IDerivation<InterpolationTriple | undefined>
}
type IState = {started: false} | IStartedState
function trackValueAtTime_keyframedTrack(
function _forKeyframedTrack(
track: BasicKeyframedTrack,
timeD: IDerivation<number>,
): IDerivation<KeyframeValueAtTime> {
): IDerivation<InterpolationTriple | undefined> {
return prism(() => {
let stateRef = prism.ref<IState>('state', {started: false})
let state = stateRef.current

View file

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

View file

@ -129,8 +129,7 @@ const PrimitivePropRow: React.FC<{
}, [leaf])
const label = leaf.pathToProp[leaf.pathToProp.length - 1]
const isSelectable =
leaf.propConf.type !== 'boolean' && leaf.propConf.type !== 'stringLiteral'
const isSelectable = true
return (
<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 {Pointer} from '@theatre/dataverse'
import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
const Container = styled.div``
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
export type ExtremumSpace = {
fromValueSpace: (v: number) => number
@ -29,83 +27,93 @@ const BasicKeyframedTrack: React.FC<{
trackId: SequenceTrackId
trackData: TrackData
color: keyof typeof graphEditorColors
}> = React.memo(({layoutP, trackData, sheetObject, trackId, color}) => {
const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false)
const lockExtremums = useMemo(() => {
const locks = new Set<VoidFn>()
return function lockExtremums() {
const shouldLock = locks.size === 0
locks.add(unlock)
if (shouldLock) setAreExtremumsLocked(true)
}> = React.memo(
({layoutP, trackData, sheetObject, trackId, color, pathToProp}) => {
const propConfig = getPropConfigByPath(
sheetObject.template.config,
pathToProp,
)!
function unlock() {
const wasLocked = locks.size > 0
locks.delete(unlock)
if (wasLocked && locks.size === 0) setAreExtremumsLocked(false)
const [areExtremumsLocked, setAreExtremumsLocked] = useState<boolean>(false)
const lockExtremums = useMemo(() => {
const locks = new Set<VoidFn>()
return function lockExtremums() {
const shouldLock = locks.size === 0
locks.add(unlock)
if (shouldLock) setAreExtremumsLocked(true)
function unlock() {
const wasLocked = locks.size > 0
locks.delete(unlock)
if (wasLocked && locks.size === 0) setAreExtremumsLocked(false)
}
return unlock
}
}, [])
return unlock
const extremumSpace: ExtremumSpace = useMemo(() => {
const extremums =
propConfig.isScalar === true
? calculateScalarExtremums(trackData.keyframes)
: calculateNonScalarExtremums(trackData.keyframes)
const fromValueSpace = (val: number): number =>
(val - extremums[0]) / (extremums[1] - extremums[0])
const toValueSpace = (ex: number): number =>
extremums[0] + deltaToValueSpace(ex)
const deltaToValueSpace = (ex: number): number =>
ex * (extremums[1] - extremums[0])
return {
fromValueSpace,
toValueSpace,
deltaToValueSpace,
lock: lockExtremums,
}
}, [trackData.keyframes])
const cachedExtremumSpace = useRef<ExtremumSpace>(
undefined as $IntentionalAny,
)
if (!areExtremumsLocked) {
cachedExtremumSpace.current = extremumSpace
}
}, [])
const extremumSpace: ExtremumSpace = useMemo(() => {
const extremums = calculateExtremums(trackData.keyframes)
const keyframeEditors = trackData.keyframes.map((kf, index) => (
<KeyframeEditor
keyframe={kf}
index={index}
trackData={trackData}
layoutP={layoutP}
sheetObject={sheetObject}
trackId={trackId}
key={'keyframe-' + kf.id}
extremumSpace={cachedExtremumSpace.current}
color={color}
/>
))
const fromValueSpace = (val: number): number =>
(val - extremums[0]) / (extremums[1] - extremums[0])
const toValueSpace = (ex: number): number =>
extremums[0] + deltaToValueSpace(ex)
const deltaToValueSpace = (ex: number): number =>
ex * (extremums[1] - extremums[0])
return {
fromValueSpace,
toValueSpace,
deltaToValueSpace,
lock: lockExtremums,
}
}, [trackData.keyframes])
const cachedExtremumSpace = useRef<ExtremumSpace>(
undefined as $IntentionalAny,
)
if (!areExtremumsLocked) {
cachedExtremumSpace.current = extremumSpace
}
const keyframeEditors = trackData.keyframes.map((kf, index) => (
<KeyframeEditor
keyframe={kf}
index={index}
trackData={trackData}
layoutP={layoutP}
sheetObject={sheetObject}
trackId={trackId}
key={'keyframe-' + kf.id}
extremumSpace={cachedExtremumSpace.current}
color={color}
/>
))
return (
<g
style={{
// @ts-ignore
'--main-color': graphEditorColors[color].iconColor,
}}
>
{keyframeEditors}
</g>
)
})
return (
<g
style={{
// @ts-ignore
'--main-color': graphEditorColors[color].iconColor,
}}
>
{keyframeEditors}
</g>
)
},
)
export default BasicKeyframedTrack
type Extremums = [min: number, max: number]
function calculateExtremums(keyframes: Keyframe[]): Extremums {
function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
let min = Infinity,
max = -Infinity
@ -127,3 +135,23 @@ function calculateExtremums(keyframes: Keyframe[]): Extremums {
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 {val} from '@theatre/dataverse'
import React from 'react'
import styled from 'styled-components'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
import type {graphEditorColors} from './GraphEditor'
import type {TrackData} from '@theatre/core/projects/store/types/SheetState_Historic'
const Container = styled.div``
const PrimitivePropGraph: React.FC<{
layoutP: Pointer<SequenceEditorPanelLayout>
@ -36,9 +32,7 @@ const PrimitivePropGraph: React.FC<{
)
return <></>
} else {
return (
<BasicKeyframedTrack {...props} trackData={trackData as TrackData} />
)
return <BasicKeyframedTrack {...props} trackData={trackData} />
}
}, [props.trackId, props.layoutP])
}