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 stableValueHash from './stableJsonStringify'
import {nanoid as generateNonSecure} from 'nanoid/non-secure' import {nanoid as generateNonSecure} from 'nanoid/non-secure'
import type {Nominal} from './Nominal' import type {Nominal} from './Nominal'
import type Sheet from '@theatre/core/sheets/Sheet'
export type KeyframeId = Nominal<'KeyframeId'> export type KeyframeId = Nominal<'KeyframeId'>
@ -106,6 +107,12 @@ export const createStudioSheetItemKey = {
position, position,
) )
}, },
forSheetAggregateKeyframe(obj: Sheet, position: number): StudioSheetItemKey {
return stableValueHash({
o: obj.address.sheetId,
pos: position,
}) as StudioSheetItemKey
},
forCompoundPropAggregateKeyframe( forCompoundPropAggregateKeyframe(
obj: SheetObject, obj: SheetObject,
pathToProp: PathToProp, pathToProp: PathToProp,

View file

@ -1,14 +1,30 @@
## The keyframe copy/paste algorithm ## The keyframe copy/paste algorithm
``` The copy and paste algorithms are specified below. Note that the copy algorithm
copy algorithm: find the closest common acnestor for the tracks selected 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} => {x, z}
- obj1.props.transform.position.{x, z} + obj1.props.transform.rotation.z => - obj1.props.transform.position.{x, z} + obj1.props.transform.rotation.z =>
{position: {x, z}, 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 => simple => 1-1
- simple => {x, y} => {x: simple, y: simple} (distribute to all) - 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} - {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 - 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 at each object, then try pasting onto its object.props
``` ```

View file

@ -2,6 +2,7 @@ import {theme} from '@theatre/studio/css'
import type { import type {
SequenceEditorTree_PrimitiveProp, SequenceEditorTree_PrimitiveProp,
SequenceEditorTree_PropWithChildren, SequenceEditorTree_PropWithChildren,
SequenceEditorTree_Sheet,
SequenceEditorTree_SheetObject, SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import type {VoidFn} from '@theatre/shared/utils/types' import type {VoidFn} from '@theatre/shared/utils/types'
@ -76,6 +77,7 @@ const LeftRowChildren = styled.ul`
const AnyCompositeRow: React.FC<{ const AnyCompositeRow: React.FC<{
leaf: leaf:
| SequenceEditorTree_Sheet
| SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PrimitiveProp
| SequenceEditorTree_PropWithChildren | SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject | SequenceEditorTree_SheetObject

View file

@ -5,7 +5,7 @@ import type {
import React from 'react' import React from 'react'
import AnyCompositeRow from './AnyCompositeRow' import AnyCompositeRow from './AnyCompositeRow'
import PrimitivePropRow from './PrimitivePropRow' 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 = ( export const decideRowByPropType = (
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp, leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
@ -31,7 +31,7 @@ const PropWithChildrenRow: React.VFC<{
label={leaf.pathToProp[leaf.pathToProp.length - 1]} label={leaf.pathToProp[leaf.pathToProp.length - 1]}
isCollapsed={leaf.isCollapsed} isCollapsed={leaf.isCollapsed}
toggleCollapsed={() => toggleCollapsed={() =>
setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, { setCollapsedSheetItem(!leaf.isCollapsed, {
sheetAddress: leaf.sheetObject.address, sheetAddress: leaf.sheetObject.address,
sheetItemKey: leaf.sheetItemKey, sheetItemKey: leaf.sheetItemKey,
}) })

View file

@ -2,7 +2,7 @@ import type {SequenceEditorTree_SheetObject} from '@theatre/studio/panels/Sequen
import React from 'react' import React from 'react'
import AnyCompositeRow from './AnyCompositeRow' import AnyCompositeRow from './AnyCompositeRow'
import {decideRowByPropType} from './PropWithChildrenRow' 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' import getStudio from '@theatre/studio/getStudio'
const LeftSheetObjectRow: React.VFC<{ const LeftSheetObjectRow: React.VFC<{
@ -22,7 +22,7 @@ const LeftSheetObjectRow: React.VFC<{
}) })
}} }}
toggleCollapsed={() => toggleCollapsed={() =>
setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, { setCollapsedSheetItem(!leaf.isCollapsed, {
sheetAddress: leaf.sheetObject.address, sheetAddress: leaf.sheetObject.address,
sheetItemKey: leaf.sheetItemKey, sheetItemKey: leaf.sheetItemKey,
}) })

View file

@ -2,20 +2,32 @@ import type {SequenceEditorTree_Sheet} from '@theatre/studio/panels/SequenceEdit
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import React from 'react' import React from 'react'
import LeftSheetObjectRow from './SheetObjectRow' import LeftSheetObjectRow from './SheetObjectRow'
import AnyCompositeRow from './AnyCompositeRow'
import {setCollapsedSheetItem} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp'
const SheetRow: React.VFC<{ const SheetRow: React.VFC<{
leaf: SequenceEditorTree_Sheet leaf: SequenceEditorTree_Sheet
}> = ({leaf}) => { }> = ({leaf}) => {
return usePrism(() => { return usePrism(() => {
return ( 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) => ( {leaf.children.map((sheetObjectLeaf) => (
<LeftSheetObjectRow <LeftSheetObjectRow
key={'sheetObject-' + sheetObjectLeaf.sheetObject.address.objectKey} key={'sheetObject-' + sheetObjectLeaf.sheetObject.address.objectKey}
leaf={sheetObjectLeaf} leaf={sheetObjectLeaf}
/> />
))} ))}
</> </AnyCompositeRow>
) )
}, [leaf]) }, [leaf])
} }

View file

@ -124,16 +124,20 @@ function useDragKeyframe(
const keyframes = props.aggregateKeyframes[props.index].keyframes const keyframes = props.aggregateKeyframes[props.index].keyframes
const {selection, viewModel} = props
const address =
viewModel.type === 'sheet'
? viewModel.sheet.address
: viewModel.sheetObject.address
if ( if (
props.selection && selection &&
props.aggregateKeyframes[props.index].selected === props.aggregateKeyframes[props.index].selected ===
AggregateKeyframePositionIsSelected.AllSelected AggregateKeyframePositionIsSelected.AllSelected
) { ) {
const {selection, viewModel} = props
const {sheetObject} = viewModel
return selection return selection
.getDragHandlers({ .getDragHandlers({
...sheetObject.address, ...address,
domNode: node!, domNode: node!,
positionAtStartOfDrag: positionAtStartOfDrag:
props.aggregateKeyframes[props.index].position, props.aggregateKeyframes[props.index].position,
@ -159,7 +163,7 @@ function useDragKeyframe(
for (const keyframe of keyframes) { for (const keyframe of keyframes) {
stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes(
{ {
...propsAtStartOfDrag.viewModel.sheetObject.address, ...keyframe.track.sheetObject.address,
trackId: keyframe.track.id, trackId: keyframe.track.id,
keyframeIds: [ keyframeIds: [
keyframe.kf.id, keyframe.kf.id,
@ -209,8 +213,7 @@ function useConnectorContextMenu(
(acc, con) => (acc, con) =>
acc.concat( acc.concat(
keyframesWithPaths({ keyframesWithPaths({
...props.editorProps.viewModel.sheetObject.address, ...con,
trackId: con.trackId,
keyframeIds: [con.left.id, con.right.id], keyframeIds: [con.left.id, con.right.id],
}) ?? [], }) ?? [],
), ),
@ -226,14 +229,20 @@ function useConnectorContextMenu(
pathToProp: pathToProp.slice(commonPath.length), pathToProp: pathToProp.slice(commonPath.length),
})) }))
const viewModel = props.editorProps.viewModel
const address =
viewModel.type === 'sheet'
? viewModel.sheet.address
: viewModel.sheetObject.address
return [ return [
{ {
label: 'Copy', label: 'Copy',
callback: () => { callback: () => {
if (props.editorProps.selection) { if (props.editorProps.selection) {
const copyableKeyframes = copyableKeyframesFromSelection( const copyableKeyframes = copyableKeyframesFromSelection(
props.editorProps.viewModel.sheetObject.address.projectId, address.projectId,
props.editorProps.viewModel.sheetObject.address.sheetId, address.sheetId,
props.editorProps.selection, props.editorProps.selection,
) )
getStudio().transaction((api) => { getStudio().transaction((api) => {
@ -260,7 +269,8 @@ function useConnectorContextMenu(
for (const con of props.utils.allConnections) { for (const con of props.utils.allConnections) {
stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes(
{ {
...props.editorProps.viewModel.sheetObject.address, ...address,
objectKey: con.objectKey,
keyframeIds: [con.left.id, con.right.id], keyframeIds: [con.left.id, con.right.id],
trackId: con.trackId, trackId: con.trackId,
}, },

View file

@ -3,7 +3,6 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import usePresence, { import usePresence, {
PresenceFlag, PresenceFlag,
} from '@theatre/studio/uiComponents/usePresence' } from '@theatre/studio/uiComponents/usePresence'
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor' import type {IAggregateKeyframeEditorProps} from './AggregateKeyframeEditor'
import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils' import type {IAggregateKeyframeEditorUtils} from './useAggregateKeyframeEditorUtils'
@ -15,7 +14,6 @@ import {
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections' } from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric' import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/types/ahistoric'
import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses' import {commonRootOfPathsToProps} from '@theatre/shared/utils/addresses'
import type {ILogger} from '@theatre/shared/logger'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
import type { import type {
PrimitivePropEditingOptions, PrimitivePropEditingOptions,
@ -96,11 +94,12 @@ function primitivePropBuild(
export function AggregateKeyframeDot( export function AggregateKeyframeDot(
props: React.PropsWithChildren<IAggregateKeyframeDotProps>, props: React.PropsWithChildren<IAggregateKeyframeDotProps>,
) { ) {
const logger = useLogger('AggregateKeyframeDot')
const {cur} = props.utils const {cur} = props.utils
const inlineEditorPopover = useKeyframeInlineEditorPopover( 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) ? sheetObjectBuild(props.editorProps.viewModel, cur.keyframes)
?.children ?? null ?.children ?? null
: propWithChildrenBuild(props.editorProps.viewModel, cur.keyframes) : propWithChildrenBuild(props.editorProps.viewModel, cur.keyframes)
@ -125,7 +124,7 @@ export function AggregateKeyframeDot(
const [ref, node] = useRefAndState<HTMLDivElement | null>(null) const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node) const [contextMenu] = useAggregateKeyframeContextMenu(props, node)
return ( return (
<> <>
@ -135,7 +134,11 @@ export function AggregateKeyframeDot(
// Need this for the dragging logic to be able to get the keyframe props // Need this for the dragging logic to be able to get the keyframe props
// based on the position. // based on the position.
{...DopeSnap.includePositionSnapAttrs(cur.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 <AggregateKeyframeVisualDot
flag={presence.flag} flag={presence.flag}
@ -150,45 +153,30 @@ export function AggregateKeyframeDot(
function useAggregateKeyframeContextMenu( function useAggregateKeyframeContextMenu(
props: IAggregateKeyframeDotProps, props: IAggregateKeyframeDotProps,
logger: ILogger,
target: HTMLDivElement | null, target: HTMLDivElement | null,
) { ) {
return useContextMenu(target, { return useContextMenu(target, {
displayName: 'Aggregate Keyframe', displayName: 'Aggregate Keyframe',
menuItems: () => { menuItems: () => {
// see AGGREGATE_COPY_PASTE.md for explanation of this const viewModel = props.editorProps.viewModel
// code that makes some keyframes with paths for copying const selection = props.editorProps.selection
// 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),
}))
return [ return [
{ {
label: props.editorProps.selection ? 'Copy (selection)' : 'Copy', label: selection ? 'Copy (selection)' : 'Copy',
callback: () => { 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( const copyableKeyframes = copyableKeyframesFromSelection(
props.editorProps.viewModel.sheetObject.address.projectId, projectId,
props.editorProps.viewModel.sheetObject.address.sheetId, sheetId,
props.editorProps.selection, selection,
) )
getStudio().transaction((api) => { getStudio().transaction((api) => {
api.stateEditors.studio.ahistoric.setClipboardKeyframes( api.stateEditors.studio.ahistoric.setClipboardKeyframes(
@ -196,6 +184,38 @@ function useAggregateKeyframeContextMenu(
) )
}) })
} else { } 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) => { getStudio().transaction((api) => {
api.stateEditors.studio.ahistoric.setClipboardKeyframes( api.stateEditors.studio.ahistoric.setClipboardKeyframes(
keyframesWithCommonRootPath, keyframesWithCommonRootPath,
@ -205,16 +225,16 @@ function useAggregateKeyframeContextMenu(
}, },
}, },
{ {
label: props.editorProps.selection ? 'Delete (selection)' : 'Delete', label: selection ? 'Delete (selection)' : 'Delete',
callback: () => { callback: () => {
if (props.editorProps.selection) { if (selection) {
props.editorProps.selection.delete() selection.delete()
} else { } else {
getStudio().transaction(({stateEditors}) => { getStudio().transaction(({stateEditors}) => {
for (const kfWithTrack of props.utils.cur.keyframes) { for (const kfWithTrack of props.utils.cur.keyframes) {
stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.deleteKeyframes(
{ {
...props.editorProps.viewModel.sheetObject.address, ...kfWithTrack.track.sheetObject.address,
keyframeIds: [kfWithTrack.kf.id], keyframeIds: [kfWithTrack.kf.id],
trackId: kfWithTrack.track.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' } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import type { import type {
SequenceEditorTree_PropWithChildren, SequenceEditorTree_PropWithChildren,
SequenceEditorTree_Sheet,
SequenceEditorTree_SheetObject, SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
@ -44,6 +45,7 @@ export type IAggregateKeyframeEditorProps = {
viewModel: viewModel:
| SequenceEditorTree_PropWithChildren | SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject | SequenceEditorTree_SheetObject
| SequenceEditorTree_Sheet
selection: undefined | DopeSheetSelection selection: undefined | DopeSheetSelection
} }

View file

@ -23,13 +23,12 @@ export function useAggregateKeyframeEditorUtils(
>, >,
) { ) {
const {index, aggregateKeyframes, selection} = props const {index, aggregateKeyframes, selection} = props
const sheetObjectAddress = props.viewModel.sheetObject.address
return usePrism(getAggregateKeyframeEditorUtilsPrismFn(props), [ return usePrism(getAggregateKeyframeEditorUtilsPrismFn(props), [
index, index,
aggregateKeyframes, aggregateKeyframes,
selection, selection,
sheetObjectAddress, props.viewModel,
]) ])
} }
@ -42,7 +41,11 @@ export function getAggregateKeyframeEditorUtilsPrismFn(
>, >,
) { ) {
const {index, aggregateKeyframes, selection} = props 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 () => { return () => {
const cur = aggregateKeyframes[index] const cur = aggregateKeyframes[index]
@ -65,24 +68,17 @@ export function getAggregateKeyframeEditorUtilsPrismFn(
const aggregatedConnections: AggregatedKeyframeConnection[] = !connected const aggregatedConnections: AggregatedKeyframeConnection[] = !connected
? [] ? []
: cur.keyframes.map(({kf, track}, i) => ({ : cur.keyframes.map(({kf, track}, i) => ({
...sheetObjectAddress, ...track.sheetObject.address,
trackId: track.id, trackId: track.id,
left: kf, left: kf,
right: next.keyframes[i].kf, right: next.keyframes[i].kf,
})) }))
const allConnections = iif(() => { const allConnections = iif(() => {
const {projectId, sheetId} = sheetObjectAddress
const selectedConnections = prism const selectedConnections = prism
.memo( .memo(
'selectedConnections', 'selectedConnections',
() => () => selectedKeyframeConnections(projectId, sheetId, selection),
selectedKeyframeConnections(
sheetObjectAddress.projectId,
sheetObjectAddress.sheetId,
selection,
),
[projectId, sheetId, selection], [projectId, sheetId, selection],
) )
.getValue() .getValue()
@ -97,7 +93,12 @@ export function getAggregateKeyframeEditorUtilsPrismFn(
const itemKey = prism.memo( const itemKey = prism.memo(
'itemKey', '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( return createStudioSheetItemKey.forSheetObjectAggregateKeyframe(
props.viewModel.sheetObject, props.viewModel.sheetObject,
cur.position, cur.position,
@ -110,7 +111,7 @@ export function getAggregateKeyframeEditorUtilsPrismFn(
) )
} }
}, },
[props.viewModel.sheetObject, cur.position], [props.viewModel, cur.position],
) )
return { return {

View file

@ -4,10 +4,11 @@ import type {
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import type { import type {
SequenceEditorTree_PropWithChildren, SequenceEditorTree_PropWithChildren,
SequenceEditorTree_Sheet,
SequenceEditorTree_SheetObject, SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import {usePrism, useVal} from '@theatre/react' 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 {prism, val, valueDerivation} from '@theatre/dataverse'
import React, {useMemo, Fragment} from 'react' import React, {useMemo, Fragment} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -19,7 +20,11 @@ import type {
IAggregateKeyframeEditorProps, IAggregateKeyframeEditorProps,
} from './AggregateKeyframeEditor/AggregateKeyframeEditor' } from './AggregateKeyframeEditor/AggregateKeyframeEditor'
import AggregateKeyframeEditor 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 {useLogger} from '@theatre/studio/uiComponents/useLogger'
import {getAggregateKeyframeEditorUtilsPrismFn} from './AggregateKeyframeEditor/useAggregateKeyframeEditorUtils' import {getAggregateKeyframeEditorUtilsPrismFn} from './AggregateKeyframeEditor/useAggregateKeyframeEditorUtils'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
@ -35,7 +40,7 @@ import {
doesPathStartWith, doesPathStartWith,
encodePathToProp, encodePathToProp,
} from '@theatre/shared/utils/addresses' } 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 type Sequence from '@theatre/core/sequences/Sequence'
import KeyframeSnapTarget, { import KeyframeSnapTarget, {
snapPositionsStateD, snapPositionsStateD,
@ -47,7 +52,7 @@ import {
snapToNone, snapToNone,
snapToSome, snapToSome,
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget' } 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' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
const AggregatedKeyframeTrackContainer = styled.div` const AggregatedKeyframeTrackContainer = styled.div`
@ -60,6 +65,7 @@ type IAggregatedKeyframeTracksProps = {
viewModel: viewModel:
| SequenceEditorTree_PropWithChildren | SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject | SequenceEditorTree_SheetObject
| SequenceEditorTree_Sheet
aggregatedKeyframes: AggregatedKeyframes aggregatedKeyframes: AggregatedKeyframes
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
} }
@ -83,7 +89,6 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
const {selectedPositions, selection} = useCollectedSelectedPositions( const {selectedPositions, selection} = useCollectedSelectedPositions(
layoutP, layoutP,
viewModel,
aggregatedKeyframes, aggregatedKeyframes,
) )
@ -118,7 +123,13 @@ function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
: emptyObject : emptyObject
const aggregateSnapPositions = useMemo( const aggregateSnapPositions = useMemo(
() => collectAggregateSnapPositions(viewModel, snapPositions), () =>
viewModel.type === 'sheet'
? collectAggregateSnapPositionsSheet(viewModel, snapPositions)
: collectAggregateSnapPositionsObjectOrCompound(
viewModel,
snapPositions,
),
[snapPositions], [snapPositions],
) )
@ -208,19 +219,22 @@ const {AllSelected, AtLeastOneUnselected, NoneSelected} =
/** Helper to put together the selected positions */ /** Helper to put together the selected positions */
function useCollectedSelectedPositions( function useCollectedSelectedPositions(
layoutP: Pointer<SequenceEditorPanelLayout>, layoutP: Pointer<SequenceEditorPanelLayout>,
viewModel:
| SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject,
aggregatedKeyframes: AggregatedKeyframes, aggregatedKeyframes: AggregatedKeyframes,
): _AggSelection { ): _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 selectionAtom = val(layoutP.selectionAtom)
const sheetObjectSelection = val( const selection = val(selectionAtom.pointer.current)
selectionAtom.pointer.current.byObjectKey[ if (!selection) return EMPTY_SELECTION
viewModel.sheetObject.address.objectKey
],
)
if (!sheetObjectSelection) return EMPTY_SELECTION
const selectedAtPositions = new Map< const selectedAtPositions = new Map<
number, number,
@ -228,34 +242,14 @@ function useCollectedSelectedPositions(
>() >()
for (const [position, kfsWithTrack] of aggregatedKeyframes.byPosition) { for (const [position, kfsWithTrack] of aggregatedKeyframes.byPosition) {
let positionIsSelected: undefined | AggregateKeyframePositionIsSelected = const positionIsSelected = allOrSomeOrNoneSelected(
undefined kfsWithTrack,
for (const kfWithTrack of kfsWithTrack) { selection,
const kfIsSelected = )
sheetObjectSelection.byTrackId[kfWithTrack.track.id]?.byKeyframeId?.[ if (
kfWithTrack.kf.id positionIsSelected !== undefined &&
] === true positionIsSelected !== NoneSelected
// -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) {
selectedAtPositions.set(position, positionIsSelected) selectedAtPositions.set(position, positionIsSelected)
} }
} }
@ -264,7 +258,38 @@ function useCollectedSelectedPositions(
selectedPositions: selectedAtPositions, selectedPositions: selectedAtPositions,
selection: val(selectionAtom.pointer.current), 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( function useAggregatedKeyframeTrackContextMenu(
@ -297,7 +322,11 @@ function pasteKeyframesContextMenuItem(
const sheet = val(props.layoutP.sheet) const sheet = val(props.layoutP.sheet)
const sequence = sheet.getSequence() 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 StudioAhistoricState.clipboard
* @see setClipboardNestedKeyframes * @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: viewModel:
| SequenceEditorTree_PropWithChildren | SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject, | SequenceEditorTree_SheetObject,
@ -322,7 +418,7 @@ function pasteKeyframes(
) { ) {
const {projectId, sheetId, objectKey} = viewModel.sheetObject.address const {projectId, sheetId, objectKey} = viewModel.sheetObject.address
const tracksByObject = valueDerivation( const trackRecords = valueDerivation(
getStudio().atomP.historic.coreByProject[projectId].sheetsById[sheetId] getStudio().atomP.historic.coreByProject[projectId].sheetsById[sheetId]
.sequence.tracksByObject[objectKey], .sequence.tracksByObject[objectKey],
).getValue() ).getValue()
@ -332,7 +428,7 @@ function pasteKeyframes(
) )
if (areKeyframesAllOnSingleTrack) { if (areKeyframesAllOnSingleTrack) {
const trackIdsOnObject = Object.keys(tracksByObject?.trackData ?? {}) const trackIdsOnObject = Object.keys(trackRecords?.trackData ?? {})
if (viewModel.type === 'sheetObject') { if (viewModel.type === 'sheetObject') {
pasteKeyframesToMultipleTracks( pasteKeyframesToMultipleTracks(
@ -342,7 +438,7 @@ function pasteKeyframes(
sequence, sequence,
) )
} else { } else {
const trackIdByPropPath = tracksByObject?.trackIdByPropPath || {} const trackIdByPropPath = trackRecords?.trackIdByPropPath || {}
const trackIdsOnCompoundProp = Object.entries(trackIdByPropPath) const trackIdsOnCompoundProp = Object.entries(trackIdByPropPath)
.filter( .filter(
@ -364,7 +460,7 @@ function pasteKeyframes(
) )
} }
} else { } else {
const trackIdByPropPath = tracksByObject?.trackIdByPropPath || {} const trackIdByPropPath = trackRecords?.trackIdByPropPath || {}
const rootPath = const rootPath =
viewModel.type === 'propWithChildren' ? viewModel.pathToProp : [] viewModel.type === 'propWithChildren' ? viewModel.pathToProp : []
@ -382,19 +478,17 @@ function pasteKeyframes(
? { ? {
keyframe, keyframe,
trackId: maybeTrackId, trackId: maybeTrackId,
address: viewModel.sheetObject.address,
} }
: null : null
}) })
.filter((result) => result !== null) as { .filter((result) => result !== null) as {
keyframe: Keyframe keyframe: Keyframe
trackId: SequenceTrackId trackId: SequenceTrackId
address: SheetObjectAddress
}[] }[]
pasteKeyframesToSpecificTracks( pasteKeyframesToSpecificTracks(placeableKeyframes, sequence)
viewModel.sheetObject.address,
placeableKeyframes,
sequence,
)
} }
} }
@ -428,10 +522,10 @@ function pasteKeyframesToMultipleTracks(
} }
function pasteKeyframesToSpecificTracks( function pasteKeyframesToSpecificTracks(
address: SheetObjectAddress,
keyframesWithTracksToPlaceThemIn: { keyframesWithTracksToPlaceThemIn: {
keyframe: Keyframe keyframe: Keyframe
trackId: SequenceTrackId trackId: SequenceTrackId
address: SheetObjectAddress
}[], }[],
sequence: Sequence, sequence: Sequence,
) { ) {
@ -441,7 +535,11 @@ function pasteKeyframesToSpecificTracks(
)?.position! )?.position!
getStudio()!.transaction(({stateEditors}) => { getStudio()!.transaction(({stateEditors}) => {
for (const {keyframe, trackId} of keyframesWithTracksToPlaceThemIn) { for (const {
keyframe,
trackId,
address,
} of keyframesWithTracksToPlaceThemIn) {
stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition( stateEditors.coreByProject.historic.sheetsById.sequence.setKeyframeAtPosition(
{ {
...address, ...address,
@ -499,11 +597,14 @@ function useDragForAggregateKeyframeDot(
getAggregateKeyframeEditorUtilsPrismFn(props), getAggregateKeyframeEditorUtilsPrismFn(props),
).getValue().cur.keyframes ).getValue().cur.keyframes
const address =
props.viewModel.type === 'sheet'
? props.viewModel.sheet.address
: props.viewModel.sheetObject.address
const tracksByObject = val( const tracksByObject = val(
getStudio()!.atomP.historic.coreByProject[ getStudio()!.atomP.historic.coreByProject[address.projectId]
props.viewModel.sheetObject.address.projectId .sheetsById[address.sheetId].sequence.tracksByObject,
].sheetsById[props.viewModel.sheetObject.address.sheetId].sequence
.tracksByObject,
)! )!
// Calculate all the valid snap positions in the sequence editor, // Calculate all the valid snap positions in the sequence editor,
@ -537,10 +638,9 @@ function useDragForAggregateKeyframeDot(
AggregateKeyframePositionIsSelected.AllSelected AggregateKeyframePositionIsSelected.AllSelected
) { ) {
const {selection, viewModel} = props const {selection, viewModel} = props
const {sheetObject} = viewModel
const handlers = selection const handlers = selection
.getDragHandlers({ .getDragHandlers({
...sheetObject.address, ...address,
domNode: containerNode!, domNode: containerNode!,
positionAtStartOfDrag: keyframes[0].kf.position, positionAtStartOfDrag: keyframes[0].kf.position,
}) })
@ -587,7 +687,7 @@ function useDragForAggregateKeyframeDot(
const original = keyframe.kf const original = keyframe.kf
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{ {
...propsAtStartOfDrag.viewModel.sheetObject.address, ...keyframe.track.sheetObject.address,
trackId: keyframe.track.id, trackId: keyframe.track.id,
keyframes: [{...original, position: newPosition}], keyframes: [{...original, position: newPosition}],
snappingFunction: val( snappingFunction: val(

View file

@ -74,7 +74,11 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
const snapToAllKeyframes = snapPositionsState.mode === 'snapToAll' const snapToAllKeyframes = snapPositionsState.mode === 'snapToAll'
const track = useMemo( const track = useMemo(
() => ({data: trackData, id: leaf.trackId}), () => ({
data: trackData,
id: leaf.trackId,
sheetObject: props.leaf.sheetObject,
}),
[trackData, leaf.trackId], [trackData, leaf.trackId],
) )

View file

@ -10,6 +10,7 @@ import type {
} from './useSingleKeyframeInlineEditorPopover' } from './useSingleKeyframeInlineEditorPopover'
import last from 'lodash-es/last' import last from 'lodash-es/last'
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools' import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
import {valueInProp} from '@theatre/shared/propTypes/utils'
const SingleKeyframePropEditorContainer = styled.div` const SingleKeyframePropEditorContainer = styled.div`
display: flex; display: flex;
@ -127,7 +128,7 @@ function PrimitivePropEditor(
<PropEditor <PropEditor
editingTools={editingTools} editingTools={editingTools}
propConfig={p.propConfig} propConfig={p.propConfig}
value={p.keyframe.value} value={valueInProp(p.keyframe.value, p.propConfig)}
autoFocus={p.autoFocusInput} autoFocus={p.autoFocusInput}
/> />
</SingleKeyframeSimplePropEditorContainer> </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( function useEditingToolsForKeyframeEditorPopover(
props: PrimitivePropEditingOptions, props: PrimitivePropEditingOptions,
) { ) {

View file

@ -18,6 +18,7 @@ import type {
import type { import type {
SequenceEditorTree_AllRowTypes, SequenceEditorTree_AllRowTypes,
SequenceEditorTree_PropWithChildren, SequenceEditorTree_PropWithChildren,
SequenceEditorTree_Sheet,
SequenceEditorTree_SheetObject, SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
@ -157,28 +158,25 @@ namespace utils {
const collectForAggregatedChildren = ( const collectForAggregatedChildren = (
logger: IUtilLogger, logger: IUtilLogger,
layout: SequenceEditorPanelLayout, layout: SequenceEditorPanelLayout,
leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren, leaf:
| SequenceEditorTree_SheetObject
| SequenceEditorTree_PropWithChildren
| SequenceEditorTree_Sheet,
bounds: SelectionBounds, bounds: SelectionBounds,
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
) => { ) => {
const sheetObject = leaf.sheetObject const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf)
const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf)
if ( if (
leaf.top + leaf.nodeHeight / 2 + HITBOX_SIZE_PX > bounds.v[0] && leaf.top + leaf.nodeHeight / 2 + HITBOX_SIZE_PX > bounds.v[0] &&
leaf.top + leaf.nodeHeight / 2 - HITBOX_SIZE_PX < bounds.v[1] leaf.top + leaf.nodeHeight / 2 - HITBOX_SIZE_PX < bounds.v[1]
) { ) {
for (const [position, keyframes] of aggregatedKeyframes.byPosition) { for (const [position, keyframes] of aggregatedKeyframes.byPosition) {
if ( const hitboxWidth = layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX)
position + layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) <= const isHitboxOutsideSelection =
bounds.h[0] position + hitboxWidth <= bounds.h[0] ||
) position - hitboxWidth >= bounds.h[1]
continue if (isHitboxOutsideSelection) continue
if (
position - layout.scaledSpace.toUnitSpace(HITBOX_SIZE_PX) >=
bounds.h[1]
)
break
for (const keyframeWithTrack of keyframes) { for (const keyframeWithTrack of keyframes) {
mutableSetDeep( mutableSetDeep(
@ -186,9 +184,11 @@ namespace utils {
(selectionByObjectKeyP) => (selectionByObjectKeyP) =>
// convenience for accessing a deep path which might not actually exist // 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 ) // through the use of pointer proxy (so we don't have to deal with undeifned )
selectionByObjectKeyP[sheetObject.address.objectKey].byTrackId[ selectionByObjectKeyP[
keyframeWithTrack.track.id keyframeWithTrack.track.sheetObject.address.objectKey
].byKeyframeId[keyframeWithTrack.kf.id], ].byTrackId[keyframeWithTrack.track.id].byKeyframeId[
keyframeWithTrack.kf.id
],
true, true,
) )
} }
@ -207,6 +207,15 @@ namespace utils {
selectionByObjectKey: DopeSheetSelection['byObjectKey'], selectionByObjectKey: DopeSheetSelection['byObjectKey'],
) => void ) => void
} = { } = {
sheet(logger, layout, leaf, bounds, selectionByObjectKey) {
collectForAggregatedChildren(
logger,
layout,
leaf,
bounds,
selectionByObjectKey,
)
},
propWithChildren(logger, layout, leaf, bounds, selectionByObjectKey) { propWithChildren(logger, layout, leaf, bounds, selectionByObjectKey) {
collectForAggregatedChildren( collectForAggregatedChildren(
logger, logger,

View file

@ -39,10 +39,7 @@ const RightPropWithChildrenRow: React.VFC<{
viewModel.pathToProp.join(), viewModel.pathToProp.join(),
) )
return usePrism(() => { return usePrism(() => {
const aggregatedKeyframes = collectAggregateKeyframesInPrism( const aggregatedKeyframes = collectAggregateKeyframesInPrism(viewModel)
logger.utilFor.internal(),
viewModel,
)
const node = ( const node = (
<AggregatedKeyframeTrack <AggregatedKeyframeTrack

View file

@ -1,12 +1,9 @@
import {theme} from '@theatre/studio/css'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {darken} from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import DopeSheetSelectionView from './DopeSheetSelectionView' import DopeSheetSelectionView from './DopeSheetSelectionView'
import HorizontallyScrollableArea from './HorizontallyScrollableArea' import HorizontallyScrollableArea from './HorizontallyScrollableArea'
import SheetRow from './SheetRow' import SheetRow from './SheetRow'
@ -22,17 +19,6 @@ const ListContainer = styled.ul`
width: ${contentWidth}px; 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<{ const Right: React.FC<{
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
}> = ({layoutP}) => { }> = ({layoutP}) => {

View file

@ -5,7 +5,6 @@ import type {Pointer} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import {decideRowByPropType} from './PropWithChildrenRow' import {decideRowByPropType} from './PropWithChildrenRow'
import RightRow from './Row' import RightRow from './Row'
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes' import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack' import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
@ -13,15 +12,8 @@ const RightSheetObjectRow: React.VFC<{
leaf: SequenceEditorTree_SheetObject leaf: SequenceEditorTree_SheetObject
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
}> = ({leaf, layoutP}) => { }> = ({leaf, layoutP}) => {
const logger = useLogger(
`RightSheetObjectRow`,
leaf.sheetObject.address.objectKey,
)
return usePrism(() => { return usePrism(() => {
const aggregatedKeyframes = collectAggregateKeyframesInPrism( const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf)
logger.utilFor.internal(),
leaf,
)
const node = ( const node = (
<AggregatedKeyframeTrack <AggregatedKeyframeTrack

View file

@ -4,14 +4,27 @@ import {usePrism} from '@theatre/react'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import RightSheetObjectRow from './SheetObjectRow' import RightSheetObjectRow from './SheetObjectRow'
import RightRow from './Row'
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
const SheetRow: React.FC<{ const SheetRow: React.FC<{
leaf: SequenceEditorTree_Sheet leaf: SequenceEditorTree_Sheet
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
}> = ({leaf, layoutP}) => { }> = ({leaf, layoutP}) => {
return usePrism(() => { return usePrism(() => {
const aggregatedKeyframes = collectAggregateKeyframesInPrism(leaf)
const node = (
<AggregatedKeyframeTrack
layoutP={layoutP}
aggregatedKeyframes={aggregatedKeyframes}
viewModel={leaf}
/>
)
return ( return (
<> <RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
{leaf.children.map((sheetObjectLeaf) => ( {leaf.children.map((sheetObjectLeaf) => (
<RightSheetObjectRow <RightSheetObjectRow
layoutP={layoutP} layoutP={layoutP}
@ -19,7 +32,7 @@ const SheetRow: React.FC<{
leaf={sheetObjectLeaf} leaf={sheetObjectLeaf}
/> />
))} ))}
</> </RightRow>
) )
}, [leaf, layoutP]) }, [leaf, layoutP])
} }

View file

@ -1,7 +1,9 @@
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import type { import type {
SequenceEditorTree_PrimitiveProp,
SequenceEditorTree_PropWithChildren, SequenceEditorTree_PropWithChildren,
SequenceEditorTree_Sheet,
SequenceEditorTree_SheetObject, SequenceEditorTree_SheetObject,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' } from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
import type { import type {
@ -13,9 +15,9 @@ import type {
Keyframe, Keyframe,
TrackData, TrackData,
} from '@theatre/core/projects/store/types/SheetState_Historic' } from '@theatre/core/projects/store/types/SheetState_Historic'
import type {IUtilLogger} from '@theatre/shared/logger'
import {encodePathToProp} from '@theatre/shared/utils/addresses' import {encodePathToProp} from '@theatre/shared/utils/addresses'
import {uniq} from 'lodash-es' 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. * An index over a series of keyframes that have been collected from different tracks.
@ -30,6 +32,7 @@ export type AggregatedKeyframes = {
export type TrackWithId = { export type TrackWithId = {
id: SequenceTrackId id: SequenceTrackId
data: TrackData data: TrackData
sheetObject: SheetObject
} }
export type KeyframeWithTrack = { export type KeyframeWithTrack = {
@ -59,67 +62,27 @@ export type KeyframeWithTrack = {
* *
*/ */
export function collectAggregateKeyframesInPrism( export function collectAggregateKeyframesInPrism(
logger: IUtilLogger, leaf:
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, | SequenceEditorTree_Sheet
| SequenceEditorTree_PropWithChildren
| SequenceEditorTree_SheetObject,
): AggregatedKeyframes { ): AggregatedKeyframes {
const sheetObject = leaf.sheetObject const tracks =
leaf.type === 'sheet'
? collectAggregateKeyframesSheet(leaf)
: collectAggregateKeyframesCompoundOrObject(leaf)
const projectId = sheetObject.address.projectId return {
byPosition: keyframesByPositionFromTrackWithIds(tracks),
const sheetObjectTracksP = tracks,
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})
}
} }
}
logger.trace('see collected of children', { function keyframesByPositionFromTrackWithIds(tracks: TrackWithId[]) {
aggregatedKeyframes,
childSimpleTracks,
})
const tracks = aggregatedKeyframes
.flatMap((a) => a.tracks)
.concat(childSimpleTracks)
const byPosition = new Map<number, KeyframeWithTrack[]>() const byPosition = new Map<number, KeyframeWithTrack[]>()
for (const track of tracks) { for (const track of tracks) {
const kfs = track.data.keyframes for (const kf of track.data.keyframes) {
for (let i = 0; i < kfs.length; i++) {
const kf = kfs[i]
let existing = byPosition.get(kf.position) let existing = byPosition.get(kf.position)
if (!existing) { if (!existing) {
existing = [] existing = []
@ -129,7 +92,7 @@ export function collectAggregateKeyframesInPrism(
kf, kf,
track, track,
itemKey: createStudioSheetItemKey.forTrackKeyframe( itemKey: createStudioSheetItemKey.forTrackKeyframe(
sheetObject, track.sheetObject,
track.id, track.id,
kf.id, kf.id,
), ),
@ -137,19 +100,28 @@ export function collectAggregateKeyframesInPrism(
} }
} }
return { return byPosition
byPosition,
tracks,
}
} }
/** function collectAggregateKeyframesSheet(
* Collects all the snap positions for an aggregate track. leaf: SequenceEditorTree_Sheet,
*/ ): TrackWithId[] {
export function collectAggregateSnapPositions( return leaf.children.flatMap(collectAggregateKeyframesCompoundOrObject)
}
function collectAggregateKeyframesCompoundOrObject(
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject, leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject,
snapTargetPositions: {[key: string]: {[key: string]: number[]}}, ): TrackWithId[] {
): number[] { return leaf.children.flatMap((childLeaf) =>
childLeaf.type === 'propWithChildren'
? collectAggregateKeyframesCompoundOrObject(childLeaf)
: collectAggregateKeyframesPrimitiveProp(childLeaf),
)
}
function collectAggregateKeyframesPrimitiveProp(
leaf: SequenceEditorTree_PrimitiveProp,
): TrackWithId[] {
const sheetObject = leaf.sheetObject const sheetObject = leaf.sheetObject
const projectId = sheetObject.address.projectId const projectId = sheetObject.address.projectId
@ -158,29 +130,67 @@ export function collectAggregateSnapPositions(
getStudio().atomP.historic.coreByProject[projectId].sheetsById[ getStudio().atomP.historic.coreByProject[projectId].sheetsById[
sheetObject.address.sheetId sheetObject.address.sheetId
].sequence.tracksByObject[sheetObject.address.objectKey] ].sequence.tracksByObject[sheetObject.address.objectKey]
const trackId = val(
sheetObjectTracksP.trackIdByPropPath[encodePathToProp(leaf.pathToProp)],
)
if (!trackId) return []
const positions: number[] = [] const trackData = val(sheetObjectTracksP.trackData[trackId])
for (const childLeaf of leaf.children) { if (!trackData) return []
if (childLeaf.type === 'primitiveProp') {
const trackId = val(
sheetObjectTracksP.trackIdByPropPath[
encodePathToProp(childLeaf.pathToProp)
],
)
if (!trackId) {
continue
}
positions.push( return [{id: trackId, data: trackData, sheetObject}]
...(snapTargetPositions[sheetObject.address.objectKey]?.[trackId] ?? }
[]),
) /**
} else if (childLeaf.type === 'propWithChildren') { * Collects all the snap positions for an aggregate track.
positions.push( */
...collectAggregateSnapPositions(childLeaf, snapTargetPositions), export function collectAggregateSnapPositionsSheet(
) leaf: SequenceEditorTree_Sheet,
} snapTargetPositions: {[key: string]: {[key: string]: number[]}},
} ): number[] {
return uniq(
return uniq(positions) 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']}, * {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( export function copyableKeyframesFromSelection(
projectId: ProjectId, projectId: ProjectId,
@ -175,7 +172,7 @@ export function keyframesWithPaths({
const encodedPropPath = propPathByTrackId[trackId] const encodedPropPath = propPathByTrackId[trackId]
if (!encodedPropPath) return null if (!encodedPropPath) return null
const pathToProp = decodePathToProp(encodedPropPath) const pathToProp = [objectKey, ...decodePathToProp(encodedPropPath)]
return keyframeIds return keyframeIds
.map((keyframeId) => ({ .map((keyframeId) => ({

View file

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

View file

@ -1,7 +1,7 @@
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type useDrag from '@theatre/studio/uiComponents/useDrag' 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 subPrism from '@theatre/shared/utils/subPrism'
import type { import type {
IRange, IRange,
@ -57,7 +57,7 @@ export type DopeSheetSelection = {
} }
> >
getDragHandlers( getDragHandlers(
origin: SheetObjectAddress & { origin: SheetAddress & {
positionAtStartOfDrag: number positionAtStartOfDrag: number
domNode: Element domNode: Element
}, },

View file

@ -58,6 +58,7 @@ export type SequenceEditorTree = SequenceEditorTree_Sheet
export type SequenceEditorTree_Sheet = SequenceEditorTree_Row<'sheet'> & { export type SequenceEditorTree_Sheet = SequenceEditorTree_Row<'sheet'> & {
sheet: Sheet sheet: Sheet
isCollapsed: boolean
children: SequenceEditorTree_SheetObject[] children: SequenceEditorTree_SheetObject[]
} }
@ -106,34 +107,44 @@ export const calculateSequenceEditorTree = (
studio: Studio, studio: Studio,
): SequenceEditorTree => { ): SequenceEditorTree => {
prism.ensurePrism() prism.ensurePrism()
let topSoFar = titleBarHeight
let nSoFar = 0
const rootShouldRender = true 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 = { const tree: SequenceEditorTree = {
type: 'sheet', type: 'sheet',
isCollapsed,
sheet, sheet,
children: [], children: [],
sheetItemKey: createStudioSheetItemKey.forSheet(), sheetItemKey: createStudioSheetItemKey.forSheet(),
shouldRender: rootShouldRender, shouldRender: rootShouldRender,
top: topSoFar, top: titleBarHeight,
depth: -1, depth: 0,
n: nSoFar, n: nSoFar,
nodeHeight: 0, // always 0 nodeHeight: rootShouldRender ? HEIGHT_OF_ANY_TITLE : 0,
heightIncludingChildren: -1, // will define this later heightIncludingChildren: -1, // calculated below
} }
if (rootShouldRender) { if (rootShouldRender) {
nSoFar += 1 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))) { for (const sheetObject of Object.values(val(sheet.objectsP))) {
if (sheetObject) { if (sheetObject) {
addObject(sheetObject, tree.children, tree.depth + 1, rootShouldRender) addObject(
sheetObject,
tree.children,
tree.depth + 1,
rootShouldRender && !isCollapsed,
)
} }
} }
tree.heightIncludingChildren = topSoFar - tree.top tree.heightIncludingChildren = topSoFar - tree.top
@ -167,9 +178,7 @@ export const calculateSequenceEditorTree = (
n: nSoFar, n: nSoFar,
sheetObject: sheetObject, sheetObject: sheetObject,
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0, nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
// Question: Why -1? Is this relevant for "shouldRender"? heightIncludingChildren: -1, // calculated below
// Perhaps this is to indicate this does not have a valid value.
heightIncludingChildren: -1,
} }
arrayOfChildren.push(row) arrayOfChildren.push(row)
@ -350,5 +359,6 @@ export const calculateSequenceEditorTree = (
topSoFar += row.nodeHeight topSoFar += row.nodeHeight
} }
console.log('tree :)', tree)
return tree return tree
} }

View file

@ -257,7 +257,7 @@ function ControlIndicators({
...s, ...s,
nearbies: getNearbyKeyframesOfTrack( nearbies: getNearbyKeyframesOfTrack(
obj, obj,
{id: s.trackId, data: s.track!}, {id: s.trackId, data: s.track!, sheetObject: obj},
sequencePosition, sequencePosition,
), ),
})) }))

View file

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