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:
Elliot 2022-09-14 12:46:59 -04:00 committed by GitHub
parent 743254a6c6
commit 39eb528af4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 491 additions and 300 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {
return usePrism(
() => val(collectedSelectedPositions(layoutP, aggregatedKeyframes)),
[layoutP, aggregatedKeyframes],
)
}
function collectedSelectedPositions(
layoutP: Pointer<SequenceEditorPanelLayout>,
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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<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 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] ?? []
}

View file

@ -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) => ({

View file

@ -5,7 +5,7 @@ import type {
WithoutSheetInstance,
} from '@theatre/shared/utils/addresses'
export function setCollapsedSheetObjectOrCompoundProp(
export function setCollapsedSheetItem(
isCollapsed: boolean,
toCollapse: {
sheetAddress: WithoutSheetInstance<SheetAddress>

View file

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

View file

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

View file

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

View file

@ -176,6 +176,7 @@ function createDerivation<T extends SerializablePrimitive>(
track && {
data: track,
id: sequenceTrackId,
sheetObject: obj,
},
sequencePosition,
)