theatre/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx
Aria Minaei e2e6754de1 Fixed a bug in dragging selections ...
where dragging props of multiple objects would only drag the props of one object
2021-09-17 16:42:22 +02:00

332 lines
10 KiB
TypeScript

import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useDrag from '@theatre/studio/uiComponents/useDrag'
import useKeyDown from '@theatre/studio/uiComponents/useKeyDown'
import useValToAtom from '@theatre/studio/uiComponents/useValToAtom'
import mutableSetDeep from '@theatre/shared/utils/mutableSetDeep'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {usePrism} from '@theatre/react'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse'
import React, {useMemo, useRef} from 'react'
import styled from 'styled-components'
import type {
DopeSheetSelection,
SequenceEditorPanelLayout,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import type {SequenceEditorTree_AllRowTypes} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
const Container = styled.div<{isShiftDown: boolean}>`
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')};
`
const DopeSheetSelectionView: React.FC<{
layoutP: Pointer<SequenceEditorPanelLayout>
}> = ({layoutP, children}) => {
const [containerRef, containerNode] = useRefAndState<HTMLDivElement | null>(
null,
)
const isShiftDown = useKeyDown('Shift')
const selectionBounds = useCaptureSelection(layoutP, containerNode)
const selectionBoundsRef = useRef<typeof selectionBounds>(selectionBounds)
selectionBoundsRef.current = selectionBounds
return (
<Container ref={containerRef} isShiftDown={isShiftDown}>
{selectionBounds && (
<SelectionRectangle state={selectionBounds} layoutP={layoutP} />
)}
{children}
</Container>
)
}
type SelectionBounds = {
positions: [from: number, to: number]
ys: [from: number, to: number]
}
function useCaptureSelection(
layoutP: Pointer<SequenceEditorPanelLayout>,
containerNode: HTMLDivElement | null,
) {
const [ref, state] = useRefAndState<SelectionBounds | null>(null)
useDrag(
containerNode,
useMemo((): Parameters<typeof useDrag>[1] => {
return {
dontBlockMouseDown: true,
lockCursorTo: 'cell',
onDragStart(event) {
if (!event.shiftKey || event.target instanceof HTMLInputElement) {
return false
}
const rect = containerNode!.getBoundingClientRect()
const posInScaledSpace = event.clientX - rect.left
const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)(
posInScaledSpace,
)
ref.current = {
positions: [posInUnitSpace, posInUnitSpace],
ys: [event.clientY - rect.top, event.clientY - rect.top],
}
val(layoutP.selectionAtom).setState({current: undefined})
},
onDrag(dx, dy, event) {
const state = ref.current!
const rect = containerNode!.getBoundingClientRect()
const posInScaledSpace = event.clientX - rect.left
const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)(
posInScaledSpace,
)
ref.current = {
positions: [ref.current!.positions[0], posInUnitSpace],
ys: [ref.current!.ys[0], event.clientY - rect.top],
}
const selection = utils.boundsToSelection(layoutP, ref.current)
val(layoutP.selectionAtom).setState({current: selection})
},
onDragEnd(dragHappened) {
ref.current = null
},
}
}, [layoutP, containerNode, ref]),
)
// useEffect(() => {
// if (!containerNode) return
// const onClick = () => {
// }
// containerNode.addEventListener('click', onClick)
// return () => {
// containerNode.removeEventListener('click', onClick)
// }
// }, [containerNode])
return state
}
namespace utils {
const collectorByLeafType: {
[K in SequenceEditorTree_AllRowTypes['type']]?: (
layoutP: Pointer<SequenceEditorPanelLayout>,
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
bounds: Exclude<SelectionBounds, null>,
selection: DopeSheetSelection,
) => void
} = {
primitiveProp(layoutP, leaf, bounds, selection) {
const {sheetObject, trackId} = leaf
const trackData = val(
getStudio()!.atomP.historic.coreByProject[sheetObject.address.projectId]
.sheetsById[sheetObject.address.sheetId].sequence.tracksByObject[
sheetObject.address.objectKey
].trackData[trackId],
)!
const toCollect = trackData!.keyframes.filter(
(kf) =>
kf.position >= bounds.positions[0] &&
kf.position <= bounds.positions[1],
)
for (const kf of trackData.keyframes) {
if (kf.position <= bounds.positions[0]) continue
if (kf.position >= bounds.positions[1]) break
mutableSetDeep(
selection,
(p) =>
p.byObjectKey[sheetObject.address.objectKey].byTrackId[trackId]
.byKeyframeId[kf.id],
true,
)
}
},
}
const collectChildren = (
layoutP: Pointer<SequenceEditorPanelLayout>,
leaf: SequenceEditorTree_AllRowTypes,
bounds: Exclude<SelectionBounds, null>,
selection: DopeSheetSelection,
) => {
if ('children' in leaf) {
for (const sub of leaf.children) {
collectFromAnyLeaf(layoutP, sub, bounds, selection)
}
}
}
function collectFromAnyLeaf(
layoutP: Pointer<SequenceEditorPanelLayout>,
leaf: SequenceEditorTree_AllRowTypes,
bounds: Exclude<SelectionBounds, null>,
selection: DopeSheetSelection,
) {
if (
bounds.ys[0] > leaf.top + leaf.heightIncludingChildren ||
leaf.top > bounds.ys[1]
) {
return
}
const collector = collectorByLeafType[leaf.type]
if (collector) {
collector(layoutP, leaf as $IntentionalAny, bounds, selection)
} else {
collectChildren(layoutP, leaf, bounds, selection)
}
}
export function boundsToSelection(
layoutP: Pointer<SequenceEditorPanelLayout>,
bounds: Exclude<SelectionBounds, null>,
): DopeSheetSelection {
const sheet = val(layoutP.tree.sheet)
const selection: DopeSheetSelection = {
type: 'DopeSheetSelection',
byObjectKey: {},
getDragHandlers(origin) {
let tempTransaction: CommitOrDiscard | undefined
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
return {
onDragStart() {
toUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
},
onDrag(dx) {
const delta = toUnitSpace(dx)
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
const transformKeyframes =
stateEditors.coreByProject.historic.sheetsById.sequence
.transformKeyframes
for (const objectKey of Object.keys(selection.byObjectKey)) {
const {byTrackId} = selection.byObjectKey[objectKey]!
for (const trackId of Object.keys(byTrackId)) {
const {byKeyframeId} = byTrackId[trackId]!
transformKeyframes({
trackId,
keyframeIds: Object.keys(byKeyframeId),
translate: delta,
scale: 1,
origin: 0,
snappingFunction: sheet.getSequence().closestGridPosition,
objectKey,
projectId: origin.projectId,
sheetId: origin.sheetId,
})
}
}
})
},
onDragEnd(dragHappened) {
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
tempTransaction = undefined
},
}
},
delete() {
getStudio()!.transaction(({stateEditors}) => {
const deleteKeyframes =
stateEditors.coreByProject.historic.sheetsById.sequence
.deleteKeyframes
for (const objectKey of Object.keys(selection.byObjectKey)) {
const {byTrackId} = selection.byObjectKey[objectKey]!
for (const trackId of Object.keys(byTrackId)) {
const {byKeyframeId} = byTrackId[trackId]!
deleteKeyframes({
...sheet.address,
objectKey,
trackId,
keyframeIds: Object.keys(byKeyframeId),
})
}
}
})
},
}
bounds = sortBounds(bounds)
const tree = val(layoutP.tree)
collectFromAnyLeaf(layoutP, tree, bounds, selection)
return selection
}
}
const SelectionRectangleDiv = styled.div`
position: absolute;
background: rgba(255, 255, 255, 0.1);
border: 1px dashed rgba(255, 255, 255, 0.4);
box-size: border-box;
`
const sortBounds = (b: SelectionBounds): SelectionBounds => {
return {
positions: [...b.positions].sort(
(a, b) => a - b,
) as SelectionBounds['positions'],
ys: [...b.ys].sort((a, b) => a - b) as SelectionBounds['ys'],
}
}
const SelectionRectangle: React.FC<{
state: Exclude<SelectionBounds, null>
layoutP: Pointer<SequenceEditorPanelLayout>
}> = ({state, layoutP}) => {
const atom = useValToAtom(state)
return usePrism(() => {
const state = val(atom.pointer)
const sorted = sortBounds(state)
const unitSpaceToScaledSpace = val(layoutP.scaledSpace.fromUnitSpace)
const positionsInScaledSpace = sorted.positions.map(unitSpaceToScaledSpace)
const top = sorted.ys[0]
const height = sorted.ys[1] - sorted.ys[0]
const left = positionsInScaledSpace[0]
const width = positionsInScaledSpace[1] - positionsInScaledSpace[0]
return (
<SelectionRectangleDiv
style={{
top: top + 'px',
height: height + 'px',
left: left + 'px',
width: width + 'px',
}}
></SelectionRectangleDiv>
)
}, [layoutP, atom])
}
export default DopeSheetSelectionView