Fix keyframe pasting to respect interpolation graph (#107)

This commit is contained in:
Elliot 2022-03-25 13:06:20 -04:00 committed by GitHub
parent ffdebebfff
commit 2bf081478f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 59 additions and 31 deletions

View file

@ -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

View file

@ -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,

View file

@ -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
}
}

View file

@ -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,
}, },

View file

@ -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) =>

View file

@ -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]

View file

@ -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)

View file

@ -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]

View file

@ -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}

View file

@ -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,
}) })
} }