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 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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue