Fix keyframe pasting to respect interpolation graph (#107)
This commit is contained in:
parent
ffdebebfff
commit
2bf081478f
10 changed files with 59 additions and 31 deletions
|
@ -506,7 +506,7 @@ export function stringLiteral<Opts extends {[key in string]: string}>(
|
|||
export type Sanitizer<T> = (value: unknown) => T | undefined
|
||||
export type Interpolator<T> = (left: T, right: T, progression: number) => T
|
||||
|
||||
interface IBasePropType<ValueType, PropTypes = ValueType> {
|
||||
export interface IBasePropType<ValueType, PropTypes = ValueType> {
|
||||
valueType: ValueType
|
||||
[propTypeSymbol]: 'TheatrePropType'
|
||||
label: string | undefined
|
||||
|
|
|
@ -23,7 +23,7 @@ import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse'
|
|||
import type SheetObjectTemplate from './SheetObjectTemplate'
|
||||
import TheatreSheetObject from './TheatreSheetObject'
|
||||
import type {Interpolator} from '@theatre/core/propTypes'
|
||||
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
|
||||
import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils'
|
||||
|
||||
// type Everything = {
|
||||
// final: SerializableMap
|
||||
|
@ -159,7 +159,6 @@ export default class SheetObject implements IdentityDerivationProvider {
|
|||
pathToProp,
|
||||
)!
|
||||
|
||||
const sanitize = propConfig.sanitize!
|
||||
const interpolate =
|
||||
propConfig.interpolate! as Interpolator<$IntentionalAny>
|
||||
|
||||
|
@ -169,21 +168,12 @@ export default class SheetObject implements IdentityDerivationProvider {
|
|||
if (!triple)
|
||||
return valsAtom.setIn(pathToProp, propConfig!.default)
|
||||
|
||||
const leftSanitized = sanitize(triple.left)
|
||||
|
||||
const left =
|
||||
typeof leftSanitized === 'undefined'
|
||||
? propConfig.default
|
||||
: leftSanitized
|
||||
const left = valueInProp(triple.left, propConfig)
|
||||
|
||||
if (triple.right === undefined)
|
||||
return valsAtom.setIn(pathToProp, left)
|
||||
|
||||
const rightSanitized = sanitize(triple.right)
|
||||
const right =
|
||||
typeof rightSanitized === 'undefined'
|
||||
? propConfig.default
|
||||
: rightSanitized
|
||||
const right = valueInProp(triple.right, propConfig)
|
||||
|
||||
return valsAtom.setIn(
|
||||
pathToProp,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
IBasePropType,
|
||||
PropTypeConfig,
|
||||
PropTypeConfig_Compound,
|
||||
PropTypeConfig_Enum,
|
||||
|
@ -28,3 +29,24 @@ export function getPropConfigByPath(
|
|||
|
||||
return getPropConfigByPath(sub, rest)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value - An arbitrary value. May be matching the prop's type or not
|
||||
* @param propConfig - The configuration object for a prop
|
||||
* @returns value if it matches the prop's type (or if the prop doesn't have a sanitizer),
|
||||
* otherwise returns the default value for the prop
|
||||
*/
|
||||
export function valueInProp<PropValueType>(
|
||||
value: unknown,
|
||||
propConfig: IBasePropType<PropValueType>,
|
||||
): PropValueType | unknown {
|
||||
const sanitize = propConfig.sanitize
|
||||
if (!sanitize) return value
|
||||
|
||||
const sanitizedVal = sanitize(value)
|
||||
if (typeof sanitizedVal === 'undefined') {
|
||||
return propConfig.default
|
||||
} else {
|
||||
return sanitizedVal
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ function pasteKeyframesContextMenuItem(
|
|||
...props.leaf.sheetObject.address,
|
||||
trackId: props.leaf.trackId,
|
||||
position: sequence.position + keyframe.position - keyframeOffset,
|
||||
handles: keyframe.handles,
|
||||
value: keyframe.value,
|
||||
snappingFunction: sequence.closestGridPosition,
|
||||
},
|
||||
|
|
|
@ -107,17 +107,19 @@ const Dot: React.FC<IProps> = (props) => {
|
|||
export default Dot
|
||||
|
||||
function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
||||
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
|
||||
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
||||
props.selection,
|
||||
)
|
||||
|
||||
const keyframeSelectionItems = maybeKeyframeIds
|
||||
? [copyKeyFrameContextMenuItem(props, maybeKeyframeIds)]
|
||||
: []
|
||||
const keyframeSelectionItem = maybeSelectedKeyframeIds
|
||||
? copyKeyFrameContextMenuItem(props, maybeSelectedKeyframeIds)
|
||||
: copyKeyFrameContextMenuItem(props, [props.keyframe.id])
|
||||
|
||||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
return [...keyframeSelectionItems, deleteItem]
|
||||
return [keyframeSelectionItem, deleteItem]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -263,7 +265,7 @@ function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
|||
|
||||
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) {
|
||||
return {
|
||||
label: 'Copy Keyframes',
|
||||
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
|
||||
callback: () => {
|
||||
const keyframes = keyframeIds.map(
|
||||
(keyframeId) =>
|
||||
|
|
|
@ -11,7 +11,8 @@ import React, {useMemo, useRef, useState} from 'react'
|
|||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
|
||||
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
||||
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
|
||||
import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils'
|
||||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
|
||||
export type ExtremumSpace = {
|
||||
fromValueSpace: (v: number) => number
|
||||
|
@ -55,7 +56,7 @@ const BasicKeyframedTrack: React.FC<{
|
|||
const extremumSpace: ExtremumSpace = useMemo(() => {
|
||||
const extremums =
|
||||
propConfig.isScalar === true
|
||||
? calculateScalarExtremums(trackData.keyframes)
|
||||
? calculateScalarExtremums(trackData.keyframes, propConfig)
|
||||
: calculateNonScalarExtremums(trackData.keyframes)
|
||||
|
||||
const fromValueSpace = (val: number): number =>
|
||||
|
@ -84,6 +85,7 @@ const BasicKeyframedTrack: React.FC<{
|
|||
|
||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||
<KeyframeEditor
|
||||
propConfig={propConfig}
|
||||
keyframe={kf}
|
||||
index={index}
|
||||
trackData={trackData}
|
||||
|
@ -114,7 +116,10 @@ export default BasicKeyframedTrack
|
|||
|
||||
type Extremums = [min: number, max: number]
|
||||
|
||||
function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
|
||||
function calculateScalarExtremums(
|
||||
keyframes: Keyframe[],
|
||||
propConfig: PropTypeConfig,
|
||||
): Extremums {
|
||||
let min = Infinity,
|
||||
max = -Infinity
|
||||
|
||||
|
@ -124,7 +129,7 @@ function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
|
|||
}
|
||||
|
||||
keyframes.forEach((cur, i) => {
|
||||
const curVal = typeof cur.value === 'number' ? cur.value : 0
|
||||
const curVal = valueInProp(cur.value, propConfig) as number
|
||||
check(curVal)
|
||||
if (!cur.connectedRight) return
|
||||
const next = keyframes[i + 1]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {valueInProp} from '@theatre/shared/propTypes/utils'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
|
@ -25,8 +26,12 @@ const Curve: React.FC<IProps> = (props) => {
|
|||
|
||||
const [contextMenu] = useConnectorContextMenu(node, props)
|
||||
|
||||
const curValue = props.isScalar ? (cur.value as number) : 0
|
||||
const nextValue = props.isScalar ? (next.value as number) : 1
|
||||
const curValue = props.isScalar
|
||||
? (valueInProp(cur.value, props.propConfig) as number)
|
||||
: 0
|
||||
const nextValue = props.isScalar
|
||||
? (valueInProp(next.value, props.propConfig) as number)
|
||||
: 1
|
||||
const leftYInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
|
||||
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(nextValue)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import CurveHandle from './CurveHandle'
|
|||
import GraphEditorDotScalar from './GraphEditorDotScalar'
|
||||
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
|
||||
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
|
||||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
|
||||
const Container = styled.g`
|
||||
/* position: absolute; */
|
||||
|
@ -32,6 +33,7 @@ const KeyframeEditor: React.FC<{
|
|||
extremumSpace: ExtremumSpace
|
||||
isScalar: boolean
|
||||
color: keyof typeof graphEditorColors
|
||||
propConfig: PropTypeConfig
|
||||
}> = (props) => {
|
||||
const {index, trackData, isScalar} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
|
|
|
@ -113,9 +113,9 @@ const GraphEditor: React.FC<{
|
|||
>
|
||||
<g
|
||||
style={{
|
||||
transform: `translate(0, ${val(
|
||||
layoutP.graphEditorDims.padding.top,
|
||||
)}px)`,
|
||||
transform: `translate(${val(
|
||||
layoutP.scaledSpace.leftPadding,
|
||||
)}px, ${val(layoutP.graphEditorDims.padding.top)}px)`,
|
||||
}}
|
||||
>
|
||||
{graphs}
|
||||
|
|
|
@ -542,6 +542,7 @@ namespace stateEditors {
|
|||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||
trackId: string
|
||||
position: number
|
||||
handles?: [number, number, number, number]
|
||||
value: T
|
||||
snappingFunction: SnappingFunction
|
||||
},
|
||||
|
@ -567,7 +568,7 @@ namespace stateEditors {
|
|||
id: generateKeyframeId(),
|
||||
position,
|
||||
connectedRight: true,
|
||||
handles: [0.5, 1, 0.5, 0],
|
||||
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||
value: p.value,
|
||||
})
|
||||
return
|
||||
|
@ -577,7 +578,7 @@ namespace stateEditors {
|
|||
id: generateKeyframeId(),
|
||||
position,
|
||||
connectedRight: leftKeyframe.connectedRight,
|
||||
handles: [0.5, 1, 0.5, 0],
|
||||
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||
value: p.value,
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue