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 React, {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import last from 'lodash-es/last'
|
|
||||||
|
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
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 {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
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 type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
@ -28,6 +23,7 @@ import {
|
||||||
snapToNone,
|
snapToNone,
|
||||||
snapToSome,
|
snapToSome,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
import {useSingleKeyframeInlineEditorPopover} from './useSingleKeyframeInlineEditorPopover'
|
||||||
|
|
||||||
export const DOT_SIZE_PX = 6
|
export const DOT_SIZE_PX = 6
|
||||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 2
|
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 [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
|
||||||
const [inlineEditorPopover, openEditor, _, isInlineEditorPopoverOpen] =
|
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, {
|
const [isDragging] = useDragForSingleKeyframeDot(node, props, {
|
||||||
onClickFromDrag(dragStartEvent) {
|
onClickFromDrag(dragStartEvent) {
|
||||||
openEditor(dragStartEvent, ref.current!)
|
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(
|
function useDragForSingleKeyframeDot(
|
||||||
node: HTMLDivElement | null,
|
node: HTMLDivElement | null,
|
||||||
props: ISingleKeyframeDotProps,
|
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) => (
|
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||||
<KeyframeEditor
|
<KeyframeEditor
|
||||||
|
pathToProp={pathToProp}
|
||||||
propConfig={propConfig}
|
propConfig={propConfig}
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
useCssCursorLock,
|
useCssCursorLock,
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
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
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -64,7 +65,22 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
|
|
||||||
const curValue = props.which === 'left' ? 0 : 1
|
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)
|
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
|
||||||
|
|
||||||
|
@ -88,6 +104,7 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{inlineEditorPopover}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -95,14 +112,15 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
|
|
||||||
export default GraphEditorDotNonScalar
|
export default GraphEditorDotNonScalar
|
||||||
|
|
||||||
function useDragKeyframe(
|
function useDragKeyframe(options: {
|
||||||
node: SVGCircleElement | null,
|
node: SVGCircleElement | null
|
||||||
_props: IProps,
|
props: IProps
|
||||||
): boolean {
|
onDetectedClick: (event: MouseEvent) => void
|
||||||
|
}): boolean {
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockFrameStampPosition(isDragging, _props.keyframe.position)
|
useLockFrameStampPosition(isDragging, options.props.keyframe.position)
|
||||||
const propsRef = useRef(_props)
|
const propsRef = useRef(options.props)
|
||||||
propsRef.current = _props
|
propsRef.current = options.props
|
||||||
|
|
||||||
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -158,6 +176,7 @@ function useDragKeyframe(
|
||||||
tempTransaction?.commit()
|
tempTransaction?.commit()
|
||||||
} else {
|
} else {
|
||||||
tempTransaction?.discard()
|
tempTransaction?.discard()
|
||||||
|
options.onDetectedClick(event)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -165,7 +184,7 @@ function useDragKeyframe(
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useDrag(node, gestureHandlers)
|
useDrag(options.node, gestureHandlers)
|
||||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
return isDragging
|
return isDragging
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
useCssCursorLock,
|
useCssCursorLock,
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
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
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -65,9 +66,23 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
|
|
||||||
const curValue = cur.value as number
|
const curValue = cur.value as number
|
||||||
|
|
||||||
const isDragging = useDragKeyframe(node, props)
|
|
||||||
|
|
||||||
const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -89,6 +104,7 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{inlineEditorPopover}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -96,14 +112,15 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
|
|
||||||
export default GraphEditorDotScalar
|
export default GraphEditorDotScalar
|
||||||
|
|
||||||
function useDragKeyframe(
|
function useDragKeyframe(options: {
|
||||||
node: SVGCircleElement | null,
|
node: SVGCircleElement | null
|
||||||
_props: IProps,
|
props: IProps
|
||||||
): boolean {
|
onDetectedClick: (event: MouseEvent) => void
|
||||||
|
}): boolean {
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
useLockFrameStampPosition(isDragging, _props.keyframe.position)
|
useLockFrameStampPosition(isDragging, options.props.keyframe.position)
|
||||||
const propsRef = useRef(_props)
|
const propsRef = useRef(options.props)
|
||||||
propsRef.current = _props
|
propsRef.current = options.props
|
||||||
|
|
||||||
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -219,6 +236,7 @@ function useDragKeyframe(
|
||||||
tempTransaction?.commit()
|
tempTransaction?.commit()
|
||||||
} else {
|
} else {
|
||||||
tempTransaction?.discard()
|
tempTransaction?.discard()
|
||||||
|
options.onDetectedClick(event)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -226,7 +244,7 @@ function useDragKeyframe(
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useDrag(node, gestureHandlers)
|
useDrag(options.node, gestureHandlers)
|
||||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'move')
|
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'move')
|
||||||
return isDragging
|
return isDragging
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import GraphEditorDotScalar from './GraphEditorDotScalar'
|
||||||
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
|
import GraphEditorDotNonScalar from './GraphEditorDotNonScalar'
|
||||||
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
|
import GraphEditorNonScalarDash from './GraphEditorNonScalarDash'
|
||||||
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
|
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
|
||||||
|
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
||||||
|
|
||||||
const Container = styled.g`
|
const Container = styled.g`
|
||||||
/* position: absolute; */
|
/* position: absolute; */
|
||||||
|
@ -30,6 +31,7 @@ type IKeyframeEditorProps = {
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
trackId: SequenceTrackId
|
trackId: SequenceTrackId
|
||||||
sheetObject: SheetObject
|
sheetObject: SheetObject
|
||||||
|
pathToProp: PathToProp
|
||||||
extremumSpace: ExtremumSpace
|
extremumSpace: ExtremumSpace
|
||||||
isScalar: boolean
|
isScalar: boolean
|
||||||
color: keyof typeof graphEditorColors
|
color: keyof typeof graphEditorColors
|
||||||
|
|
|
@ -19,7 +19,7 @@ export type AbsolutePlacementBoxConstraints = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipWrapper: React.FC<{
|
const TooltipWrapper: React.FC<{
|
||||||
target: HTMLElement | SVGElement
|
target: HTMLElement | SVGElement | Element
|
||||||
onClickOutside?: (e: MouseEvent) => void
|
onClickOutside?: (e: MouseEvent) => void
|
||||||
children: () => React.ReactElement
|
children: () => React.ReactElement
|
||||||
onPointerOutside?: {
|
onPointerOutside?: {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {contextMenuShownContext} from '@theatre/studio/panels/DetailPanel/Detail
|
||||||
|
|
||||||
export type OpenFn = (
|
export type OpenFn = (
|
||||||
e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number},
|
e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number},
|
||||||
target: HTMLElement,
|
target: HTMLElement | SVGElement | Element,
|
||||||
) => void
|
) => void
|
||||||
type CloseFn = (reason: string) => void
|
type CloseFn = (reason: string) => void
|
||||||
type State =
|
type State =
|
||||||
|
@ -20,7 +20,7 @@ type State =
|
||||||
clientX: number
|
clientX: number
|
||||||
clientY: number
|
clientY: number
|
||||||
}
|
}
|
||||||
target: HTMLElement
|
target: HTMLElement | SVGElement | Element
|
||||||
opts: Opts
|
opts: Opts
|
||||||
onPointerOutside?: {
|
onPointerOutside?: {
|
||||||
threshold: number
|
threshold: number
|
||||||
|
|
Loading…
Reference in a new issue