Perfectly precise selections (#195)
Co-authored-by: Aria Minaei <aria.minaei@gmail.com>
This commit is contained in:
parent
c33467b4d0
commit
c0fd71e4f9
2 changed files with 99 additions and 45 deletions
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue