From c0fd71e4f9eb56dbeb9331dd8de158c06120066c Mon Sep 17 00:00:00 2001 From: Andrew Prifer <2991360+AndrewPrifer@users.noreply.github.com> Date: Mon, 6 Jun 2022 12:24:50 +0200 Subject: [PATCH] Perfectly precise selections (#195) Co-authored-by: Aria Minaei --- .../Right/DopeSheetSelectionView.tsx | 141 ++++++++++++------ .../SequenceEditorPanel/layout/layout.ts | 3 + 2 files changed, 99 insertions(+), 45 deletions(-) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx index 185059c..dc772a3 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx @@ -25,6 +25,8 @@ import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes' import type {ILogger, IUtilLogger} from '@theatre/shared/logger' import {useLogger} from '@theatre/studio/uiComponents/useLogger' +const HITBOX_SIZE_PX = 5 + const Container = styled.div<{isShiftDown: boolean}>` cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')}; ` @@ -50,9 +52,20 @@ const DopeSheetSelectionView: React.FC<{ ) } +/** + * The horizontal and vertical bounds of the selection, each represented by a tuple in the form of [from, to]. + */ type SelectionBounds = { - positions: [from: number, to: number] - ys: [from: number, to: number] + /** + * The horizontal bounds of the selection as a tuple of "from" and "to" coordinates, "from" representing the start of the drag. + * + * TODO - use nominal types here to clarify which space these numbers are in + */ + h: [from: number, to: number] + /** + * The vertical bounds of the selection as a tuple of "from" and "to" coordinates, "from" representing the start of the drag. + */ + v: [from: number, to: number] } function useCaptureSelection( @@ -62,6 +75,7 @@ function useCaptureSelection( const [ref, state] = useRefAndState(null) const logger = useLogger('useCaptureSelection') + useDrag( containerNode, useMemo((): Parameters[1] => { @@ -75,15 +89,21 @@ function useCaptureSelection( } const rect = containerNode!.getBoundingClientRect() - const posInScaledSpace = event.clientX - rect.left + // all the `val()` calls here are meant to be read cold + + const posInScaledSpace = + event.clientX - + rect.left - + // selection is happening in left padded space, convert it to normal space + val(layoutP.scaledSpace.leftPadding) const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)( posInScaledSpace, ) ref.current = { - positions: [posInUnitSpace, posInUnitSpace], - ys: [event.clientY - rect.top, event.clientY - rect.top], + h: [posInUnitSpace, posInUnitSpace], + v: [event.clientY - rect.top, event.clientY - rect.top], } val(layoutP.selectionAtom).setState({current: undefined}) @@ -93,20 +113,24 @@ function useCaptureSelection( // const state = ref.current! const rect = containerNode!.getBoundingClientRect() - const posInScaledSpace = event.clientX - rect.left + const posInScaledSpace = + event.clientX - + rect.left - + // selection is happening in left padded space, convert it to normal space + val(layoutP.scaledSpace.leftPadding) const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)( posInScaledSpace, ) ref.current = { - positions: [ref.current!.positions[0], posInUnitSpace], - ys: [ref.current!.ys[0], event.clientY - rect.top], + h: [ref.current!.h[0], posInUnitSpace], + v: [ref.current!.v[0], event.clientY - rect.top], } const selection = utils.boundsToSelection( logger, - layoutP, + val(layoutP), ref.current, ) val(layoutP.selectionAtom).setState({current: selection}) @@ -126,7 +150,7 @@ function useCaptureSelection( namespace utils { const collectForAggregatedChildren = ( logger: IUtilLogger, - layoutP: Pointer, + layout: SequenceEditorPanelLayout, leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren, bounds: SelectionBounds, selectionByObjectKey: DopeSheetSelection['byObjectKey'], @@ -134,13 +158,21 @@ namespace utils { const sheetObject = leaf.sheetObject const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf) - const bottom = leaf.top + leaf.nodeHeight - if (bottom > bounds.ys[0]) { + if ( + leaf.top + leaf.nodeHeight / 2 + HITBOX_SIZE_PX > bounds.v[0] && + leaf.top + leaf.nodeHeight / 2 - HITBOX_SIZE_PX < bounds.v[1] + ) { for (const [position, keyframes] of aggregatedKeyframes.byPosition) { - if (position <= bounds.positions[0]) continue - if (position >= bounds.positions[1]) break - - // yes selected + if ( + position + layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) <= + bounds.h[0] + ) + continue + if ( + position - layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) >= + bounds.h[1] + ) + break for (const keyframeWithTrack of keyframes) { mutableSetDeep( @@ -157,37 +189,37 @@ namespace utils { } } - collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) + collectChildren(logger, layout, leaf, bounds, selectionByObjectKey) } const collectorByLeafType: { [K in SequenceEditorTree_AllRowTypes['type']]?: ( logger: IUtilLogger, - layoutP: Pointer, + layout: SequenceEditorPanelLayout, leaf: Extract, bounds: SelectionBounds, selectionByObjectKey: DopeSheetSelection['byObjectKey'], ) => void } = { - propWithChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) { + propWithChildren(logger, layout, leaf, bounds, selectionByObjectKey) { collectForAggregatedChildren( logger, - layoutP, + layout, leaf, bounds, selectionByObjectKey, ) }, - sheetObject(logger, layoutP, leaf, bounds, selectionByObjectKey) { + sheetObject(logger, layout, leaf, bounds, selectionByObjectKey) { collectForAggregatedChildren( logger, - layoutP, + layout, leaf, bounds, selectionByObjectKey, ) }, - primitiveProp(logger, layoutP, leaf, bounds, selectionByObjectKey) { + primitiveProp(logger, layout, leaf, bounds, selectionByObjectKey) { const {sheetObject, trackId} = leaf const trackData = val( getStudio().atomP.historic.coreByProject[sheetObject.address.projectId] @@ -196,9 +228,26 @@ namespace utils { ].trackData[trackId], )! + if ( + bounds.v[0] > + leaf.top + leaf.heightIncludingChildren / 2 + HITBOX_SIZE_PX || + leaf.top + leaf.heightIncludingChildren / 2 - HITBOX_SIZE_PX > + bounds.v[1] + ) { + return + } + for (const kf of trackData.keyframes) { - if (kf.position <= bounds.positions[0]) continue - if (kf.position >= bounds.positions[1]) break + if ( + kf.position + layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) <= + bounds.h[0] + ) + continue + if ( + kf.position - layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) >= + bounds.h[1] + ) + break mutableSetDeep( selectionByObjectKey, @@ -216,21 +265,21 @@ namespace utils { const collectChildren = ( logger: IUtilLogger, - layoutP: Pointer, + layout: SequenceEditorPanelLayout, leaf: SequenceEditorTree_AllRowTypes, bounds: SelectionBounds, selectionByObjectKey: DopeSheetSelection['byObjectKey'], ) => { if ('children' in leaf) { for (const sub of leaf.children) { - collectFromAnyLeaf(logger, layoutP, sub, bounds, selectionByObjectKey) + collectFromAnyLeaf(logger, layout, sub, bounds, selectionByObjectKey) } } } function collectFromAnyLeaf( logger: IUtilLogger, - layoutP: Pointer, + layout: SequenceEditorPanelLayout, leaf: SequenceEditorTree_AllRowTypes, bounds: SelectionBounds, selectionByObjectKey: DopeSheetSelection['byObjectKey'], @@ -239,8 +288,8 @@ namespace utils { if (!leaf.shouldRender) return if ( - bounds.ys[0] > leaf.top + leaf.heightIncludingChildren || - leaf.top > bounds.ys[1] + bounds.v[0] > leaf.top + leaf.heightIncludingChildren || + leaf.top > bounds.v[1] ) { return } @@ -248,34 +297,34 @@ namespace utils { if (collector) { collector( logger, - layoutP, + layout, leaf as $IntentionalAny, bounds, selectionByObjectKey, ) } else { - collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) + collectChildren(logger, layout, leaf, bounds, selectionByObjectKey) } } export function boundsToSelection( logger: ILogger, - layoutP: Pointer, + layout: SequenceEditorPanelLayout, bounds: SelectionBounds, ): DopeSheetSelection { const selectionByObjectKey: DopeSheetSelection['byObjectKey'] = {} bounds = sortBounds(bounds) - const tree = val(layoutP.tree) + const tree = layout.tree collectFromAnyLeaf( logger.utilFor.internal(), - layoutP, + layout, tree, bounds, selectionByObjectKey, ) - const sheet = val(layoutP.tree.sheet) + const sheet = layout.tree.sheet return { type: 'DopeSheetSelection', byObjectKey: selectionByObjectKey, @@ -285,7 +334,7 @@ namespace utils { onDragStart() { let tempTransaction: CommitOrDiscard | undefined - const toUnitSpace = val(layoutP.scaledSpace.toUnitSpace) + const toUnitSpace = layout.scaledSpace.toUnitSpace return { onDrag(dx, _, event) { @@ -366,15 +415,13 @@ 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; + box-sizing: 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'], + h: [...b.h].sort((a, b) => a - b) as SelectionBounds['h'], + v: [...b.v].sort((a, b) => a - b) as SelectionBounds['v'], } } @@ -389,11 +436,15 @@ const SelectionRectangle: React.VFC<{ const sorted = sortBounds(state) const unitSpaceToScaledSpace = val(layoutP.scaledSpace.fromUnitSpace) + const leftPadding = val(layoutP.scaledSpace.leftPadding) - const positionsInScaledSpace = sorted.positions.map(unitSpaceToScaledSpace) + const positionsInScaledSpace = sorted.h + .map(unitSpaceToScaledSpace) + // bounds are in normal space, convert them left-padded space + .map((coord) => coord + leftPadding) - const top = sorted.ys[0] - const height = sorted.ys[1] - sorted.ys[0] + const top = sorted.v[0] + const height = sorted.v[1] - sorted.v[0] const left = positionsInScaledSpace[0] const width = positionsInScaledSpace[1] - positionsInScaledSpace[0] diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts index f644226..8d3cd5d 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts @@ -141,6 +141,9 @@ export type SequenceEditorPanelLayout = { } unitSpace: {} scaledSpace: { + /** + * TODO - scaledSpace with and without leftPadding are two different spaces. See if we can divide them so + */ leftPadding: number fromUnitSpace(u: number): number toUnitSpace(s: number): number