diff --git a/theatre/shared/src/utils/ids.ts b/theatre/shared/src/utils/ids.ts index 194a99d..a8021cb 100644 --- a/theatre/shared/src/utils/ids.ts +++ b/theatre/shared/src/utils/ids.ts @@ -3,6 +3,7 @@ import type {PathToProp} from './addresses' import stableValueHash from './stableJsonStringify' import {nanoid as generateNonSecure} from 'nanoid/non-secure' import type {Nominal} from './Nominal' +import type Sheet from '@theatre/core/sheets/Sheet' export type KeyframeId = Nominal<'KeyframeId'> @@ -106,6 +107,12 @@ export const createStudioSheetItemKey = { position, ) }, + forSheetAggregateKeyframe(obj: Sheet, position: number): StudioSheetItemKey { + return stableValueHash({ + o: obj.address.sheetId, + pos: position, + }) as StudioSheetItemKey + }, forCompoundPropAggregateKeyframe( obj: SheetObject, pathToProp: PathToProp, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/AGGREGATE_COPY_PASTE.md b/theatre/studio/src/panels/SequenceEditorPanel/AGGREGATE_COPY_PASTE.md index abf018a..b0b2bcc 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/AGGREGATE_COPY_PASTE.md +++ b/theatre/studio/src/panels/SequenceEditorPanel/AGGREGATE_COPY_PASTE.md @@ -1,14 +1,30 @@ ## The keyframe copy/paste algorithm -``` -copy algorithm: find the closest common acnestor for the tracks selected +The copy and paste algorithms are specified below. Note that the copy algorithm +is written with some capital letters to emphasize organization, its not an +actual language or anything. The copy algorithm changed recently and the +examples are more up-to-date than the paste algorithm because pasting stayed the +same. -- obj1.props.transform.position.x => simple +``` +ALGORITHM copy: + +LET PATH = + CASE copy selection / single track THEN the path relative to the closest common ancestor for the tracks selected + CASE copy aggregate track THEN the path relative the aggregate track compoundProp/sheetObject/sheet + +FOR EXAMPLE CASE copy selection / single track: +- obj1.props.transform.position.x => x - obj1.props.transform.position.{x, z} => {x, z} - obj1.props.transform.position.{x, z} + obj1.props.transform.rotation.z => {position: {x, z}, rotation: {z}} -paste: +FOR EXAMPLE CASE copy aggregate track: +- sheet.obj1.props.transform.position => {x, y, z} +- sheet.obj1.props.transform => {position: {x, y, z}, rotation: {x, y, z}} +- sheet => { obj1: { props: { transform: {position: {x, y, z}, rotation: {x, y, z}}}}} + +ALGORITHM: paste: - simple => simple => 1-1 - simple => {x, y} => {x: simple, y: simple} (distribute to all) @@ -25,4 +41,4 @@ paste: - {x, y} => {object(not a prop): {x, y}} => {x, y} - What this means is that, in case of objects and sheets, we do a forEach at each object, then try pasting onto its object.props -``` \ No newline at end of file +``` diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx index 5b3bad0..7f9db6f 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/AnyCompositeRow.tsx @@ -2,6 +2,7 @@ import {theme} from '@theatre/studio/css' import type { SequenceEditorTree_PrimitiveProp, SequenceEditorTree_PropWithChildren, + SequenceEditorTree_Sheet, SequenceEditorTree_SheetObject, } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import type {VoidFn} from '@theatre/shared/utils/types' @@ -76,6 +77,7 @@ const LeftRowChildren = styled.ul` const AnyCompositeRow: React.FC<{ leaf: + | SequenceEditorTree_Sheet | SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx index 9c85ff6..b850b14 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/PropWithChildrenRow.tsx @@ -5,7 +5,7 @@ import type { import React from 'react' import AnyCompositeRow from './AnyCompositeRow' import PrimitivePropRow from './PrimitivePropRow' -import {setCollapsedSheetObjectOrCompoundProp} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp' +import {setCollapsedSheetItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp' export const decideRowByPropType = ( leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp, @@ -31,7 +31,7 @@ const PropWithChildrenRow: React.VFC<{ label={leaf.pathToProp[leaf.pathToProp.length - 1]} isCollapsed={leaf.isCollapsed} toggleCollapsed={() => - setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, { + setCollapsedSheetItem(!leaf.isCollapsed, { sheetAddress: leaf.sheetObject.address, sheetItemKey: leaf.sheetItemKey, }) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx index a5075b1..4ae810a 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetObjectRow.tsx @@ -2,7 +2,7 @@ import type {SequenceEditorTree_SheetObject} from '@theatre/studio/panels/Sequen import React from 'react' import AnyCompositeRow from './AnyCompositeRow' import {decideRowByPropType} from './PropWithChildrenRow' -import {setCollapsedSheetObjectOrCompoundProp} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp' +import {setCollapsedSheetItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp' import getStudio from '@theatre/studio/getStudio' const LeftSheetObjectRow: React.VFC<{ @@ -22,7 +22,7 @@ const LeftSheetObjectRow: React.VFC<{ }) }} toggleCollapsed={() => - setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, { + setCollapsedSheetItem(!leaf.isCollapsed, { sheetAddress: leaf.sheetObject.address, sheetItemKey: leaf.sheetItemKey, }) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx index 49c9b91..6be3adc 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Left/SheetRow.tsx @@ -2,20 +2,32 @@ import type {SequenceEditorTree_Sheet} from '@theatre/studio/panels/SequenceEdit import {usePrism} from '@theatre/react' import React from 'react' import LeftSheetObjectRow from './SheetObjectRow' +import AnyCompositeRow from './AnyCompositeRow' +import {setCollapsedSheetItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp' const SheetRow: React.VFC<{ leaf: SequenceEditorTree_Sheet }> = ({leaf}) => { return usePrism(() => { return ( - <> + { + setCollapsedSheetItem(!leaf.isCollapsed, { + sheetAddress: leaf.sheet.address, + sheetItemKey: leaf.sheetItemKey, + }) + }} + > {leaf.children.map((sheetObjectLeaf) => ( ))} - + ) }, [leaf]) } 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 index 218602d..cf6f53e 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeConnector.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeConnector.tsx @@ -124,16 +124,20 @@ function useDragKeyframe( const keyframes = props.aggregateKeyframes[props.index].keyframes + const {selection, viewModel} = props + const address = + viewModel.type === 'sheet' + ? viewModel.sheet.address + : viewModel.sheetObject.address + if ( - props.selection && + selection && props.aggregateKeyframes[props.index].selected === AggregateKeyframePositionIsSelected.AllSelected ) { - const {selection, viewModel} = props - const {sheetObject} = viewModel return selection .getDragHandlers({ - ...sheetObject.address, + ...address, domNode: node!, positionAtStartOfDrag: props.aggregateKeyframes[props.index].position, @@ -159,7 +163,7 @@ function useDragKeyframe( for (const keyframe of keyframes) { stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes( { - ...propsAtStartOfDrag.viewModel.sheetObject.address, + ...keyframe.track.sheetObject.address, trackId: keyframe.track.id, keyframeIds: [ keyframe.kf.id, @@ -209,8 +213,7 @@ function useConnectorContextMenu( (acc, con) => acc.concat( keyframesWithPaths({ - ...props.editorProps.viewModel.sheetObject.address, - trackId: con.trackId, + ...con, keyframeIds: [con.left.id, con.right.id], }) ?? [], ), @@ -226,14 +229,20 @@ function useConnectorContextMenu( pathToProp: pathToProp.slice(commonPath.length), })) + const viewModel = props.editorProps.viewModel + const address = + viewModel.type === 'sheet' + ? viewModel.sheet.address + : viewModel.sheetObject.address + return [ { label: 'Copy', callback: () => { if (props.editorProps.selection) { const copyableKeyframes = copyableKeyframesFromSelection( - props.editorProps.viewModel.sheetObject.address.projectId, - props.editorProps.viewModel.sheetObject.address.sheetId, + address.projectId, + address.sheetId, props.editorProps.selection, ) getStudio().transaction((api) => { @@ -260,7 +269,8 @@ function useConnectorContextMenu( for (const con of props.utils.allConnections) { stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes( { - ...props.editorProps.viewModel.sheetObject.address, + ...address, + objectKey: con.objectKey, keyframeIds: [con.left.id, con.right.id], trackId: con.trackId, }, 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 index d2c01c8..88ebdfc 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeDot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeDot.tsx @@ -3,7 +3,6 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState' import usePresence, { PresenceFlag, } from '@theatre/studio/uiComponents/usePresence' -import {useLogger} from '@theatre/studio/uiComponents/useLogger' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor' import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils' @@ -15,7 +14,6 @@ import { } from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections' import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric' import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses' -import type {ILogger} from '@theatre/shared/logger' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' import type { PrimitivePropEditingOptions, @@ -96,11 +94,12 @@ function primitivePropBuild( export function AggregateKeyframeDot( props: React.PropsWithChildren, ) { - const logger = useLogger('AggregateKeyframeDot') const {cur} = props.utils const inlineEditorPopover = useKeyframeInlineEditorPopover( - props.editorProps.viewModel.type === 'sheetObject' + props.editorProps.viewModel.type === 'sheet' + ? null + : props.editorProps.viewModel.type === 'sheetObject' ? sheetObjectBuild(props.editorProps.viewModel, cur.keyframes) ?.children ?? null : propWithChildrenBuild(props.editorProps.viewModel, cur.keyframes) @@ -125,7 +124,7 @@ export function AggregateKeyframeDot( const [ref, node] = useRefAndState(null) - const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node) + const [contextMenu] = useAggregateKeyframeContextMenu(props, node) return ( <> @@ -135,7 +134,11 @@ export function AggregateKeyframeDot( // Need this for the dragging logic to be able to get the keyframe props // based on the position. {...DopeSnap.includePositionSnapAttrs(cur.position)} - onClick={(e) => inlineEditorPopover.open(e, ref.current!)} + onClick={(e) => + props.editorProps.viewModel.type !== 'sheet' + ? inlineEditorPopover.open(e, ref.current!) + : null + } /> { - // see AGGREGATE_COPY_PASTE.md for explanation of this - // code that makes some keyframes with paths for copying - // to clipboard - const kfs = props.utils.cur.keyframes.reduce( - (acc, kfWithTrack) => - acc.concat( - keyframesWithPaths({ - ...props.editorProps.viewModel.sheetObject.address, - trackId: kfWithTrack.track.id, - keyframeIds: [kfWithTrack.kf.id], - }) ?? [], - ), - [] as KeyframeWithPathToPropFromCommonRoot[], - ) - - const commonPath = commonRootOfPathsToProps( - kfs.map((kf) => kf.pathToProp), - ) - - const keyframesWithCommonRootPath = kfs.map(({keyframe, pathToProp}) => ({ - keyframe, - pathToProp: pathToProp.slice(commonPath.length), - })) + const viewModel = props.editorProps.viewModel + const selection = props.editorProps.selection return [ { - label: props.editorProps.selection ? 'Copy (selection)' : 'Copy', + label: selection ? 'Copy (selection)' : 'Copy', callback: () => { - if (props.editorProps.selection) { + // see AGGREGATE_COPY_PASTE.md for explanation of this + // code that makes some keyframes with paths for copying + // to clipboard + if (selection) { + const {projectId, sheetId} = + viewModel.type === 'sheet' + ? viewModel.sheet.address + : viewModel.sheetObject.address const copyableKeyframes = copyableKeyframesFromSelection( - props.editorProps.viewModel.sheetObject.address.projectId, - props.editorProps.viewModel.sheetObject.address.sheetId, - props.editorProps.selection, + projectId, + sheetId, + selection, ) getStudio().transaction((api) => { api.stateEditors.studio.ahistoric.setClipboardKeyframes( @@ -196,6 +184,38 @@ function useAggregateKeyframeContextMenu( ) }) } else { + const kfs: KeyframeWithPathToPropFromCommonRoot[] = + props.utils.cur.keyframes.flatMap( + (kfWithTrack) => + keyframesWithPaths({ + ...kfWithTrack.track.sheetObject.address, + trackId: kfWithTrack.track.id, + keyframeIds: [kfWithTrack.kf.id], + }) ?? [], + ) + + const basePathRelativeToSheet = + viewModel.type === 'sheet' + ? [] + : viewModel.type === 'sheetObject' + ? [viewModel.sheetObject.address.objectKey] + : viewModel.type === 'propWithChildren' + ? [ + viewModel.sheetObject.address.objectKey, + ...viewModel.pathToProp, + ] + : [] // should be unreachable unless new viewModel/leaf types are added + const commonPath = commonRootOfPathsToProps([ + basePathRelativeToSheet, + ...kfs.map((kf) => kf.pathToProp), + ]) + + const keyframesWithCommonRootPath = kfs.map( + ({keyframe, pathToProp}) => ({ + keyframe, + pathToProp: pathToProp.slice(commonPath.length), + }), + ) getStudio().transaction((api) => { api.stateEditors.studio.ahistoric.setClipboardKeyframes( keyframesWithCommonRootPath, @@ -205,16 +225,16 @@ function useAggregateKeyframeContextMenu( }, }, { - label: props.editorProps.selection ? 'Delete (selection)' : 'Delete', + label: selection ? 'Delete (selection)' : 'Delete', callback: () => { - if (props.editorProps.selection) { - props.editorProps.selection.delete() + if (selection) { + selection.delete() } else { getStudio().transaction(({stateEditors}) => { for (const kfWithTrack of props.utils.cur.keyframes) { stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes( { - ...props.editorProps.viewModel.sheetObject.address, + ...kfWithTrack.track.sheetObject.address, keyframeIds: [kfWithTrack.kf.id], trackId: kfWithTrack.track.id, }, @@ -226,8 +246,5 @@ function useAggregateKeyframeContextMenu( }, ] }, - onOpen() { - logger._debug('Show aggregate keyframe', props) - }, }) } 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 index 1c461d2..5e2f84e 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeEditor.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/AggregateKeyframeEditor.tsx @@ -5,6 +5,7 @@ import type { } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type { SequenceEditorTree_PropWithChildren, + SequenceEditorTree_Sheet, SequenceEditorTree_SheetObject, } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import type {Pointer} from '@theatre/dataverse' @@ -44,6 +45,7 @@ export type IAggregateKeyframeEditorProps = { viewModel: | SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject + | SequenceEditorTree_Sheet selection: undefined | DopeSheetSelection } 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 index 56a57b8..b7a4bf3 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/useAggregateKeyframeEditorUtils.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregateKeyframeEditor/useAggregateKeyframeEditorUtils.tsx @@ -23,13 +23,12 @@ export function useAggregateKeyframeEditorUtils( >, ) { const {index, aggregateKeyframes, selection} = props - const sheetObjectAddress = props.viewModel.sheetObject.address return usePrism(getAggregateKeyframeEditorUtilsPrismFn(props), [ index, aggregateKeyframes, selection, - sheetObjectAddress, + props.viewModel, ]) } @@ -42,7 +41,11 @@ export function getAggregateKeyframeEditorUtilsPrismFn( >, ) { const {index, aggregateKeyframes, selection} = props - const sheetObjectAddress = props.viewModel.sheetObject.address + + const {projectId, sheetId} = + props.viewModel.type === 'sheet' + ? props.viewModel.sheet.address + : props.viewModel.sheetObject.address return () => { const cur = aggregateKeyframes[index] @@ -65,24 +68,17 @@ export function getAggregateKeyframeEditorUtilsPrismFn( const aggregatedConnections: AggregatedKeyframeConnection[] = !connected ? [] : cur.keyframes.map(({kf, track}, i) => ({ - ...sheetObjectAddress, + ...track.sheetObject.address, 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, - ), + () => selectedKeyframeConnections(projectId, sheetId, selection), [projectId, sheetId, selection], ) .getValue() @@ -97,7 +93,12 @@ export function getAggregateKeyframeEditorUtilsPrismFn( const itemKey = prism.memo( 'itemKey', () => { - if (props.viewModel.type === 'sheetObject') { + if (props.viewModel.type === 'sheet') { + return createStudioSheetItemKey.forSheetAggregateKeyframe( + props.viewModel.sheet, + cur.position, + ) + } else if (props.viewModel.type === 'sheetObject') { return createStudioSheetItemKey.forSheetObjectAggregateKeyframe( props.viewModel.sheetObject, cur.position, @@ -110,7 +111,7 @@ export function getAggregateKeyframeEditorUtilsPrismFn( ) } }, - [props.viewModel.sheetObject, cur.position], + [props.viewModel, cur.position], ) return { 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 2693561..bc9eb92 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack.tsx @@ -4,10 +4,11 @@ import type { } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type { SequenceEditorTree_PropWithChildren, + SequenceEditorTree_Sheet, SequenceEditorTree_SheetObject, } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import {usePrism, useVal} from '@theatre/react' -import type {Pointer} from '@theatre/dataverse' +import type {IDerivation, Pointer} from '@theatre/dataverse' import {prism, val, valueDerivation} from '@theatre/dataverse' import React, {useMemo, Fragment} from 'react' import styled from 'styled-components' @@ -19,7 +20,11 @@ import type { IAggregateKeyframeEditorProps, } from './AggregateKeyframeEditor/AggregateKeyframeEditor' import AggregateKeyframeEditor from './AggregateKeyframeEditor/AggregateKeyframeEditor' -import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' +import type { + AggregatedKeyframes, + KeyframeWithTrack, +} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' +import {collectAggregateSnapPositionsObjectOrCompound} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' import {useLogger} from '@theatre/studio/uiComponents/useLogger' import {getAggregateKeyframeEditorUtilsPrismFn} from './AggregateKeyframeEditor/useAggregateKeyframeEditorUtils' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' @@ -35,7 +40,7 @@ import { doesPathStartWith, encodePathToProp, } from '@theatre/shared/utils/addresses' -import type {SequenceTrackId} from '@theatre/shared/utils/ids' +import type {ObjectAddressKey, SequenceTrackId} from '@theatre/shared/utils/ids' import type Sequence from '@theatre/core/sequences/Sequence' import KeyframeSnapTarget, { snapPositionsStateD, @@ -47,7 +52,7 @@ import { snapToNone, snapToSome, } from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget' -import {collectAggregateSnapPositions} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' +import {collectAggregateSnapPositionsSheet} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' const AggregatedKeyframeTrackContainer = styled.div` @@ -60,6 +65,7 @@ type IAggregatedKeyframeTracksProps = { viewModel: | SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject + | SequenceEditorTree_Sheet aggregatedKeyframes: AggregatedKeyframes layoutP: Pointer } @@ -83,7 +89,6 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) { const {selectedPositions, selection} = useCollectedSelectedPositions( layoutP, - viewModel, aggregatedKeyframes, ) @@ -118,7 +123,13 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) { : emptyObject const aggregateSnapPositions = useMemo( - () => collectAggregateSnapPositions(viewModel, snapPositions), + () => + viewModel.type === 'sheet' + ? collectAggregateSnapPositionsSheet(viewModel, snapPositions) + : collectAggregateSnapPositionsObjectOrCompound( + viewModel, + snapPositions, + ), [snapPositions], ) @@ -208,19 +219,22 @@ const {AllSelected, AtLeastOneUnselected, NoneSelected} = /** Helper to put together the selected positions */ function useCollectedSelectedPositions( layoutP: Pointer, - viewModel: - | SequenceEditorTree_PropWithChildren - | SequenceEditorTree_SheetObject, aggregatedKeyframes: AggregatedKeyframes, ): _AggSelection { - return usePrism(() => { + return usePrism( + () => val(collectedSelectedPositions(layoutP, aggregatedKeyframes)), + [layoutP, aggregatedKeyframes], + ) +} + +function collectedSelectedPositions( + layoutP: Pointer, + aggregatedKeyframes: AggregatedKeyframes, +): IDerivation<_AggSelection> { + return prism(() => { const selectionAtom = val(layoutP.selectionAtom) - const sheetObjectSelection = val( - selectionAtom.pointer.current.byObjectKey[ - viewModel.sheetObject.address.objectKey - ], - ) - if (!sheetObjectSelection) return EMPTY_SELECTION + const selection = val(selectionAtom.pointer.current) + if (!selection) return EMPTY_SELECTION const selectedAtPositions = new Map< number, @@ -228,34 +242,14 @@ function useCollectedSelectedPositions( >() for (const [position, kfsWithTrack] of aggregatedKeyframes.byPosition) { - let positionIsSelected: undefined | AggregateKeyframePositionIsSelected = - undefined - for (const kfWithTrack of kfsWithTrack) { - const kfIsSelected = - sheetObjectSelection.byTrackId[kfWithTrack.track.id]?.byKeyframeId?.[ - kfWithTrack.kf.id - ] === true - // -1/10: This sux - // undefined = have not encountered - if (positionIsSelected === undefined) { - // first item - if (kfIsSelected) { - positionIsSelected = AllSelected - } else { - positionIsSelected = NoneSelected - } - } else if (kfIsSelected) { - if (positionIsSelected === NoneSelected) { - positionIsSelected = AtLeastOneUnselected - } - } else { - if (positionIsSelected === AllSelected) { - positionIsSelected = AtLeastOneUnselected - } - } - } - - if (positionIsSelected != null) { + const positionIsSelected = allOrSomeOrNoneSelected( + kfsWithTrack, + selection, + ) + if ( + positionIsSelected !== undefined && + positionIsSelected !== NoneSelected + ) { selectedAtPositions.set(position, positionIsSelected) } } @@ -264,7 +258,38 @@ function useCollectedSelectedPositions( selectedPositions: selectedAtPositions, selection: val(selectionAtom.pointer.current), } - }, [layoutP, aggregatedKeyframes]) + }) +} + +function allOrSomeOrNoneSelected( + keyframeWithTracks: KeyframeWithTrack[], + selection: DopeSheetSelection, +): AggregateKeyframePositionIsSelected | undefined { + let positionIsSelected: undefined | AggregateKeyframePositionIsSelected = + undefined + + for (const {track, kf} of keyframeWithTracks) { + const kfIsSelected = + selection.byObjectKey[track.sheetObject.address.objectKey]?.byTrackId[ + track.id + ]?.byKeyframeId?.[kf.id] === true + if (positionIsSelected === undefined) { + if (kfIsSelected) { + positionIsSelected = AllSelected + } else { + positionIsSelected = NoneSelected + } + } else if (kfIsSelected) { + if (positionIsSelected === NoneSelected) { + positionIsSelected = AtLeastOneUnselected + } + } else { + if (positionIsSelected === AllSelected) { + positionIsSelected = AtLeastOneUnselected + } + } + } + return positionIsSelected } function useAggregatedKeyframeTrackContextMenu( @@ -297,7 +322,11 @@ function pasteKeyframesContextMenuItem( const sheet = val(props.layoutP.sheet) const sequence = sheet.getSequence() - pasteKeyframes(props.viewModel, keyframes, sequence) + if (props.viewModel.type === 'sheet') { + pasteKeyframesSheet(props.viewModel, keyframes, sequence) + } else { + pasteKeyframesObjectOrCompound(props.viewModel, keyframes, sequence) + } }, } } @@ -313,7 +342,74 @@ function pasteKeyframesContextMenuItem( * @see StudioAhistoricState.clipboard * @see setClipboardNestedKeyframes */ -function pasteKeyframes( +function pasteKeyframesSheet( + viewModel: SequenceEditorTree_Sheet, + keyframes: KeyframeWithPathToPropFromCommonRoot[], + sequence: Sequence, +) { + const {projectId, sheetId, sheetInstanceId} = viewModel.sheet.address + + const areKeyframesAllOnSingleTrack = keyframes.every( + ({pathToProp}) => pathToProp.length === 0, + ) + + if (areKeyframesAllOnSingleTrack) { + for (const object of viewModel.children.map((child) => child.sheetObject)) { + const tracksByObject = valueDerivation( + getStudio().atomP.historic.coreByProject[projectId].sheetsById[sheetId] + .sequence.tracksByObject[object.address.objectKey], + ).getValue() + + const trackIdsOnObject = Object.keys(tracksByObject?.trackData ?? {}) + + pasteKeyframesToMultipleTracks( + object.address, + trackIdsOnObject, + keyframes, + sequence, + ) + } + } else { + const tracksByObject = valueDerivation( + getStudio().atomP.historic.coreByProject[projectId].sheetsById[sheetId] + .sequence.tracksByObject, + ).getValue() + + const placeableKeyframes = keyframes + .map(({keyframe, pathToProp}) => { + const objectKey = pathToProp[0] as ObjectAddressKey + const relativePathToProp = pathToProp.slice(1) + const pathToPropEncoded = encodePathToProp([...relativePathToProp]) + + const trackIdByPropPath = + tracksByObject?.[objectKey]?.trackIdByPropPath ?? {} + + const maybeTrackId = trackIdByPropPath[pathToPropEncoded] + + return maybeTrackId + ? { + keyframe, + trackId: maybeTrackId, + address: { + objectKey, + projectId, + sheetId, + sheetInstanceId, + }, + } + : null + }) + .filter((result) => result !== null) as { + keyframe: Keyframe + trackId: SequenceTrackId + address: SheetObjectAddress + }[] + + pasteKeyframesToSpecificTracks(placeableKeyframes, sequence) + } +} + +function pasteKeyframesObjectOrCompound( viewModel: | SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, @@ -322,7 +418,7 @@ function pasteKeyframes( ) { const {projectId, sheetId, objectKey} = viewModel.sheetObject.address - const tracksByObject = valueDerivation( + const trackRecords = valueDerivation( getStudio().atomP.historic.coreByProject[projectId].sheetsById[sheetId] .sequence.tracksByObject[objectKey], ).getValue() @@ -332,7 +428,7 @@ function pasteKeyframes( ) if (areKeyframesAllOnSingleTrack) { - const trackIdsOnObject = Object.keys(tracksByObject?.trackData ?? {}) + const trackIdsOnObject = Object.keys(trackRecords?.trackData ?? {}) if (viewModel.type === 'sheetObject') { pasteKeyframesToMultipleTracks( @@ -342,7 +438,7 @@ function pasteKeyframes( sequence, ) } else { - const trackIdByPropPath = tracksByObject?.trackIdByPropPath || {} + const trackIdByPropPath = trackRecords?.trackIdByPropPath || {} const trackIdsOnCompoundProp = Object.entries(trackIdByPropPath) .filter( @@ -364,7 +460,7 @@ function pasteKeyframes( ) } } else { - const trackIdByPropPath = tracksByObject?.trackIdByPropPath || {} + const trackIdByPropPath = trackRecords?.trackIdByPropPath || {} const rootPath = viewModel.type === 'propWithChildren' ? viewModel.pathToProp : [] @@ -382,19 +478,17 @@ function pasteKeyframes( ? { keyframe, trackId: maybeTrackId, + address: viewModel.sheetObject.address, } : null }) .filter((result) => result !== null) as { keyframe: Keyframe trackId: SequenceTrackId + address: SheetObjectAddress }[] - pasteKeyframesToSpecificTracks( - viewModel.sheetObject.address, - placeableKeyframes, - sequence, - ) + pasteKeyframesToSpecificTracks(placeableKeyframes, sequence) } } @@ -428,10 +522,10 @@ function pasteKeyframesToMultipleTracks( } function pasteKeyframesToSpecificTracks( - address: SheetObjectAddress, keyframesWithTracksToPlaceThemIn: { keyframe: Keyframe trackId: SequenceTrackId + address: SheetObjectAddress }[], sequence: Sequence, ) { @@ -441,7 +535,11 @@ function pasteKeyframesToSpecificTracks( )?.position! getStudio()!.transaction(({stateEditors}) => { - for (const {keyframe, trackId} of keyframesWithTracksToPlaceThemIn) { + for (const { + keyframe, + trackId, + address, + } of keyframesWithTracksToPlaceThemIn) { stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( { ...address, @@ -499,11 +597,14 @@ function useDragForAggregateKeyframeDot( getAggregateKeyframeEditorUtilsPrismFn(props), ).getValue().cur.keyframes + const address = + props.viewModel.type === 'sheet' + ? props.viewModel.sheet.address + : props.viewModel.sheetObject.address + const tracksByObject = val( - getStudio()!.atomP.historic.coreByProject[ - props.viewModel.sheetObject.address.projectId - ].sheetsById[props.viewModel.sheetObject.address.sheetId].sequence - .tracksByObject, + getStudio()!.atomP.historic.coreByProject[address.projectId] + .sheetsById[address.sheetId].sequence.tracksByObject, )! // Calculate all the valid snap positions in the sequence editor, @@ -537,10 +638,9 @@ function useDragForAggregateKeyframeDot( AggregateKeyframePositionIsSelected.AllSelected ) { const {selection, viewModel} = props - const {sheetObject} = viewModel const handlers = selection .getDragHandlers({ - ...sheetObject.address, + ...address, domNode: containerNode!, positionAtStartOfDrag: keyframes[0].kf.position, }) @@ -587,7 +687,7 @@ function useDragForAggregateKeyframeDot( const original = keyframe.kf stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( { - ...propsAtStartOfDrag.viewModel.sheetObject.address, + ...keyframe.track.sheetObject.address, trackId: keyframe.track.id, keyframes: [{...original, position: newPosition}], snappingFunction: val( diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx index 0a3dfd3..c11dc75 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -74,7 +74,11 @@ const BasicKeyframedTrack: React.VFC = React.memo( const snapToAllKeyframes = snapPositionsState.mode === 'snapToAll' const track = useMemo( - () => ({data: trackData, id: leaf.trackId}), + () => ({ + data: trackData, + id: leaf.trackId, + sheetObject: props.leaf.sheetObject, + }), [trackData, leaf.trackId], ) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForSingleKeyframe.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForSingleKeyframe.tsx index ef5022c..c25ac2b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForSingleKeyframe.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/DeterminePropEditorForSingleKeyframe.tsx @@ -10,6 +10,7 @@ import type { } from './useSingleKeyframeInlineEditorPopover' import last from 'lodash-es/last' import {useTempTransactionEditingTools} from './useTempTransactionEditingTools' +import {valueInProp} from '@theatre/shared/propTypes/utils' const SingleKeyframePropEditorContainer = styled.div` display: flex; @@ -127,7 +128,7 @@ function PrimitivePropEditor( @@ -136,6 +137,10 @@ function PrimitivePropEditor( } } +// These editing tools are distinct from the editing tools used in the +// prop editors in the details panel: These editing tools edit the value of a keyframe +// while the details editing tools edit the value of the sequence at the playhead +// (potentially creating a new keyframe). function useEditingToolsForKeyframeEditorPopover( props: PrimitivePropEditingOptions, ) { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx index 2956163..dadf020 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx @@ -18,6 +18,7 @@ import type { import type { SequenceEditorTree_AllRowTypes, SequenceEditorTree_PropWithChildren, + SequenceEditorTree_Sheet, SequenceEditorTree_SheetObject, } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' @@ -157,28 +158,25 @@ namespace utils { const collectForAggregatedChildren = ( logger: IUtilLogger, layout: SequenceEditorPanelLayout, - leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren, + leaf: + | SequenceEditorTree_SheetObject + | SequenceEditorTree_PropWithChildren + | SequenceEditorTree_Sheet, bounds: SelectionBounds, selectionByObjectKey: DopeSheetSelection['byObjectKey'], ) => { - const sheetObject = leaf.sheetObject - const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf) + const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf) if ( leaf.top + leaf.nodeHeight / 2 + HITBOX_SIZE_PX > bounds.v[0] && leaf.top + leaf.nodeHeight / 2 - HITBOX_SIZE_PX < bounds.v[1] ) { for (const [position, keyframes] of aggregatedKeyframes.byPosition) { - if ( - position + layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) <= - bounds.h[0] - ) - continue - if ( - position - layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) >= - bounds.h[1] - ) - break + const hitboxWidth = layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) + const isHitboxOutsideSelection = + position + hitboxWidth <= bounds.h[0] || + position - hitboxWidth >= bounds.h[1] + if (isHitboxOutsideSelection) continue for (const keyframeWithTrack of keyframes) { mutableSetDeep( @@ -186,9 +184,11 @@ namespace utils { (selectionByObjectKeyP) => // convenience for accessing a deep path which might not actually exist // through the use of pointer proxy (so we don't have to deal with undeifned ) - selectionByObjectKeyP[sheetObject.address.objectKey].byTrackId[ - keyframeWithTrack.track.id - ].byKeyframeId[keyframeWithTrack.kf.id], + selectionByObjectKeyP[ + keyframeWithTrack.track.sheetObject.address.objectKey + ].byTrackId[keyframeWithTrack.track.id].byKeyframeId[ + keyframeWithTrack.kf.id + ], true, ) } @@ -207,6 +207,15 @@ namespace utils { selectionByObjectKey: DopeSheetSelection['byObjectKey'], ) => void } = { + sheet(logger, layout, leaf, bounds, selectionByObjectKey) { + collectForAggregatedChildren( + logger, + layout, + leaf, + bounds, + selectionByObjectKey, + ) + }, propWithChildren(logger, layout, leaf, bounds, selectionByObjectKey) { collectForAggregatedChildren( logger, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx index 335949b..ac88f4b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/PropWithChildrenRow.tsx @@ -39,10 +39,7 @@ const RightPropWithChildrenRow: React.VFC<{ viewModel.pathToProp.join(), ) return usePrism(() => { - const aggregatedKeyframes = collectAggregateKeyframesInPrism( - logger.utilFor.internal(), - viewModel, - ) + const aggregatedKeyframes = collectAggregateKeyframesInPrism(viewModel) const node = ( ` - position: absolute; - top: 0; - right: 0; - width: ${(props) => props.width}px; - bottom: 0; - z-index: ${() => zIndexes.rightBackground}; - overflow: hidden; - background: ${darken(1 * 0.03, theme.panel.bg)}; -` - const Right: React.FC<{ layoutP: Pointer }> = ({layoutP}) => { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx index 1be0414..27eddb2 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/SheetObjectRow.tsx @@ -5,7 +5,6 @@ import type {Pointer} from '@theatre/dataverse' import React from 'react' import {decideRowByPropType} from './PropWithChildrenRow' import RightRow from './Row' -import {useLogger} from '@theatre/studio/uiComponents/useLogger' import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes' import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack' @@ -13,15 +12,8 @@ const RightSheetObjectRow: React.VFC<{ leaf: SequenceEditorTree_SheetObject layoutP: Pointer }> = ({leaf, layoutP}) => { - const logger = useLogger( - `RightSheetObjectRow`, - leaf.sheetObject.address.objectKey, - ) return usePrism(() => { - const aggregatedKeyframes = collectAggregateKeyframesInPrism( - logger.utilFor.internal(), - leaf, - ) + const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf) const node = ( }> = ({leaf, layoutP}) => { return usePrism(() => { + const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf) + + const node = ( + + ) + return ( - <> + {leaf.children.map((sheetObjectLeaf) => ( ))} - + ) }, [leaf, layoutP]) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes.tsx index 333b242..81702e5 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes.tsx @@ -1,7 +1,9 @@ import getStudio from '@theatre/studio/getStudio' import {val} from '@theatre/dataverse' import type { + SequenceEditorTree_PrimitiveProp, SequenceEditorTree_PropWithChildren, + SequenceEditorTree_Sheet, SequenceEditorTree_SheetObject, } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' import type { @@ -13,9 +15,9 @@ import type { Keyframe, TrackData, } from '@theatre/core/projects/store/types/SheetState_Historic' -import type {IUtilLogger} from '@theatre/shared/logger' import {encodePathToProp} from '@theatre/shared/utils/addresses' import {uniq} from 'lodash-es' +import type SheetObject from '@theatre/core/sheetObjects/SheetObject' /** * An index over a series of keyframes that have been collected from different tracks. @@ -30,6 +32,7 @@ export type AggregatedKeyframes = { export type TrackWithId = { id: SequenceTrackId data: TrackData + sheetObject: SheetObject } export type KeyframeWithTrack = { @@ -59,67 +62,27 @@ export type KeyframeWithTrack = { * */ export function collectAggregateKeyframesInPrism( - logger: IUtilLogger, - leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, + leaf: + | SequenceEditorTree_Sheet + | SequenceEditorTree_PropWithChildren + | SequenceEditorTree_SheetObject, ): AggregatedKeyframes { - const sheetObject = leaf.sheetObject + const tracks = + leaf.type === 'sheet' + ? collectAggregateKeyframesSheet(leaf) + : collectAggregateKeyframesCompoundOrObject(leaf) - const projectId = sheetObject.address.projectId - - const sheetObjectTracksP = - getStudio().atomP.historic.coreByProject[projectId].sheetsById[ - sheetObject.address.sheetId - ].sequence.tracksByObject[sheetObject.address.objectKey] - - const aggregatedKeyframes: AggregatedKeyframes[] = [] - const childSimpleTracks: TrackWithId[] = [] - for (const childLeaf of leaf.children) { - if (childLeaf.type === 'primitiveProp') { - const trackId = val( - sheetObjectTracksP.trackIdByPropPath[ - encodePathToProp(childLeaf.pathToProp) - ], - ) - if (!trackId) { - logger.trace('missing track id?', {childLeaf}) - continue - } - - const trackData = val(sheetObjectTracksP.trackData[trackId]) - if (!trackData) { - logger.trace('missing track data?', {trackId, childLeaf}) - continue - } - - childSimpleTracks.push({id: trackId, data: trackData}) - } else if (childLeaf.type === 'propWithChildren') { - aggregatedKeyframes.push( - collectAggregateKeyframesInPrism( - logger.named('propWithChildren', childLeaf.pathToProp.join()), - childLeaf, - ), - ) - } else { - const _exhaustive: never = childLeaf - logger.error('unexpected kind of prop', {childLeaf}) - } + return { + byPosition: keyframesByPositionFromTrackWithIds(tracks), + tracks, } +} - logger.trace('see collected of children', { - aggregatedKeyframes, - childSimpleTracks, - }) - - const tracks = aggregatedKeyframes - .flatMap((a) => a.tracks) - .concat(childSimpleTracks) - +function keyframesByPositionFromTrackWithIds(tracks: TrackWithId[]) { const byPosition = new Map() for (const track of tracks) { - const kfs = track.data.keyframes - for (let i = 0; i < kfs.length; i++) { - const kf = kfs[i] + for (const kf of track.data.keyframes) { let existing = byPosition.get(kf.position) if (!existing) { existing = [] @@ -129,7 +92,7 @@ export function collectAggregateKeyframesInPrism( kf, track, itemKey: createStudioSheetItemKey.forTrackKeyframe( - sheetObject, + track.sheetObject, track.id, kf.id, ), @@ -137,19 +100,28 @@ export function collectAggregateKeyframesInPrism( } } - return { - byPosition, - tracks, - } + return byPosition } -/** - * Collects all the snap positions for an aggregate track. - */ -export function collectAggregateSnapPositions( +function collectAggregateKeyframesSheet( + leaf: SequenceEditorTree_Sheet, +): TrackWithId[] { + return leaf.children.flatMap(collectAggregateKeyframesCompoundOrObject) +} + +function collectAggregateKeyframesCompoundOrObject( leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, - snapTargetPositions: {[key: string]: {[key: string]: number[]}}, -): number[] { +): TrackWithId[] { + return leaf.children.flatMap((childLeaf) => + childLeaf.type === 'propWithChildren' + ? collectAggregateKeyframesCompoundOrObject(childLeaf) + : collectAggregateKeyframesPrimitiveProp(childLeaf), + ) +} + +function collectAggregateKeyframesPrimitiveProp( + leaf: SequenceEditorTree_PrimitiveProp, +): TrackWithId[] { const sheetObject = leaf.sheetObject const projectId = sheetObject.address.projectId @@ -158,29 +130,67 @@ export function collectAggregateSnapPositions( getStudio().atomP.historic.coreByProject[projectId].sheetsById[ sheetObject.address.sheetId ].sequence.tracksByObject[sheetObject.address.objectKey] + const trackId = val( + sheetObjectTracksP.trackIdByPropPath[encodePathToProp(leaf.pathToProp)], + ) + if (!trackId) return [] - const positions: number[] = [] - for (const childLeaf of leaf.children) { - if (childLeaf.type === 'primitiveProp') { - const trackId = val( - sheetObjectTracksP.trackIdByPropPath[ - encodePathToProp(childLeaf.pathToProp) - ], - ) - if (!trackId) { - continue - } + const trackData = val(sheetObjectTracksP.trackData[trackId]) + if (!trackData) return [] - positions.push( - ...(snapTargetPositions[sheetObject.address.objectKey]?.[trackId] ?? - []), - ) - } else if (childLeaf.type === 'propWithChildren') { - positions.push( - ...collectAggregateSnapPositions(childLeaf, snapTargetPositions), - ) - } - } - - return uniq(positions) + return [{id: trackId, data: trackData, sheetObject}] +} + +/** + * Collects all the snap positions for an aggregate track. + */ +export function collectAggregateSnapPositionsSheet( + leaf: SequenceEditorTree_Sheet, + snapTargetPositions: {[key: string]: {[key: string]: number[]}}, +): number[] { + return uniq( + leaf.children.flatMap((childLeaf) => + collectAggregateSnapPositionsObjectOrCompound( + childLeaf, + snapTargetPositions, + ), + ), + ) +} + +export function collectAggregateSnapPositionsObjectOrCompound( + leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, + snapTargetPositions: {[key: string]: {[key: string]: number[]}}, +): number[] { + return uniq( + leaf.children.flatMap((childLeaf) => + childLeaf.type === 'propWithChildren' + ? collectAggregateSnapPositionsObjectOrCompound( + childLeaf, + snapTargetPositions, + ) + : collectAggregateSnapPositionsPrimitiveProp( + childLeaf, + snapTargetPositions, + ), + ), + ) +} + +function collectAggregateSnapPositionsPrimitiveProp( + leaf: SequenceEditorTree_PrimitiveProp, + 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 trackId = val( + sheetObjectTracksP.trackIdByPropPath[encodePathToProp(leaf.pathToProp)], + ) + if (!trackId) return [] + + return snapTargetPositions[sheetObject.address.objectKey]?.[trackId] ?? [] } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/selections.ts b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/selections.ts index ce1a4da..7af802a 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/selections.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/selections.ts @@ -108,9 +108,6 @@ export function selectedKeyframeConnections( * {keyframe, pathToProp: ['scale', 'x']}, * ] * ``` - * - * TODO - we don't yet support copying/pasting keyframes from multiple objects to multiple objects. - * The main reason is that we don't yet have an aggregate track for several objects. */ export function copyableKeyframesFromSelection( projectId: ProjectId, @@ -175,7 +172,7 @@ export function keyframesWithPaths({ const encodedPropPath = propPathByTrackId[trackId] if (!encodedPropPath) return null - const pathToProp = decodePathToProp(encodedPropPath) + const pathToProp = [objectKey, ...decodePathToProp(encodedPropPath)] return keyframeIds .map((keyframeId) => ({ diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp.tsx index bf01ec4..834f11c 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp.tsx @@ -5,7 +5,7 @@ import type { WithoutSheetInstance, } from '@theatre/shared/utils/addresses' -export function setCollapsedSheetObjectOrCompoundProp( +export function setCollapsedSheetItem( isCollapsed: boolean, toCollapse: { sheetAddress: WithoutSheetInstance diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts index b7b0586..5318c11 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/layout.ts @@ -1,7 +1,7 @@ import type Sheet from '@theatre/core/sheets/Sheet' import getStudio from '@theatre/studio/getStudio' import type useDrag from '@theatre/studio/uiComponents/useDrag' -import type {SheetObjectAddress} from '@theatre/shared/utils/addresses' +import type {SheetAddress} from '@theatre/shared/utils/addresses' import subPrism from '@theatre/shared/utils/subPrism' import type { IRange, @@ -57,7 +57,7 @@ export type DopeSheetSelection = { } > getDragHandlers( - origin: SheetObjectAddress & { + origin: SheetAddress & { positionAtStartOfDrag: number domNode: Element }, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts index ecbf42d..8991b77 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts +++ b/theatre/studio/src/panels/SequenceEditorPanel/layout/tree.ts @@ -58,6 +58,7 @@ export type SequenceEditorTree = SequenceEditorTree_Sheet export type SequenceEditorTree_Sheet = SequenceEditorTree_Row<'sheet'> & { sheet: Sheet + isCollapsed: boolean children: SequenceEditorTree_SheetObject[] } @@ -106,34 +107,44 @@ export const calculateSequenceEditorTree = ( studio: Studio, ): SequenceEditorTree => { prism.ensurePrism() - let topSoFar = titleBarHeight - let nSoFar = 0 const rootShouldRender = true + let topSoFar = titleBarHeight + (rootShouldRender ? HEIGHT_OF_ANY_TITLE : 0) + let nSoFar = 0 + + const collapsableItemSetP = + studio.atomP.ahistoric.projects.stateByProjectId[sheet.address.projectId] + .stateBySheetId[sheet.address.sheetId].sequence.collapsableItems + + const isCollapsedP = + collapsableItemSetP.byId[createStudioSheetItemKey.forSheet()].isCollapsed + const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false const tree: SequenceEditorTree = { type: 'sheet', + isCollapsed, sheet, children: [], sheetItemKey: createStudioSheetItemKey.forSheet(), shouldRender: rootShouldRender, - top: topSoFar, - depth: -1, + top: titleBarHeight, + depth: 0, n: nSoFar, - nodeHeight: 0, // always 0 - heightIncludingChildren: -1, // will define this later + nodeHeight: rootShouldRender ? HEIGHT_OF_ANY_TITLE : 0, + heightIncludingChildren: -1, // calculated below } if (rootShouldRender) { nSoFar += 1 } - const collapsableItemSetP = - studio.atomP.ahistoric.projects.stateByProjectId[sheet.address.projectId] - .stateBySheetId[sheet.address.sheetId].sequence.collapsableItems - for (const sheetObject of Object.values(val(sheet.objectsP))) { if (sheetObject) { - addObject(sheetObject, tree.children, tree.depth + 1, rootShouldRender) + addObject( + sheetObject, + tree.children, + tree.depth + 1, + rootShouldRender && !isCollapsed, + ) } } tree.heightIncludingChildren = topSoFar - tree.top @@ -167,9 +178,7 @@ export const calculateSequenceEditorTree = ( n: nSoFar, sheetObject: sheetObject, nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0, - // Question: Why -1? Is this relevant for "shouldRender"? - // Perhaps this is to indicate this does not have a valid value. - heightIncludingChildren: -1, + heightIncludingChildren: -1, // calculated below } arrayOfChildren.push(row) @@ -350,5 +359,6 @@ export const calculateSequenceEditorTree = ( topSoFar += row.nodeHeight } + console.log('tree :)', tree) return tree } diff --git a/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx index 2f88560..e1c8f28 100644 --- a/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx +++ b/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx @@ -257,7 +257,7 @@ function ControlIndicators({ ...s, nearbies: getNearbyKeyframesOfTrack( obj, - {id: s.trackId, data: s.track!}, + {id: s.trackId, data: s.track!, sheetObject: obj}, sequencePosition, ), })) diff --git a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx index 8d5be14..1d5ed87 100644 --- a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx +++ b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx @@ -176,6 +176,7 @@ function createDerivation( track && { data: track, id: sequenceTrackId, + sheetObject: obj, }, sequencePosition, )