feat: Add inline keyframe editing in graph editor
This commit is contained in:
parent
c303748ca9
commit
d4f572a744
8 changed files with 124 additions and 60 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
}
|
|
@ -94,6 +94,7 @@ const BasicKeyframedTrack: React.VFC<{
|
|||
|
||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||
<KeyframeEditor
|
||||
pathToProp={pathToProp}
|
||||
propConfig={propConfig}
|
||||
keyframe={kf}
|
||||
index={index}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?: {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue