feat: Add inline keyframe editing in graph editor

This commit is contained in:
Cole Lawrence 2022-06-10 11:07:55 -04:00
parent c303748ca9
commit d4f572a744
8 changed files with 124 additions and 60 deletions

View file

@ -1,6 +1,5 @@
import React, {useMemo, useRef} from 'react'
import styled from 'styled-components'
import last from 'lodash-es/last'
import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
@ -12,11 +11,7 @@ import {val} from '@theatre/dataverse'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
@ -28,6 +23,7 @@ import {
snapToNone,
snapToSome,
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
import {useSingleKeyframeInlineEditorPopover} from './useSingleKeyframeInlineEditorPopover'
export const DOT_SIZE_PX = 6
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 2
@ -95,7 +91,14 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
const [inlineEditorPopover, openEditor, _, isInlineEditorPopoverOpen] =
useSingleKeyframeInlineEditorPopover(props)
useSingleKeyframeInlineEditorPopover({
keyframe: props.keyframe,
pathToProp: props.leaf.pathToProp,
propConf: props.leaf.propConf,
sheetObject: props.leaf.sheetObject,
trackId: props.leaf.trackId,
})
const [isDragging] = useDragForSingleKeyframeDot(node, props, {
onClickFromDrag(dragStartEvent) {
openEditor(dragStartEvent, ref.current!)
@ -180,38 +183,6 @@ function useSingleKeyframeContextMenu(
})
}
/** The editor that pops up when directly clicking a Keyframe. */
function useSingleKeyframeInlineEditorPopover(props: ISingleKeyframeDotProps) {
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp)
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
<BasicPopover showPopoverEdgeTriangle>
<DeterminePropEditorForSingleKeyframe
propConfig={props.leaf.propConf}
editingTools={editingTools}
keyframeValue={props.keyframe.value}
displayLabel={label != null ? String(label) : undefined}
/>
</BasicPopover>
))
}
function useEditingToolsForKeyframeEditorPopover(
props: ISingleKeyframeDotProps,
) {
const obj = props.leaf.sheetObject
return useTempTransactionEditingTools(({stateEditors}, value) => {
const newKeyframe = {...props.keyframe, value}
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes({
...obj.address,
trackId: props.leaf.trackId,
keyframes: [newKeyframe],
snappingFunction: obj.sheet.getSequence().closestGridPosition,
})
})
}
function useDragForSingleKeyframeDot(
node: HTMLDivElement | null,
props: ISingleKeyframeDotProps,

View file

@ -0,0 +1,53 @@
import React from 'react'
import last from 'lodash-es/last'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {PropTypeConfig} from '@theatre/core/propTypes'
import type {PathToProp} from '@theatre/shared/utils/addresses'
/** The editor that pops up when directly clicking a Keyframe. */
export function useSingleKeyframeInlineEditorPopover(
props: SingleKeyframeEditingOptions,
) {
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
const label = props.propConf.label ?? last(props.pathToProp)
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
<BasicPopover showPopoverEdgeTriangle>
<DeterminePropEditorForSingleKeyframe
propConfig={props.propConf}
editingTools={editingTools}
keyframeValue={props.keyframe.value}
displayLabel={label != null ? String(label) : undefined}
/>
</BasicPopover>
))
}
type SingleKeyframeEditingOptions = {
keyframe: Keyframe
propConf: PropTypeConfig
sheetObject: SheetObject
trackId: SequenceTrackId
pathToProp: PathToProp
}
function useEditingToolsForKeyframeEditorPopover(
props: SingleKeyframeEditingOptions,
) {
const obj = props.sheetObject
return useTempTransactionEditingTools(({stateEditors}, value) => {
const newKeyframe = {...props.keyframe, value}
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes({
...obj.address,
trackId: props.trackId,
keyframes: [newKeyframe],
snappingFunction: obj.sheet.getSequence().closestGridPosition,
})
})
}

View file

@ -94,6 +94,7 @@ const BasicKeyframedTrack: React.VFC<{
const keyframeEditors = trackData.keyframes.map((kf, index) => (
<KeyframeEditor
pathToProp={pathToProp}
propConfig={propConfig}
keyframe={kf}
index={index}

View file

@ -16,6 +16,7 @@ import {
useCssCursorLock,
} from '@theatre/studio/uiComponents/PointerEventsHandler'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
export const dotSize = 6
@ -64,7 +65,22 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
const curValue = props.which === 'left' ? 0 : 1
const isDragging = useDragKeyframe(node, props)
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] =
useSingleKeyframeInlineEditorPopover({
keyframe: props.keyframe,
pathToProp: props.pathToProp,
propConf: props.propConfig,
sheetObject: props.sheetObject,
trackId: props.trackId,
})
const isDragging = useDragKeyframe({
node,
props,
// dragging does not work with also having a click listener
onDetectedClick: (event) =>
openEditor(event, event.target instanceof Element ? event.target : node!),
})
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
@ -88,6 +104,7 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
}}
/>
{inlineEditorPopover}
{contextMenu}
</>
)
@ -95,14 +112,15 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
export default GraphEditorDotNonScalar
function useDragKeyframe(
node: SVGCircleElement | null,
_props: IProps,
): boolean {
function useDragKeyframe(options: {
node: SVGCircleElement | null
props: IProps
onDetectedClick: (event: MouseEvent) => void
}): boolean {
const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, _props.keyframe.position)
const propsRef = useRef(_props)
propsRef.current = _props
useLockFrameStampPosition(isDragging, options.props.keyframe.position)
const propsRef = useRef(options.props)
propsRef.current = options.props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
return {
@ -158,6 +176,7 @@ function useDragKeyframe(
tempTransaction?.commit()
} else {
tempTransaction?.discard()
options.onDetectedClick(event)
}
},
}
@ -165,7 +184,7 @@ function useDragKeyframe(
}
}, [])
useDrag(node, gestureHandlers)
useDrag(options.node, gestureHandlers)
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return isDragging
}

View file

@ -16,6 +16,7 @@ import {
useCssCursorLock,
} from '@theatre/studio/uiComponents/PointerEventsHandler'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
export const dotSize = 6
@ -65,9 +66,23 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
const curValue = cur.value as number
const isDragging = useDragKeyframe(node, props)
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] =
useSingleKeyframeInlineEditorPopover({
keyframe: props.keyframe,
pathToProp: props.pathToProp,
propConf: props.propConfig,
sheetObject: props.sheetObject,
trackId: props.trackId,
})
const isDragging = useDragKeyframe({
node,
props,
// dragging does not work with also having a click listener
onDetectedClick: (event) =>
openEditor(event, event.target instanceof Element ? event.target : node!),
})
return (
<>
@ -89,6 +104,7 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
}}
/>
{inlineEditorPopover}
{contextMenu}
</>
)
@ -96,14 +112,15 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
export default GraphEditorDotScalar
function useDragKeyframe(
node: SVGCircleElement | null,
_props: IProps,
): boolean {
function useDragKeyframe(options: {
node: SVGCircleElement | null
props: IProps
onDetectedClick: (event: MouseEvent) => void
}): boolean {
const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, _props.keyframe.position)
const propsRef = useRef(_props)
propsRef.current = _props
useLockFrameStampPosition(isDragging, options.props.keyframe.position)
const propsRef = useRef(options.props)
propsRef.current = options.props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
return {
@ -219,6 +236,7 @@ function useDragKeyframe(
tempTransaction?.commit()
} else {
tempTransaction?.discard()
options.onDetectedClick(event)
}
},
}
@ -226,7 +244,7 @@ function useDragKeyframe(
}
}, [])
useDrag(node, gestureHandlers)
useDrag(options.node, gestureHandlers)
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'move')
return isDragging
}

View file

@ -16,6 +16,7 @@ import GraphEditorDotScalar from './GraphEditorDotScalar'
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import type {PathToProp} from '@theatre/shared/utils/addresses'
const Container = styled.g`
/* position: absolute; */
@ -30,6 +31,7 @@ type IKeyframeEditorProps = {
layoutP: Pointer<SequenceEditorPanelLayout>
trackId: SequenceTrackId
sheetObject: SheetObject
pathToProp: PathToProp
extremumSpace: ExtremumSpace
isScalar: boolean
color: keyof typeof graphEditorColors

View file

@ -19,7 +19,7 @@ export type AbsolutePlacementBoxConstraints = {
}
const TooltipWrapper: React.FC<{
target: HTMLElement | SVGElement
target: HTMLElement | SVGElement | Element
onClickOutside?: (e: MouseEvent) => void
children: () => React.ReactElement
onPointerOutside?: {

View file

@ -9,7 +9,7 @@ import {contextMenuShownContext} from '@theatre/studio/panels/DetailPanel/Detail
export type OpenFn = (
e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number},
target: HTMLElement,
target: HTMLElement | SVGElement | Element,
) => void
type CloseFn = (reason: string) => void
type State =
@ -20,7 +20,7 @@ type State =
clientX: number
clientY: number
}
target: HTMLElement
target: HTMLElement | SVGElement | Element
opts: Opts
onPointerOutside?: {
threshold: number