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 React, {useMemo, useRef} from 'react'
|
||||
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 type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
|
@ -21,6 +20,13 @@ import {
|
|||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric'
|
||||
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 = {
|
||||
editorProps: IAggregateKeyframeEditorProps
|
||||
|
@ -41,17 +47,11 @@ export function AggregateKeyframeDot(
|
|||
},
|
||||
})
|
||||
|
||||
const [contextMenu] = useAggregateKeyframeContextMenu(props, node)
|
||||
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HitZone
|
||||
ref={ref}
|
||||
{...DopeSnapHitZoneUI.reactProps({
|
||||
isDragging,
|
||||
position: cur.position,
|
||||
})}
|
||||
/>
|
||||
<HitZone ref={ref} />
|
||||
<AggregateKeyframeVisualDot
|
||||
isAllHere={cur.allHere}
|
||||
isSelected={cur.selected}
|
||||
|
@ -63,6 +63,7 @@ export function AggregateKeyframeDot(
|
|||
|
||||
function useAggregateKeyframeContextMenu(
|
||||
props: IAggregateKeyframeDotProps,
|
||||
logger: ILogger,
|
||||
target: HTMLDivElement | null,
|
||||
) {
|
||||
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 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 (
|
||||
props.selection &&
|
||||
props.aggregateKeyframes[props.index].selected ===
|
||||
|
@ -172,7 +208,7 @@ function useDragForAggregateKeyframeDot(
|
|||
) {
|
||||
const {selection, viewModel} = props
|
||||
const {sheetObject} = viewModel
|
||||
const hanlders = selection
|
||||
const handlers = selection
|
||||
.getDragHandlers({
|
||||
...sheetObject.address,
|
||||
domNode: node!,
|
||||
|
@ -180,7 +216,16 @@ function useDragForAggregateKeyframeDot(
|
|||
})
|
||||
.onDragStart(event)
|
||||
|
||||
return hanlders && {...hanlders, onClick: options.onClickFromDrag}
|
||||
return (
|
||||
handlers && {
|
||||
...handlers,
|
||||
onClick: options.onClickFromDrag,
|
||||
onDragEnd: (...args) => {
|
||||
handlers.onDragEnd?.(...args)
|
||||
snapToNone()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const propsAtStartOfDrag = props
|
||||
|
@ -227,6 +272,8 @@ function useDragForAggregateKeyframeDot(
|
|||
} else {
|
||||
tempTransaction?.discard()
|
||||
}
|
||||
|
||||
snapToNone()
|
||||
},
|
||||
onClick(ev) {
|
||||
options.onClickFromDrag(ev)
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||
import styled from 'styled-components'
|
||||
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_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||
|
@ -20,16 +20,15 @@ export const HitZone = styled.div`
|
|||
z-index: 2;
|
||||
cursor: ew-resize;
|
||||
|
||||
${DopeSnapHitZoneUI.CSS}
|
||||
position: absolute;
|
||||
${absoluteDims(12)};
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||
}
|
||||
|
||||
&:hover + ${DotContainer},
|
||||
#pointer-root.draggingPositionInSequenceEditor &:hover + ${DotContainer},
|
||||
// notice "," css "or"
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} + ${DotContainer} {
|
||||
&:hover
|
||||
+ ${DotContainer},
|
||||
#pointer-root.draggingPositionInSequenceEditor
|
||||
&:hover
|
||||
+ ${DotContainer} {
|
||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -7,11 +7,11 @@ import type {
|
|||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
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 {valueDerivation} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import React, {Fragment, useMemo} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import type {IContextMenuItem} 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 {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type {
|
||||
SheetObjectAddress} from '@theatre/shared/utils/addresses';
|
||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import {
|
||||
decodePathToProp,
|
||||
doesPathStartWith,
|
||||
encodePathToProp
|
||||
encodePathToProp,
|
||||
} from '@theatre/shared/utils/addresses'
|
||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import type Sequence from '@theatre/core/sequences/Sequence'
|
||||
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`
|
||||
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) => (
|
||||
<Fragment key={'agg-' + keyframes[0].kf.id}>
|
||||
{snapToAllKeyframes && (
|
||||
<KeyframeSnapTarget
|
||||
layoutP={layoutP}
|
||||
leaf={viewModel}
|
||||
position={position}
|
||||
/>
|
||||
)}
|
||||
<AggregateKeyframeEditor
|
||||
index={index}
|
||||
layoutP={layoutP}
|
||||
viewModel={viewModel}
|
||||
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={
|
||||
selectedPositions.has(position) === true ? selection : undefined
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))
|
||||
|
||||
return (
|
||||
|
@ -111,6 +144,7 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
|
|||
}}
|
||||
>
|
||||
{keyframeEditors}
|
||||
{snapTargets}
|
||||
{contextMenu}
|
||||
</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 {SequenceEditorTree_PrimitiveProp} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
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 {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import React, {Fragment} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import SingleKeyframeEditor from './KeyframeEditor/SingleKeyframeEditor'
|
||||
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 {arePathsEqual} from '@theatre/shared/utils/addresses'
|
||||
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types'
|
||||
import KeyframeSnapTarget, {
|
||||
snapPositionsStateD,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
@ -58,15 +61,45 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
|||
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) => (
|
||||
<Fragment key={'keyframe-' + kf.id}>
|
||||
{snapToAllKeyframes && (
|
||||
<KeyframeSnapTarget
|
||||
layoutP={layoutP}
|
||||
leaf={leaf}
|
||||
position={kf.position}
|
||||
/>
|
||||
)}
|
||||
<SingleKeyframeEditor
|
||||
keyframe={kf}
|
||||
index={index}
|
||||
trackData={trackData}
|
||||
layoutP={layoutP}
|
||||
leaf={leaf}
|
||||
key={'keyframe-' + kf.id}
|
||||
selection={selectedKeyframeIds[kf.id] === true ? selection : undefined}
|
||||
selection={
|
||||
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}
|
||||
{snapTargets}
|
||||
{contextMenu}
|
||||
</Container>
|
||||
)
|
||||
|
|
|
@ -5,8 +5,8 @@ import last from 'lodash-es/last'
|
|||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useDrag 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 {val} from '@theatre/dataverse'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
@ -19,10 +19,15 @@ import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
|
|||
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
|
||||
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
import type {ILogger} from '@theatre/shared/logger'
|
||||
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
|
||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||
|
@ -49,17 +54,11 @@ const HitZone = styled.div`
|
|||
z-index: 1;
|
||||
cursor: ew-resize;
|
||||
|
||||
${DopeSnapHitZoneUI.CSS}
|
||||
position: absolute;
|
||||
${absoluteDims(12)};
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||
}
|
||||
|
||||
&:hover
|
||||
+ ${Diamond},
|
||||
// notice , "or" in CSS
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
||||
+ ${Diamond} {
|
||||
&:hover + ${Diamond} {
|
||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||
}
|
||||
`
|
||||
|
@ -82,13 +81,7 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<HitZone
|
||||
ref={ref}
|
||||
{...DopeSnapHitZoneUI.reactProps({
|
||||
isDragging,
|
||||
position: props.keyframe.position,
|
||||
})}
|
||||
/>
|
||||
<HitZone ref={ref} />
|
||||
<Diamond isSelected={!!props.selection} />
|
||||
{inlineEditorPopover}
|
||||
{contextMenu}
|
||||
|
@ -210,6 +203,37 @@ function useDragForSingleKeyframeDot(
|
|||
debugName: 'KeyframeDot/useDragKeyframe',
|
||||
onDragStart(event) {
|
||||
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) {
|
||||
const {selection, leaf} = props
|
||||
const {sheetObject} = leaf
|
||||
|
@ -226,7 +250,16 @@ function useDragForSingleKeyframeDot(
|
|||
// 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
|
||||
// together
|
||||
return handlers && {...handlers, onClick: options.onClickFromDrag}
|
||||
return (
|
||||
handlers && {
|
||||
...handlers,
|
||||
onClick: options.onClickFromDrag,
|
||||
onDragEnd: (...args) => {
|
||||
handlers.onDragEnd?.(...args)
|
||||
snapToNone()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const propsAtStartOfDrag = props
|
||||
|
@ -272,6 +305,8 @@ function useDragForSingleKeyframeDot(
|
|||
} else {
|
||||
tempTransaction?.discard()
|
||||
}
|
||||
|
||||
snapToNone()
|
||||
},
|
||||
onClick(ev) {
|
||||
options.onClickFromDrag(ev)
|
||||
|
|
|
@ -12,6 +12,7 @@ import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
|||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import type {IRange} from '@theatre/shared/utils/types'
|
||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||
import {snapToAll, snapToNone} from './KeyframeSnapTarget'
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
|
@ -115,6 +116,8 @@ function useDragPlayheadHandlers(
|
|||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||
setIsSeeking(true)
|
||||
|
||||
snapToAll()
|
||||
|
||||
return {
|
||||
onDrag(dx: number, _, event) {
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
|
@ -135,6 +138,7 @@ function useDragPlayheadHandlers(
|
|||
},
|
||||
onDragEnd() {
|
||||
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'
|
||||
import type {IUtilLogger} from '@theatre/shared/logger'
|
||||
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.
|
||||
|
@ -128,3 +129,45 @@ export function collectAggregateKeyframesInPrism(
|
|||
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 {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import {DopeSnapHitZoneUI} from './DopeSnapHitZoneUI'
|
||||
import {
|
||||
snapToAll,
|
||||
snapToNone,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||
|
||||
const MARKER_SIZE_W_PX = 12
|
||||
const MARKER_SIZE_H_PX = 12
|
||||
|
@ -221,6 +225,8 @@ function useDragMarker(
|
|||
const toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace)
|
||||
let tempTransaction: CommitOrDiscard | undefined
|
||||
|
||||
snapToAll()
|
||||
|
||||
return {
|
||||
onDrag(dx, _dy, event) {
|
||||
const original = markerAtStartOfDrag
|
||||
|
@ -250,6 +256,8 @@ function useDragMarker(
|
|||
onDragEnd(dragHappened) {
|
||||
if (dragHappened) tempTransaction?.commit()
|
||||
else tempTransaction?.discard()
|
||||
|
||||
snapToNone()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
@ -27,6 +27,10 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
|||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {generateSequenceMarkerId} from '@theatre/shared/utils/ids'
|
||||
import DopeSnap from './DopeSnap'
|
||||
import {
|
||||
snapToAll,
|
||||
snapToNone,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||
|
||||
const Container = styled.div<{isVisible: boolean}>`
|
||||
--thumbColor: #00e0ff;
|
||||
|
@ -214,6 +218,8 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||
setIsSeeking(true)
|
||||
|
||||
snapToAll()
|
||||
|
||||
return {
|
||||
onDrag(dx, _, event) {
|
||||
const deltaPos = scaledSpaceToUnitSpace(dx)
|
||||
|
@ -227,6 +233,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
},
|
||||
onDragEnd(dragHappened) {
|
||||
setIsSeeking(false)
|
||||
snapToNone()
|
||||
},
|
||||
onClick(e) {
|
||||
openPopover(e, thumbRef.current!)
|
||||
|
|
Loading…
Reference in a new issue