Add Sheet aggregate track (#284)
* Add Sheet aggregate track * Update aggregate track keyframe copy algorithm * Fix keyframe value sanitization * Fix aggregate selections to be properly undefined * Fix TS errors * Remove incorrect comment and improve var name
This commit is contained in:
parent
743254a6c6
commit
39eb528af4
25 changed files with 491 additions and 300 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<AnyCompositeRow
|
||||
leaf={leaf}
|
||||
label={leaf.sheet.address.sheetId}
|
||||
isCollapsed={leaf.isCollapsed}
|
||||
toggleCollapsed={() => {
|
||||
setCollapsedSheetItem(!leaf.isCollapsed, {
|
||||
sheetAddress: leaf.sheet.address,
|
||||
sheetItemKey: leaf.sheetItemKey,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{leaf.children.map((sheetObjectLeaf) => (
|
||||
<LeftSheetObjectRow
|
||||
key={'sheetObject-' + sheetObjectLeaf.sheetObject.address.objectKey}
|
||||
leaf={sheetObjectLeaf}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</AnyCompositeRow>
|
||||
)
|
||||
}, [leaf])
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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<IAggregateKeyframeDotProps>,
|
||||
) {
|
||||
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<HTMLDivElement | null>(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
|
||||
}
|
||||
/>
|
||||
<AggregateKeyframeVisualDot
|
||||
flag={presence.flag}
|
||||
|
@ -150,45 +153,30 @@ export function AggregateKeyframeDot(
|
|||
|
||||
function useAggregateKeyframeContextMenu(
|
||||
props: IAggregateKeyframeDotProps,
|
||||
logger: ILogger,
|
||||
target: HTMLDivElement | null,
|
||||
) {
|
||||
return useContextMenu(target, {
|
||||
displayName: 'Aggregate Keyframe',
|
||||
menuItems: () => {
|
||||
// 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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<SequenceEditorPanelLayout>
|
||||
}
|
||||
|
@ -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<SequenceEditorPanelLayout>,
|
||||
viewModel:
|
||||
| SequenceEditorTree_PropWithChildren
|
||||
| SequenceEditorTree_SheetObject,
|
||||
aggregatedKeyframes: AggregatedKeyframes,
|
||||
): _AggSelection {
|
||||
return usePrism(() => {
|
||||
const selectionAtom = val(layoutP.selectionAtom)
|
||||
const sheetObjectSelection = val(
|
||||
selectionAtom.pointer.current.byObjectKey[
|
||||
viewModel.sheetObject.address.objectKey
|
||||
],
|
||||
return usePrism(
|
||||
() => val(collectedSelectedPositions(layoutP, aggregatedKeyframes)),
|
||||
[layoutP, aggregatedKeyframes],
|
||||
)
|
||||
if (!sheetObjectSelection) return EMPTY_SELECTION
|
||||
}
|
||||
|
||||
function collectedSelectedPositions(
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
aggregatedKeyframes: AggregatedKeyframes,
|
||||
): IDerivation<_AggSelection> {
|
||||
return prism(() => {
|
||||
const selectionAtom = val(layoutP.selectionAtom)
|
||||
const selection = val(selectionAtom.pointer.current)
|
||||
if (!selection) return EMPTY_SELECTION
|
||||
|
||||
const selectedAtPositions = new Map<
|
||||
number,
|
||||
|
@ -228,17 +242,38 @@ function useCollectedSelectedPositions(
|
|||
>()
|
||||
|
||||
for (const [position, kfsWithTrack] of aggregatedKeyframes.byPosition) {
|
||||
const positionIsSelected = allOrSomeOrNoneSelected(
|
||||
kfsWithTrack,
|
||||
selection,
|
||||
)
|
||||
if (
|
||||
positionIsSelected !== undefined &&
|
||||
positionIsSelected !== NoneSelected
|
||||
) {
|
||||
selectedAtPositions.set(position, positionIsSelected)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedPositions: selectedAtPositions,
|
||||
selection: val(selectionAtom.pointer.current),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function allOrSomeOrNoneSelected(
|
||||
keyframeWithTracks: KeyframeWithTrack[],
|
||||
selection: DopeSheetSelection,
|
||||
): AggregateKeyframePositionIsSelected | undefined {
|
||||
let positionIsSelected: undefined | AggregateKeyframePositionIsSelected =
|
||||
undefined
|
||||
for (const kfWithTrack of kfsWithTrack) {
|
||||
|
||||
for (const {track, kf} of keyframeWithTracks) {
|
||||
const kfIsSelected =
|
||||
sheetObjectSelection.byTrackId[kfWithTrack.track.id]?.byKeyframeId?.[
|
||||
kfWithTrack.kf.id
|
||||
] === true
|
||||
// -1/10: This sux
|
||||
// undefined = have not encountered
|
||||
selection.byObjectKey[track.sheetObject.address.objectKey]?.byTrackId[
|
||||
track.id
|
||||
]?.byKeyframeId?.[kf.id] === true
|
||||
if (positionIsSelected === undefined) {
|
||||
// first item
|
||||
if (kfIsSelected) {
|
||||
positionIsSelected = AllSelected
|
||||
} else {
|
||||
|
@ -254,17 +289,7 @@ function useCollectedSelectedPositions(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positionIsSelected != null) {
|
||||
selectedAtPositions.set(position, positionIsSelected)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedPositions: selectedAtPositions,
|
||||
selection: val(selectionAtom.pointer.current),
|
||||
}
|
||||
}, [layoutP, aggregatedKeyframes])
|
||||
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(
|
||||
|
|
|
@ -74,7 +74,11 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = 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],
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
|||
<PropEditor
|
||||
editingTools={editingTools}
|
||||
propConfig={p.propConfig}
|
||||
value={p.keyframe.value}
|
||||
value={valueInProp(p.keyframe.value, p.propConfig)}
|
||||
autoFocus={p.autoFocusInput}
|
||||
/>
|
||||
</SingleKeyframeSimplePropEditorContainer>
|
||||
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = (
|
||||
<AggregatedKeyframeTrack
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import {theme} from '@theatre/studio/css'
|
||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {darken} from 'polished'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
import DopeSheetSelectionView from './DopeSheetSelectionView'
|
||||
import HorizontallyScrollableArea from './HorizontallyScrollableArea'
|
||||
import SheetRow from './SheetRow'
|
||||
|
@ -22,17 +19,6 @@ const ListContainer = styled.ul`
|
|||
width: ${contentWidth}px;
|
||||
`
|
||||
|
||||
const Background = styled.div<{width: number}>`
|
||||
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<SequenceEditorPanelLayout>
|
||||
}> = ({layoutP}) => {
|
||||
|
|
|
@ -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<SequenceEditorPanelLayout>
|
||||
}> = ({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 = (
|
||||
<AggregatedKeyframeTrack
|
||||
|
|
|
@ -4,14 +4,27 @@ import {usePrism} from '@theatre/react'
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import RightSheetObjectRow from './SheetObjectRow'
|
||||
import RightRow from './Row'
|
||||
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||
|
||||
const SheetRow: React.FC<{
|
||||
leaf: SequenceEditorTree_Sheet
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({leaf, layoutP}) => {
|
||||
return usePrism(() => {
|
||||
const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf)
|
||||
|
||||
const node = (
|
||||
<AggregatedKeyframeTrack
|
||||
layoutP={layoutP}
|
||||
aggregatedKeyframes={aggregatedKeyframes}
|
||||
viewModel={leaf}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
||||
{leaf.children.map((sheetObjectLeaf) => (
|
||||
<RightSheetObjectRow
|
||||
layoutP={layoutP}
|
||||
|
@ -19,7 +32,7 @@ const SheetRow: React.FC<{
|
|||
leaf={sheetObjectLeaf}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</RightRow>
|
||||
)
|
||||
}, [leaf, layoutP])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
return {
|
||||
byPosition: keyframesByPositionFromTrackWithIds(tracks),
|
||||
tracks,
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
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<number, KeyframeWithTrack[]>()
|
||||
|
||||
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 positions: number[] = []
|
||||
for (const childLeaf of leaf.children) {
|
||||
if (childLeaf.type === 'primitiveProp') {
|
||||
const trackId = val(
|
||||
sheetObjectTracksP.trackIdByPropPath[
|
||||
encodePathToProp(childLeaf.pathToProp)
|
||||
],
|
||||
sheetObjectTracksP.trackIdByPropPath[encodePathToProp(leaf.pathToProp)],
|
||||
)
|
||||
if (!trackId) {
|
||||
continue
|
||||
}
|
||||
if (!trackId) return []
|
||||
|
||||
positions.push(
|
||||
...(snapTargetPositions[sheetObject.address.objectKey]?.[trackId] ??
|
||||
[]),
|
||||
)
|
||||
} else if (childLeaf.type === 'propWithChildren') {
|
||||
positions.push(
|
||||
...collectAggregateSnapPositions(childLeaf, snapTargetPositions),
|
||||
)
|
||||
}
|
||||
}
|
||||
const trackData = val(sheetObjectTracksP.trackData[trackId])
|
||||
if (!trackData) return []
|
||||
|
||||
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] ?? []
|
||||
}
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -5,7 +5,7 @@ import type {
|
|||
WithoutSheetInstance,
|
||||
} from '@theatre/shared/utils/addresses'
|
||||
|
||||
export function setCollapsedSheetObjectOrCompoundProp(
|
||||
export function setCollapsedSheetItem(
|
||||
isCollapsed: boolean,
|
||||
toCollapse: {
|
||||
sheetAddress: WithoutSheetInstance<SheetAddress>
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}))
|
||||
|
|
|
@ -176,6 +176,7 @@ function createDerivation<T extends SerializablePrimitive>(
|
|||
track && {
|
||||
data: track,
|
||||
id: sequenceTrackId,
|
||||
sheetObject: obj,
|
||||
},
|
||||
sequencePosition,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue