diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor.tsx deleted file mode 100644 index fb16056..0000000 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor.tsx +++ /dev/null @@ -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 - 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 = ( - 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 ( - - ) - }, - ) - - const [nodeRef, node] = useRefAndState(null) - - return ( - - - {connected ? ( - { - if (node) openPopover(e, node) - }} - /> - ) : ( - noConnector - )} - {popoverNode} - - ) -} - -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 ( - - - - ) -}) - -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, -) { - return ( - <> - - - {props.isAllHere ? ( - - ) : ( - - )} - - - ) -} - -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) => ( - - - - -) - -// when the aggregate keyframes are sparse across tracks at this position -const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => ( - - - -) - -export default AggregateKeyframeEditor diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeConnector.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeConnector.tsx new file mode 100644 index 0000000..2c83477 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeConnector.tsx @@ -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 ( + + + + ) +}) + +export const AggregateKeyframeConnector: React.VFC = + (props) => { + const [nodeRef, node] = useRefAndState(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 ( + + ) + }, + ) + + 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 ? ( + <> + { + 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(() => { + 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) +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeDot.tsx new file mode 100644 index 0000000..7f9a9a3 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeDot.tsx @@ -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, +) { + const logger = useLogger('AggregateKeyframeDot') + const {cur} = props.utils + + const [ref, node] = useRefAndState(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 ( + <> + + + {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(() => { + 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] +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeEditor.tsx new file mode 100644 index 0000000..2165318 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeEditor.tsx @@ -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 + 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 = ( + props, +) => { + const utils = useAggregateKeyframeEditorUtils(props) + + return ( + + + + + ) +} + +export default AggregateKeyframeEditor diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeVisualDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeVisualDot.tsx new file mode 100644 index 0000000..9636647 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeVisualDot.tsx @@ -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 ( + + {props.isAllHere ? ( + + ) : ( + + )} + + ) +} +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) => ( + + + + +) +// when the aggregate keyframes are sparse across tracks at this position +const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => ( + + + +) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/iif.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/iif.tsx new file mode 100644 index 0000000..58b71b8 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/iif.tsx @@ -0,0 +1,3 @@ +export function iif any>(fn: F): ReturnType { + return fn() +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/useAggregateKeyframeEditorUtils.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/useAggregateKeyframeEditorUtils.tsx new file mode 100644 index 0000000..0904f94 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/useAggregateKeyframeEditorUtils.tsx @@ -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]) +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx index 4026d53..890101f 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx @@ -14,8 +14,8 @@ import React from 'react' import styled from 'styled-components' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useRefAndState from '@theatre/studio/utils/useRefAndState' -import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor' -import AggregateKeyframeEditor from './AggregateKeyframeEditor' +import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor/AggregateKeyframeEditor' +import AggregateKeyframeEditor from './AggregateKeyframeEditor/AggregateKeyframeEditor' import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' import {useLogger} from '@theatre/studio/uiComponents/useLogger' @@ -66,12 +66,14 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) { ...aggregatedKeyframes.byPosition.entries(), ] .sort((a, b) => a[0] - b[0]) - .map(([position, keyframes]) => ({ - position, - keyframes, - selected: selectedPositions.get(position), - allHere: keyframes.length === aggregatedKeyframes.tracks.length, - })) + .map( + ([position, keyframes]): IAggregateKeyframesAtPosition => ({ + position, + keyframes, + selected: selectedPositions.get(position), + allHere: keyframes.length === aggregatedKeyframes.tracks.length, + }), + ) const keyframeEditors = posKfs.map(({position, keyframes}, index) => (