All prop sequencing (#48)
This commit is contained in:
parent
52f65af689
commit
4a65c6e91c
19 changed files with 280 additions and 97 deletions
|
@ -3,14 +3,27 @@ import type {UseDragOpts} from './useDrag'
|
|||
import useDrag from './useDrag'
|
||||
import React, {useLayoutEffect, useMemo, useState} from 'react'
|
||||
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'
|
||||
|
||||
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 = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
test: types.string('Hello?', {interpolate: textInterpolate}),
|
||||
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<{
|
||||
|
@ -23,11 +36,17 @@ const Box: React.FC<{
|
|||
|
||||
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(() => {
|
||||
const unsubscribeFromChanges = onChange(obj.props, (newValues) => {
|
||||
setPos(newValues)
|
||||
setState(newValues)
|
||||
})
|
||||
return unsubscribeFromChanges
|
||||
}, [id])
|
||||
|
@ -50,7 +69,13 @@ const Box: React.FC<{
|
|||
firstOnDragCalled = true
|
||||
}
|
||||
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) {
|
||||
|
@ -73,16 +98,20 @@ const Box: React.FC<{
|
|||
}}
|
||||
ref={setDivRef}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
background: 'gray',
|
||||
width: 300,
|
||||
height: 300,
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
left: pos.x + 'px',
|
||||
top: pos.y + 'px',
|
||||
left: state.x + 'px',
|
||||
top: state.y + 'px',
|
||||
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',
|
||||
top: 0,
|
||||
bottom: '0',
|
||||
background: 'black',
|
||||
background: '#333',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
|
|
|
@ -39,7 +39,7 @@ export type TrackData = BasicKeyframedTrack
|
|||
|
||||
export type Keyframe = {
|
||||
id: KeyframeId
|
||||
value: number
|
||||
value: unknown
|
||||
position: number
|
||||
handles: [leftX: number, leftY: number, rightX: number, rightY: number]
|
||||
connectedRight: boolean
|
||||
|
|
|
@ -8,9 +8,9 @@ import type {
|
|||
import {sanitizeCompoundProps} from './internals'
|
||||
import {propTypeSymbol} from './internals'
|
||||
|
||||
const validateCommonOpts = (
|
||||
const validateCommonOpts = <T>(
|
||||
fnCallSignature: string,
|
||||
opts?: PropTypeConfigOpts,
|
||||
opts?: PropTypeConfigOpts<T>,
|
||||
) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (opts === undefined) return
|
||||
|
@ -70,9 +70,10 @@ const validateCommonOpts = (
|
|||
*/
|
||||
export const compound = <Props extends IShorthandCompoundProps>(
|
||||
props: Props,
|
||||
opts?: PropTypeConfigOpts,
|
||||
opts?: PropTypeConfigOpts<Props>,
|
||||
): PropTypeConfig_Compound<
|
||||
ShorthandCompoundPropsToLonghandCompoundProps<Props>
|
||||
ShorthandCompoundPropsToLonghandCompoundProps<Props>,
|
||||
Props
|
||||
> => {
|
||||
validateCommonOpts('t.compound(props, opts)', opts)
|
||||
return {
|
||||
|
@ -81,6 +82,9 @@ export const compound = <Props extends IShorthandCompoundProps>(
|
|||
valueType: null as $IntentionalAny,
|
||||
[propTypeSymbol]: 'TheatrePropType',
|
||||
label: opts?.label,
|
||||
sanitize: opts?.sanitize,
|
||||
interpolate: opts?.interpolate,
|
||||
isScalar: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,7 +135,7 @@ export const number = (
|
|||
nudgeFn?: PropTypeConfig_Number['nudgeFn']
|
||||
range?: PropTypeConfig_Number['range']
|
||||
nudgeMultiplier?: number
|
||||
} & PropTypeConfigOpts,
|
||||
} & PropTypeConfigOpts<number>,
|
||||
): PropTypeConfig_Number => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
validateCommonOpts('t.number(defaultValue, opts)', opts)
|
||||
|
@ -202,6 +206,15 @@ export const number = (
|
|||
nudgeFn: opts?.nudgeFn ?? defaultNumberNudgeFn,
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +240,7 @@ export const number = (
|
|||
*/
|
||||
export const boolean = (
|
||||
defaultValue: boolean,
|
||||
opts?: PropTypeConfigOpts,
|
||||
opts?: PropTypeConfigOpts<boolean>,
|
||||
): PropTypeConfig_Boolean => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
validateCommonOpts('t.boolean(defaultValue, opts)', opts)
|
||||
|
@ -245,6 +258,15 @@ export const boolean = (
|
|||
valueType: null as $IntentionalAny,
|
||||
[propTypeSymbol]: 'TheatrePropType',
|
||||
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 = (
|
||||
defaultValue: string,
|
||||
opts?: PropTypeConfigOpts,
|
||||
opts?: PropTypeConfigOpts<string>,
|
||||
): PropTypeConfig_String => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
validateCommonOpts('t.string(defaultValue, opts)', opts)
|
||||
|
@ -289,6 +311,12 @@ export const string = (
|
|||
valueType: null as $IntentionalAny,
|
||||
[propTypeSymbol]: 'TheatrePropType',
|
||||
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?: 'menu' | 'switch'} & PropTypeConfigOpts,
|
||||
opts?: {as?: 'menu' | 'switch'} & PropTypeConfigOpts<
|
||||
Extract<keyof Opts, string>
|
||||
>,
|
||||
): PropTypeConfig_StringLiteral<Extract<keyof Opts, string>> {
|
||||
return {
|
||||
type: 'stringLiteral',
|
||||
|
@ -336,13 +366,30 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
|
|||
valueType: null as $IntentionalAny,
|
||||
as: opts?.as ?? 'menu',
|
||||
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
|
||||
[propTypeSymbol]: 'TheatrePropType'
|
||||
label: string | undefined
|
||||
isScalar: boolean
|
||||
sanitize?: Sanitizer<PropTypes>
|
||||
interpolate?: Interpolator<PropTypes>
|
||||
}
|
||||
|
||||
export interface PropTypeConfig_Number extends IBasePropType<number> {
|
||||
|
@ -381,9 +428,18 @@ export interface PropTypeConfig_Boolean extends IBasePropType<boolean> {
|
|||
default: boolean
|
||||
}
|
||||
|
||||
export interface PropTypeConfigOpts {
|
||||
label?: string
|
||||
export interface PropTypeConfig_Color<ColorObject>
|
||||
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> {
|
||||
type: 'string'
|
||||
default: string
|
||||
|
@ -400,8 +456,13 @@ export interface PropTypeConfig_StringLiteral<T extends string>
|
|||
/**
|
||||
*
|
||||
*/
|
||||
export interface PropTypeConfig_Compound<Props extends IValidCompoundProps>
|
||||
extends IBasePropType<{[K in keyof Props]: Props[K]['valueType']}> {
|
||||
export interface PropTypeConfig_Compound<
|
||||
Props extends IValidCompoundProps,
|
||||
PropTypes = Props,
|
||||
> extends IBasePropType<
|
||||
{[K in keyof Props]: Props[K]['valueType']},
|
||||
PropTypes
|
||||
> {
|
||||
type: 'compound'
|
||||
props: Record<string, PropTypeConfig>
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ 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 default function trackValueAtTime(
|
||||
trackP: Pointer<TrackData | undefined>,
|
||||
timeD: IDerivation<number>,
|
||||
): IDerivation<unknown> {
|
||||
): IDerivation<KeyframeValueAtTime> {
|
||||
return prism(() => {
|
||||
const track = val(trackP)
|
||||
const driverD = prism.memo(
|
||||
|
@ -20,7 +24,7 @@ export default function trackValueAtTime(
|
|||
if (!track) {
|
||||
return new ConstantDerivation(undefined)
|
||||
} else if (track.type === 'BasicKeyframedTrack') {
|
||||
return trackValueAtTime_basicKeyframedTrack(track, timeD)
|
||||
return trackValueAtTime_keyframedTrack(track, timeD)
|
||||
} else {
|
||||
logger.error(`Track type not yet supported.`)
|
||||
return new ConstantDerivation(undefined)
|
||||
|
@ -37,14 +41,15 @@ type IStartedState = {
|
|||
started: true
|
||||
validFrom: number
|
||||
validTo: number
|
||||
der: IDerivation<unknown>
|
||||
der: IDerivation<KeyframeValueAtTime>
|
||||
}
|
||||
|
||||
type IState = {started: false} | IStartedState
|
||||
|
||||
function trackValueAtTime_basicKeyframedTrack(
|
||||
function trackValueAtTime_keyframedTrack(
|
||||
track: BasicKeyframedTrack,
|
||||
timeD: IDerivation<number>,
|
||||
): IDerivation<unknown> {
|
||||
): IDerivation<KeyframeValueAtTime> {
|
||||
return prism(() => {
|
||||
let stateRef = prism.ref<IState>('state', {started: false})
|
||||
let state = stateRef.current
|
||||
|
@ -52,7 +57,7 @@ function trackValueAtTime_basicKeyframedTrack(
|
|||
const time = timeD.getValue()
|
||||
|
||||
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()
|
||||
|
@ -61,7 +66,7 @@ function trackValueAtTime_basicKeyframedTrack(
|
|||
|
||||
const undefinedConstD = new ConstantDerivation(undefined)
|
||||
|
||||
const pp = (
|
||||
const updateState = (
|
||||
progressionD: IDerivation<number>,
|
||||
track: BasicKeyframedTrack,
|
||||
): IStartedState => {
|
||||
|
@ -138,7 +143,7 @@ const states = {
|
|||
started: true,
|
||||
validFrom: -Infinity,
|
||||
validTo: kf.position,
|
||||
der: new ConstantDerivation(kf.value),
|
||||
der: new ConstantDerivation({left: kf.value, progression: 0}),
|
||||
}
|
||||
},
|
||||
lastKeyframe(kf: Keyframe): IStartedState {
|
||||
|
@ -146,7 +151,7 @@ const states = {
|
|||
started: true,
|
||||
validFrom: kf.position,
|
||||
validTo: Infinity,
|
||||
der: new ConstantDerivation(kf.value),
|
||||
der: new ConstantDerivation({left: kf.value, progression: 0}),
|
||||
}
|
||||
},
|
||||
between(
|
||||
|
@ -159,7 +164,7 @@ const states = {
|
|||
started: true,
|
||||
validFrom: left.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)
|
||||
return left.value + valueProgression * (right.value - left.value)
|
||||
return {
|
||||
left: left.value,
|
||||
right: right.value,
|
||||
progression: valueProgression,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type {KeyframeValueAtTime} from '@theatre/core/sequences/trackValueAtTime'
|
||||
import trackValueAtTime from '@theatre/core/sequences/trackValueAtTime'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||
|
@ -21,6 +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'
|
||||
|
||||
// type Everything = {
|
||||
// final: SerializableMap
|
||||
|
@ -153,7 +156,32 @@ export default class SheetObject implements IdentityDerivationProvider {
|
|||
const derivation = this._trackIdToDerivation(trackId)
|
||||
|
||||
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
|
||||
.changesWithoutValues()
|
||||
|
@ -177,12 +205,11 @@ export default class SheetObject implements IdentityDerivationProvider {
|
|||
|
||||
protected _trackIdToDerivation(
|
||||
trackId: SequenceTrackId,
|
||||
): IDerivation<unknown> {
|
||||
): IDerivation<KeyframeValueAtTime | 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@ import set from 'lodash-es/set'
|
|||
import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject'
|
||||
import SheetObject from './SheetObject'
|
||||
import logger from '@theatre/shared/logger'
|
||||
import type {
|
||||
PropTypeConfig,
|
||||
PropTypeConfig_Compound,
|
||||
} from '@theatre/core/propTypes'
|
||||
|
||||
export type IPropPathToTrackIdTree = {
|
||||
[key in string]?: SequenceTrackId | IPropPathToTrackIdTree
|
||||
|
@ -32,7 +36,9 @@ export type IPropPathToTrackIdTree = {
|
|||
export default class SheetObjectTemplate {
|
||||
readonly address: WithoutSheetInstance<SheetObjectAddress>
|
||||
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
|
||||
protected _config: Atom<SheetObjectConfig<$IntentionalAny>>
|
||||
protected _config: Atom<
|
||||
SheetObjectConfig<PropTypeConfig_Compound<$IntentionalAny>>
|
||||
>
|
||||
readonly _cache = new SimpleCache()
|
||||
readonly project: Project
|
||||
|
||||
|
@ -87,13 +93,14 @@ export default class SheetObjectTemplate {
|
|||
this.address.sheetId
|
||||
]
|
||||
|
||||
return (
|
||||
const value =
|
||||
val(
|
||||
pointerToSheetState.staticOverrides.byObject[
|
||||
this.address.objectKey
|
||||
],
|
||||
) || {}
|
||||
)
|
||||
|
||||
return value
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -139,8 +146,17 @@ export default class SheetObjectTemplate {
|
|||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const propConfig = get(this.config.props, pathToProp) as
|
||||
| PropTypeConfig
|
||||
| undefined
|
||||
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
|
||||
}
|
||||
|
||||
|
|
8
theatre/globals.d.ts
vendored
8
theatre/globals.d.ts
vendored
|
@ -46,7 +46,13 @@ declare module 'propose' {
|
|||
// 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 'webpack-notifier'
|
||||
declare module 'case-sensitive-paths-webpack-plugin'
|
||||
|
|
|
@ -29,6 +29,7 @@ import {persistStateOfStudio} from './persistStateOfStudio'
|
|||
import {isSheetObject} from '@theatre/shared/instanceTypes'
|
||||
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
|
||||
import {generateDiskStateRevision} from './generateDiskStateRevision'
|
||||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
|
||||
export type Drafts = {
|
||||
historic: Draft<StudioHistoricState>
|
||||
|
@ -146,6 +147,7 @@ export default class StudioStore {
|
|||
if (typeof v === 'undefined' || v === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const propAddress = {...root.address, pathToProp}
|
||||
|
||||
const trackId = get(
|
||||
|
@ -154,6 +156,12 @@ export default class StudioStore {
|
|||
) as $FixMe as SequenceTrackId | undefined
|
||||
|
||||
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()
|
||||
seq.position = seq.closestGridPosition(seq.position)
|
||||
stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition(
|
||||
|
|
|
@ -16,10 +16,10 @@ export const getPropTypeByPointer = (
|
|||
pointerToProp: SheetObject['propsP'],
|
||||
obj: SheetObject,
|
||||
): PropTypeConfig => {
|
||||
const rootConf = obj.template.config.props
|
||||
const rootConf = obj.template.config
|
||||
|
||||
const p = getPointerParts(pointerToProp).path
|
||||
let conf: PropTypeConfig = rootConf as PropTypeConfig
|
||||
let conf = rootConf as PropTypeConfig
|
||||
|
||||
while (p.length !== 0) {
|
||||
const key = p.shift()
|
||||
|
|
|
@ -248,8 +248,10 @@ export function useEditingToolsForPrimitiveProp<
|
|||
callback: () => {
|
||||
getStudio()!.transaction(({stateEditors}) => {
|
||||
const propAddress = {...obj.address, pathToProp}
|
||||
|
||||
stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsSequenced(
|
||||
propAddress,
|
||||
propConfig,
|
||||
)
|
||||
})
|
||||
},
|
||||
|
@ -309,5 +311,5 @@ type Shade =
|
|||
| 'Sequened_NotBeingInterpolated'
|
||||
|
||||
function isPropConfSequencable(conf: PropTypeConfig): boolean {
|
||||
return conf.type === 'number'
|
||||
return conf.type === 'number' || (!!conf.sanitize && !!conf.interpolate)
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ const IconContainer = styled.button<{
|
|||
? graphEditorColors[props.graphEditorColor].iconColor
|
||||
: nextPrevCursorsTheme.offColor};
|
||||
|
||||
&:hover {
|
||||
&:not([disabled]):hover {
|
||||
color: white;
|
||||
}
|
||||
`
|
||||
|
@ -129,6 +129,9 @@ const PrimitivePropRow: React.FC<{
|
|||
}, [leaf])
|
||||
|
||||
const label = leaf.pathToProp[leaf.pathToProp.length - 1]
|
||||
const isSelectable =
|
||||
leaf.propConf.type !== 'boolean' && leaf.propConf.type !== 'stringLiteral'
|
||||
|
||||
return (
|
||||
<Container depth={leaf.depth}>
|
||||
<Head
|
||||
|
@ -144,6 +147,8 @@ const PrimitivePropRow: React.FC<{
|
|||
onClick={toggleSelect}
|
||||
isSelected={isSelected === true}
|
||||
graphEditorColor={possibleColor ?? '1'}
|
||||
style={{opacity: isSelectable ? 1 : 0.25}}
|
||||
disabled={!isSelectable}
|
||||
>
|
||||
<GraphIcon />
|
||||
</IconContainer>
|
||||
|
|
|
@ -5,7 +5,7 @@ import {usePrism} from '@theatre/react'
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
|
||||
import KeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
|
||||
import Row from './Row'
|
||||
|
||||
const PrimitivePropRow: React.FC<{
|
||||
|
@ -30,12 +30,9 @@ const PrimitivePropRow: React.FC<{
|
|||
return <Row leaf={leaf} node={<div />}></Row>
|
||||
} else {
|
||||
const node = (
|
||||
<BasicKeyframedTrack
|
||||
layoutP={layoutP}
|
||||
trackData={trackData}
|
||||
leaf={leaf}
|
||||
/>
|
||||
<KeyframedTrack layoutP={layoutP} trackData={trackData} leaf={leaf} />
|
||||
)
|
||||
|
||||
return <Row leaf={leaf} node={node}></Row>
|
||||
}
|
||||
}, [leaf, layoutP])
|
||||
|
|
|
@ -115,13 +115,14 @@ function calculateExtremums(keyframes: Keyframe[]): Extremums {
|
|||
}
|
||||
|
||||
keyframes.forEach((cur, i) => {
|
||||
check(cur.value)
|
||||
const curVal = typeof cur.value === 'number' ? cur.value : 0
|
||||
check(curVal)
|
||||
if (!cur.connectedRight) return
|
||||
const next = keyframes[i + 1]
|
||||
if (!next) return
|
||||
const diff = next.value - cur.value
|
||||
check(cur.value + cur.handles[3] * diff)
|
||||
check(cur.value + next.handles[1] * diff)
|
||||
const diff = (typeof next.value === 'number' ? next.value : 1) - curVal
|
||||
check(curVal + cur.handles[3] * diff)
|
||||
check(curVal + next.handles[1] * diff)
|
||||
})
|
||||
|
||||
return [min, max]
|
||||
|
|
|
@ -19,21 +19,16 @@ const Curve: React.FC<IProps> = (props) => {
|
|||
const cur = trackData.keyframes[index]
|
||||
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 [nodeRef, node] = useRefAndState<SVGPathElement | null>(null)
|
||||
|
||||
const [contextMenu] = useConnectorContextMenu(node, props)
|
||||
|
||||
const leftYInExtremumSpace = props.extremumSpace.fromValueSpace(cur.value)
|
||||
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(next.value)
|
||||
const curValue = typeof cur.value === 'number' ? cur.value : 0
|
||||
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
|
||||
|
||||
|
|
|
@ -72,20 +72,23 @@ const CurveHandle: React.FC<IProps> = (props) => {
|
|||
// 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 heightInExtremumSpace =
|
||||
valInExtremumSpace -
|
||||
props.extremumSpace.fromValueSpace(
|
||||
props.which === 'left' ? cur.value : next.value,
|
||||
props.which === 'left' ? curValue : nextValue,
|
||||
)
|
||||
|
||||
const lineTransform = transformBox(
|
||||
props.which === 'left' ? cur.position : next.position,
|
||||
props.extremumSpace.fromValueSpace(
|
||||
props.which === 'left' ? cur.value : next.value,
|
||||
props.which === 'left' ? curValue : nextValue,
|
||||
),
|
||||
posInUnitSpace - (props.which === 'left' ? cur.position : next.position),
|
||||
heightInExtremumSpace,
|
||||
|
@ -165,7 +168,9 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
|
|||
const dYInValueSpace =
|
||||
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') {
|
||||
const handleX = clamp(cur.handles[2] + dPosInKeyframeDiffSpace, 0, 1)
|
||||
|
|
|
@ -59,9 +59,12 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
const next = trackData.keyframes[index + 1]
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
@ -140,7 +143,7 @@ function useDragKeyframe(
|
|||
const cur: Keyframe = {
|
||||
...original,
|
||||
position: original.position + deltaPos,
|
||||
value: original.value + dYInValueSpace,
|
||||
value: (original.value as number) + dYInValueSpace,
|
||||
handles: [...original.handles],
|
||||
}
|
||||
updatedKeyframes.push(cur)
|
||||
|
@ -149,29 +152,41 @@ function useDragKeyframe(
|
|||
const prev =
|
||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index - 1]
|
||||
|
||||
if (prev && Math.abs(original.value - prev.value) > 0) {
|
||||
const newPrev: Keyframe = {...prev, handles: [...prev.handles]}
|
||||
if (
|
||||
prev &&
|
||||
Math.abs((original.value as number) - (prev.value as number)) > 0
|
||||
) {
|
||||
const newPrev: Keyframe = {
|
||||
...prev,
|
||||
handles: [...prev.handles],
|
||||
}
|
||||
updatedKeyframes.push(newPrev)
|
||||
newPrev.handles[3] = preserveRightHandle(
|
||||
prev.handles[3],
|
||||
prev.value,
|
||||
prev.value,
|
||||
original.value,
|
||||
cur.value,
|
||||
prev.value as number,
|
||||
prev.value as number,
|
||||
original.value as number,
|
||||
cur.value as number,
|
||||
)
|
||||
}
|
||||
const next =
|
||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index + 1]
|
||||
|
||||
if (next && Math.abs(original.value - next.value) > 0) {
|
||||
const newNext: Keyframe = {...next, handles: [...next.handles]}
|
||||
if (
|
||||
next &&
|
||||
Math.abs((original.value as number) - (next.value as number)) > 0
|
||||
) {
|
||||
const newNext: Keyframe = {
|
||||
...next,
|
||||
handles: [...next.handles],
|
||||
}
|
||||
updatedKeyframes.push(newNext)
|
||||
newNext.handles[1] = preserveLeftHandle(
|
||||
newNext.handles[1],
|
||||
newNext.value,
|
||||
newNext.value,
|
||||
original.value,
|
||||
cur.value,
|
||||
newNext.value as number,
|
||||
newNext.value as number,
|
||||
original.value as number,
|
||||
cur.value as number,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ const KeyframeEditor: React.FC<{
|
|||
const next = trackData.keyframes[index + 1]
|
||||
|
||||
const connected = cur.connectedRight && !!next
|
||||
const shouldShowCurve = connected && next.value - cur.value !== 0
|
||||
const shouldShowCurve = connected && next.value !== cur.value
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
|
@ -10,6 +10,7 @@ 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``
|
||||
|
||||
|
@ -21,7 +22,7 @@ const PrimitivePropGraph: React.FC<{
|
|||
color: keyof typeof graphEditorColors
|
||||
}> = (props) => {
|
||||
return usePrism(() => {
|
||||
const {sheetObject, trackId, pathToProp} = props
|
||||
const {sheetObject, trackId} = props
|
||||
const trackData = val(
|
||||
getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId]
|
||||
.sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[
|
||||
|
@ -35,7 +36,9 @@ const PrimitivePropGraph: React.FC<{
|
|||
)
|
||||
return <></>
|
||||
} else {
|
||||
return <BasicKeyframedTrack {...props} trackData={trackData} />
|
||||
return (
|
||||
<BasicKeyframedTrack {...props} trackData={trackData as TrackData} />
|
||||
)
|
||||
}
|
||||
}, [props.trackId, props.layoutP])
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
} from '@theatre/shared/instanceTypes'
|
||||
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
||||
import type SheetObjectTemplate from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
||||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
|
||||
export const setDrafts__onlyMeantToBeCalledByTransaction = (
|
||||
drafts: undefined | Drafts,
|
||||
|
@ -432,6 +433,7 @@ namespace stateEditors {
|
|||
|
||||
export function setPrimitivePropAsSequenced(
|
||||
p: WithoutSheetInstance<PropAddress>,
|
||||
config: PropTypeConfig,
|
||||
) {
|
||||
const tracks = _ensureTracksOfObject(p)
|
||||
const pathEncoded = encodePathToProp(p.pathToProp)
|
||||
|
@ -439,6 +441,7 @@ namespace stateEditors {
|
|||
if (typeof possibleTrackId === 'string') return
|
||||
|
||||
const trackId = generateSequenceTrackId()
|
||||
|
||||
tracks.trackData[trackId] = {
|
||||
type: 'BasicKeyframedTrack',
|
||||
keyframes: [],
|
||||
|
@ -502,11 +505,11 @@ namespace stateEditors {
|
|||
* Sets a keyframe at the exact specified position.
|
||||
* Any position snapping should be done by the caller.
|
||||
*/
|
||||
export function setKeyframeAtPosition(
|
||||
export function setKeyframeAtPosition<T>(
|
||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
trackId: string
|
||||
position: number
|
||||
value: number
|
||||
value: T
|
||||
snappingFunction: SnappingFunction
|
||||
},
|
||||
) {
|
||||
|
@ -608,7 +611,7 @@ namespace stateEditors {
|
|||
)
|
||||
}
|
||||
|
||||
export function replaceKeyframes(
|
||||
export function replaceKeyframes<T>(
|
||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
trackId: string
|
||||
keyframes: Array<Keyframe>
|
||||
|
@ -620,7 +623,8 @@ namespace stateEditors {
|
|||
const initialKeyframes = current(track.keyframes)
|
||||
const sanitizedKeyframes = p.keyframes
|
||||
.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)))
|
||||
return false
|
||||
|
||||
|
|
Loading…
Reference in a new issue