Perfectly precise selections (#195)

Co-authored-by: Aria Minaei <aria.minaei@gmail.com>
This commit is contained in:
Andrew Prifer 2022-06-06 12:24:50 +02:00 committed by GitHub
parent c33467b4d0
commit c0fd71e4f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 99 additions and 45 deletions

View file

@ -25,6 +25,8 @@ import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
import type {ILogger, IUtilLogger} from '@theatre/shared/logger' import type {ILogger, IUtilLogger} from '@theatre/shared/logger'
import {useLogger} from '@theatre/studio/uiComponents/useLogger' import {useLogger} from '@theatre/studio/uiComponents/useLogger'
const HITBOX_SIZE_PX = 5
const Container = styled.div<{isShiftDown: boolean}>` const Container = styled.div<{isShiftDown: boolean}>`
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')}; 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 = { 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( function useCaptureSelection(
@ -62,6 +75,7 @@ function useCaptureSelection(
const [ref, state] = useRefAndState<SelectionBounds | null>(null) const [ref, state] = useRefAndState<SelectionBounds | null>(null)
const logger = useLogger('useCaptureSelection') const logger = useLogger('useCaptureSelection')
useDrag( useDrag(
containerNode, containerNode,
useMemo((): Parameters<typeof useDrag>[1] => { useMemo((): Parameters<typeof useDrag>[1] => {
@ -75,15 +89,21 @@ function useCaptureSelection(
} }
const rect = containerNode!.getBoundingClientRect() 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)( const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)(
posInScaledSpace, posInScaledSpace,
) )
ref.current = { ref.current = {
positions: [posInUnitSpace, posInUnitSpace], h: [posInUnitSpace, posInUnitSpace],
ys: [event.clientY - rect.top, event.clientY - rect.top], v: [event.clientY - rect.top, event.clientY - rect.top],
} }
val(layoutP.selectionAtom).setState({current: undefined}) val(layoutP.selectionAtom).setState({current: undefined})
@ -93,20 +113,24 @@ function useCaptureSelection(
// const state = ref.current! // const state = ref.current!
const rect = containerNode!.getBoundingClientRect() 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)( const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)(
posInScaledSpace, posInScaledSpace,
) )
ref.current = { ref.current = {
positions: [ref.current!.positions[0], posInUnitSpace], h: [ref.current!.h[0], posInUnitSpace],
ys: [ref.current!.ys[0], event.clientY - rect.top], v: [ref.current!.v[0], event.clientY - rect.top],
} }
const selection = utils.boundsToSelection( const selection = utils.boundsToSelection(
logger, logger,
layoutP, val(layoutP),
ref.current, ref.current,
) )
val(layoutP.selectionAtom).setState({current: selection}) val(layoutP.selectionAtom).setState({current: selection})
@ -126,7 +150,7 @@ function useCaptureSelection(
namespace utils { namespace utils {
const collectForAggregatedChildren = ( const collectForAggregatedChildren = (
logger: IUtilLogger, logger: IUtilLogger,
layoutP: Pointer<SequenceEditorPanelLayout>, layout: SequenceEditorPanelLayout,
leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren, leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren,
bounds: SelectionBounds, bounds: SelectionBounds,
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
@ -134,13 +158,21 @@ namespace utils {
const sheetObject = leaf.sheetObject const sheetObject = leaf.sheetObject
const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf) const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf)
const bottom = leaf.top + leaf.nodeHeight if (
if (bottom > bounds.ys[0]) { 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) { for (const [position, keyframes] of aggregatedKeyframes.byPosition) {
if (position <= bounds.positions[0]) continue if (
if (position >= bounds.positions[1]) break position + layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) <=
bounds.h[0]
// yes selected )
continue
if (
position - layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) >=
bounds.h[1]
)
break
for (const keyframeWithTrack of keyframes) { for (const keyframeWithTrack of keyframes) {
mutableSetDeep( mutableSetDeep(
@ -157,37 +189,37 @@ namespace utils {
} }
} }
collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) collectChildren(logger, layout, leaf, bounds, selectionByObjectKey)
} }
const collectorByLeafType: { const collectorByLeafType: {
[K in SequenceEditorTree_AllRowTypes['type']]?: ( [K in SequenceEditorTree_AllRowTypes['type']]?: (
logger: IUtilLogger, logger: IUtilLogger,
layoutP: Pointer<SequenceEditorPanelLayout>, layout: SequenceEditorPanelLayout,
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>, leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
bounds: SelectionBounds, bounds: SelectionBounds,
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
) => void ) => void
} = { } = {
propWithChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) { propWithChildren(logger, layout, leaf, bounds, selectionByObjectKey) {
collectForAggregatedChildren( collectForAggregatedChildren(
logger, logger,
layoutP, layout,
leaf, leaf,
bounds, bounds,
selectionByObjectKey, selectionByObjectKey,
) )
}, },
sheetObject(logger, layoutP, leaf, bounds, selectionByObjectKey) { sheetObject(logger, layout, leaf, bounds, selectionByObjectKey) {
collectForAggregatedChildren( collectForAggregatedChildren(
logger, logger,
layoutP, layout,
leaf, leaf,
bounds, bounds,
selectionByObjectKey, selectionByObjectKey,
) )
}, },
primitiveProp(logger, layoutP, leaf, bounds, selectionByObjectKey) { primitiveProp(logger, layout, leaf, bounds, selectionByObjectKey) {
const {sheetObject, trackId} = leaf const {sheetObject, trackId} = leaf
const trackData = val( const trackData = val(
getStudio().atomP.historic.coreByProject[sheetObject.address.projectId] getStudio().atomP.historic.coreByProject[sheetObject.address.projectId]
@ -196,9 +228,26 @@ namespace utils {
].trackData[trackId], ].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) { for (const kf of trackData.keyframes) {
if (kf.position <= bounds.positions[0]) continue if (
if (kf.position >= bounds.positions[1]) break 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( mutableSetDeep(
selectionByObjectKey, selectionByObjectKey,
@ -216,21 +265,21 @@ namespace utils {
const collectChildren = ( const collectChildren = (
logger: IUtilLogger, logger: IUtilLogger,
layoutP: Pointer<SequenceEditorPanelLayout>, layout: SequenceEditorPanelLayout,
leaf: SequenceEditorTree_AllRowTypes, leaf: SequenceEditorTree_AllRowTypes,
bounds: SelectionBounds, bounds: SelectionBounds,
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
) => { ) => {
if ('children' in leaf) { if ('children' in leaf) {
for (const sub of leaf.children) { for (const sub of leaf.children) {
collectFromAnyLeaf(logger, layoutP, sub, bounds, selectionByObjectKey) collectFromAnyLeaf(logger, layout, sub, bounds, selectionByObjectKey)
} }
} }
} }
function collectFromAnyLeaf( function collectFromAnyLeaf(
logger: IUtilLogger, logger: IUtilLogger,
layoutP: Pointer<SequenceEditorPanelLayout>, layout: SequenceEditorPanelLayout,
leaf: SequenceEditorTree_AllRowTypes, leaf: SequenceEditorTree_AllRowTypes,
bounds: SelectionBounds, bounds: SelectionBounds,
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
@ -239,8 +288,8 @@ namespace utils {
if (!leaf.shouldRender) return if (!leaf.shouldRender) return
if ( if (
bounds.ys[0] > leaf.top + leaf.heightIncludingChildren || bounds.v[0] > leaf.top + leaf.heightIncludingChildren ||
leaf.top > bounds.ys[1] leaf.top > bounds.v[1]
) { ) {
return return
} }
@ -248,34 +297,34 @@ namespace utils {
if (collector) { if (collector) {
collector( collector(
logger, logger,
layoutP, layout,
leaf as $IntentionalAny, leaf as $IntentionalAny,
bounds, bounds,
selectionByObjectKey, selectionByObjectKey,
) )
} else { } else {
collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) collectChildren(logger, layout, leaf, bounds, selectionByObjectKey)
} }
} }
export function boundsToSelection( export function boundsToSelection(
logger: ILogger, logger: ILogger,
layoutP: Pointer<SequenceEditorPanelLayout>, layout: SequenceEditorPanelLayout,
bounds: SelectionBounds, bounds: SelectionBounds,
): DopeSheetSelection { ): DopeSheetSelection {
const selectionByObjectKey: DopeSheetSelection['byObjectKey'] = {} const selectionByObjectKey: DopeSheetSelection['byObjectKey'] = {}
bounds = sortBounds(bounds) bounds = sortBounds(bounds)
const tree = val(layoutP.tree) const tree = layout.tree
collectFromAnyLeaf( collectFromAnyLeaf(
logger.utilFor.internal(), logger.utilFor.internal(),
layoutP, layout,
tree, tree,
bounds, bounds,
selectionByObjectKey, selectionByObjectKey,
) )
const sheet = val(layoutP.tree.sheet) const sheet = layout.tree.sheet
return { return {
type: 'DopeSheetSelection', type: 'DopeSheetSelection',
byObjectKey: selectionByObjectKey, byObjectKey: selectionByObjectKey,
@ -285,7 +334,7 @@ namespace utils {
onDragStart() { onDragStart() {
let tempTransaction: CommitOrDiscard | undefined let tempTransaction: CommitOrDiscard | undefined
const toUnitSpace = val(layoutP.scaledSpace.toUnitSpace) const toUnitSpace = layout.scaledSpace.toUnitSpace
return { return {
onDrag(dx, _, event) { onDrag(dx, _, event) {
@ -366,15 +415,13 @@ const SelectionRectangleDiv = styled.div`
position: absolute; position: absolute;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border: 1px dashed rgba(255, 255, 255, 0.4); border: 1px dashed rgba(255, 255, 255, 0.4);
box-size: border-box; box-sizing: border-box;
` `
const sortBounds = (b: SelectionBounds): SelectionBounds => { const sortBounds = (b: SelectionBounds): SelectionBounds => {
return { return {
positions: [...b.positions].sort( h: [...b.h].sort((a, b) => a - b) as SelectionBounds['h'],
(a, b) => a - b, v: [...b.v].sort((a, b) => a - b) as SelectionBounds['v'],
) as SelectionBounds['positions'],
ys: [...b.ys].sort((a, b) => a - b) as SelectionBounds['ys'],
} }
} }
@ -389,11 +436,15 @@ const SelectionRectangle: React.VFC<{
const sorted = sortBounds(state) const sorted = sortBounds(state)
const unitSpaceToScaledSpace = val(layoutP.scaledSpace.fromUnitSpace) 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 top = sorted.v[0]
const height = sorted.ys[1] - sorted.ys[0] const height = sorted.v[1] - sorted.v[0]
const left = positionsInScaledSpace[0] const left = positionsInScaledSpace[0]
const width = positionsInScaledSpace[1] - positionsInScaledSpace[0] const width = positionsInScaledSpace[1] - positionsInScaledSpace[0]

View file

@ -141,6 +141,9 @@ export type SequenceEditorPanelLayout = {
} }
unitSpace: {} unitSpace: {}
scaledSpace: { scaledSpace: {
/**
* TODO - scaledSpace with and without leftPadding are two different spaces. See if we can divide them so
*/
leftPadding: number leftPadding: number
fromUnitSpace(u: number): number fromUnitSpace(u: number): number
toUnitSpace(s: number): number toUnitSpace(s: number): number