Reorganize AggregateKeyframeEditor
This commit is contained in:
parent
f222cc61dd
commit
eb15229463
8 changed files with 659 additions and 381 deletions
|
@ -1,373 +0,0 @@
|
||||||
import type {
|
|
||||||
Keyframe,
|
|
||||||
TrackData,
|
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
|
||||||
import type {
|
|
||||||
DopeSheetSelection,
|
|
||||||
SequenceEditorPanelLayout,
|
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
|
||||||
import type {
|
|
||||||
SequenceEditorTree_PropWithChildren,
|
|
||||||
SequenceEditorTree_SheetObject,
|
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
|
||||||
import {prism} from '@theatre/dataverse'
|
|
||||||
import {val} from '@theatre/dataverse'
|
|
||||||
import React from 'react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
|
||||||
import {ConnectorLine} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
|
||||||
import {AggregateKeyframePositionIsSelected} from './AggregatedKeyframeTrack'
|
|
||||||
import type {KeyframeWithTrack} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
|
||||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
|
||||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
|
||||||
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
|
||||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
|
||||||
import CurveEditorPopover, {
|
|
||||||
isConnectionEditingInCurvePopover,
|
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
|
||||||
import {usePrism} from '@theatre/react'
|
|
||||||
import {selectedKeyframeConnections} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
|
||||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
|
||||||
|
|
||||||
const POPOVER_MARGIN_PX = 5
|
|
||||||
|
|
||||||
const AggregateKeyframeEditorContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
`
|
|
||||||
|
|
||||||
const EasingPopoverWrapper = styled(BasicPopover)`
|
|
||||||
--popover-outer-stroke: transparent;
|
|
||||||
--popover-inner-stroke: rgba(26, 28, 30, 0.97);
|
|
||||||
`
|
|
||||||
|
|
||||||
const noConnector = <></>
|
|
||||||
|
|
||||||
export type IAggregateKeyframesAtPosition = {
|
|
||||||
position: number
|
|
||||||
/** all tracks have a keyframe for this position (otherwise, false means 'partial') */
|
|
||||||
allHere: boolean
|
|
||||||
selected: AggregateKeyframePositionIsSelected | undefined
|
|
||||||
keyframes: {
|
|
||||||
kf: Keyframe
|
|
||||||
track: {
|
|
||||||
id: SequenceTrackId
|
|
||||||
data: TrackData
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type AggregatedKeyframeConnection = SheetObjectAddress & {
|
|
||||||
trackId: SequenceTrackId
|
|
||||||
left: Keyframe
|
|
||||||
right: Keyframe
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IAggregateKeyframeEditorProps = {
|
|
||||||
index: number
|
|
||||||
aggregateKeyframes: IAggregateKeyframesAtPosition[]
|
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
|
||||||
viewModel:
|
|
||||||
| SequenceEditorTree_PropWithChildren
|
|
||||||
| SequenceEditorTree_SheetObject
|
|
||||||
selection: undefined | DopeSheetSelection
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO we're spending a lot of cycles on each render of each aggreagte keyframes.
|
|
||||||
*
|
|
||||||
* Each keyframe node is doing O(N) operations, N being the number of underlying
|
|
||||||
* keyframes it represetns.
|
|
||||||
*
|
|
||||||
* The biggest example is the `isConnectionEditingInCurvePopover()` call which is run
|
|
||||||
* for every underlying keyframe, every time this component is rendered.
|
|
||||||
*
|
|
||||||
* We can optimize this away by doing all of this work _once_ when a curve editor popover
|
|
||||||
* is open. This would require having some kind of stable identity for each aggregate row.
|
|
||||||
* Let's defer that work until other interactive keyframe editing PRs are merged in.
|
|
||||||
*/
|
|
||||||
const AggregateKeyframeEditor: React.VFC<IAggregateKeyframeEditorProps> = (
|
|
||||||
props,
|
|
||||||
) => {
|
|
||||||
const {cur, connected, isAggregateEditingInCurvePopover} =
|
|
||||||
useAggregateKeyframeEditorUtils(props)
|
|
||||||
|
|
||||||
const {isPointerBeingCaptured} = usePointerCapturing(
|
|
||||||
'AggregateKeyframeEditor Connector',
|
|
||||||
)
|
|
||||||
|
|
||||||
const [popoverNode, openPopover, closePopover] = usePopover(
|
|
||||||
() => {
|
|
||||||
const rightDims = val(props.layoutP.rightDims)
|
|
||||||
|
|
||||||
return {
|
|
||||||
debugName: 'Connector',
|
|
||||||
closeWhenPointerIsDistant: !isPointerBeingCaptured(),
|
|
||||||
constraints: {
|
|
||||||
minX: rightDims.screenX + POPOVER_MARGIN_PX,
|
|
||||||
maxX: rightDims.screenX + rightDims.width - POPOVER_MARGIN_PX,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return (
|
|
||||||
<AggregateCurveEditorPopover {...props} closePopover={closePopover} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AggregateKeyframeEditorContainer
|
|
||||||
style={{
|
|
||||||
top: `${props.viewModel.nodeHeight / 2}px`,
|
|
||||||
left: `calc(${val(
|
|
||||||
props.layoutP.scaledSpace.leftPadding,
|
|
||||||
)}px + calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
|
||||||
cur.position
|
|
||||||
}px))`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AggregateKeyframeDot
|
|
||||||
keyframes={cur.keyframes}
|
|
||||||
position={cur.position}
|
|
||||||
theme={{
|
|
||||||
isSelected: cur.selected,
|
|
||||||
}}
|
|
||||||
isAllHere={cur.allHere}
|
|
||||||
/>
|
|
||||||
{connected ? (
|
|
||||||
<ConnectorLine
|
|
||||||
ref={nodeRef}
|
|
||||||
connectorLengthInUnitSpace={connected.length}
|
|
||||||
isPopoverOpen={isAggregateEditingInCurvePopover}
|
|
||||||
// if all keyframe aggregates are selected
|
|
||||||
isSelected={connected.selected}
|
|
||||||
openPopover={(e) => {
|
|
||||||
if (node) openPopover(e, node)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
noConnector
|
|
||||||
)}
|
|
||||||
{popoverNode}
|
|
||||||
</AggregateKeyframeEditorContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useAggregateKeyframeEditorUtils(
|
|
||||||
props: Pick<
|
|
||||||
IAggregateKeyframeEditorProps,
|
|
||||||
'index' | 'aggregateKeyframes' | 'selection' | 'viewModel'
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
const {index, aggregateKeyframes, selection} = props
|
|
||||||
const sheetObjectAddress = props.viewModel.sheetObject.address
|
|
||||||
|
|
||||||
return usePrism(() => {
|
|
||||||
const cur = aggregateKeyframes[index]
|
|
||||||
const next = aggregateKeyframes[index + 1]
|
|
||||||
|
|
||||||
const curAndNextAggregateKeyframesMatch =
|
|
||||||
next &&
|
|
||||||
cur.keyframes.length === next.keyframes.length &&
|
|
||||||
cur.keyframes.every(({track}, ind) => next.keyframes[ind].track === track)
|
|
||||||
|
|
||||||
const connected = curAndNextAggregateKeyframesMatch
|
|
||||||
? {
|
|
||||||
length: next.position - cur.position,
|
|
||||||
selected:
|
|
||||||
cur.selected === AggregateKeyframePositionIsSelected.AllSelected &&
|
|
||||||
next.selected === AggregateKeyframePositionIsSelected.AllSelected,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
|
|
||||||
const aggregatedConnections: AggregatedKeyframeConnection[] = !connected
|
|
||||||
? []
|
|
||||||
: cur.keyframes.map(({kf, track}, i) => ({
|
|
||||||
...sheetObjectAddress,
|
|
||||||
trackId: track.id,
|
|
||||||
left: kf,
|
|
||||||
right: next.keyframes[i].kf,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const {projectId, sheetId} = sheetObjectAddress
|
|
||||||
|
|
||||||
const selectedConnections = prism
|
|
||||||
.memo(
|
|
||||||
'selectedConnections',
|
|
||||||
() =>
|
|
||||||
selectedKeyframeConnections(
|
|
||||||
sheetObjectAddress.projectId,
|
|
||||||
sheetObjectAddress.sheetId,
|
|
||||||
selection,
|
|
||||||
),
|
|
||||||
[projectId, sheetId, selection],
|
|
||||||
)
|
|
||||||
.getValue()
|
|
||||||
|
|
||||||
const allConnections = [...aggregatedConnections, ...selectedConnections]
|
|
||||||
|
|
||||||
const isAggregateEditingInCurvePopover = aggregatedConnections.every(
|
|
||||||
(con) => isConnectionEditingInCurvePopover(con),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {cur, connected, isAggregateEditingInCurvePopover, allConnections}
|
|
||||||
}, [index, aggregateKeyframes, selection, sheetObjectAddress])
|
|
||||||
}
|
|
||||||
|
|
||||||
const AggregateCurveEditorPopover: React.FC<
|
|
||||||
IAggregateKeyframeEditorProps & {closePopover: (reason: string) => void}
|
|
||||||
> = React.forwardRef((props, ref) => {
|
|
||||||
const {allConnections} = useAggregateKeyframeEditorUtils(props)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EasingPopoverWrapper
|
|
||||||
showPopoverEdgeTriangle={false}
|
|
||||||
// @ts-ignore @todo
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<CurveEditorPopover
|
|
||||||
curveConnection={allConnections[0]}
|
|
||||||
additionalConnections={allConnections}
|
|
||||||
onRequestClose={props.closePopover}
|
|
||||||
/>
|
|
||||||
</EasingPopoverWrapper>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const DOT_SIZE_PX = 16
|
|
||||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
|
||||||
|
|
||||||
/** The keyframe diamond ◆ */
|
|
||||||
const DotContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
${absoluteDims(DOT_SIZE_PX)}
|
|
||||||
z-index: 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const HitZone = styled.div`
|
|
||||||
z-index: 2;
|
|
||||||
/* TEMP: Disabled until interactivity */
|
|
||||||
/* cursor: ew-resize; */
|
|
||||||
|
|
||||||
${DopeSnapHitZoneUI.CSS}
|
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
|
||||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TEMP: Disabled until interactivity */
|
|
||||||
/* &:hover + ${DotContainer}, */
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor &:hover + ${DotContainer},
|
|
||||||
// notice "," css "or"
|
|
||||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} + ${DotContainer} {
|
|
||||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const AggregateKeyframeDot = React.forwardRef(AggregateKeyframeDot_ref)
|
|
||||||
function AggregateKeyframeDot_ref(
|
|
||||||
props: React.PropsWithChildren<{
|
|
||||||
theme: IDotThemeValues
|
|
||||||
isAllHere: boolean
|
|
||||||
position: number
|
|
||||||
keyframes: KeyframeWithTrack[]
|
|
||||||
}>,
|
|
||||||
ref: React.ForwardedRef<HTMLDivElement>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HitZone
|
|
||||||
ref={ref}
|
|
||||||
{...DopeSnapHitZoneUI.reactProps({
|
|
||||||
isDragging: false,
|
|
||||||
position: props.position,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<DotContainer>
|
|
||||||
{props.isAllHere ? (
|
|
||||||
<AggregateDotAllHereSvg {...props.theme} />
|
|
||||||
) : (
|
|
||||||
<AggregateDotSomeHereSvg {...props.theme} />
|
|
||||||
)}
|
|
||||||
</DotContainer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IDotThemeValues = {
|
|
||||||
isSelected: AggregateKeyframePositionIsSelected | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const SELECTED_COLOR = '#b8e4e2'
|
|
||||||
const DEFAULT_PRIMARY_COLOR = '#40AAA4'
|
|
||||||
const DEFAULT_SECONDARY_COLOR = '#45747C'
|
|
||||||
const selectionColorAll = (theme: IDotThemeValues) =>
|
|
||||||
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
|
||||||
? SELECTED_COLOR
|
|
||||||
: theme.isSelected ===
|
|
||||||
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
|
||||||
? DEFAULT_PRIMARY_COLOR
|
|
||||||
: DEFAULT_SECONDARY_COLOR
|
|
||||||
const selectionColorSome = (theme: IDotThemeValues) =>
|
|
||||||
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
|
||||||
? SELECTED_COLOR
|
|
||||||
: theme.isSelected ===
|
|
||||||
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
|
||||||
? DEFAULT_PRIMARY_COLOR
|
|
||||||
: DEFAULT_SECONDARY_COLOR
|
|
||||||
|
|
||||||
const AggregateDotAllHereSvg = (theme: IDotThemeValues) => (
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
x="4.46443"
|
|
||||||
y="10.0078"
|
|
||||||
width="5"
|
|
||||||
height="5"
|
|
||||||
transform="rotate(-45 4.46443 10.0078)"
|
|
||||||
fill="#212327" // background knockout fill
|
|
||||||
stroke={selectionColorSome(theme)}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="3.75732"
|
|
||||||
y="6.01953"
|
|
||||||
width="6"
|
|
||||||
height="6"
|
|
||||||
transform="rotate(-45 3.75732 6.01953)"
|
|
||||||
fill={selectionColorAll(theme)}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
// when the aggregate keyframes are sparse across tracks at this position
|
|
||||||
const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => (
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
x="4.46443"
|
|
||||||
y="8"
|
|
||||||
width="5"
|
|
||||||
height="5"
|
|
||||||
transform="rotate(-45 4.46443 8)"
|
|
||||||
fill="#23262B"
|
|
||||||
stroke={selectionColorAll(theme)}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default AggregateKeyframeEditor
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import {val} from '@theatre/dataverse'
|
||||||
|
import React, {useMemo, useRef} from 'react'
|
||||||
|
import {ConnectorLine} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||||
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
|
||||||
|
import CurveEditorPopover from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
||||||
|
import {useAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
|
||||||
|
import type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||||
|
|
||||||
|
const POPOVER_MARGIN_PX = 5
|
||||||
|
const EasingPopoverWrapper = styled(BasicPopover)`
|
||||||
|
--popover-outer-stroke: transparent;
|
||||||
|
--popover-inner-stroke: rgba(26, 28, 30, 0.97);
|
||||||
|
`
|
||||||
|
export const AggregateCurveEditorPopover: React.FC<
|
||||||
|
IAggregateKeyframeEditorProps & {closePopover: (reason: string) => void}
|
||||||
|
> = React.forwardRef((props, ref) => {
|
||||||
|
const {allConnections} = useAggregateKeyframeEditorUtils(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EasingPopoverWrapper
|
||||||
|
showPopoverEdgeTriangle={false}
|
||||||
|
// @ts-ignore @todo
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<CurveEditorPopover
|
||||||
|
curveConnection={allConnections[0]}
|
||||||
|
additionalConnections={allConnections}
|
||||||
|
onRequestClose={props.closePopover}
|
||||||
|
/>
|
||||||
|
</EasingPopoverWrapper>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AggregateKeyframeConnector: React.VFC<IAggregateKeyframeConnectorProps> =
|
||||||
|
(props) => {
|
||||||
|
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
const {editorProps} = props
|
||||||
|
|
||||||
|
const [isDragging] = useDragKeyframe(node, props.editorProps)
|
||||||
|
|
||||||
|
const [popoverNode, openPopover, closePopover] = usePopover(
|
||||||
|
() => {
|
||||||
|
const rightDims = val(editorProps.layoutP.rightDims)
|
||||||
|
|
||||||
|
return {
|
||||||
|
debugName: 'Connector',
|
||||||
|
constraints: {
|
||||||
|
minX: rightDims.screenX + POPOVER_MARGIN_PX,
|
||||||
|
maxX: rightDims.screenX + rightDims.width - POPOVER_MARGIN_PX,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<AggregateCurveEditorPopover
|
||||||
|
{...editorProps}
|
||||||
|
closePopover={closePopover}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const {connected, isAggregateEditingInCurvePopover} = props.utils
|
||||||
|
|
||||||
|
// We don't want to interrupt an existing drag, so in order to persist the dragged
|
||||||
|
// html node, we just set the connector length to 0, but we don't remove it yet.
|
||||||
|
return connected || isDragging ? (
|
||||||
|
<>
|
||||||
|
<ConnectorLine
|
||||||
|
ref={nodeRef}
|
||||||
|
connectorLengthInUnitSpace={connected ? connected.length : 0}
|
||||||
|
isSelected={connected ? connected.selected : false}
|
||||||
|
isPopoverOpen={isAggregateEditingInCurvePopover}
|
||||||
|
openPopover={(e) => {
|
||||||
|
if (node) openPopover(e, node)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{popoverNode}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type IAggregateKeyframeConnectorProps = {
|
||||||
|
utils: IAggregateKeyframeEditorUtils
|
||||||
|
editorProps: IAggregateKeyframeEditorProps
|
||||||
|
}
|
||||||
|
function useDragKeyframe(
|
||||||
|
node: HTMLDivElement | null,
|
||||||
|
editorProps: IAggregateKeyframeEditorProps,
|
||||||
|
) {
|
||||||
|
const propsRef = useRef(editorProps)
|
||||||
|
propsRef.current = editorProps
|
||||||
|
|
||||||
|
const gestureHandlers = useMemo<UseDragOpts>(() => {
|
||||||
|
return {
|
||||||
|
debugName: 'useDragKeyframe',
|
||||||
|
lockCSSCursorTo: 'ew-resize',
|
||||||
|
onDragStart(event) {
|
||||||
|
const props = propsRef.current
|
||||||
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
|
|
||||||
|
const keyframes = props.aggregateKeyframes[props.index].keyframes
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.selection &&
|
||||||
|
props.aggregateKeyframes[props.index].selected ===
|
||||||
|
AggregateKeyframePositionIsSelected.AllSelected
|
||||||
|
) {
|
||||||
|
const {selection, viewModel} = props
|
||||||
|
const {sheetObject} = viewModel
|
||||||
|
return selection
|
||||||
|
.getDragHandlers({
|
||||||
|
...sheetObject.address,
|
||||||
|
domNode: node!,
|
||||||
|
positionAtStartOfDrag:
|
||||||
|
props.aggregateKeyframes[props.index].position,
|
||||||
|
})
|
||||||
|
.onDragStart(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsAtStartOfDrag = props
|
||||||
|
const sequence = val(propsAtStartOfDrag.layoutP.sheet).getSequence()
|
||||||
|
|
||||||
|
const toUnitSpace = val(
|
||||||
|
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDrag(dx, dy, event) {
|
||||||
|
const delta = toUnitSpace(dx)
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
|
}
|
||||||
|
tempTransaction = getStudio().tempTransaction(({stateEditors}) => {
|
||||||
|
for (const keyframe of keyframes) {
|
||||||
|
stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes(
|
||||||
|
{
|
||||||
|
...propsAtStartOfDrag.viewModel.sheetObject.address,
|
||||||
|
trackId: keyframe.track.id,
|
||||||
|
keyframeIds: [
|
||||||
|
keyframe.kf.id,
|
||||||
|
keyframe.track.data.keyframes[
|
||||||
|
keyframe.track.data.keyframes.indexOf(keyframe.kf) + 1
|
||||||
|
].id,
|
||||||
|
],
|
||||||
|
translate: delta,
|
||||||
|
scale: 1,
|
||||||
|
origin: 0,
|
||||||
|
snappingFunction: sequence.closestGridPosition,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onDragEnd(dragHappened) {
|
||||||
|
if (dragHappened) {
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.commit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.discard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return useDrag(node, gestureHandlers)
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
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'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
|
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
|
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||||
|
import type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor'
|
||||||
|
import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
|
||||||
|
import {AggregateKeyframeVisualDot, HitZone} from './AggregateKeyframeVisualDot'
|
||||||
|
|
||||||
|
type IAggregateKeyframeDotProps = {
|
||||||
|
editorProps: IAggregateKeyframeEditorProps
|
||||||
|
utils: IAggregateKeyframeEditorUtils
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AggregateKeyframeDot(
|
||||||
|
props: React.PropsWithChildren<IAggregateKeyframeDotProps>,
|
||||||
|
) {
|
||||||
|
const logger = useLogger('AggregateKeyframeDot')
|
||||||
|
const {cur} = props.utils
|
||||||
|
|
||||||
|
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
const [isDragging] = useDragForAggregateKeyframeDot(node, props, {
|
||||||
|
onClickFromDrag(dragStartEvent) {
|
||||||
|
// TODO Aggregate inline keyframe editor
|
||||||
|
// openEditor(dragStartEvent, ref.current!)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [contextMenu] = useAggregateKeyframeContextMenu(node, () =>
|
||||||
|
logger._debug('Show Aggregate Keyframe', props),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HitZone
|
||||||
|
ref={ref}
|
||||||
|
{...DopeSnapHitZoneUI.reactProps({
|
||||||
|
isDragging,
|
||||||
|
position: cur.position,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<AggregateKeyframeVisualDot
|
||||||
|
isAllHere={cur.allHere}
|
||||||
|
isSelected={cur.selected}
|
||||||
|
/>
|
||||||
|
{contextMenu}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAggregateKeyframeContextMenu(
|
||||||
|
target: HTMLDivElement | null,
|
||||||
|
debugOnOpen: () => void,
|
||||||
|
) {
|
||||||
|
// TODO: missing features: delete, copy + paste
|
||||||
|
return useContextMenu(target, {
|
||||||
|
displayName: 'Aggregate Keyframe',
|
||||||
|
menuItems: () => {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
onOpen() {
|
||||||
|
debugOnOpen()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDragForAggregateKeyframeDot(
|
||||||
|
node: HTMLDivElement | null,
|
||||||
|
props: IAggregateKeyframeDotProps,
|
||||||
|
options: {
|
||||||
|
/**
|
||||||
|
* hmm: this is a hack so we can actually receive the
|
||||||
|
* {@link MouseEvent} from the drag event handler and use
|
||||||
|
* it for positioning the popup.
|
||||||
|
*/
|
||||||
|
onClickFromDrag(dragStartEvent: MouseEvent): void
|
||||||
|
},
|
||||||
|
): [isDragging: boolean] {
|
||||||
|
const propsRef = useRef(props.editorProps)
|
||||||
|
propsRef.current = props.editorProps
|
||||||
|
const keyframesRef = useRef(props.utils.cur.keyframes)
|
||||||
|
keyframesRef.current = props.utils.cur.keyframes
|
||||||
|
|
||||||
|
const useDragOpts = useMemo<UseDragOpts>(() => {
|
||||||
|
return {
|
||||||
|
debugName: 'AggregateKeyframeDot/useDragKeyframe',
|
||||||
|
onDragStart(event) {
|
||||||
|
const props = propsRef.current
|
||||||
|
const keyframes = keyframesRef.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.selection &&
|
||||||
|
props.aggregateKeyframes[props.index].selected ===
|
||||||
|
AggregateKeyframePositionIsSelected.AllSelected
|
||||||
|
) {
|
||||||
|
const {selection, viewModel} = props
|
||||||
|
const {sheetObject} = viewModel
|
||||||
|
return selection
|
||||||
|
.getDragHandlers({
|
||||||
|
...sheetObject.address,
|
||||||
|
domNode: node!,
|
||||||
|
positionAtStartOfDrag: keyframes[0].kf.position,
|
||||||
|
})
|
||||||
|
.onDragStart(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsAtStartOfDrag = props
|
||||||
|
const toUnitSpace = val(
|
||||||
|
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
|
||||||
|
)
|
||||||
|
|
||||||
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDrag(dx, dy, event) {
|
||||||
|
const newPosition = Math.max(
|
||||||
|
// check if our event hoversover a [data-pos] element
|
||||||
|
DopeSnap.checkIfMouseEventSnapToPos(event, {
|
||||||
|
ignore: node,
|
||||||
|
}) ??
|
||||||
|
// if we don't find snapping target, check the distance dragged + original position
|
||||||
|
keyframes[0].kf.position + toUnitSpace(dx),
|
||||||
|
// sanitize to minimum of zero
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
tempTransaction?.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
|
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
|
||||||
|
for (const keyframe of keyframes) {
|
||||||
|
const original = keyframe.kf
|
||||||
|
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
|
||||||
|
{
|
||||||
|
...propsAtStartOfDrag.viewModel.sheetObject.address,
|
||||||
|
trackId: keyframe.track.id,
|
||||||
|
keyframes: [{...original, position: newPosition}],
|
||||||
|
snappingFunction: val(
|
||||||
|
propsAtStartOfDrag.layoutP.sheet,
|
||||||
|
).getSequence().closestGridPosition,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onDragEnd(dragHappened) {
|
||||||
|
if (dragHappened) {
|
||||||
|
tempTransaction?.commit()
|
||||||
|
} else {
|
||||||
|
tempTransaction?.discard()
|
||||||
|
options.onClickFromDrag(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [isDragging] = useDrag(node, useDragOpts)
|
||||||
|
|
||||||
|
useLockFrameStampPosition(isDragging, props.utils.cur.position)
|
||||||
|
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||||
|
|
||||||
|
return [isDragging]
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
|
import type {
|
||||||
|
DopeSheetSelection,
|
||||||
|
SequenceEditorPanelLayout,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
|
import type {
|
||||||
|
SequenceEditorTree_PropWithChildren,
|
||||||
|
SequenceEditorTree_SheetObject,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
import {val} from '@theatre/dataverse'
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||||
|
import type {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
import type {KeyframeWithTrack} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
|
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||||
|
import {AggregateKeyframeConnector} from './AggregateKeyframeConnector'
|
||||||
|
import {useAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
|
||||||
|
import {AggregateKeyframeDot} from './AggregateKeyframeDot'
|
||||||
|
|
||||||
|
const AggregateKeyframeEditorContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
`
|
||||||
|
|
||||||
|
export type IAggregateKeyframesAtPosition = {
|
||||||
|
position: number
|
||||||
|
/** all tracks have a keyframe for this position (otherwise, false means 'partial') */
|
||||||
|
allHere: boolean
|
||||||
|
selected: AggregateKeyframePositionIsSelected | undefined
|
||||||
|
keyframes: KeyframeWithTrack[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AggregatedKeyframeConnection = SheetObjectAddress & {
|
||||||
|
trackId: SequenceTrackId
|
||||||
|
left: Keyframe
|
||||||
|
right: Keyframe
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IAggregateKeyframeEditorProps = {
|
||||||
|
index: number
|
||||||
|
aggregateKeyframes: IAggregateKeyframesAtPosition[]
|
||||||
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
|
viewModel:
|
||||||
|
| SequenceEditorTree_PropWithChildren
|
||||||
|
| SequenceEditorTree_SheetObject
|
||||||
|
selection: undefined | DopeSheetSelection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO we're spending a lot of cycles on each render of each aggreagte keyframes.
|
||||||
|
*
|
||||||
|
* Each keyframe node is doing O(N) operations, N being the number of underlying
|
||||||
|
* keyframes it represetns.
|
||||||
|
*
|
||||||
|
* The biggest example is the `isConnectionEditingInCurvePopover()` call which is run
|
||||||
|
* for every underlying keyframe, every time this component is rendered.
|
||||||
|
*
|
||||||
|
* We can optimize this away by doing all of this work _once_ when a curve editor popover
|
||||||
|
* is open. This would require having some kind of stable identity for each aggregate row.
|
||||||
|
* Let's defer that work until other interactive keyframe editing PRs are merged in.
|
||||||
|
*/
|
||||||
|
const AggregateKeyframeEditor: React.VFC<IAggregateKeyframeEditorProps> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
|
const utils = useAggregateKeyframeEditorUtils(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AggregateKeyframeEditorContainer
|
||||||
|
style={{
|
||||||
|
top: `${props.viewModel.nodeHeight / 2}px`,
|
||||||
|
left: `calc(${val(
|
||||||
|
props.layoutP.scaledSpace.leftPadding,
|
||||||
|
)}px + calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||||
|
utils.cur.position
|
||||||
|
}px))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AggregateKeyframeDot editorProps={props} utils={utils} />
|
||||||
|
<AggregateKeyframeConnector editorProps={props} utils={utils} />
|
||||||
|
</AggregateKeyframeEditorContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AggregateKeyframeEditor
|
|
@ -0,0 +1,121 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
const DOT_SIZE_PX = 16
|
||||||
|
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||||
|
|
||||||
|
/** The keyframe diamond ◆ */
|
||||||
|
const DotContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
${absoluteDims(DOT_SIZE_PX)}
|
||||||
|
z-index: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
// hmm kinda weird to organize like this (exporting `HitZone`). Maybe there's a way to re-use
|
||||||
|
// this interpolation of `DotContainer` using something like extended components or something.
|
||||||
|
export const HitZone = styled.div`
|
||||||
|
z-index: 2;
|
||||||
|
cursor: ew-resize;
|
||||||
|
|
||||||
|
${DopeSnapHitZoneUI.CSS}
|
||||||
|
|
||||||
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
|
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover + ${DotContainer},
|
||||||
|
#pointer-root.draggingPositionInSequenceEditor &:hover + ${DotContainer},
|
||||||
|
// notice "," css "or"
|
||||||
|
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} + ${DotContainer} {
|
||||||
|
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export function AggregateKeyframeVisualDot(props: {
|
||||||
|
isSelected: AggregateKeyframePositionIsSelected | undefined
|
||||||
|
isAllHere: boolean
|
||||||
|
}) {
|
||||||
|
const theme: IDotThemeValues = {
|
||||||
|
isSelected: props.isSelected,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DotContainer>
|
||||||
|
{props.isAllHere ? (
|
||||||
|
<AggregateDotAllHereSvg {...theme} />
|
||||||
|
) : (
|
||||||
|
<AggregateDotSomeHereSvg {...theme} />
|
||||||
|
)}
|
||||||
|
</DotContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type IDotThemeValues = {
|
||||||
|
isSelected: AggregateKeyframePositionIsSelected | undefined
|
||||||
|
}
|
||||||
|
const SELECTED_COLOR = '#b8e4e2'
|
||||||
|
const DEFAULT_PRIMARY_COLOR = '#40AAA4'
|
||||||
|
const DEFAULT_SECONDARY_COLOR = '#45747C'
|
||||||
|
const selectionColorAll = (theme: IDotThemeValues) =>
|
||||||
|
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
||||||
|
? SELECTED_COLOR
|
||||||
|
: theme.isSelected ===
|
||||||
|
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
||||||
|
? DEFAULT_PRIMARY_COLOR
|
||||||
|
: DEFAULT_SECONDARY_COLOR
|
||||||
|
const selectionColorSome = (theme: IDotThemeValues) =>
|
||||||
|
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
||||||
|
? SELECTED_COLOR
|
||||||
|
: theme.isSelected ===
|
||||||
|
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
||||||
|
? DEFAULT_PRIMARY_COLOR
|
||||||
|
: DEFAULT_SECONDARY_COLOR
|
||||||
|
const AggregateDotAllHereSvg = (theme: IDotThemeValues) => (
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="4.46443"
|
||||||
|
y="10.0078"
|
||||||
|
width="5"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(-45 4.46443 10.0078)"
|
||||||
|
fill="#212327" // background knockout fill
|
||||||
|
stroke={selectionColorSome(theme)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="3.75732"
|
||||||
|
y="6.01953"
|
||||||
|
width="6"
|
||||||
|
height="6"
|
||||||
|
transform="rotate(-45 3.75732 6.01953)"
|
||||||
|
fill={selectionColorAll(theme)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
// when the aggregate keyframes are sparse across tracks at this position
|
||||||
|
const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => (
|
||||||
|
<svg
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="4.46443"
|
||||||
|
y="8"
|
||||||
|
width="5"
|
||||||
|
height="5"
|
||||||
|
transform="rotate(-45 4.46443 8)"
|
||||||
|
fill="#23262B"
|
||||||
|
stroke={selectionColorAll(theme)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function iif<F extends () => any>(fn: F): ReturnType<F> {
|
||||||
|
return fn()
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import {prism} from '@theatre/dataverse'
|
||||||
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
import {isConnectionEditingInCurvePopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
||||||
|
import {usePrism} from '@theatre/react'
|
||||||
|
import {selectedKeyframeConnections} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||||
|
import type {
|
||||||
|
IAggregateKeyframeEditorProps,
|
||||||
|
AggregatedKeyframeConnection,
|
||||||
|
} from './AggregateKeyframeEditor'
|
||||||
|
import {iif} from './iif'
|
||||||
|
|
||||||
|
export type IAggregateKeyframeEditorUtils = ReturnType<
|
||||||
|
typeof useAggregateKeyframeEditorUtils
|
||||||
|
>
|
||||||
|
|
||||||
|
// I think this was pulled out for performance
|
||||||
|
// 1/10: Not sure this is properly split up
|
||||||
|
export function useAggregateKeyframeEditorUtils(
|
||||||
|
props: Pick<
|
||||||
|
IAggregateKeyframeEditorProps,
|
||||||
|
'index' | 'aggregateKeyframes' | 'selection' | 'viewModel'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const {index, aggregateKeyframes, selection} = props
|
||||||
|
const sheetObjectAddress = props.viewModel.sheetObject.address
|
||||||
|
|
||||||
|
return usePrism(() => {
|
||||||
|
const cur = aggregateKeyframes[index]
|
||||||
|
const next = aggregateKeyframes[index + 1]
|
||||||
|
|
||||||
|
const curAndNextAggregateKeyframesMatch =
|
||||||
|
next &&
|
||||||
|
cur.keyframes.length === next.keyframes.length &&
|
||||||
|
cur.keyframes.every(({track}, ind) => next.keyframes[ind].track === track)
|
||||||
|
|
||||||
|
const connected = curAndNextAggregateKeyframesMatch
|
||||||
|
? {
|
||||||
|
length: next.position - cur.position,
|
||||||
|
selected:
|
||||||
|
cur.selected === AggregateKeyframePositionIsSelected.AllSelected &&
|
||||||
|
next.selected === AggregateKeyframePositionIsSelected.AllSelected,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
|
const aggregatedConnections: AggregatedKeyframeConnection[] = !connected
|
||||||
|
? []
|
||||||
|
: cur.keyframes.map(({kf, track}, i) => ({
|
||||||
|
...sheetObjectAddress,
|
||||||
|
trackId: track.id,
|
||||||
|
left: kf,
|
||||||
|
right: next.keyframes[i].kf,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const allConnections = iif(() => {
|
||||||
|
const {projectId, sheetId} = sheetObjectAddress
|
||||||
|
|
||||||
|
const selectedConnections = prism
|
||||||
|
.memo(
|
||||||
|
'selectedConnections',
|
||||||
|
() =>
|
||||||
|
selectedKeyframeConnections(
|
||||||
|
sheetObjectAddress.projectId,
|
||||||
|
sheetObjectAddress.sheetId,
|
||||||
|
selection,
|
||||||
|
),
|
||||||
|
[projectId, sheetId, selection],
|
||||||
|
)
|
||||||
|
.getValue()
|
||||||
|
|
||||||
|
return [...aggregatedConnections, ...selectedConnections]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAggregateEditingInCurvePopover = aggregatedConnections.every(
|
||||||
|
(con) => isConnectionEditingInCurvePopover(con),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
cur,
|
||||||
|
connected,
|
||||||
|
isAggregateEditingInCurvePopover,
|
||||||
|
allConnections,
|
||||||
|
}
|
||||||
|
}, [index, aggregateKeyframes, selection, sheetObjectAddress])
|
||||||
|
}
|
|
@ -14,8 +14,8 @@ import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor'
|
import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor/AggregateKeyframeEditor'
|
||||||
import AggregateKeyframeEditor from './AggregateKeyframeEditor'
|
import AggregateKeyframeEditor from './AggregateKeyframeEditor/AggregateKeyframeEditor'
|
||||||
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'
|
||||||
|
|
||||||
|
@ -66,12 +66,14 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
|
||||||
...aggregatedKeyframes.byPosition.entries(),
|
...aggregatedKeyframes.byPosition.entries(),
|
||||||
]
|
]
|
||||||
.sort((a, b) => a[0] - b[0])
|
.sort((a, b) => a[0] - b[0])
|
||||||
.map(([position, keyframes]) => ({
|
.map(
|
||||||
|
([position, keyframes]): IAggregateKeyframesAtPosition => ({
|
||||||
position,
|
position,
|
||||||
keyframes,
|
keyframes,
|
||||||
selected: selectedPositions.get(position),
|
selected: selectedPositions.get(position),
|
||||||
allHere: keyframes.length === aggregatedKeyframes.tracks.length,
|
allHere: keyframes.length === aggregatedKeyframes.tracks.length,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
|
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
|
||||||
<AggregateKeyframeEditor
|
<AggregateKeyframeEditor
|
||||||
|
|
Loading…
Reference in a new issue