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 {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<SelectionBounds | null>(null)
const logger = useLogger('useCaptureSelection')
useDrag(
containerNode,
useMemo((): Parameters<typeof useDrag>[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<SequenceEditorPanelLayout>,
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<SequenceEditorPanelLayout>,
layout: SequenceEditorPanelLayout,
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
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<SequenceEditorPanelLayout>,
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<SequenceEditorPanelLayout>,
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<SequenceEditorPanelLayout>,
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]

View file

@ -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