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 Sanitizer<T> = (value: unknown) => T | undefined
|
||||||
export type Interpolator<T> = (left: T, right: T, progression: number) => T
|
export type Interpolator<T> = (left: T, right: T, progression: number) => T
|
||||||
|
|
||||||
interface IBasePropType<ValueType, PropTypes = ValueType> {
|
export interface IBasePropType<ValueType, PropTypes = ValueType> {
|
||||||
valueType: ValueType
|
valueType: ValueType
|
||||||
[propTypeSymbol]: 'TheatrePropType'
|
[propTypeSymbol]: 'TheatrePropType'
|
||||||
label: string | undefined
|
label: string | undefined
|
||||||
|
|
|
@ -23,7 +23,7 @@ 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 type {Interpolator} from '@theatre/core/propTypes'
|
import type {Interpolator} from '@theatre/core/propTypes'
|
||||||
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
|
import {getPropConfigByPath, valueInProp} from '@theatre/shared/propTypes/utils'
|
||||||
|
|
||||||
// type Everything = {
|
// type Everything = {
|
||||||
// final: SerializableMap
|
// final: SerializableMap
|
||||||
|
@ -159,7 +159,6 @@ export default class SheetObject implements IdentityDerivationProvider {
|
||||||
pathToProp,
|
pathToProp,
|
||||||
)!
|
)!
|
||||||
|
|
||||||
const sanitize = propConfig.sanitize!
|
|
||||||
const interpolate =
|
const interpolate =
|
||||||
propConfig.interpolate! as Interpolator<$IntentionalAny>
|
propConfig.interpolate! as Interpolator<$IntentionalAny>
|
||||||
|
|
||||||
|
@ -169,21 +168,12 @@ export default class SheetObject implements IdentityDerivationProvider {
|
||||||
if (!triple)
|
if (!triple)
|
||||||
return valsAtom.setIn(pathToProp, propConfig!.default)
|
return valsAtom.setIn(pathToProp, propConfig!.default)
|
||||||
|
|
||||||
const leftSanitized = sanitize(triple.left)
|
const left = valueInProp(triple.left, propConfig)
|
||||||
|
|
||||||
const left =
|
|
||||||
typeof leftSanitized === 'undefined'
|
|
||||||
? propConfig.default
|
|
||||||
: leftSanitized
|
|
||||||
|
|
||||||
if (triple.right === undefined)
|
if (triple.right === undefined)
|
||||||
return valsAtom.setIn(pathToProp, left)
|
return valsAtom.setIn(pathToProp, left)
|
||||||
|
|
||||||
const rightSanitized = sanitize(triple.right)
|
const right = valueInProp(triple.right, propConfig)
|
||||||
const right =
|
|
||||||
typeof rightSanitized === 'undefined'
|
|
||||||
? propConfig.default
|
|
||||||
: rightSanitized
|
|
||||||
|
|
||||||
return valsAtom.setIn(
|
return valsAtom.setIn(
|
||||||
pathToProp,
|
pathToProp,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
IBasePropType,
|
||||||
PropTypeConfig,
|
PropTypeConfig,
|
||||||
PropTypeConfig_Compound,
|
PropTypeConfig_Compound,
|
||||||
PropTypeConfig_Enum,
|
PropTypeConfig_Enum,
|
||||||
|
@ -28,3 +29,24 @@ export function getPropConfigByPath(
|
||||||
|
|
||||||
return getPropConfigByPath(sub, rest)
|
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,
|
...props.leaf.sheetObject.address,
|
||||||
trackId: props.leaf.trackId,
|
trackId: props.leaf.trackId,
|
||||||
position: sequence.position + keyframe.position - keyframeOffset,
|
position: sequence.position + keyframe.position - keyframeOffset,
|
||||||
|
handles: keyframe.handles,
|
||||||
value: keyframe.value,
|
value: keyframe.value,
|
||||||
snappingFunction: sequence.closestGridPosition,
|
snappingFunction: sequence.closestGridPosition,
|
||||||
},
|
},
|
||||||
|
|
|
@ -107,17 +107,19 @@ const Dot: React.FC<IProps> = (props) => {
|
||||||
export default Dot
|
export default Dot
|
||||||
|
|
||||||
function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
||||||
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
|
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
||||||
|
props.selection,
|
||||||
|
)
|
||||||
|
|
||||||
const keyframeSelectionItems = maybeKeyframeIds
|
const keyframeSelectionItem = maybeSelectedKeyframeIds
|
||||||
? [copyKeyFrameContextMenuItem(props, maybeKeyframeIds)]
|
? copyKeyFrameContextMenuItem(props, maybeSelectedKeyframeIds)
|
||||||
: []
|
: copyKeyFrameContextMenuItem(props, [props.keyframe.id])
|
||||||
|
|
||||||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||||
|
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
items: () => {
|
items: () => {
|
||||||
return [...keyframeSelectionItems, deleteItem]
|
return [keyframeSelectionItem, deleteItem]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -263,7 +265,7 @@ function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
||||||
|
|
||||||
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) {
|
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) {
|
||||||
return {
|
return {
|
||||||
label: 'Copy Keyframes',
|
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const keyframes = keyframeIds.map(
|
const keyframes = keyframeIds.map(
|
||||||
(keyframeId) =>
|
(keyframeId) =>
|
||||||
|
|
|
@ -11,7 +11,8 @@ import React, {useMemo, useRef, useState} from 'react'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
|
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
|
||||||
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
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 = {
|
export type ExtremumSpace = {
|
||||||
fromValueSpace: (v: number) => number
|
fromValueSpace: (v: number) => number
|
||||||
|
@ -55,7 +56,7 @@ const BasicKeyframedTrack: React.FC<{
|
||||||
const extremumSpace: ExtremumSpace = useMemo(() => {
|
const extremumSpace: ExtremumSpace = useMemo(() => {
|
||||||
const extremums =
|
const extremums =
|
||||||
propConfig.isScalar === true
|
propConfig.isScalar === true
|
||||||
? calculateScalarExtremums(trackData.keyframes)
|
? calculateScalarExtremums(trackData.keyframes, propConfig)
|
||||||
: calculateNonScalarExtremums(trackData.keyframes)
|
: calculateNonScalarExtremums(trackData.keyframes)
|
||||||
|
|
||||||
const fromValueSpace = (val: number): number =>
|
const fromValueSpace = (val: number): number =>
|
||||||
|
@ -84,6 +85,7 @@ const BasicKeyframedTrack: React.FC<{
|
||||||
|
|
||||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||||
<KeyframeEditor
|
<KeyframeEditor
|
||||||
|
propConfig={propConfig}
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
trackData={trackData}
|
trackData={trackData}
|
||||||
|
@ -114,7 +116,10 @@ export default BasicKeyframedTrack
|
||||||
|
|
||||||
type Extremums = [min: number, max: number]
|
type Extremums = [min: number, max: number]
|
||||||
|
|
||||||
function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
|
function calculateScalarExtremums(
|
||||||
|
keyframes: Keyframe[],
|
||||||
|
propConfig: PropTypeConfig,
|
||||||
|
): Extremums {
|
||||||
let min = Infinity,
|
let min = Infinity,
|
||||||
max = -Infinity
|
max = -Infinity
|
||||||
|
|
||||||
|
@ -124,7 +129,7 @@ function calculateScalarExtremums(keyframes: Keyframe[]): Extremums {
|
||||||
}
|
}
|
||||||
|
|
||||||
keyframes.forEach((cur, i) => {
|
keyframes.forEach((cur, i) => {
|
||||||
const curVal = typeof cur.value === 'number' ? cur.value : 0
|
const curVal = valueInProp(cur.value, propConfig) as number
|
||||||
check(curVal)
|
check(curVal)
|
||||||
if (!cur.connectedRight) return
|
if (!cur.connectedRight) return
|
||||||
const next = keyframes[i + 1]
|
const next = keyframes[i + 1]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {valueInProp} from '@theatre/shared/propTypes/utils'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
@ -25,8 +26,12 @@ const Curve: React.FC<IProps> = (props) => {
|
||||||
|
|
||||||
const [contextMenu] = useConnectorContextMenu(node, props)
|
const [contextMenu] = useConnectorContextMenu(node, props)
|
||||||
|
|
||||||
const curValue = props.isScalar ? (cur.value as number) : 0
|
const curValue = props.isScalar
|
||||||
const nextValue = props.isScalar ? (next.value as number) : 1
|
? (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 leftYInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
|
||||||
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(nextValue)
|
const rightYInExtremumSpace = props.extremumSpace.fromValueSpace(nextValue)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import CurveHandle from './CurveHandle'
|
||||||
import GraphEditorDotScalar from './GraphEditorDotScalar'
|
import GraphEditorDotScalar from './GraphEditorDotScalar'
|
||||||
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
|
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
|
||||||
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
|
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
|
||||||
|
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||||
|
|
||||||
const Container = styled.g`
|
const Container = styled.g`
|
||||||
/* position: absolute; */
|
/* position: absolute; */
|
||||||
|
@ -32,6 +33,7 @@ const KeyframeEditor: React.FC<{
|
||||||
extremumSpace: ExtremumSpace
|
extremumSpace: ExtremumSpace
|
||||||
isScalar: boolean
|
isScalar: boolean
|
||||||
color: keyof typeof graphEditorColors
|
color: keyof typeof graphEditorColors
|
||||||
|
propConfig: PropTypeConfig
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const {index, trackData, isScalar} = props
|
const {index, trackData, isScalar} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
|
|
|
@ -113,9 +113,9 @@ const GraphEditor: React.FC<{
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(0, ${val(
|
transform: `translate(${val(
|
||||||
layoutP.graphEditorDims.padding.top,
|
layoutP.scaledSpace.leftPadding,
|
||||||
)}px)`,
|
)}px, ${val(layoutP.graphEditorDims.padding.top)}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{graphs}
|
{graphs}
|
||||||
|
|
|
@ -542,6 +542,7 @@ namespace stateEditors {
|
||||||
p: WithoutSheetInstance<SheetObjectAddress> & {
|
p: WithoutSheetInstance<SheetObjectAddress> & {
|
||||||
trackId: string
|
trackId: string
|
||||||
position: number
|
position: number
|
||||||
|
handles?: [number, number, number, number]
|
||||||
value: T
|
value: T
|
||||||
snappingFunction: SnappingFunction
|
snappingFunction: SnappingFunction
|
||||||
},
|
},
|
||||||
|
@ -567,7 +568,7 @@ namespace stateEditors {
|
||||||
id: generateKeyframeId(),
|
id: generateKeyframeId(),
|
||||||
position,
|
position,
|
||||||
connectedRight: true,
|
connectedRight: true,
|
||||||
handles: [0.5, 1, 0.5, 0],
|
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||||
value: p.value,
|
value: p.value,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -577,7 +578,7 @@ namespace stateEditors {
|
||||||
id: generateKeyframeId(),
|
id: generateKeyframeId(),
|
||||||
position,
|
position,
|
||||||
connectedRight: leftKeyframe.connectedRight,
|
connectedRight: leftKeyframe.connectedRight,
|
||||||
handles: [0.5, 1, 0.5, 0],
|
handles: p.handles || [0.5, 1, 0.5, 0],
|
||||||
value: p.value,
|
value: p.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue