Perfect snapping behavior for all snap targets (#203)
Co-authored-by: Andrew Prifer <AndrewPrifer@users.noreply.github.com>
This commit is contained in:
parent
3b3a1b1d8a
commit
b323588d78
10 changed files with 405 additions and 65 deletions
|
@ -1,7 +1,6 @@
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React, {useMemo, useRef} from 'react'
|
import React, {useMemo, useRef} from 'react'
|
||||||
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
|
@ -21,6 +20,13 @@ import {
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||||
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric'
|
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric'
|
||||||
import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses'
|
import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses'
|
||||||
|
import type {ILogger} from '@theatre/shared/logger'
|
||||||
|
import {
|
||||||
|
collectKeyframeSnapPositions,
|
||||||
|
snapToNone,
|
||||||
|
snapToSome,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
|
|
||||||
type IAggregateKeyframeDotProps = {
|
type IAggregateKeyframeDotProps = {
|
||||||
editorProps: IAggregateKeyframeEditorProps
|
editorProps: IAggregateKeyframeEditorProps
|
||||||
|
@ -41,17 +47,11 @@ export function AggregateKeyframeDot(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [contextMenu] = useAggregateKeyframeContextMenu(props, node)
|
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone ref={ref} />
|
||||||
ref={ref}
|
|
||||||
{...DopeSnapHitZoneUI.reactProps({
|
|
||||||
isDragging,
|
|
||||||
position: cur.position,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<AggregateKeyframeVisualDot
|
<AggregateKeyframeVisualDot
|
||||||
isAllHere={cur.allHere}
|
isAllHere={cur.allHere}
|
||||||
isSelected={cur.selected}
|
isSelected={cur.selected}
|
||||||
|
@ -63,6 +63,7 @@ export function AggregateKeyframeDot(
|
||||||
|
|
||||||
function useAggregateKeyframeContextMenu(
|
function useAggregateKeyframeContextMenu(
|
||||||
props: IAggregateKeyframeDotProps,
|
props: IAggregateKeyframeDotProps,
|
||||||
|
logger: ILogger,
|
||||||
target: HTMLDivElement | null,
|
target: HTMLDivElement | null,
|
||||||
) {
|
) {
|
||||||
return useContextMenu(target, {
|
return useContextMenu(target, {
|
||||||
|
@ -138,6 +139,9 @@ function useAggregateKeyframeContextMenu(
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
onOpen() {
|
||||||
|
logger._debug('Show aggregate keyframe', props)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +169,38 @@ function useDragForAggregateKeyframeDot(
|
||||||
const props = propsRef.current
|
const props = propsRef.current
|
||||||
const keyframes = keyframesRef.current
|
const keyframes = keyframesRef.current
|
||||||
|
|
||||||
|
const tracksByObject = val(
|
||||||
|
getStudio()!.atomP.historic.coreByProject[
|
||||||
|
props.viewModel.sheetObject.address.projectId
|
||||||
|
].sheetsById[props.viewModel.sheetObject.address.sheetId].sequence
|
||||||
|
.tracksByObject,
|
||||||
|
)!
|
||||||
|
|
||||||
|
// Calculate all the valid snap positions in the sequence editor,
|
||||||
|
// excluding the child keyframes of this aggregate, and any selection it is part of.
|
||||||
|
const snapPositions = collectKeyframeSnapPositions(
|
||||||
|
tracksByObject,
|
||||||
|
function shouldIncludeKeyfram(keyframe, {trackId, objectKey}) {
|
||||||
|
return (
|
||||||
|
// we exclude all the child keyframes of this aggregate keyframe from being a snap target
|
||||||
|
keyframes.every(
|
||||||
|
(kfWithTrack) => keyframe.id !== kfWithTrack.kf.id,
|
||||||
|
) &&
|
||||||
|
!(
|
||||||
|
// if all of the children of the current aggregate keyframe are in a selection,
|
||||||
|
(
|
||||||
|
props.selection &&
|
||||||
|
// then we exclude them and all other keyframes in the selection from being snap targets
|
||||||
|
props.selection.byObjectKey[objectKey]?.byTrackId[trackId]
|
||||||
|
?.byKeyframeId[keyframe.id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
snapToSome(snapPositions)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.selection &&
|
props.selection &&
|
||||||
props.aggregateKeyframes[props.index].selected ===
|
props.aggregateKeyframes[props.index].selected ===
|
||||||
|
@ -172,7 +208,7 @@ function useDragForAggregateKeyframeDot(
|
||||||
) {
|
) {
|
||||||
const {selection, viewModel} = props
|
const {selection, viewModel} = props
|
||||||
const {sheetObject} = viewModel
|
const {sheetObject} = viewModel
|
||||||
const hanlders = selection
|
const handlers = selection
|
||||||
.getDragHandlers({
|
.getDragHandlers({
|
||||||
...sheetObject.address,
|
...sheetObject.address,
|
||||||
domNode: node!,
|
domNode: node!,
|
||||||
|
@ -180,7 +216,16 @@ function useDragForAggregateKeyframeDot(
|
||||||
})
|
})
|
||||||
.onDragStart(event)
|
.onDragStart(event)
|
||||||
|
|
||||||
return hanlders && {...hanlders, onClick: options.onClickFromDrag}
|
return (
|
||||||
|
handlers && {
|
||||||
|
...handlers,
|
||||||
|
onClick: options.onClickFromDrag,
|
||||||
|
onDragEnd: (...args) => {
|
||||||
|
handlers.onDragEnd?.(...args)
|
||||||
|
snapToNone()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsAtStartOfDrag = props
|
const propsAtStartOfDrag = props
|
||||||
|
@ -227,6 +272,8 @@ function useDragForAggregateKeyframeDot(
|
||||||
} else {
|
} else {
|
||||||
tempTransaction?.discard()
|
tempTransaction?.discard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snapToNone()
|
||||||
},
|
},
|
||||||
onClick(ev) {
|
onClick(ev) {
|
||||||
options.onClickFromDrag(ev)
|
options.onClickFromDrag(ev)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
|
||||||
const DOT_SIZE_PX = 16
|
const DOT_SIZE_PX = 16
|
||||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||||
|
@ -20,16 +20,15 @@ export const HitZone = styled.div`
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
||||||
${DopeSnapHitZoneUI.CSS}
|
position: absolute;
|
||||||
|
${absoluteDims(12)};
|
||||||
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
&:hover
|
||||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
+ ${DotContainer},
|
||||||
}
|
#pointer-root.draggingPositionInSequenceEditor
|
||||||
|
&:hover
|
||||||
&:hover + ${DotContainer},
|
+ ${DotContainer} {
|
||||||
#pointer-root.draggingPositionInSequenceEditor &:hover + ${DotContainer},
|
|
||||||
// notice "," css "or"
|
|
||||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} + ${DotContainer} {
|
|
||||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -7,11 +7,11 @@ import type {
|
||||||
SequenceEditorTree_SheetObject,
|
SequenceEditorTree_SheetObject,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import {usePrism} from '@theatre/react'
|
import {usePrism, useVal} from '@theatre/react'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {valueDerivation} from '@theatre/dataverse'
|
import {valueDerivation} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React, {Fragment, useMemo} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
|
@ -21,16 +21,20 @@ import AggregateKeyframeEditor from './AggregateKeyframeEditor/AggregateKeyframe
|
||||||
import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {
|
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||||
SheetObjectAddress} from '@theatre/shared/utils/addresses';
|
|
||||||
import {
|
import {
|
||||||
decodePathToProp,
|
decodePathToProp,
|
||||||
doesPathStartWith,
|
doesPathStartWith,
|
||||||
encodePathToProp
|
encodePathToProp,
|
||||||
} from '@theatre/shared/utils/addresses'
|
} from '@theatre/shared/utils/addresses'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||||
import type Sequence from '@theatre/core/sequences/Sequence'
|
import type Sequence from '@theatre/core/sequences/Sequence'
|
||||||
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
|
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
|
||||||
|
import {collectAggregateSnapPositions} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
|
import KeyframeSnapTarget, {
|
||||||
|
snapPositionsStateD,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
import {emptyObject} from '@theatre/shared/utils'
|
||||||
|
|
||||||
const AggregatedKeyframeTrackContainer = styled.div`
|
const AggregatedKeyframeTrackContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -88,19 +92,48 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const snapPositionsState = useVal(snapPositionsStateD)
|
||||||
|
|
||||||
|
const snapToAllKeyframes = snapPositionsState.mode === 'snapToAll'
|
||||||
|
|
||||||
|
const snapPositions =
|
||||||
|
snapPositionsState.mode === 'snapToSome'
|
||||||
|
? snapPositionsState.positions
|
||||||
|
: emptyObject
|
||||||
|
|
||||||
|
const aggregateSnapPositions = useMemo(
|
||||||
|
() => collectAggregateSnapPositions(viewModel, snapPositions),
|
||||||
|
[snapPositions],
|
||||||
|
)
|
||||||
|
|
||||||
|
const snapTargets = aggregateSnapPositions.map((position) => (
|
||||||
|
<KeyframeSnapTarget
|
||||||
|
key={'snap-target-' + position}
|
||||||
|
layoutP={layoutP}
|
||||||
|
leaf={viewModel}
|
||||||
|
position={position}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
|
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
|
||||||
|
<Fragment key={'agg-' + keyframes[0].kf.id}>
|
||||||
|
{snapToAllKeyframes && (
|
||||||
|
<KeyframeSnapTarget
|
||||||
|
layoutP={layoutP}
|
||||||
|
leaf={viewModel}
|
||||||
|
position={position}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<AggregateKeyframeEditor
|
<AggregateKeyframeEditor
|
||||||
index={index}
|
index={index}
|
||||||
layoutP={layoutP}
|
layoutP={layoutP}
|
||||||
viewModel={viewModel}
|
viewModel={viewModel}
|
||||||
aggregateKeyframes={posKfs}
|
aggregateKeyframes={posKfs}
|
||||||
// To ensure that while dragging, we don't lose reference to the
|
|
||||||
// aggregate we're trying to drag.
|
|
||||||
key={'agg-' + keyframes[0].kf.id}
|
|
||||||
selection={
|
selection={
|
||||||
selectedPositions.has(position) === true ? selection : undefined
|
selectedPositions.has(position) === true ? selection : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Fragment>
|
||||||
))
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -111,6 +144,7 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{keyframeEditors}
|
{keyframeEditors}
|
||||||
|
{snapTargets}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</AggregatedKeyframeTrackContainer>
|
</AggregatedKeyframeTrackContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,10 @@ import type {TrackData} from '@theatre/core/projects/store/types/SheetState_Hist
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import type {SequenceEditorTree_PrimitiveProp} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
import type {SequenceEditorTree_PrimitiveProp} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import {usePrism} from '@theatre/react'
|
import {usePrism, useVal} from '@theatre/react'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React, {Fragment} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import SingleKeyframeEditor from './KeyframeEditor/SingleKeyframeEditor'
|
import SingleKeyframeEditor from './KeyframeEditor/SingleKeyframeEditor'
|
||||||
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
|
@ -14,6 +14,9 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import {arePathsEqual} from '@theatre/shared/utils/addresses'
|
import {arePathsEqual} from '@theatre/shared/utils/addresses'
|
||||||
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
|
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
|
||||||
|
import KeyframeSnapTarget, {
|
||||||
|
snapPositionsStateD,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -58,15 +61,45 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
||||||
props,
|
props,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const snapPositionsState = useVal(snapPositionsStateD)
|
||||||
|
|
||||||
|
const snapPositions =
|
||||||
|
snapPositionsState.mode === 'snapToSome'
|
||||||
|
? snapPositionsState.positions[leaf.sheetObject.address.objectKey]?.[
|
||||||
|
leaf.trackId
|
||||||
|
]
|
||||||
|
: [] ?? []
|
||||||
|
|
||||||
|
const snapToAllKeyframes = snapPositionsState.mode === 'snapToAll'
|
||||||
|
|
||||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||||
|
<Fragment key={'keyframe-' + kf.id}>
|
||||||
|
{snapToAllKeyframes && (
|
||||||
|
<KeyframeSnapTarget
|
||||||
|
layoutP={layoutP}
|
||||||
|
leaf={leaf}
|
||||||
|
position={kf.position}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SingleKeyframeEditor
|
<SingleKeyframeEditor
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
trackData={trackData}
|
trackData={trackData}
|
||||||
layoutP={layoutP}
|
layoutP={layoutP}
|
||||||
leaf={leaf}
|
leaf={leaf}
|
||||||
key={'keyframe-' + kf.id}
|
selection={
|
||||||
selection={selectedKeyframeIds[kf.id] === true ? selection : undefined}
|
selectedKeyframeIds[kf.id] === true ? selection : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
|
||||||
|
const snapTargets = snapPositions.map((position) => (
|
||||||
|
<KeyframeSnapTarget
|
||||||
|
key={'snap-target-' + position}
|
||||||
|
layoutP={layoutP}
|
||||||
|
leaf={leaf}
|
||||||
|
position={position}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -78,6 +111,7 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{keyframeEditors}
|
{keyframeEditors}
|
||||||
|
{snapTargets}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import last from 'lodash-es/last'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
|
||||||
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
@ -19,10 +19,15 @@ import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
|
||||||
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
|
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
|
||||||
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
|
||||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
import type {ILogger} from '@theatre/shared/logger'
|
import type {ILogger} from '@theatre/shared/logger'
|
||||||
import {copyableKeyframesFromSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
import {copyableKeyframesFromSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||||
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
import {
|
||||||
|
collectKeyframeSnapPositions,
|
||||||
|
snapToNone,
|
||||||
|
snapToSome,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
|
||||||
export const DOT_SIZE_PX = 6
|
export const DOT_SIZE_PX = 6
|
||||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||||
|
@ -49,17 +54,11 @@ const HitZone = styled.div`
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
||||||
${DopeSnapHitZoneUI.CSS}
|
position: absolute;
|
||||||
|
${absoluteDims(12)};
|
||||||
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
&:hover + ${Diamond} {
|
||||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
+ ${Diamond},
|
|
||||||
// notice , "or" in CSS
|
|
||||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
|
||||||
+ ${Diamond} {
|
|
||||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -82,13 +81,7 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone ref={ref} />
|
||||||
ref={ref}
|
|
||||||
{...DopeSnapHitZoneUI.reactProps({
|
|
||||||
isDragging,
|
|
||||||
position: props.keyframe.position,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Diamond isSelected={!!props.selection} />
|
<Diamond isSelected={!!props.selection} />
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
|
@ -210,6 +203,37 @@ function useDragForSingleKeyframeDot(
|
||||||
debugName: 'KeyframeDot/useDragKeyframe',
|
debugName: 'KeyframeDot/useDragKeyframe',
|
||||||
onDragStart(event) {
|
onDragStart(event) {
|
||||||
const props = propsRef.current
|
const props = propsRef.current
|
||||||
|
|
||||||
|
const tracksByObject = val(
|
||||||
|
getStudio()!.atomP.historic.coreByProject[
|
||||||
|
props.leaf.sheetObject.address.projectId
|
||||||
|
].sheetsById[props.leaf.sheetObject.address.sheetId].sequence
|
||||||
|
.tracksByObject,
|
||||||
|
)!
|
||||||
|
|
||||||
|
const snapPositions = collectKeyframeSnapPositions(
|
||||||
|
tracksByObject,
|
||||||
|
// Calculate all the valid snap positions in the sequence editor,
|
||||||
|
// excluding this keyframe, and any selection it is part of.
|
||||||
|
function shouldIncludeKeyfram(keyframe, {trackId, objectKey}) {
|
||||||
|
return (
|
||||||
|
// we exclude this keyframe from being a snap target
|
||||||
|
keyframe.id !== props.keyframe.id &&
|
||||||
|
!(
|
||||||
|
// if the current dragged keyframe is in the selection,
|
||||||
|
(
|
||||||
|
props.selection &&
|
||||||
|
// then we exclude it and all other keyframes in the selection from being snap targets
|
||||||
|
props.selection.byObjectKey[objectKey]?.byTrackId[trackId]
|
||||||
|
?.byKeyframeId[keyframe.id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
snapToSome(snapPositions)
|
||||||
|
|
||||||
if (props.selection) {
|
if (props.selection) {
|
||||||
const {selection, leaf} = props
|
const {selection, leaf} = props
|
||||||
const {sheetObject} = leaf
|
const {sheetObject} = leaf
|
||||||
|
@ -226,7 +250,16 @@ function useDragForSingleKeyframeDot(
|
||||||
// in the future, we may want to show an multi-editor, like in the
|
// in the future, we may want to show an multi-editor, like in the
|
||||||
// single tween editor, so that selected keyframes' values can be changed
|
// single tween editor, so that selected keyframes' values can be changed
|
||||||
// together
|
// together
|
||||||
return handlers && {...handlers, onClick: options.onClickFromDrag}
|
return (
|
||||||
|
handlers && {
|
||||||
|
...handlers,
|
||||||
|
onClick: options.onClickFromDrag,
|
||||||
|
onDragEnd: (...args) => {
|
||||||
|
handlers.onDragEnd?.(...args)
|
||||||
|
snapToNone()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsAtStartOfDrag = props
|
const propsAtStartOfDrag = props
|
||||||
|
@ -272,6 +305,8 @@ function useDragForSingleKeyframeDot(
|
||||||
} else {
|
} else {
|
||||||
tempTransaction?.discard()
|
tempTransaction?.discard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snapToNone()
|
||||||
},
|
},
|
||||||
onClick(ev) {
|
onClick(ev) {
|
||||||
options.onClickFromDrag(ev)
|
options.onClickFromDrag(ev)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import type {IRange} from '@theatre/shared/utils/types'
|
import type {IRange} from '@theatre/shared/utils/types'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||||
|
import {snapToAll, snapToNone} from './KeyframeSnapTarget'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -115,6 +116,8 @@ function useDragPlayheadHandlers(
|
||||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||||
setIsSeeking(true)
|
setIsSeeking(true)
|
||||||
|
|
||||||
|
snapToAll()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDrag(dx: number, _, event) {
|
onDrag(dx: number, _, event) {
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
|
@ -135,6 +138,7 @@ function useDragPlayheadHandlers(
|
||||||
},
|
},
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
setIsSeeking(false)
|
setIsSeeking(false)
|
||||||
|
snapToNone()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
import {Box, val} from '@theatre/dataverse'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
||||||
|
import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||||
|
import type {
|
||||||
|
BasicKeyframedTrack,
|
||||||
|
HistoricPositionalSequence,
|
||||||
|
Keyframe,
|
||||||
|
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
|
|
||||||
|
const HitZone = styled.div`
|
||||||
|
z-index: 1;
|
||||||
|
cursor: ew-resize;
|
||||||
|
|
||||||
|
${DopeSnapHitZoneUI.CSS}
|
||||||
|
|
||||||
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
|
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
`
|
||||||
|
|
||||||
|
export type ISnapTargetPRops = {
|
||||||
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
|
leaf: {nodeHeight: number}
|
||||||
|
position: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyframeSnapTarget: React.VFC<ISnapTargetPRops> = (props) => {
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
style={{
|
||||||
|
top: `${props.leaf.nodeHeight / 2}px`,
|
||||||
|
left: `calc(${val(
|
||||||
|
props.layoutP.scaledSpace.leftPadding,
|
||||||
|
)}px + calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||||
|
props.position
|
||||||
|
}px))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HitZone
|
||||||
|
{...DopeSnapHitZoneUI.reactProps({
|
||||||
|
isDragging: false,
|
||||||
|
position: props.position,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyframeSnapTarget
|
||||||
|
|
||||||
|
export type KeyframeSnapPositions = {
|
||||||
|
[objectKey: ObjectAddressKey]: {
|
||||||
|
[trackId: SequenceTrackId]: number[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateB = new Box<
|
||||||
|
| {
|
||||||
|
// all keyframes must be snap targets
|
||||||
|
mode: 'snapToAll'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// only these keyframes must be snap targets
|
||||||
|
mode: 'snapToSome'
|
||||||
|
positions: KeyframeSnapPositions
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// no keyframe should be a snap target
|
||||||
|
mode: 'snapToNone'
|
||||||
|
}
|
||||||
|
>({mode: 'snapToNone'})
|
||||||
|
|
||||||
|
export const snapPositionsStateD = stateB.derivation
|
||||||
|
|
||||||
|
export function snapToAll() {
|
||||||
|
stateB.set({mode: 'snapToAll'})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snapToNone() {
|
||||||
|
stateB.set({mode: 'snapToNone'})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snapToSome(positions: KeyframeSnapPositions) {
|
||||||
|
stateB.set({mode: 'snapToSome', positions})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectKeyframeSnapPositions(
|
||||||
|
tracksByObject: HistoricPositionalSequence['tracksByObject'],
|
||||||
|
shouldIncludeKeyframe: (
|
||||||
|
kf: Keyframe,
|
||||||
|
track: {
|
||||||
|
trackId: SequenceTrackId
|
||||||
|
trackData: BasicKeyframedTrack
|
||||||
|
objectKey: ObjectAddressKey
|
||||||
|
},
|
||||||
|
) => boolean,
|
||||||
|
): KeyframeSnapPositions {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(tracksByObject).map(
|
||||||
|
([objectKey, trackDataAndTrackIdByPropPath]) => [
|
||||||
|
objectKey,
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(trackDataAndTrackIdByPropPath!.trackData).map(
|
||||||
|
([trackId, track]) => [
|
||||||
|
trackId,
|
||||||
|
track!.keyframes
|
||||||
|
.filter((kf) =>
|
||||||
|
shouldIncludeKeyframe(kf, {
|
||||||
|
trackId,
|
||||||
|
trackData: track!,
|
||||||
|
objectKey,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map((keyframe) => keyframe.position),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import type {IUtilLogger} from '@theatre/shared/logger'
|
import type {IUtilLogger} from '@theatre/shared/logger'
|
||||||
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
||||||
|
import {uniq} from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An index over a series of keyframes that have been collected from different tracks.
|
* An index over a series of keyframes that have been collected from different tracks.
|
||||||
|
@ -128,3 +129,45 @@ export function collectAggregateKeyframesInPrism(
|
||||||
tracks,
|
tracks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects all the snap positions for an aggregate track.
|
||||||
|
*/
|
||||||
|
export function collectAggregateSnapPositions(
|
||||||
|
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject,
|
||||||
|
snapTargetPositions: {[key: string]: {[key: string]: number[]}},
|
||||||
|
): number[] {
|
||||||
|
const sheetObject = leaf.sheetObject
|
||||||
|
|
||||||
|
const projectId = sheetObject.address.projectId
|
||||||
|
|
||||||
|
const sheetObjectTracksP =
|
||||||
|
getStudio().atomP.historic.coreByProject[projectId].sheetsById[
|
||||||
|
sheetObject.address.sheetId
|
||||||
|
].sequence.tracksByObject[sheetObject.address.objectKey]
|
||||||
|
|
||||||
|
const positions: number[] = []
|
||||||
|
for (const childLeaf of leaf.children) {
|
||||||
|
if (childLeaf.type === 'primitiveProp') {
|
||||||
|
const trackId = val(
|
||||||
|
sheetObjectTracksP.trackIdByPropPath[
|
||||||
|
encodePathToProp(childLeaf.pathToProp)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if (!trackId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.push(
|
||||||
|
...(snapTargetPositions[sheetObject.address.objectKey]?.[trackId] ??
|
||||||
|
[]),
|
||||||
|
)
|
||||||
|
} else if (childLeaf.type === 'propWithChildren') {
|
||||||
|
positions.push(
|
||||||
|
...collectAggregateSnapPositions(childLeaf, snapTargetPositions),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniq(positions)
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@ import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEdito
|
||||||
import DopeSnap from './DopeSnap'
|
import DopeSnap from './DopeSnap'
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
import {DopeSnapHitZoneUI} from './DopeSnapHitZoneUI'
|
import {DopeSnapHitZoneUI} from './DopeSnapHitZoneUI'
|
||||||
|
import {
|
||||||
|
snapToAll,
|
||||||
|
snapToNone,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
|
||||||
const MARKER_SIZE_W_PX = 12
|
const MARKER_SIZE_W_PX = 12
|
||||||
const MARKER_SIZE_H_PX = 12
|
const MARKER_SIZE_H_PX = 12
|
||||||
|
@ -221,6 +225,8 @@ function useDragMarker(
|
||||||
const toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace)
|
const toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace)
|
||||||
let tempTransaction: CommitOrDiscard | undefined
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
|
|
||||||
|
snapToAll()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDrag(dx, _dy, event) {
|
onDrag(dx, _dy, event) {
|
||||||
const original = markerAtStartOfDrag
|
const original = markerAtStartOfDrag
|
||||||
|
@ -250,6 +256,8 @@ function useDragMarker(
|
||||||
onDragEnd(dragHappened) {
|
onDragEnd(dragHappened) {
|
||||||
if (dragHappened) tempTransaction?.commit()
|
if (dragHappened) tempTransaction?.commit()
|
||||||
else tempTransaction?.discard()
|
else tempTransaction?.discard()
|
||||||
|
|
||||||
|
snapToNone()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,10 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import {generateSequenceMarkerId} from '@theatre/shared/utils/ids'
|
import {generateSequenceMarkerId} from '@theatre/shared/utils/ids'
|
||||||
import DopeSnap from './DopeSnap'
|
import DopeSnap from './DopeSnap'
|
||||||
|
import {
|
||||||
|
snapToAll,
|
||||||
|
snapToNone,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
|
||||||
const Container = styled.div<{isVisible: boolean}>`
|
const Container = styled.div<{isVisible: boolean}>`
|
||||||
--thumbColor: #00e0ff;
|
--thumbColor: #00e0ff;
|
||||||
|
@ -214,6 +218,8 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||||
setIsSeeking(true)
|
setIsSeeking(true)
|
||||||
|
|
||||||
|
snapToAll()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDrag(dx, _, event) {
|
onDrag(dx, _, event) {
|
||||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||||
|
@ -227,6 +233,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
},
|
},
|
||||||
onDragEnd(dragHappened) {
|
onDragEnd(dragHappened) {
|
||||||
setIsSeeking(false)
|
setIsSeeking(false)
|
||||||
|
snapToNone()
|
||||||
},
|
},
|
||||||
onClick(e) {
|
onClick(e) {
|
||||||
openPopover(e, thumbRef.current!)
|
openPopover(e, thumbRef.current!)
|
||||||
|
|
Loading…
Reference in a new issue