diff --git a/theatre/core/src/propTypes/index.ts b/theatre/core/src/propTypes/index.ts index d5b220f..4840e67 100644 --- a/theatre/core/src/propTypes/index.ts +++ b/theatre/core/src/propTypes/index.ts @@ -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 = ( - fnCallSignature: string, - opts?: PropTypeConfigOpts, -) => { +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 = ( */ export const compound = ( props: Props, - opts?: PropTypeConfigOpts, + opts?: { + label?: string + }, ): PropTypeConfig_Compound< ShorthandCompoundPropsToLonghandCompoundProps, 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, + label?: string + sanitize?: Sanitizer + interpolate?: Interpolator + }, ): 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, + opts?: { + label?: string + sanitize?: Sanitizer + interpolate?: Interpolator + }, ): 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(left: T): T { + return left +} + /** * A string prop type * @@ -291,7 +315,11 @@ export const boolean = ( */ export const string = ( defaultValue: string, - opts?: PropTypeConfigOpts, + opts?: { + label?: string + sanitize?: Sanitizer + interpolate?: Interpolator + }, ): 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.as Determines if editor is shown as a menu or a switch. Either 'menu' or 'switch'. Default: 'menu' */ - opts?: {as?: 'menu' | 'switch'} & PropTypeConfigOpts< - Extract - >, + opts?: {as?: 'menu' | 'switch'; label?: string}, ): PropTypeConfig_StringLiteral> { return { type: 'stringLiteral', @@ -364,15 +390,14 @@ export function stringLiteral( 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) - : 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 { isScalar?: true sanitize?: Sanitizer interpolate?: Interpolator + default: ValueType } export interface PropTypeConfig_Number extends IBasePropType { @@ -424,16 +450,8 @@ export interface PropTypeConfig_Boolean extends IBasePropType { default: boolean } -export interface PropTypeConfig_Color - extends IBasePropType { - type: 'color' - default: ColorObject -} - -export interface PropTypeConfigOpts { +interface CommonOpts { label?: string - sanitize?: Sanitizer - interpolate?: Interpolator } export interface PropTypeConfig_String extends IBasePropType { diff --git a/theatre/core/src/sequences/trackValueAtTime.ts b/theatre/core/src/sequences/interpolationTripleAtPosition.ts similarity index 93% rename from theatre/core/src/sequences/trackValueAtTime.ts rename to theatre/core/src/sequences/interpolationTripleAtPosition.ts index 85c880a..0d29b25 100644 --- a/theatre/core/src/sequences/trackValueAtTime.ts +++ b/theatre/core/src/sequences/interpolationTripleAtPosition.ts @@ -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, timeD: IDerivation, -): IDerivation { +): IDerivation { 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 + der: IDerivation } type IState = {started: false} | IStartedState -function trackValueAtTime_keyframedTrack( +function _forKeyframedTrack( track: BasicKeyframedTrack, timeD: IDerivation, -): IDerivation { +): IDerivation { return prism(() => { let stateRef = prism.ref('state', {started: false}) let state = stateRef.current diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 3c8de43..08d2bad 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -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 - 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 { + ): IDerivation { 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> { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx index 2b1df84..3e7eff5 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PrimitivePropRow.tsx @@ -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 ( diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx index d8dc299..eee9563 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -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(false) - const lockExtremums = useMemo(() => { - const locks = new Set() - 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(false) + const lockExtremums = useMemo(() => { + const locks = new Set() + 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( + undefined as $IntentionalAny, + ) + if (!areExtremumsLocked) { + cachedExtremumSpace.current = extremumSpace } - }, []) - const extremumSpace: ExtremumSpace = useMemo(() => { - const extremums = calculateExtremums(trackData.keyframes) + const keyframeEditors = trackData.keyframes.map((kf, index) => ( + + )) - 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( - undefined as $IntentionalAny, - ) - if (!areExtremumsLocked) { - cachedExtremumSpace.current = extremumSpace - } - - const keyframeEditors = trackData.keyframes.map((kf, index) => ( - - )) - - return ( - - {keyframeEditors} - - ) -}) + return ( + + {keyframeEditors} + + ) + }, +) 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] +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/PrimitivePropGraph.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/PrimitivePropGraph.tsx index c2e39ca..fead2b8 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/PrimitivePropGraph.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/PrimitivePropGraph.tsx @@ -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 @@ -36,9 +32,7 @@ const PrimitivePropGraph: React.FC<{ ) return <> } else { - return ( - - ) + return } }, [props.trackId, props.layoutP]) }