Fix aggregate keyframe dragging stopping in an edge case when the key for the drag element changes (#189)

Co-authored-by: Cole Lawrence <cole@colelawrence.com>
This commit is contained in:
Andrew Prifer 2022-06-08 18:57:58 +02:00 committed by GitHub
parent 6b0b9f0ba6
commit a90aee96f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 279 additions and 191 deletions

View file

@ -1,19 +1,11 @@
import {val} from '@theatre/dataverse'
import React, {useMemo, useRef} from 'react'
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
import React from 'react'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import getStudio from '@theatre/studio/getStudio'
import useDrag from '@theatre/studio/uiComponents/useDrag'
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
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 type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor'
import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
import {AggregateKeyframeVisualDot, HitZone} from './AggregateKeyframeVisualDot'
import getStudio from '@theatre/studio/getStudio'
import {
copyableKeyframesFromSelection,
keyframesWithPaths,
@ -21,12 +13,7 @@ import {
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric'
import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses'
import type {ILogger} from '@theatre/shared/logger'
import {
collectKeyframeSnapPositions,
snapToNone,
snapToSome,
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
type IAggregateKeyframeDotProps = {
editorProps: IAggregateKeyframeEditorProps
@ -40,18 +27,17 @@ export function AggregateKeyframeDot(
const {cur} = props.utils
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragForAggregateKeyframeDot(node, props, {
onClickFromDrag(dragStartEvent) {
// TODO Aggregate inline keyframe editor
// openEditor(dragStartEvent, ref.current!)
},
})
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node)
return (
<>
<HitZone ref={ref} />
<HitZone
ref={ref}
// Need this for the dragging logic to be able to get the keyframe props
// based on the position.
{...DopeSnap.includePositionSnapAttrs(cur.position)}
/>
<AggregateKeyframeVisualDot
isAllHere={cur.allHere}
isSelected={cur.selected}
@ -144,149 +130,3 @@ function useAggregateKeyframeContextMenu(
},
})
}
function useDragForAggregateKeyframeDot(
node: HTMLDivElement | null,
props: IAggregateKeyframeDotProps,
options: {
/**
* hmm: this is a hack so we can actually receive the
* {@link MouseEvent} from the drag event handler and use
* it for positioning the popup.
*/
onClickFromDrag(dragStartEvent: MouseEvent): void
},
): [isDragging: boolean] {
const propsRef = useRef(props.editorProps)
propsRef.current = props.editorProps
const keyframesRef = useRef(props.utils.cur.keyframes)
keyframesRef.current = props.utils.cur.keyframes
const useDragOpts = useMemo<UseDragOpts>(() => {
return {
debugName: 'AggregateKeyframeDot/useDragKeyframe',
onDragStart(event) {
const props = propsRef.current
const keyframes = keyframesRef.current
const tracksByObject = val(
getStudio()!.atomP.historic.coreByProject[
props.viewModel.sheetObject.address.projectId
].sheetsById[props.viewModel.sheetObject.address.sheetId].sequence
.tracksByObject,
)!
// Calculate all the valid snap positions in the sequence editor,
// excluding the child keyframes of this aggregate, and any selection it is part of.
const snapPositions = collectKeyframeSnapPositions(
tracksByObject,
function shouldIncludeKeyfram(keyframe, {trackId, objectKey}) {
return (
// we exclude all the child keyframes of this aggregate keyframe from being a snap target
keyframes.every(
(kfWithTrack) => keyframe.id !== kfWithTrack.kf.id,
) &&
!(
// if all of the children of the current aggregate keyframe are in a selection,
(
props.selection &&
// then we exclude them and all other keyframes in the selection from being snap targets
props.selection.byObjectKey[objectKey]?.byTrackId[trackId]
?.byKeyframeId[keyframe.id]
)
)
)
},
)
snapToSome(snapPositions)
if (
props.selection &&
props.aggregateKeyframes[props.index].selected ===
AggregateKeyframePositionIsSelected.AllSelected
) {
const {selection, viewModel} = props
const {sheetObject} = viewModel
const handlers = selection
.getDragHandlers({
...sheetObject.address,
domNode: node!,
positionAtStartOfDrag: keyframes[0].kf.position,
})
.onDragStart(event)
return (
handlers && {
...handlers,
onClick: options.onClickFromDrag,
onDragEnd: (...args) => {
handlers.onDragEnd?.(...args)
snapToNone()
},
}
)
}
const propsAtStartOfDrag = props
const toUnitSpace = val(
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
)
let tempTransaction: CommitOrDiscard | undefined
return {
onDrag(dx, dy, event) {
const newPosition = Math.max(
// check if our event hoversover a [data-pos] element
DopeSnap.checkIfMouseEventSnapToPos(event, {
ignore: node,
}) ??
// if we don't find snapping target, check the distance dragged + original position
keyframes[0].kf.position + toUnitSpace(dx),
// sanitize to minimum of zero
0,
)
tempTransaction?.discard()
tempTransaction = undefined
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
for (const keyframe of keyframes) {
const original = keyframe.kf
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.viewModel.sheetObject.address,
trackId: keyframe.track.id,
keyframes: [{...original, position: newPosition}],
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
}
})
},
onDragEnd(dragHappened) {
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
snapToNone()
},
onClick(ev) {
options.onClickFromDrag(ev)
},
}
},
}
}, [])
const [isDragging] = useDrag(node, useDragOpts)
useLockFrameStampPosition(isDragging, props.utils.cur.position)
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return [isDragging]
}

View file

@ -24,7 +24,26 @@ export function useAggregateKeyframeEditorUtils(
const {index, aggregateKeyframes, selection} = props
const sheetObjectAddress = props.viewModel.sheetObject.address
return usePrism(() => {
return usePrism(getAggregateKeyframeEditorUtilsPrismFn(props), [
index,
aggregateKeyframes,
selection,
sheetObjectAddress,
])
}
// I think this was pulled out for performance
// 1/10: Not sure this is properly split up
export function getAggregateKeyframeEditorUtilsPrismFn(
props: Pick<
IAggregateKeyframeEditorProps,
'index' | 'aggregateKeyframes' | 'selection' | 'viewModel'
>,
) {
const {index, aggregateKeyframes, selection} = props
const sheetObjectAddress = props.viewModel.sheetObject.address
return () => {
const cur = aggregateKeyframes[index]
const next = aggregateKeyframes[index + 1]
@ -80,5 +99,5 @@ export function useAggregateKeyframeEditorUtils(
isAggregateEditingInCurvePopover,
allConnections,
}
}, [index, aggregateKeyframes, selection, sheetObjectAddress])
}
}

View file

@ -6,20 +6,28 @@ import type {
SequenceEditorTree_PropWithChildren,
SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import {usePrism, useVal} from '@theatre/react'
import type {Pointer} from '@theatre/dataverse'
import {valueDerivation} from '@theatre/dataverse'
import {val} from '@theatre/dataverse'
import React, {Fragment, useMemo} from 'react'
import {prism, val, valueDerivation} from '@theatre/dataverse'
import React, {useMemo, Fragment} from 'react'
import styled from 'styled-components'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor/AggregateKeyframeEditor'
import type {
IAggregateKeyframesAtPosition,
IAggregateKeyframeEditorProps,
} from './AggregateKeyframeEditor/AggregateKeyframeEditor'
import AggregateKeyframeEditor from './AggregateKeyframeEditor/AggregateKeyframeEditor'
import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
import {getAggregateKeyframeEditorUtilsPrismFn} from './AggregateKeyframeEditor/useAggregateKeyframeEditorUtils'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useDrag from '@theatre/studio/uiComponents/useDrag'
import {useLockFrameStampPositionRef} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
import getStudio from '@theatre/studio/getStudio'
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
import {
@ -29,12 +37,18 @@ import {
} from '@theatre/shared/utils/addresses'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import type Sequence from '@theatre/core/sequences/Sequence'
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
import {collectAggregateSnapPositions} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
import KeyframeSnapTarget, {
snapPositionsStateD,
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
import {emptyObject} from '@theatre/shared/utils'
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
import {
collectKeyframeSnapPositions,
snapToNone,
snapToSome,
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
import {collectAggregateSnapPositions} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
const AggregatedKeyframeTrackContainer = styled.div`
position: relative;
@ -115,24 +129,47 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
/>
))
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
<Fragment key={'agg-' + keyframes[0].kf.id}>
const keyframeEditorProps = posKfs.map(
(
{position, keyframes},
index,
): {editorProps: IAggregateKeyframeEditorProps; position: number} => ({
position,
editorProps: {
index,
layoutP,
viewModel,
aggregateKeyframes: posKfs,
selection: selectedPositions.has(position) ? selection : undefined,
},
}),
)
const [isDragging] = useDragForAggregateKeyframeDot(
containerNode,
(position) => {
return keyframeEditorProps.find(
(editorProp) => editorProp.position === position,
)?.editorProps
},
{
onClickFromDrag(dragStartEvent) {
// TODO Aggregate inline keyframe editor
// openEditor(dragStartEvent, ref.current!)
},
},
)
const keyframeEditors = keyframeEditorProps.map((props, i) => (
<Fragment key={'agg-' + posKfs[i].keyframes[0].kf.id}>
{snapToAllKeyframes && (
<KeyframeSnapTarget
layoutP={layoutP}
leaf={viewModel}
position={position}
position={props.position}
/>
)}
<AggregateKeyframeEditor
index={index}
layoutP={layoutP}
viewModel={viewModel}
aggregateKeyframes={posKfs}
selection={
selectedPositions.has(position) === true ? selection : undefined
}
/>
<AggregateKeyframeEditor {...props.editorProps} />
</Fragment>
))
@ -422,3 +459,163 @@ function earliestKeyframe(keyframes: Keyframe[]) {
}
return curEarliest
}
function useDragForAggregateKeyframeDot(
containerNode: HTMLDivElement | null,
getPropsForPosition: (
position: number,
) => IAggregateKeyframeEditorProps | undefined,
options: {
/**
* hmm: this is a hack so we can actually receive the
* {@link MouseEvent} from the drag event handler and use
* it for positioning the popup.
*/
onClickFromDrag(dragStartEvent: MouseEvent): void
},
): [isDragging: boolean] {
const logger = useLogger('useDragForAggregateKeyframeDot')
const frameStampLock = useLockFrameStampPositionRef()
const useDragOpts = useMemo<UseDragOpts>(() => {
return {
debugName: 'AggregateKeyframeDot/useDragKeyframe',
onDragStart(event) {
logger._debug('onDragStart', {target: event.target})
console.log(event.target)
const positionToFind = Number((event.target as HTMLElement).dataset.pos)
const props = getPropsForPosition(positionToFind)
if (!props) {
console.log('exit')
logger._debug('no props found for ', {positionToFind})
return false
}
frameStampLock(true, positionToFind)
const keyframes = prism(
getAggregateKeyframeEditorUtilsPrismFn(props),
).getValue().cur.keyframes
const tracksByObject = val(
getStudio()!.atomP.historic.coreByProject[
props.viewModel.sheetObject.address.projectId
].sheetsById[props.viewModel.sheetObject.address.sheetId].sequence
.tracksByObject,
)!
// Calculate all the valid snap positions in the sequence editor,
// excluding the child keyframes of this aggregate, and any selection it is part of.
const snapPositions = collectKeyframeSnapPositions(
tracksByObject,
function shouldIncludeKeyfram(keyframe, {trackId, objectKey}) {
return (
// we exclude all the child keyframes of this aggregate keyframe from being a snap target
keyframes.every(
(kfWithTrack) => keyframe.id !== kfWithTrack.kf.id,
) &&
!(
// if all of the children of the current aggregate keyframe are in a selection,
(
props.selection &&
// then we exclude them and all other keyframes in the selection from being snap targets
props.selection.byObjectKey[objectKey]?.byTrackId[trackId]
?.byKeyframeId[keyframe.id]
)
)
)
},
)
snapToSome(snapPositions)
if (
props.selection &&
props.aggregateKeyframes[props.index].selected ===
AggregateKeyframePositionIsSelected.AllSelected
) {
const {selection, viewModel} = props
const {sheetObject} = viewModel
const handlers = selection
.getDragHandlers({
...sheetObject.address,
domNode: containerNode!,
positionAtStartOfDrag: keyframes[0].kf.position,
})
.onDragStart(event)
return (
handlers && {
...handlers,
onClick: options.onClickFromDrag,
onDragEnd: (...args) => {
handlers.onDragEnd?.(...args)
snapToNone()
},
}
)
}
const propsAtStartOfDrag = props
const toUnitSpace = val(
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
)
let tempTransaction: CommitOrDiscard | undefined
return {
onDrag(dx, dy, event) {
const newPosition = Math.max(
// check if our event hovers over a [data-pos] element
DopeSnap.checkIfMouseEventSnapToPos(event, {
// ignore: node,
}) ??
// if we don't find snapping target, check the distance dragged + original position
keyframes[0].kf.position + toUnitSpace(dx),
// sanitize to minimum of zero
0,
)
frameStampLock(true, newPosition)
tempTransaction?.discard()
tempTransaction = undefined
tempTransaction = getStudio().tempTransaction(({stateEditors}) => {
for (const keyframe of keyframes) {
const original = keyframe.kf
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.viewModel.sheetObject.address,
trackId: keyframe.track.id,
keyframes: [{...original, position: newPosition}],
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
}
})
},
onDragEnd(dragHappened) {
frameStampLock(false, -1)
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
options.onClickFromDrag(event)
}
snapToNone()
},
onClick(ev) {
options.onClickFromDrag(ev)
},
}
},
}
}, [getPropsForPosition, options.onClickFromDrag])
const [isDragging] = useDrag(containerNode, useDragOpts)
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return [isDragging]
}

View file

@ -123,6 +123,39 @@ const FrameStampPositionProvider: React.FC<{
export const useFrameStampPositionD = () => useContext(context).currentD
/** Version of {@link useLockFrameStampPosition} which allows you to directly set status of a lock. */
export const useLockFrameStampPositionRef = () => {
const {getLock} = useContext(context)
const lockRef = useRef<undefined | ReturnType<typeof getLock>>()
useLayoutEffect(() => {
return () => {
lockRef.current!.unlock()
}
}, [val])
return useMemo(() => {
let prevShouldLock: false | {pos: number} = false
return (shouldLock: boolean, posValue: number) => {
if (shouldLock === prevShouldLock) return
if (shouldLock) {
if (!prevShouldLock) {
lockRef.current = getLock()
lockRef.current.set(posValue)
prevShouldLock = {pos: posValue}
} else if (prevShouldLock.pos !== posValue) {
lockRef.current?.set(posValue)
} else {
// all the same params
}
} else {
lockRef.current!.unlock()
prevShouldLock = false
}
}
}, [getLock])
}
export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => {
const {getLock} = useContext(context)
const lockRef = useRef<undefined | ReturnType<typeof getLock>>()
@ -179,7 +212,6 @@ const pointerPositionInUnitSpace = (
return prism(() => {
const rightDims = val(layoutP.rightDims)
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
const leftPadding = val(layoutP.scaledSpace.leftPadding)
const mousePos = val(mousePositionD)
if (!mousePos) return [-1, FrameStampPositionType.hidden]