All prop sequencing (#48)

This commit is contained in:
cory-glooh 2021-11-02 13:50:08 +00:00 committed by GitHub
parent 52f65af689
commit 4a65c6e91c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 280 additions and 97 deletions

View file

@ -3,14 +3,27 @@ import type {UseDragOpts} from './useDrag'
import useDrag from './useDrag' import useDrag from './useDrag'
import React, {useLayoutEffect, useMemo, useState} from 'react' import React, {useLayoutEffect, useMemo, useState} from 'react'
import type {IProject, ISheet} from '@theatre/core' import type {IProject, ISheet} from '@theatre/core'
import {onChange} from '@theatre/core' import {onChange, types} from '@theatre/core'
import type {IScrub, IStudio} from '@theatre/studio' import type {IScrub, IStudio} from '@theatre/studio'
studio.initialize() studio.initialize({usePersistentStorage: false})
const textInterpolate = (left: string, right: string, progression: number) => {
if (!left || right.startsWith(left)) {
const length = Math.floor(
Math.max(0, (right.length - left.length) * progression),
)
return left + right.slice(left.length, left.length + length)
}
return left
}
const boxObjectConfig = { const boxObjectConfig = {
x: 0, test: types.string('Hello?', {interpolate: textInterpolate}),
y: 0, testLiteral: types.stringLiteral('a', {a: 'Option A', b: 'Option B'}),
bool: types.boolean(false),
x: types.number(200),
y: types.number(200),
} }
const Box: React.FC<{ const Box: React.FC<{
@ -23,11 +36,17 @@ const Box: React.FC<{
const isSelected = selection.includes(obj) const isSelected = selection.includes(obj)
const [pos, setPos] = useState<{x: number; y: number}>({x: 0, y: 0}) const [state, setState] = useState<{
x: number
y: number
test: string
testLiteral: string
bool: boolean
}>(obj.value)
useLayoutEffect(() => { useLayoutEffect(() => {
const unsubscribeFromChanges = onChange(obj.props, (newValues) => { const unsubscribeFromChanges = onChange(obj.props, (newValues) => {
setPos(newValues) setState(newValues)
}) })
return unsubscribeFromChanges return unsubscribeFromChanges
}, [id]) }, [id])
@ -50,7 +69,13 @@ const Box: React.FC<{
firstOnDragCalled = true firstOnDragCalled = true
} }
scrub!.capture(({set}) => { scrub!.capture(({set}) => {
set(obj.props, {x: x + initial.x, y: y + initial.y}) set(obj.props, {
x: x + initial.x,
y: y + initial.y,
test: initial.test,
testLiteral: initial.testLiteral,
bool: initial.bool,
})
}) })
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
@ -73,16 +98,20 @@ const Box: React.FC<{
}} }}
ref={setDivRef} ref={setDivRef}
style={{ style={{
width: 100, width: 300,
height: 100, height: 300,
background: 'gray', color: 'white',
position: 'absolute', position: 'absolute',
left: pos.x + 'px', left: state.x + 'px',
top: pos.y + 'px', top: state.y + 'px',
boxSizing: 'border-box', boxSizing: 'border-box',
border: isSelected ? '1px solid #5a92fa' : '1px solid transparent', border: isSelected ? '1px solid #5a92fa' : '1px solid white',
}} }}
></div> >
<pre style={{margin: 0, padding: '1rem'}}>
{JSON.stringify(state, null, 4)}
</pre>
</div>
) )
} }
@ -109,7 +138,7 @@ export const Scene: React.FC<{project: IProject}> = ({project}) => {
right: '0', right: '0',
top: 0, top: 0,
bottom: '0', bottom: '0',
background: 'black', background: '#333',
}} }}
> >
<button <button

View file

