Perfect snapping behavior for all snap targets (#203)

Co-authored-by: Andrew Prifer <AndrewPrifer@users.noreply.github.com>
This commit is contained in:
Andrew Prifer 2022-06-08 12:55:55 +02:00 committed by GitHub
parent 3b3a1b1d8a
commit b323588d78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 405 additions and 65 deletions

View file

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

View file

@ -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)}
} }
` `

View file

@ -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,21 +92,50 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
}), }),
) )
const keyframeEditors = posKfs.map(({position, keyframes}, index) => ( const snapPositionsState = useVal(snapPositionsStateD)
<AggregateKeyframeEditor
index={index} 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} layoutP={layoutP}
viewModel={viewModel} leaf={viewModel}
aggregateKeyframes={posKfs} position={position}
// 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
}
/> />
)) ))
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}
selection={
selectedPositions.has(position) === true ? selection : undefined
}
/>
</Fragment>
))
return ( return (
<AggregatedKeyframeTrackContainer <AggregatedKeyframeTrackContainer
ref={containerRef} ref={containerRef}
@ -111,6 +144,7 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
}} }}
> >
{keyframeEditors} {keyframeEditors}
{snapTargets}
{contextMenu} {contextMenu}
</AggregatedKeyframeTrackContainer> </AggregatedKeyframeTrackContainer>
) )

View file

@ -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) => (
<SingleKeyframeEditor <Fragment key={'keyframe-' + kf.id}>
keyframe={kf} {snapToAllKeyframes && (
index={index} <KeyframeSnapTarget
trackData={trackData} layoutP={layoutP}
leaf={leaf}
position={kf.position}
/>
)}
<SingleKeyframeEditor
keyframe={kf}
index={index}
trackData={trackData}
layoutP={layoutP}
leaf={leaf}
selection={
selectedKeyframeIds[kf.id] === true ? selection : undefined
}
/>
</Fragment>
))
const snapTargets = snapPositions.map((position) => (
<KeyframeSnapTarget
key={'snap-target-' + position}
layoutP={layoutP} layoutP={layoutP}
leaf={leaf} leaf={leaf}
key={'keyframe-' + kf.id} position={position}
selection={selectedKeyframeIds[kf.id] === true ? selection : undefined}
/> />
)) ))
@ -78,6 +111,7 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
}} }}
> >
{keyframeEditors} {keyframeEditors}
{snapTargets}
{contextMenu} {contextMenu}
</Container> </Container>
) )

View file

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

View file

@ -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()
}, },
} }
}, },

View file

@ -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),
],
),
),
],
),
)
}

View file

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

View file

@ -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()
}, },
} }
}, },

View file

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