@ -39,7 +39,7 @@ export type TrackData = BasicKeyframedTrack
export type Keyframe = { export type Keyframe = {
id: KeyframeId id: KeyframeId
value: number value: unknown
position: number position: number
handles: [leftX: number, leftY: number, rightX: number, rightY: number] handles: [leftX: number, leftY: number, rightX: number, rightY: number]
connectedRight: boolean connectedRight: boolean

View file

@ -8,9 +8,9 @@ import type {
import {sanitizeCompoundProps} from './internals' import {sanitizeCompoundProps} from './internals'
import {propTypeSymbol} from './internals' import {propTypeSymbol} from './internals'
const validateCommonOpts = ( const validateCommonOpts = <T>(
fnCallSignature: string, fnCallSignature: string,
opts?: PropTypeConfigOpts, opts?: PropTypeConfigOpts<T>,
) => { ) => {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
if (opts === undefined) return if (opts === undefined) return
@ -70,9 +70,10 @@ const validateCommonOpts = (
*/ */
export const compound = <Props extends IShorthandCompoundProps>( export const compound = <Props extends IShorthandCompoundProps>(
props: Props, props: Props,
opts?: PropTypeConfigOpts, opts?: PropTypeConfigOpts<Props>,
): PropTypeConfig_Compound< ): PropTypeConfig_Compound<
ShorthandCompoundPropsToLonghandCompoundProps<Props> ShorthandCompoundPropsToLonghandCompoundProps<Props>,
Props
> => { > => {
validateCommonOpts('t.compound(props, opts)', opts) validateCommonOpts('t.compound(props, opts)', opts)
return { return {
@ -81,6 +82,9 @@ export const compound = <Props extends IShorthandCompoundProps>(
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
sanitize: opts?.sanitize,
interpolate: opts?.interpolate,
isScalar: false,
} }
} }
@ -131,7 +135,7 @@ export const number = (
nudgeFn?: PropTypeConfig_Number['nudgeFn'] nudgeFn?: PropTypeConfig_Number['nudgeFn']
range?: PropTypeConfig_Number['range'] range?: PropTypeConfig_Number['range']
nudgeMultiplier?: number nudgeMultiplier?: number
} & PropTypeConfigOpts, } & PropTypeConfigOpts<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)
@ -202,6 +206,15 @@ export const number = (
nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn, nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn,
nudgeMultiplier: nudgeMultiplier:
typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1, typeof opts?.nudgeMultiplier === 'number' ? opts.nudgeMultiplier : 1,
isScalar: true,
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)
},
} }
} }
@ -227,7 +240,7 @@ export const number = (
*/ */
export const boolean = ( export const boolean = (
defaultValue: boolean, defaultValue: boolean,
opts?: PropTypeConfigOpts, opts?: PropTypeConfigOpts<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)
@ -245,6 +258,15 @@ export const boolean = (
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
isScalar: false,
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
},
} }
} }
@ -271,7 +293,7 @@ export const boolean = (
*/ */
export const string = ( export const string = (
defaultValue: string, defaultValue: string,
opts?: PropTypeConfigOpts, opts?: PropTypeConfigOpts<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)
@ -289,6 +311,12 @@ export const string = (
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
[propTypeSymbol]: 'TheatrePropType', [propTypeSymbol]: 'TheatrePropType',
label: opts?.label, label: opts?.label,
isScalar: false,
sanitize(value: unknown) {
if (opts?.sanitize) return opts.sanitize(value)
return typeof value === 'string' ? value : undefined
},
interpolate: opts?.interpolate,
} }
} }
@ -326,7 +354,9 @@ 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'} & PropTypeConfigOpts<
Extract<keyof Opts, string>
>,
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> { ): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
return { return {
type: 'stringLiteral', type: 'stringLiteral',
@ -336,13 +366,30 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
valueType: null as $IntentionalAny, valueType: null as $IntentionalAny,
as: opts?.as ?? 'menu', as: opts?.as ?? 'menu',
label: opts?.label, label: opts?.label,
isScalar: false,
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
},
} }
} }
interface IBasePropType<ValueType> { export type Sanitizer<T> = (value: unknown) => T | undefined
export type Interpolator<T> = (left: T, right: T, progression: number) => T
interface IBasePropType<ValueType, PropTypes = ValueType> {
valueType: ValueType valueType: ValueType
[propTypeSymbol]: 'TheatrePropType' [propTypeSymbol]: 'TheatrePropType'
label: string | undefined label: string | undefined
isScalar: boolean
sanitize?: Sanitizer<PropTypes>
interpolate?: Interpolator<PropTypes>
} }
export interface PropTypeConfig_Number extends IBasePropType<number> { export interface PropTypeConfig_Number extends IBasePropType<number> {
@ -381,9 +428,18 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
default: boolean default: boolean
} }
export interface PropTypeConfigOpts { export interface PropTypeConfig_Color<ColorObject>
label?: string extends IBasePropType<ColorObject> {
type: 'color'
default: ColorObject
} }
export interface PropTypeConfigOpts<ValueType> {
label?: string
sanitize?: Sanitizer<ValueType>
interpolate?: Interpolator<ValueType>
}
export interface PropTypeConfig_String extends IBasePropType<string> { export interface PropTypeConfig_String extends IBasePropType<string> {
type: 'string' type: 'string'
default: string default: string
@ -400,8 +456,13 @@ export interface PropTypeConfig_StringLiteral<T extends string>
/** /**
* *
*/ */
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps> export interface PropTypeConfig_Compound<
extends IBasePropType<{[K in keyof Props]: Props[K]['valueType']}> { Props extends IValidCompoundProps,
PropTypes = Props,
> extends IBasePropType<
{[K in keyof Props]: Props[K]['valueType']},
PropTypes
> {
type: 'compound' type: 'compound'
props: Record<string, PropTypeConfig> props: Record<string, PropTypeConfig>
} }

View file

@ -8,10 +8,14 @@ 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 =
| {left: unknown; right?: unknown; progression: number}
| undefined
export default function trackValueAtTime( export default function trackValueAtTime(
trackP: Pointer<TrackData | undefined>, trackP: Pointer<TrackData | undefined>,
timeD: IDerivation<number>, timeD: IDerivation<number>,
): IDerivation<unknown> { ): IDerivation<KeyframeValueAtTime> {
return prism(() => { return prism(() => {
const track = val(trackP) const track = val(trackP)
const driverD = prism.memo( const driverD = prism.memo(
@ -20,7 +24,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_basicKeyframedTrack(track, timeD) return trackValueAtTime_keyframedTrack(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)
@ -37,14 +41,15 @@ type IStartedState = {
started: true started: true
validFrom: number validFrom: number
validTo: number validTo: number
der: IDerivation<unknown> der: IDerivation<KeyframeValueAtTime>
} }
type IState = {started: false} | IStartedState type IState = {started: false} | IStartedState
function trackValueAtTime_basicKeyframedTrack( function trackValueAtTime_keyframedTrack(
track: BasicKeyframedTrack, track: BasicKeyframedTrack,
timeD: IDerivation<number>, timeD: IDerivation<number>,
): IDerivation<unknown> { ): IDerivation<KeyframeValueAtTime> {
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
@ -52,7 +57,7 @@ function trackValueAtTime_basicKeyframedTrack(
const time = timeD.getValue() const time = timeD.getValue()
if (!state.started || time < state.validFrom || state.validTo <= time) { if (!state.started || time < state.validFrom || state.validTo <= time) {
stateRef.current = state = pp(timeD, track) stateRef.current = state = updateState(timeD, track)
} }
return state.der.getValue() return state.der.getValue()
@ -61,7 +66,7 @@ function trackValueAtTime_basicKeyframedTrack(
const undefinedConstD = new ConstantDerivation(undefined) const undefinedConstD = new ConstantDerivation(undefined)
const pp = ( const updateState = (
progressionD: IDerivation<number>, progressionD: IDerivation<number>,
track: BasicKeyframedTrack, track: BasicKeyframedTrack,
): IStartedState => { ): IStartedState => {
@ -138,7 +143,7 @@ const states = {
started: true, started: true,
validFrom: -Infinity, validFrom: -Infinity,
validTo: kf.position, validTo: kf.position,
der: new ConstantDerivation(kf.value), der: new ConstantDerivation({left: kf.value, progression: 0}),
} }
}, },
lastKeyframe(kf: Keyframe): IStartedState { lastKeyframe(kf: Keyframe): IStartedState {
@ -146,7 +151,7 @@ const states = {
started: true, started: true,
validFrom: kf.position, validFrom: kf.position,
validTo: Infinity, validTo: Infinity,
der: new ConstantDerivation(kf.value), der: new ConstantDerivation({left: kf.value, progression: 0}),
} }
}, },
between( between(
@ -159,7 +164,7 @@ const states = {
started: true, started: true,
validFrom: left.position, validFrom: left.position,
validTo: right.position, validTo: right.position,
der: new ConstantDerivation(left.value), der: new ConstantDerivation({left: left.value, progression: 0}),
} }
} }
@ -182,7 +187,11 @@ const states = {
) )
const valueProgression = solver.solveSimple(progression) const valueProgression = solver.solveSimple(progression)
return left.value + valueProgression * (right.value - left.value) return {
left: left.value,
right: right.value,
progression: valueProgression,
}
}) })
return { return {

View file

@ -1,3 +1,4 @@
import type {KeyframeValueAtTime} from '@theatre/core/sequences/trackValueAtTime'
import trackValueAtTime from '@theatre/core/sequences/trackValueAtTime' import trackValueAtTime from '@theatre/core/sequences/trackValueAtTime'
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'
@ -21,6 +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, PropTypeConfig} from '@theatre/core/propTypes'
// type Everything = { // type Everything = {
// final: SerializableMap // final: SerializableMap
@ -153,7 +156,32 @@ export default class SheetObject implements IdentityDerivationProvider {
const derivation = this._trackIdToDerivation(trackId) const derivation = this._trackIdToDerivation(trackId)
const updateSequenceValueFromItsDerivation = () => { const updateSequenceValueFromItsDerivation = () => {
valsAtom.setIn(pathToProp, derivation.getValue()) 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 untap = derivation const untap = derivation
.changesWithoutValues() .changesWithoutValues()
@ -177,12 +205,11 @@ export default class SheetObject implements IdentityDerivationProvider {
protected _trackIdToDerivation( protected _trackIdToDerivation(
trackId: SequenceTrackId, trackId: SequenceTrackId,
): IDerivation<unknown> { ): IDerivation<KeyframeValueAtTime | 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 trackValueAtTime(trackP, timeD)
} }

View file

@ -24,6 +24,10 @@ import set from 'lodash-es/set'
import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject' import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject'
import SheetObject from './SheetObject' import SheetObject from './SheetObject'
import logger from '@theatre/shared/logger' import logger from '@theatre/shared/logger'
import type {
PropTypeConfig,
PropTypeConfig_Compound,
} from '@theatre/core/propTypes'
export type IPropPathToTrackIdTree = { export type IPropPathToTrackIdTree = {
[key in string]?: SequenceTrackId | IPropPathToTrackIdTree [key in string]?: SequenceTrackId | IPropPathToTrackIdTree
@ -32,7 +36,9 @@ export type IPropPathToTrackIdTree = {
export default class SheetObjectTemplate { export default class SheetObjectTemplate {
readonly address: WithoutSheetInstance<SheetObjectAddress> readonly address: WithoutSheetInstance<SheetObjectAddress>
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate' readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
protected _config: Atom<SheetObjectConfig<$IntentionalAny>> protected _config: Atom<
SheetObjectConfig<PropTypeConfig_Compound<$IntentionalAny>>
>
readonly _cache = new SimpleCache() readonly _cache = new SimpleCache()
readonly project: Project readonly project: Project
@ -87,13 +93,14 @@ export default class SheetObjectTemplate {
this.address.sheetId this.address.sheetId
] ]
return ( const value =
val( val(
pointerToSheetState.staticOverrides.byObject[ pointerToSheetState.staticOverrides.byObject[
this.address.objectKey this.address.objectKey
], ],
) || {} ) || {}
)
return value
}), }),
) )
} }
@ -139,8 +146,17 @@ export default class SheetObjectTemplate {
) )
continue continue
} }
const propConfig = get(this.config.props, pathToProp) as
| PropTypeConfig
| undefined
const defaultValue = get(defaults, pathToProp) const defaultValue = get(defaults, pathToProp)
if (typeof defaultValue !== 'number') {
if (
typeof defaultValue !== 'number' &&
(!propConfig?.sanitize || !propConfig.interpolate)
) {
//@checking defaultValue because tests don't provide prop config, and fail if this is omitted.
continue continue
} }

View file

@ -46,7 +46,13 @@ declare module 'propose' {
// export default inspect // export default inspect
// } // }
declare module 'timing-function/lib/UnitBezier' declare module 'timing-function/lib/UnitBezier' {
export default class UnitBezier {
constructor(p1x: numbe, p1y: number, p2x: number, p2y: number)
solve(progression: number, epsilon: number)
solveSimple(progression: number)
}
}
declare module 'clean-webpack-plugin' declare module 'clean-webpack-plugin'
declare module 'webpack-notifier' declare module 'webpack-notifier'
declare module 'case-sensitive-paths-webpack-plugin' declare module 'case-sensitive-paths-webpack-plugin'

View file

@ -29,6 +29,7 @@ import {persistStateOfStudio} from './persistStateOfStudio'
import {isSheetObject} from '@theatre/shared/instanceTypes' import {isSheetObject} from '@theatre/shared/instanceTypes'
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes' import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
import {generateDiskStateRevision} from './generateDiskStateRevision' import {generateDiskStateRevision} from './generateDiskStateRevision'
import type {PropTypeConfig} from '@theatre/core/propTypes'
export type Drafts = { export type Drafts = {
historic: Draft<StudioHistoricState> historic: Draft<StudioHistoricState>
@ -146,6 +147,7 @@ export default class StudioStore {
if (typeof v === 'undefined' || v === null) { if (typeof v === 'undefined' || v === null) {
return return
} }
const propAddress = {...root.address, pathToProp} const propAddress = {...root.address, pathToProp}
const trackId = get( const trackId = get(
@ -154,6 +156,12 @@ export default class StudioStore {
) as $FixMe as SequenceTrackId | undefined ) as $FixMe as SequenceTrackId | undefined
if (typeof trackId === 'string') { if (typeof trackId === 'string') {
const propConfig = get(
root.template.config.props,
pathToProp,
) as PropTypeConfig | undefined
if (propConfig?.sanitize) v = propConfig.sanitize(v)
const seq = root.sheet.getSequence() const seq = root.sheet.getSequence()
seq.position = seq.closestGridPosition(seq.position) seq.position = seq.closestGridPosition(seq.position)
stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition(

View file

@ -16,10 +16,10 @@ export const getPropTypeByPointer = (
pointerToProp: SheetObject['propsP'], pointerToProp: SheetObject['propsP'],
obj: SheetObject, obj: SheetObject,
): PropTypeConfig => { ): PropTypeConfig => {
const rootConf = obj.template.config.props const rootConf = obj.template.config
const p = getPointerParts(pointerToProp).path const p = getPointerParts(pointerToProp).path
let conf: PropTypeConfig = rootConf as PropTypeConfig let conf = rootConf as PropTypeConfig
while (p.length !== 0) { while (p.length !== 0) {
const key = p.shift() const key = p.shift()

View file

@ -248,8 +248,10 @@ export function useEditingToolsForPrimitiveProp<
callback: () => { callback: () => {
getStudio()!.transaction(({stateEditors}) => { getStudio()!.transaction(({stateEditors}) => {
const propAddress = {...obj.address, pathToProp} const propAddress = {...obj.address, pathToProp}
stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsSequenced( stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsSequenced(
propAddress, propAddress,
propConfig,
) )
}) })
}, },
@ -309,5 +311,5 @@ type Shade =
| 'Sequened_NotBeingInterpolated' | 'Sequened_NotBeingInterpolated'
function isPropConfSequencable(conf: PropTypeConfig): boolean { function isPropConfSequencable(conf: PropTypeConfig): boolean {
return conf.type === 'number' return conf.type === 'number' || (!!conf.sanitize && !!conf.interpolate)
} }

View file

@ -52,7 +52,7 @@ const IconContainer = styled.button<{
? graphEditorColors[props.graphEditorColor].iconColor ? graphEditorColors[props.graphEditorColor].iconColor
: nextPrevCursorsTheme.offColor}; : nextPrevCursorsTheme.offColor};
&:hover { &:not([disabled]):hover {
color: white; color: white;
} }
` `
@ -129,6 +129,9 @@ const PrimitivePropRow: React.FC<{
}, [leaf]) }, [leaf])
const label = leaf.pathToProp[leaf.pathToProp.length - 1] const label = leaf.pathToProp[leaf.pathToProp.length - 1]
const isSelectable =
leaf.propConf.type !== 'boolean' && leaf.propConf.type !== 'stringLiteral'
return ( return (
<Container depth={leaf.depth}> <Container depth={leaf.depth}>
<Head <Head
@ -144,6 +147,8 @@ const PrimitivePropRow: React.FC<{
onClick={toggleSelect} onClick={toggleSelect}
isSelected={isSelected === true} isSelected={isSelected === true}
graphEditorColor={possibleColor ?? '1'} graphEditorColor={possibleColor ?? '1'}
style={{opacity: isSelectable ? 1 : 0.25}}
disabled={!isSelectable}
> >
<GraphIcon /> <GraphIcon />
</IconContainer> </IconContainer>

View file

@ -5,7 +5,7 @@ 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 BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack' import KeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
import Row from './Row' import Row from './Row'
const PrimitivePropRow: React.FC<{ const PrimitivePropRow: React.FC<{
@ -30,12 +30,9 @@ const PrimitivePropRow: React.FC<{
return <Row leaf={leaf} node={<div />}></Row> return <Row leaf={leaf} node={<div />}></Row>
} else { } else {
const node = ( const node = (
<BasicKeyframedTrack <KeyframedTrack layoutP={layoutP} trackData={trackData} leaf={leaf} />
layoutP={layoutP}
trackData={trackData}
leaf={leaf}
/>
) )
return <Row leaf={leaf} node={node}></Row> return <Row leaf={leaf} node={node}></Row>
} }
}, [leaf, layoutP]) }, [leaf, layoutP])

View file

@ -115,13 +115,14 @@ function calculateExtremums(keyframes: Keyframe[]): Extremums {
} }
keyframes.forEach((cur, i) => { keyframes.forEach((cur, i) => {
check(cur.value) const curVal = typeof cur.value === 'number' ? cur.value : 0
check(curVal)
if (!cur.connectedRight) return if (!cur.connectedRight) return
const next = keyframes[i + 1] const next = keyframes[i + 1]
if (!next) return if (!next) return
const diff = next.value - cur.value const diff = (typeof next.value === 'number' ? next.value : 1) - curVal
check(cur.value + cur.handles[3] * diff) check(curVal + cur.handles[3] * diff)
check(cur.value + next.handles[1] * diff) check(curVal + next.handles[1] * diff)
}) })
return [min, max] return [min, max]

View file

@ -19,21 +19,16 @@ const Curve: React.FC<IProps> = (props) => {
const cur = trackData.keyframes[index] const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]
const handles = [
cur.handles[2],
cur.handles[3],
next.handles[0],
next.handles[1],
]
const connectorLengthInUnitSpace = next.position - cur.position const connectorLengthInUnitSpace = next.position - cur.position
const [nodeRef, node] = useRefAndState<SVGPathElement | null>(null) const [nodeRef, node] = useRefAndState<SVGPathElement | null>(null)
const [contextMenu] = useConnectorContextMenu(node, props) const [contextMenu] = useConnectorContextMenu(node, props)
const leftYInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value) const curValue = typeof cur.value === 'number' ? cur.value : 0
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(next.value) const nextValue = typeof next.value === 'number' ? next.value : 1
const leftYInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(nextValue)
const heightInExtremumSpace = rightYInExtremumSpace - leftYInExtremumSpace const heightInExtremumSpace = rightYInExtremumSpace - leftYInExtremumSpace

View file

@ -72,20 +72,23 @@ const CurveHandle: React.FC<IProps> = (props) => {
// debugger // debugger
} }
const value = cur.value + (next.value - cur.value) * valInDiffSpace const curValue = typeof cur.value === 'number' ? cur.value : 0
const nextValue = typeof next.value === 'number' ? next.value : 1
const value = curValue + (nextValue - curValue) * valInDiffSpace
const valInExtremumSpace = props.extremumSpace.fromValueSpace(value) const valInExtremumSpace = props.extremumSpace.fromValueSpace(value)
const heightInExtremumSpace = const heightInExtremumSpace =
valInExtremumSpace - valInExtremumSpace -
props.extremumSpace.fromValueSpace( props.extremumSpace.fromValueSpace(
props.which === 'left' ? cur.value : next.value, props.which === 'left' ? curValue : nextValue,
) )
const lineTransform = transformBox( const lineTransform = transformBox(
props.which === 'left' ? cur.position : next.position, props.which === 'left' ? cur.position : next.position,
props.extremumSpace.fromValueSpace( props.extremumSpace.fromValueSpace(
props.which === 'left' ? cur.value : next.value, props.which === 'left' ? curValue : nextValue,
), ),
posInUnitSpace - (props.which === 'left' ? cur.position : next.position), posInUnitSpace - (props.which === 'left' ? cur.position : next.position),
heightInExtremumSpace, heightInExtremumSpace,
@ -165,7 +168,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
const dYInValueSpace = const dYInValueSpace =
propsAtStartOfDrag.extremumSpace.deltaToValueSpace(dYInExtremumSpace) propsAtStartOfDrag.extremumSpace.deltaToValueSpace(dYInExtremumSpace)
const dyInKeyframeDiffSpace = dYInValueSpace / (next.value - cur.value) const curValue = typeof cur.value === 'number' ? cur.value : 0
const nextValue = typeof next.value === 'number' ? next.value : 1
const dyInKeyframeDiffSpace = dYInValueSpace / (nextValue - curValue)
if (propsAtStartOfDrag.which === 'left') { if (propsAtStartOfDrag.which === 'left') {
const handleX = clamp(cur.handles[2] + dPosInKeyframeDiffSpace, 0, 1) const handleX = clamp(cur.handles[2] + dPosInKeyframeDiffSpace, 0, 1)

View file

@ -59,9 +59,12 @@ const Dot: React.FC<IProps> = (props) => {
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]
const [contextMenu] = useKeyframeContextMenu(node, props) const [contextMenu] = useKeyframeContextMenu(node, props)
const isDragging = useDragKeyframe(node, props) const isDragging =
typeof cur.value === 'number' ? useDragKeyframe(node, props) : false
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value) const cyInExtremumSpace = props.extremumSpace.fromValueSpace(
typeof cur.value === 'number' ? cur.value : 0,
)
return ( return (
<> <>
@ -140,7 +143,7 @@ function useDragKeyframe(
const cur: Keyframe = { const cur: Keyframe = {
...original, ...original,
position: original.position + deltaPos, position: original.position + deltaPos,
value: original.value + dYInValueSpace, value: (original.value as number) + dYInValueSpace,
handles: [...original.handles], handles: [...original.handles],
} }
updatedKeyframes.push(cur) updatedKeyframes.push(cur)
@ -149,29 +152,41 @@ function useDragKeyframe(
const prev = const prev =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index - 1] propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index - 1]
if (prev && Math.abs(original.value - prev.value) > 0) { if (
const newPrev: Keyframe = {...prev, handles: [...prev.handles]} prev &&
Math.abs((original.value as number) - (prev.value as number)) > 0
) {
const newPrev: Keyframe = {
...prev,
handles: [...prev.handles],
}
updatedKeyframes.push(newPrev) updatedKeyframes.push(newPrev)
newPrev.handles[3] = preserveRightHandle( newPrev.handles[3] = preserveRightHandle(
prev.handles[3], prev.handles[3],
prev.value, prev.value as number,
prev.value, prev.value as number,
original.value, original.value as number,
cur.value, cur.value as number,
) )
} }
const next = const next =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index + 1] propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index + 1]
if (next && Math.abs(original.value - next.value) > 0) { if (
const newNext: Keyframe = {...next, handles: [...next.handles]} next &&
Math.abs((original.value as number) - (next.value as number)) > 0
) {
const newNext: Keyframe = {
...next,
handles: [...next.handles],
}
updatedKeyframes.push(newNext) updatedKeyframes.push(newNext)
newNext.handles[1] = preserveLeftHandle( newNext.handles[1] = preserveLeftHandle(
newNext.handles[1], newNext.handles[1],
newNext.value, newNext.value as number,
newNext.value, newNext.value as number,
original.value, original.value as number,
cur.value, cur.value as number,
) )
} }
} }

View file

@ -35,7 +35,7 @@ const KeyframeEditor: React.FC<{
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]
const connected = cur.connectedRight && !!next const connected = cur.connectedRight && !!next
const shouldShowCurve = connected && next.value - cur.value !== 0 const shouldShowCurve = connected && next.value !== cur.value
return ( return (
<Container> <Container>

View file

@ -10,6 +10,7 @@ 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 Container = styled.div``
@ -21,7 +22,7 @@ const PrimitivePropGraph: React.FC<{
color: keyof typeof graphEditorColors color: keyof typeof graphEditorColors
}> = (props) => { }> = (props) => {
return usePrism(() => { return usePrism(() => {
const {sheetObject, trackId, pathToProp} = props const {sheetObject, trackId} = props
const trackData = val( const trackData = val(
getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId] getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId]
.sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[ .sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[
@ -35,7 +36,9 @@ const PrimitivePropGraph: React.FC<{
) )
return <></> return <></>
} else { } else {
return <BasicKeyframedTrack {...props} trackData={trackData} /> return (
<BasicKeyframedTrack {...props} trackData={trackData as TrackData} />
)
} }
}, [props.trackId, props.layoutP]) }, [props.trackId, props.layoutP])
} }

View file

@ -46,6 +46,7 @@ import {
} from '@theatre/shared/instanceTypes' } from '@theatre/shared/instanceTypes'
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate' import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
import type SheetObjectTemplate from '@theatre/core/sheetObjects/SheetObjectTemplate' import type SheetObjectTemplate from '@theatre/core/sheetObjects/SheetObjectTemplate'
import type {PropTypeConfig} from '@theatre/core/propTypes'
export const setDrafts__onlyMeantToBeCalledByTransaction = ( export const setDrafts__onlyMeantToBeCalledByTransaction = (
drafts: undefined | Drafts, drafts: undefined | Drafts,
@ -432,6 +433,7 @@ namespace stateEditors {
export function setPrimitivePropAsSequenced( export function setPrimitivePropAsSequenced(
p: WithoutSheetInstance<PropAddress>, p: WithoutSheetInstance<PropAddress>,
config: PropTypeConfig,
) { ) {
const tracks = _ensureTracksOfObject(p) const tracks = _ensureTracksOfObject(p)
const pathEncoded = encodePathToProp(p.pathToProp) const pathEncoded = encodePathToProp(p.pathToProp)
@ -439,6 +441,7 @@ namespace stateEditors {
if (typeof possibleTrackId === 'string') return if (typeof possibleTrackId === 'string') return
const trackId = generateSequenceTrackId() const trackId = generateSequenceTrackId()
tracks.trackData[trackId] = { tracks.trackData[trackId] = {
type: 'BasicKeyframedTrack', type: 'BasicKeyframedTrack',
keyframes: [], keyframes: [],
@ -502,11 +505,11 @@ namespace stateEditors {
* Sets a keyframe at the exact specified position. * Sets a keyframe at the exact specified position.
* Any position snapping should be done by the caller. * Any position snapping should be done by the caller.
*/ */
export function setKeyframeAtPosition( export function setKeyframeAtPosition<T>(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: string
position: number position: number
value: number value: T
snappingFunction: SnappingFunction snappingFunction: SnappingFunction
}, },
) { ) {
@ -608,7 +611,7 @@ namespace stateEditors {
) )
} }
export function replaceKeyframes( export function replaceKeyframes<T>(
p: WithoutSheetInstance<SheetObjectAddress> & { p: WithoutSheetInstance<SheetObjectAddress> & {
trackId: string trackId: string
keyframes: Array<Keyframe> keyframes: Array<Keyframe>
@ -620,7 +623,8 @@ namespace stateEditors {
const initialKeyframes = current(track.keyframes) const initialKeyframes = current(track.keyframes)
const sanitizedKeyframes = p.keyframes const sanitizedKeyframes = p.keyframes
.filter((kf) => { .filter((kf) => {
if (!isFinite(kf.value)) return false if (typeof kf.value === 'number' && !isFinite(kf.value))
return false
if (!kf.handles.every((handleValue) => isFinite(handleValue))) if (!kf.handles.every((handleValue) => isFinite(handleValue)))
return false return false