UX: Add "PresenceFlag" item indicators (usePresence) (#184)
* feat/dev: Add usePresence and enable for keyframes & keyframe cursors * Enable hovered styles for AggregateKeyframeDot * Enable hovered styles for graph editor keyframes
This commit is contained in:
parent
87070bcdf3
commit
e8c8168f0b
26 changed files with 666 additions and 138 deletions
|
@ -68,6 +68,9 @@ export function generateSequenceMarkerId(): SequenceMarkerId {
|
||||||
* versioning happens where something needs to
|
* versioning happens where something needs to
|
||||||
*/
|
*/
|
||||||
export const createStudioSheetItemKey = {
|
export const createStudioSheetItemKey = {
|
||||||
|
forSheet(): StudioSheetItemKey {
|
||||||
|
return 'sheet' as StudioSheetItemKey
|
||||||
|
},
|
||||||
forSheetObject(obj: SheetObject): StudioSheetItemKey {
|
forSheetObject(obj: SheetObject): StudioSheetItemKey {
|
||||||
return stableValueHash({
|
return stableValueHash({
|
||||||
o: obj.address.objectKey,
|
o: obj.address.objectKey,
|
||||||
|
@ -82,4 +85,36 @@ export const createStudioSheetItemKey = {
|
||||||
p: pathToProp,
|
p: pathToProp,
|
||||||
}) as StudioSheetItemKey
|
}) as StudioSheetItemKey
|
||||||
},
|
},
|
||||||
|
forTrackKeyframe(
|
||||||
|
obj: SheetObject,
|
||||||
|
trackId: SequenceTrackId,
|
||||||
|
keyframeId: KeyframeId,
|
||||||
|
): StudioSheetItemKey {
|
||||||
|
return stableValueHash({
|
||||||
|
o: obj.address.objectKey,
|
||||||
|
t: trackId,
|
||||||
|
k: keyframeId,
|
||||||
|
}) as StudioSheetItemKey
|
||||||
|
},
|
||||||
|
forSheetObjectAggregateKeyframe(
|
||||||
|
obj: SheetObject,
|
||||||
|
position: number,
|
||||||
|
): StudioSheetItemKey {
|
||||||
|
return createStudioSheetItemKey.forCompoundPropAggregateKeyframe(
|
||||||
|
obj,
|
||||||
|
[],
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
forCompoundPropAggregateKeyframe(
|
||||||
|
obj: SheetObject,
|
||||||
|
pathToProp: PathToProp,
|
||||||
|
position: number,
|
||||||
|
): StudioSheetItemKey {
|
||||||
|
return stableValueHash({
|
||||||
|
o: obj.address.objectKey,
|
||||||
|
p: pathToProp,
|
||||||
|
pos: position,
|
||||||
|
}) as StudioSheetItemKey
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, {
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {isProject, isSheetObject} from '@theatre/shared/instanceTypes'
|
import {isProject, isSheetObject} from '@theatre/shared/instanceTypes'
|
||||||
|
@ -21,6 +22,7 @@ import useHotspot from '@theatre/studio/uiComponents/useHotspot'
|
||||||
import {Box, prism, val} from '@theatre/dataverse'
|
import {Box, prism, val} from '@theatre/dataverse'
|
||||||
import EmptyState from './EmptyState'
|
import EmptyState from './EmptyState'
|
||||||
import useLockSet from '@theatre/studio/uiComponents/useLockSet'
|
import useLockSet from '@theatre/studio/uiComponents/useLockSet'
|
||||||
|
import {usePresenceListenersOnRootElement} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
const headerHeight = `32px`
|
const headerHeight = `32px`
|
||||||
|
|
||||||
|
@ -106,6 +108,9 @@ const DetailPanel: React.FC<{}> = (props) => {
|
||||||
|
|
||||||
const showDetailsPanel = pin || hotspotActive || isContextMenuShown
|
const showDetailsPanel = pin || hotspotActive || isContextMenuShown
|
||||||
|
|
||||||
|
const [containerElt, setContainerElt] = useState<null | HTMLDivElement>(null)
|
||||||
|
usePresenceListenersOnRootElement(containerElt)
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const selection = getOutlineSelection()
|
const selection = getOutlineSelection()
|
||||||
|
|
||||||
|
@ -115,6 +120,7 @@ const DetailPanel: React.FC<{}> = (props) => {
|
||||||
<Container
|
<Container
|
||||||
data-testid="DetailPanel-Object"
|
data-testid="DetailPanel-Object"
|
||||||
pin={showDetailsPanel}
|
pin={showDetailsPanel}
|
||||||
|
ref={setContainerElt}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
isDetailPanelHoveredB.set(true)
|
isDetailPanelHoveredB.set(true)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -31,7 +31,10 @@ 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, leaf)
|
setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, {
|
||||||
|
sheetAddress: leaf.sheetObject.address,
|
||||||
|
sheetItemKey: leaf.sheetItemKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{leaf.children.map((propLeaf) => decideRowByPropType(propLeaf))}
|
{leaf.children.map((propLeaf) => decideRowByPropType(propLeaf))}
|
||||||
|
|
|
@ -22,7 +22,10 @@ const LeftSheetObjectRow: React.VFC<{
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
toggleCollapsed={() =>
|
toggleCollapsed={() =>
|
||||||
setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, leaf)
|
setCollapsedSheetObjectOrCompoundProp(!leaf.isCollapsed, {
|
||||||
|
sheetAddress: leaf.sheetObject.address,
|
||||||
|
sheetItemKey: leaf.sheetItemKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{leaf.children.map((leaf) => decideRowByPropType(leaf))}
|
{leaf.children.map((leaf) => decideRowByPropType(leaf))}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
import usePresence, {
|
||||||
|
PresenceFlag,
|
||||||
|
} from '@theatre/studio/uiComponents/usePresence'
|
||||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
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'
|
||||||
|
@ -26,6 +29,22 @@ export function AggregateKeyframeDot(
|
||||||
const logger = useLogger('AggregateKeyframeDot')
|
const logger = useLogger('AggregateKeyframeDot')
|
||||||
const {cur} = props.utils
|
const {cur} = props.utils
|
||||||
|
|
||||||
|
const presence = usePresence(props.utils.itemKey)
|
||||||
|
presence.useRelations(
|
||||||
|
() =>
|
||||||
|
cur.keyframes.map((kf) => ({
|
||||||
|
affects: kf.itemKey,
|
||||||
|
flag: PresenceFlag.Primary,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
// Hmm: Is this a valid fix for the changing size of the useEffect's dependency array?
|
||||||
|
// also: does it work properly with selections?
|
||||||
|
cur.keyframes
|
||||||
|
.map((keyframeWithTrack) => keyframeWithTrack.track.id)
|
||||||
|
.join('-'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node)
|
const [contextMenu] = useAggregateKeyframeContextMenu(props, logger, node)
|
||||||
|
@ -34,11 +53,13 @@ export function AggregateKeyframeDot(
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
{...presence.attrs}
|
||||||
// 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)}
|
||||||
/>
|
/>
|
||||||
<AggregateKeyframeVisualDot
|
<AggregateKeyframeVisualDot
|
||||||
|
flag={presence.flag}
|
||||||
isAllHere={cur.allHere}
|
isAllHere={cur.allHere}
|
||||||
isSelected={cur.selected}
|
isSelected={cur.selected}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
import {PresenceFlag} from '@theatre/studio/uiComponents/usePresence'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
|
@ -34,11 +35,13 @@ export const HitZone = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
export function AggregateKeyframeVisualDot(props: {
|
export function AggregateKeyframeVisualDot(props: {
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
isSelected: AggregateKeyframePositionIsSelected | undefined
|
isSelected: AggregateKeyframePositionIsSelected | undefined
|
||||||
isAllHere: boolean
|
isAllHere: boolean
|
||||||
}) {
|
}) {
|
||||||
const theme: IDotThemeValues = {
|
const theme: IDotThemeValues = {
|
||||||
isSelected: props.isSelected,
|
isSelected: props.isSelected,
|
||||||
|
flag: props.flag,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -53,6 +56,7 @@ export function AggregateKeyframeVisualDot(props: {
|
||||||
}
|
}
|
||||||
type IDotThemeValues = {
|
type IDotThemeValues = {
|
||||||
isSelected: AggregateKeyframePositionIsSelected | undefined
|
isSelected: AggregateKeyframePositionIsSelected | undefined
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
}
|
}
|
||||||
const SELECTED_COLOR = '#F2C95C'
|
const SELECTED_COLOR = '#F2C95C'
|
||||||
const DEFAULT_PRIMARY_COLOR = '#40AAA4'
|
const DEFAULT_PRIMARY_COLOR = '#40AAA4'
|
||||||
|
@ -95,6 +99,8 @@ const AggregateDotAllHereSvg = (theme: IDotThemeValues) => (
|
||||||
height="6"
|
height="6"
|
||||||
transform="rotate(-45 3.75732 6.01953)"
|
transform="rotate(-45 3.75732 6.01953)"
|
||||||
fill={selectionColorAll(theme)}
|
fill={selectionColorAll(theme)}
|
||||||
|
stroke={theme.flag === PresenceFlag.Primary ? 'white' : undefined}
|
||||||
|
strokeWidth={theme.flag === PresenceFlag.Primary ? '2px' : undefined}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -114,7 +120,10 @@ const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => (
|
||||||
height="5"
|
height="5"
|
||||||
transform="rotate(-45 4.46443 8)"
|
transform="rotate(-45 4.46443 8)"
|
||||||
fill="#23262B"
|
fill="#23262B"
|
||||||
stroke={selectionColorAll(theme)}
|
stroke={
|
||||||
|
theme.flag === PresenceFlag.Primary ? 'white' : selectionColorAll(theme)
|
||||||
|
}
|
||||||
|
strokeWidth={theme.flag === PresenceFlag.Primary ? '2px' : undefined}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {prism} from '@theatre/dataverse'
|
import {prism} from '@theatre/dataverse'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
import {AggregateKeyframePositionIsSelected} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
import {isConnectionEditingInCurvePopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
import {isConnectionEditingInCurvePopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/CurveEditorPopover/CurveEditorPopover'
|
||||||
import {usePrism} from '@theatre/react'
|
import {usePrism} from '@theatre/react'
|
||||||
|
@ -93,7 +94,27 @@ export function getAggregateKeyframeEditorUtilsPrismFn(
|
||||||
(con) => isConnectionEditingInCurvePopover(con),
|
(con) => isConnectionEditingInCurvePopover(con),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const itemKey = prism.memo(
|
||||||
|
'itemKey',
|
||||||
|
() => {
|
||||||
|
if (props.viewModel.type === 'sheetObject') {
|
||||||
|
return createStudioSheetItemKey.forSheetObjectAggregateKeyframe(
|
||||||
|
props.viewModel.sheetObject,
|
||||||
|
cur.position,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return createStudioSheetItemKey.forCompoundPropAggregateKeyframe(
|
||||||
|
props.viewModel.sheetObject,
|
||||||
|
props.viewModel.pathToProp,
|
||||||
|
cur.position,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[props.viewModel.sheetObject, cur.position],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
itemKey,
|
||||||
cur,
|
cur,
|
||||||
connected,
|
connected,
|
||||||
isAggregateEditingInCurvePopover,
|
isAggregateEditingInCurvePopover,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type {KeyframeWithPathToPropFromCommonRoot} from '@theatre/studio/store/t
|
||||||
import KeyframeSnapTarget, {
|
import KeyframeSnapTarget, {
|
||||||
snapPositionsStateD,
|
snapPositionsStateD,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -82,9 +83,17 @@ const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SingleKeyframeEditor
|
<SingleKeyframeEditor
|
||||||
|
itemKey={createStudioSheetItemKey.forTrackKeyframe(
|
||||||
|
leaf.sheetObject,
|
||||||
|
leaf.trackId,
|
||||||
|
kf.id,
|
||||||
|
)}
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
trackData={trackData}
|
track={{
|
||||||
|
data: trackData,
|
||||||
|
id: leaf.trackId,
|
||||||
|
}}
|
||||||
layoutP={layoutP}
|
layoutP={layoutP}
|
||||||
leaf={leaf}
|
leaf={leaf}
|
||||||
selection={
|
selection={
|
||||||
|
|
|
@ -35,9 +35,9 @@ type IBasicKeyframeConnectorProps = ISingleKeyframeEditorProps
|
||||||
const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
||||||
props,
|
props,
|
||||||
) => {
|
) => {
|
||||||
const {index, trackData} = props
|
const {index, track} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = track.data.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = track.data.keyframes[index + 1]
|
||||||
|
|
||||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
@ -99,7 +99,11 @@ export default BasicKeyframeConnector
|
||||||
const SingleCurveEditorPopover: React.FC<
|
const SingleCurveEditorPopover: React.FC<
|
||||||
IBasicKeyframeConnectorProps & {closePopover: (reason: string) => void}
|
IBasicKeyframeConnectorProps & {closePopover: (reason: string) => void}
|
||||||
> = React.forwardRef((props, ref) => {
|
> = React.forwardRef((props, ref) => {
|
||||||
const {index, trackData, selection} = props
|
const {
|
||||||
|
index,
|
||||||
|
track: {data: trackData},
|
||||||
|
selection,
|
||||||
|
} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
|
||||||
|
@ -161,7 +165,7 @@ function useDragKeyframe(
|
||||||
...sheetObject.address,
|
...sheetObject.address,
|
||||||
domNode: node!,
|
domNode: node!,
|
||||||
positionAtStartOfDrag:
|
positionAtStartOfDrag:
|
||||||
props.trackData.keyframes[props.index].position,
|
props.track.data.keyframes[props.index].position,
|
||||||
})
|
})
|
||||||
.onDragStart(event)
|
.onDragStart(event)
|
||||||
}
|
}
|
||||||
|
@ -187,7 +191,7 @@ function useDragKeyframe(
|
||||||
trackId: propsAtStartOfDrag.leaf.trackId,
|
trackId: propsAtStartOfDrag.leaf.trackId,
|
||||||
keyframeIds: [
|
keyframeIds: [
|
||||||
propsAtStartOfDrag.keyframe.id,
|
propsAtStartOfDrag.keyframe.id,
|
||||||
propsAtStartOfDrag.trackData.keyframes[
|
propsAtStartOfDrag.track.data.keyframes[
|
||||||
propsAtStartOfDrag.index + 1
|
propsAtStartOfDrag.index + 1
|
||||||
].id,
|
].id,
|
||||||
],
|
],
|
||||||
|
|
|
@ -69,7 +69,6 @@ type ICurveSegmentEditorProps = {
|
||||||
|
|
||||||
const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
const CurveSegmentEditor: React.VFC<ICurveSegmentEditorProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
curveConnection,
|
|
||||||
curveConnection: {left, right},
|
curveConnection: {left, right},
|
||||||
backgroundConnections,
|
backgroundConnections,
|
||||||
} = props
|
} = props
|
||||||
|
|
|
@ -24,6 +24,9 @@ import {
|
||||||
snapToSome,
|
snapToSome,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget'
|
||||||
import {useSingleKeyframeInlineEditorPopover} from './useSingleKeyframeInlineEditorPopover'
|
import {useSingleKeyframeInlineEditorPopover} from './useSingleKeyframeInlineEditorPopover'
|
||||||
|
import usePresence, {
|
||||||
|
PresenceFlag,
|
||||||
|
} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
export const DOT_SIZE_PX = 6
|
export const DOT_SIZE_PX = 6
|
||||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 2
|
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 2
|
||||||
|
@ -50,7 +53,11 @@ const selectBacgroundForDiamond = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type IDiamond = {isSelected: boolean; isInlineEditorPopoverOpen: boolean}
|
type IDiamond = {
|
||||||
|
isSelected: boolean
|
||||||
|
isInlineEditorPopoverOpen: boolean
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/** The keyframe diamond ◆ */
|
/** The keyframe diamond ◆ */
|
||||||
const Diamond = styled.div<IDiamond>`
|
const Diamond = styled.div<IDiamond>`
|
||||||
|
@ -60,6 +67,9 @@ const Diamond = styled.div<IDiamond>`
|
||||||
background: ${(props) => selectBacgroundForDiamond(props)};
|
background: ${(props) => selectBacgroundForDiamond(props)};
|
||||||
transform: rotateZ(45deg);
|
transform: rotateZ(45deg);
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.flag === PresenceFlag.Primary ? 'outline: 2px solid white;' : ''};
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`
|
`
|
||||||
|
@ -86,7 +96,8 @@ type ISingleKeyframeDotProps = ISingleKeyframeEditorProps
|
||||||
|
|
||||||
/** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
|
/** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
|
||||||
const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||||
const logger = useLogger('SingleKeyframeDot')
|
const logger = useLogger('SingleKeyframeDot', props.keyframe.id)
|
||||||
|
const presence = usePresence(props.itemKey)
|
||||||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
|
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
|
||||||
|
@ -110,10 +121,12 @@ const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={ref}
|
ref={ref}
|
||||||
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
||||||
|
{...presence.attrs}
|
||||||
/>
|
/>
|
||||||
<Diamond
|
<Diamond
|
||||||
isSelected={!!props.selection}
|
isSelected={!!props.selection}
|
||||||
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
isInlineEditorPopoverOpen={isInlineEditorPopoverOpen}
|
||||||
|
flag={presence.flag}
|
||||||
/>
|
/>
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
|
@ -242,7 +255,7 @@ function useDragForSingleKeyframeDot(
|
||||||
...sheetObject.address,
|
...sheetObject.address,
|
||||||
domNode: node!,
|
domNode: node!,
|
||||||
positionAtStartOfDrag:
|
positionAtStartOfDrag:
|
||||||
props.trackData.keyframes[props.index].position,
|
props.track.data.keyframes[props.index].position,
|
||||||
})
|
})
|
||||||
.onDragStart(event)
|
.onDragStart(event)
|
||||||
|
|
||||||
|
@ -272,7 +285,7 @@ function useDragForSingleKeyframeDot(
|
||||||
return {
|
return {
|
||||||
onDrag(dx, dy, event) {
|
onDrag(dx, dy, event) {
|
||||||
const original =
|
const original =
|
||||||
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
|
propsAtStartOfDrag.track.data.keyframes[propsAtStartOfDrag.index]
|
||||||
const newPosition = Math.max(
|
const newPosition = Math.max(
|
||||||
// check if our event hoversover a [data-pos] element
|
// check if our event hoversover a [data-pos] element
|
||||||
DopeSnap.checkIfMouseEventSnapToPos(event, {
|
DopeSnap.checkIfMouseEventSnapToPos(event, {
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import type {
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
Keyframe,
|
|
||||||
TrackData,
|
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
|
||||||
import type {
|
import type {
|
||||||
DopeSheetSelection,
|
DopeSheetSelection,
|
||||||
SequenceEditorPanelLayout,
|
SequenceEditorPanelLayout,
|
||||||
|
@ -13,6 +10,8 @@ import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import SingleKeyframeConnector from './BasicKeyframeConnector'
|
import SingleKeyframeConnector from './BasicKeyframeConnector'
|
||||||
import SingleKeyframeDot from './SingleKeyframeDot'
|
import SingleKeyframeDot from './SingleKeyframeDot'
|
||||||
|
import type {TrackWithId} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
|
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
|
|
||||||
const SingleKeyframeEditorContainer = styled.div`
|
const SingleKeyframeEditorContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -23,14 +22,19 @@ const noConnector = <></>
|
||||||
export type ISingleKeyframeEditorProps = {
|
export type ISingleKeyframeEditorProps = {
|
||||||
index: number
|
index: number
|
||||||
keyframe: Keyframe
|
keyframe: Keyframe
|
||||||
trackData: TrackData
|
track: TrackWithId
|
||||||
|
itemKey: StudioSheetItemKey
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
leaf: SequenceEditorTree_PrimitiveProp
|
leaf: SequenceEditorTree_PrimitiveProp
|
||||||
selection: undefined | DopeSheetSelection
|
selection: undefined | DopeSheetSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleKeyframeEditor: React.VFC<ISingleKeyframeEditorProps> = (props) => {
|
const SingleKeyframeEditor: React.VFC<ISingleKeyframeEditorProps> = (props) => {
|
||||||
const {index, trackData} = props
|
const {
|
||||||
|
index,
|
||||||
|
keyframe,
|
||||||
|
track: {data: trackData},
|
||||||
|
} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
|
||||||
|
@ -47,7 +51,7 @@ const SingleKeyframeEditor: React.VFC<ISingleKeyframeEditorProps> = (props) => {
|
||||||
}px))`,
|
}px))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SingleKeyframeDot {...props} />
|
<SingleKeyframeDot {...props} itemKey={props.itemKey} />
|
||||||
{connected ? <SingleKeyframeConnector {...props} /> : noConnector}
|
{connected ? <SingleKeyframeConnector {...props} /> : noConnector}
|
||||||
</SingleKeyframeEditorContainer>
|
</SingleKeyframeEditorContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,11 @@ import type {
|
||||||
SequenceEditorTree_PropWithChildren,
|
SequenceEditorTree_PropWithChildren,
|
||||||
SequenceEditorTree_SheetObject,
|
SequenceEditorTree_SheetObject,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {
|
||||||
|
SequenceTrackId,
|
||||||
|
StudioSheetItemKey,
|
||||||
|
} from '@theatre/shared/utils/ids'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import type {
|
import type {
|
||||||
Keyframe,
|
Keyframe,
|
||||||
TrackData,
|
TrackData,
|
||||||
|
@ -31,6 +35,7 @@ export type TrackWithId = {
|
||||||
export type KeyframeWithTrack = {
|
export type KeyframeWithTrack = {
|
||||||
kf: Keyframe
|
kf: Keyframe
|
||||||
track: TrackWithId
|
track: TrackWithId
|
||||||
|
itemKey: StudioSheetItemKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,7 +125,15 @@ export function collectAggregateKeyframesInPrism(
|
||||||
existing = []
|
existing = []
|
||||||
byPosition.set(kf.position, existing)
|
byPosition.set(kf.position, existing)
|
||||||
}
|
}
|
||||||
existing.push({kf, track})
|
existing.push({
|
||||||
|
kf,
|
||||||
|
track,
|
||||||
|
itemKey: createStudioSheetItemKey.forTrackKeyframe(
|
||||||
|
sheetObject,
|
||||||
|
track.id,
|
||||||
|
kf.id,
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,22 @@
|
||||||
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
import type {
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
SheetAddress,
|
||||||
|
WithoutSheetInstance,
|
||||||
|
} from '@theatre/shared/utils/addresses'
|
||||||
|
|
||||||
export function setCollapsedSheetObjectOrCompoundProp(
|
export function setCollapsedSheetObjectOrCompoundProp(
|
||||||
isCollapsed: boolean,
|
isCollapsed: boolean,
|
||||||
toCollapse:
|
toCollapse: {
|
||||||
| {
|
sheetAddress: WithoutSheetInstance<SheetAddress>
|
||||||
sheetObject: SheetObject
|
sheetItemKey: StudioSheetItemKey
|
||||||
}
|
},
|
||||||
| {
|
|
||||||
sheetObject: SheetObject
|
|
||||||
pathToProp: PathToProp
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const itemKey: StudioSheetItemKey =
|
|
||||||
'pathToProp' in toCollapse
|
|
||||||
? createStudioSheetItemKey.forSheetObjectProp(
|
|
||||||
toCollapse.sheetObject,
|
|
||||||
toCollapse.pathToProp,
|
|
||||||
)
|
|
||||||
: createStudioSheetItemKey.forSheetObject(toCollapse.sheetObject)
|
|
||||||
|
|
||||||
getStudio().transaction(({stateEditors}) => {
|
getStudio().transaction(({stateEditors}) => {
|
||||||
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.sequenceEditorCollapsableItems.set(
|
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.sequenceEditorCollapsableItems.set(
|
||||||
{
|
{
|
||||||
...toCollapse.sheetObject.address,
|
...toCollapse.sheetAddress,
|
||||||
studioSheetItemKey: itemKey,
|
studioSheetItemKey: toCollapse.sheetItemKey,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import React, {useMemo, useRef, useState} from 'react'
|
import React, {useMemo, useRef, useState} from 'react'
|
||||||
|
@ -96,6 +97,11 @@ const BasicKeyframedTrack: React.VFC<{
|
||||||
<KeyframeEditor
|
<KeyframeEditor
|
||||||
pathToProp={pathToProp}
|
pathToProp={pathToProp}
|
||||||
propConfig={propConfig}
|
propConfig={propConfig}
|
||||||
|
itemKey={createStudioSheetItemKey.forTrackKeyframe(
|
||||||
|
sheetObject,
|
||||||
|
trackId,
|
||||||
|
kf.id,
|
||||||
|
)}
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
trackData={trackData}
|
trackData={trackData}
|
||||||
|
|
|
@ -17,6 +17,9 @@ import {
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||||
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
|
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
|
||||||
|
import usePresence, {
|
||||||
|
PresenceFlag,
|
||||||
|
} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -58,11 +61,13 @@ type IProps = Parameters<typeof KeyframeEditor>[0] & {which: 'left' | 'right'}
|
||||||
const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
const [ref, node] = useRefAndState<SVGCircleElement | null>(null)
|
const [ref, node] = useRefAndState<SVGCircleElement | null>(null)
|
||||||
|
|
||||||
const {index, trackData} = props
|
const {index, trackData, itemKey} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||||
|
|
||||||
|
const presence = usePresence(itemKey)
|
||||||
|
|
||||||
const curValue = props.which === 'left' ? 0 : 1
|
const curValue = props.which === 'left' ? 0 : 1
|
||||||
|
|
||||||
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] =
|
const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] =
|
||||||
|
@ -93,6 +98,7 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
}}
|
}}
|
||||||
|
{...presence.attrs}
|
||||||
{...includeLockFrameStampAttrs(cur.position)}
|
{...includeLockFrameStampAttrs(cur.position)}
|
||||||
{...DopeSnap.includePositionSnapAttrs(cur.position)}
|
{...DopeSnap.includePositionSnapAttrs(cur.position)}
|
||||||
className={isDragging ? 'beingDragged' : ''}
|
className={isDragging ? 'beingDragged' : ''}
|
||||||
|
@ -102,6 +108,7 @@ const GraphEditorDotNonScalar: React.VFC<IProps> = (props) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
|
fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
|
|
|
@ -17,6 +17,9 @@ import {
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||||
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
|
import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover'
|
||||||
|
import usePresence, {
|
||||||
|
PresenceFlag,
|
||||||
|
} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
export const dotSize = 6
|
export const dotSize = 6
|
||||||
|
|
||||||
|
@ -60,9 +63,9 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
|
|
||||||
const {index, trackData} = props
|
const {index, trackData} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||||
|
const presence = usePresence(props.itemKey)
|
||||||
|
|
||||||
const curValue = cur.value as number
|
const curValue = cur.value as number
|
||||||
|
|
||||||
|
@ -95,6 +98,7 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
}}
|
}}
|
||||||
{...includeLockFrameStampAttrs(cur.position)}
|
{...includeLockFrameStampAttrs(cur.position)}
|
||||||
{...DopeSnap.includePositionSnapAttrs(cur.position)}
|
{...DopeSnap.includePositionSnapAttrs(cur.position)}
|
||||||
|
{...presence.attrs}
|
||||||
className={isDragging ? 'beingDragged' : ''}
|
className={isDragging ? 'beingDragged' : ''}
|
||||||
/>
|
/>
|
||||||
<Circle
|
<Circle
|
||||||
|
@ -102,6 +106,7 @@ const GraphEditorDotScalar: React.VFC<IProps> = (props) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`,
|
||||||
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`,
|
||||||
|
fill: presence.flag === PresenceFlag.Primary ? 'white' : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
|
|
|
@ -4,7 +4,10 @@ import type {
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {
|
||||||
|
SequenceTrackId,
|
||||||
|
StudioSheetItemKey,
|
||||||
|
} from '@theatre/shared/utils/ids'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
@ -28,6 +31,7 @@ type IKeyframeEditorProps = {
|
||||||
index: number
|
index: number
|
||||||
keyframe: Keyframe
|
keyframe: Keyframe
|
||||||
trackData: TrackData
|
trackData: TrackData
|
||||||
|
itemKey: StudioSheetItemKey
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
trackId: SequenceTrackId
|
trackId: SequenceTrackId
|
||||||
sheetObject: SheetObject
|
sheetObject: SheetObject
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {usePrism} from '@theatre/react'
|
||||||
import {valToAtom} from '@theatre/shared/utils/valToAtom'
|
import {valToAtom} from '@theatre/shared/utils/valToAtom'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {prism, val} from '@theatre/dataverse'
|
import {prism, val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React, {useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import DopeSheet from './DopeSheet/DopeSheet'
|
import DopeSheet from './DopeSheet/DopeSheet'
|
||||||
|
@ -28,6 +28,7 @@ import {
|
||||||
TitleBar_Punctuation,
|
TitleBar_Punctuation,
|
||||||
} from '@theatre/studio/panels/BasePanel/common'
|
} from '@theatre/studio/panels/BasePanel/common'
|
||||||
import type {UIPanelId} from '@theatre/shared/utils/ids'
|
import type {UIPanelId} from '@theatre/shared/utils/ids'
|
||||||
|
import {usePresenceListenersOnRootElement} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
const Container = styled(PanelWrapper)`
|
const Container = styled(PanelWrapper)`
|
||||||
z-index: ${panelZIndexes.sequenceEditorPanel};
|
z-index: ${panelZIndexes.sequenceEditorPanel};
|
||||||
|
@ -99,7 +100,10 @@ const SequenceEditorPanel: React.VFC<{}> = (props) => {
|
||||||
|
|
||||||
const Content: React.VFC<{}> = () => {
|
const Content: React.VFC<{}> = () => {
|
||||||
const {dims} = usePanel()
|
const {dims} = usePanel()
|
||||||
|
const [containerNode, setContainerNode] = useState<null | HTMLDivElement>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
usePresenceListenersOnRootElement(containerNode)
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const panelSize = prism.memo(
|
const panelSize = prism.memo(
|
||||||
'panelSize',
|
'panelSize',
|
||||||
|
@ -161,7 +165,14 @@ const Content: React.VFC<{}> = () => {
|
||||||
const graphEditorOpen = val(layoutP.graphEditorDims.isOpen)
|
const graphEditorOpen = val(layoutP.graphEditorDims.isOpen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container ref={containerRef}>
|
<Container
|
||||||
|
ref={(elt) => {
|
||||||
|
containerRef(elt as HTMLDivElement)
|
||||||
|
if (elt !== containerNode) {
|
||||||
|
setContainerNode(elt as HTMLDivElement)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<LeftBackground style={{width: `${val(layoutP.leftDims.width)}px`}} />
|
<LeftBackground style={{width: `${val(layoutP.leftDims.width)}px`}} />
|
||||||
<FrameStampPositionProvider layoutP={layoutP}>
|
<FrameStampPositionProvider layoutP={layoutP}>
|
||||||
<Header layoutP={layoutP} />
|
<Header layoutP={layoutP} />
|
||||||
|
@ -174,7 +185,7 @@ const Content: React.VFC<{}> = () => {
|
||||||
</FrameStampPositionProvider>
|
</FrameStampPositionProvider>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}, [dims])
|
}, [dims, containerNode])
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
const Header: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
|
|
|
@ -8,7 +8,10 @@ import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
||||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||||
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
import type {PathToProp} from '@theatre/shared/utils/addresses'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {
|
||||||
|
SequenceTrackId,
|
||||||
|
StudioSheetItemKey,
|
||||||
|
} from '@theatre/shared/utils/ids'
|
||||||
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
import {prism, val, valueDerivation} from '@theatre/dataverse'
|
import {prism, val, valueDerivation} from '@theatre/dataverse'
|
||||||
|
@ -30,6 +33,8 @@ export type SequenceEditorTree_Row<TypeName extends string> = {
|
||||||
|
|
||||||
/** Visual indentation */
|
/** Visual indentation */
|
||||||
depth: number
|
depth: number
|
||||||
|
/** A convenient studio sheet localized identifier for managing presence and ephemeral visual effects. */
|
||||||
|
sheetItemKey: StudioSheetItemKey
|
||||||
/**
|
/**
|
||||||
* This is a part of the tree, but it is not rendered at all,
|
* This is a part of the tree, but it is not rendered at all,
|
||||||
* and it doesn't contribute to height.
|
* and it doesn't contribute to height.
|
||||||
|
@ -107,6 +112,7 @@ export const calculateSequenceEditorTree = (
|
||||||
type: 'sheet',
|
type: 'sheet',
|
||||||
sheet,
|
sheet,
|
||||||
children: [],
|
children: [],
|
||||||
|
sheetItemKey: createStudioSheetItemKey.forSheet(),
|
||||||
shouldRender: rootShouldRender,
|
shouldRender: rootShouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
depth: -1,
|
depth: -1,
|
||||||
|
@ -151,6 +157,7 @@ export const calculateSequenceEditorTree = (
|
||||||
const row: SequenceEditorTree_SheetObject = {
|
const row: SequenceEditorTree_SheetObject = {
|
||||||
type: 'sheetObject',
|
type: 'sheetObject',
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
|
sheetItemKey: createStudioSheetItemKey.forSheetObject(sheetObject),
|
||||||
shouldRender,
|
shouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
children: [],
|
children: [],
|
||||||
|
@ -270,6 +277,10 @@ export const calculateSequenceEditorTree = (
|
||||||
type: 'propWithChildren',
|
type: 'propWithChildren',
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
pathToProp,
|
pathToProp,
|
||||||
|
sheetItemKey: createStudioSheetItemKey.forSheetObjectProp(
|
||||||
|
sheetObject,
|
||||||
|
pathToProp,
|
||||||
|
),
|
||||||
sheetObject: sheetObject,
|
sheetObject: sheetObject,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
|
@ -316,6 +327,10 @@ export const calculateSequenceEditorTree = (
|
||||||
type: 'primitiveProp',
|
type: 'primitiveProp',
|
||||||
propConf: propConf,
|
propConf: propConf,
|
||||||
depth: level,
|
depth: level,
|
||||||
|
sheetItemKey: createStudioSheetItemKey.forSheetObjectProp(
|
||||||
|
sheetObject,
|
||||||
|
pathToProp,
|
||||||
|
),
|
||||||
sheetObject: sheetObject,
|
sheetObject: sheetObject,
|
||||||
pathToProp,
|
pathToProp,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
|
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import type {VoidFn} from '@theatre/shared/utils/types'
|
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import {transparentize} from 'polished'
|
import {transparentize} from 'polished'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled, {css} from 'styled-components'
|
import styled, {css} from 'styled-components'
|
||||||
|
import {PresenceFlag} from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
import usePresence from '@theatre/studio/uiComponents/usePresence'
|
||||||
|
|
||||||
export type NearbyKeyframesControls = {
|
export type NearbyKeyframesControls = {
|
||||||
prev?: Pick<Keyframe, 'position'> & {jump: VoidFn}
|
prev?: Pick<Keyframe, 'position'> & {
|
||||||
cur: {type: 'on'; toggle: VoidFn} | {type: 'off'; toggle: VoidFn}
|
jump: VoidFn
|
||||||
next?: Pick<Keyframe, 'position'> & {jump: VoidFn}
|
itemKey: StudioSheetItemKey
|
||||||
|
}
|
||||||
|
cur:
|
||||||
|
| {type: 'on'; toggle: VoidFn; itemKey: StudioSheetItemKey}
|
||||||
|
| {type: 'off'; toggle: VoidFn}
|
||||||
|
next?: Pick<Keyframe, 'position'> & {
|
||||||
|
jump: VoidFn
|
||||||
|
itemKey: StudioSheetItemKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -70,21 +81,34 @@ export const nextPrevCursorsTheme = {
|
||||||
onColor: '#e0c917',
|
onColor: '#e0c917',
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurButton = styled(Button)<{isOn: boolean}>`
|
const CurButton = styled(Button)<{
|
||||||
|
isOn: boolean
|
||||||
|
presence: PresenceFlag | undefined
|
||||||
|
}>`
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #e0c917;
|
color: #e0c917;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: ${(props) =>
|
color: ${(props) =>
|
||||||
props.isOn ? nextPrevCursorsTheme.onColor : nextPrevCursorsTheme.offColor};
|
props.presence === PresenceFlag.Primary
|
||||||
|
? 'white'
|
||||||
|
: props.isOn
|
||||||
|
? nextPrevCursorsTheme.onColor
|
||||||
|
: nextPrevCursorsTheme.offColor};
|
||||||
`
|
`
|
||||||
|
|
||||||
const pointerEventsNone = css`
|
const pointerEventsNone = css`
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
`
|
`
|
||||||
|
|
||||||
const PrevOrNextButton = styled(Button)<{available: boolean}>`
|
const PrevOrNextButton = styled(Button)<{
|
||||||
|
available: boolean
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
|
}>`
|
||||||
color: ${(props) =>
|
color: ${(props) =>
|
||||||
props.available
|
props.flag === PresenceFlag.Primary
|
||||||
|
? 'white'
|
||||||
|
: props.available
|
||||||
? nextPrevCursorsTheme.onColor
|
? nextPrevCursorsTheme.onColor
|
||||||
: nextPrevCursorsTheme.offColor};
|
: nextPrevCursorsTheme.offColor};
|
||||||
|
|
||||||
|
@ -92,15 +116,20 @@ const PrevOrNextButton = styled(Button)<{available: boolean}>`
|
||||||
props.available ? pointerEventsAutoInNormalMode : pointerEventsNone};
|
props.available ? pointerEventsAutoInNormalMode : pointerEventsNone};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Prev = styled(PrevOrNextButton)<{available: boolean}>`
|
const Prev = styled(PrevOrNextButton)<{
|
||||||
|
available: boolean
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
|
}>`
|
||||||
transform: translateX(2px);
|
transform: translateX(2px);
|
||||||
${Container}:hover & {
|
${Container}:hover & {
|
||||||
transform: translateX(-7px);
|
transform: translateX(-7px);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const Next = styled(PrevOrNextButton)<{available: boolean}>`
|
const Next = styled(PrevOrNextButton)<{
|
||||||
|
available: boolean
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
|
}>`
|
||||||
transform: translateX(-2px);
|
transform: translateX(-2px);
|
||||||
|
|
||||||
${Container}:hover & {
|
${Container}:hover & {
|
||||||
transform: translateX(7px);
|
transform: translateX(7px);
|
||||||
}
|
}
|
||||||
|
@ -165,16 +194,37 @@ namespace Icons {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NextPrevKeyframeCursors: React.FC<NearbyKeyframesControls> = (props) => {
|
const NextPrevKeyframeCursors: React.VFC<NearbyKeyframesControls> = (props) => {
|
||||||
|
const prevPresence = usePresence(props.prev?.itemKey)
|
||||||
|
const curPresence = usePresence(
|
||||||
|
props.cur?.type === 'on' ? props.cur.itemKey : undefined,
|
||||||
|
)
|
||||||
|
const nextPresence = usePresence(props.next?.itemKey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Prev available={!!props.prev} onClick={props.prev?.jump}>
|
<Prev
|
||||||
|
available={!!props.prev}
|
||||||
|
onClick={props.prev?.jump}
|
||||||
|
flag={prevPresence.flag}
|
||||||
|
{...prevPresence.attrs}
|
||||||
|
>
|
||||||
<Icons.Prev />
|
<Icons.Prev />
|
||||||
</Prev>
|
</Prev>
|
||||||
<CurButton isOn={props.cur.type === 'on'} onClick={props.cur.toggle}>
|
<CurButton
|
||||||
|
isOn={props.cur.type === 'on'}
|
||||||
|
onClick={props.cur.toggle}
|
||||||
|
presence={curPresence.flag}
|
||||||
|
{...curPresence.attrs}
|
||||||
|
>
|
||||||
<Icons.Cur />
|
<Icons.Cur />
|
||||||
</CurButton>
|
</CurButton>
|
||||||
<Next available={!!props.next} onClick={props.next?.jump}>
|
<Next
|
||||||
|
available={!!props.next}
|
||||||
|
onClick={props.next?.jump}
|
||||||
|
flag={nextPresence.flag}
|
||||||
|
{...nextPresence.attrs}
|
||||||
|
>
|
||||||
<Icons.Next />
|
<Icons.Next />
|
||||||
</Next>
|
</Next>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -2,7 +2,12 @@ import type {
|
||||||
TrackData,
|
TrackData,
|
||||||
Keyframe,
|
Keyframe,
|
||||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import last from 'lodash-es/last'
|
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
|
import type {
|
||||||
|
KeyframeWithTrack,
|
||||||
|
TrackWithId,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
|
|
||||||
const cache = new WeakMap<
|
const cache = new WeakMap<
|
||||||
TrackData,
|
TrackData,
|
||||||
|
@ -12,48 +17,67 @@ const cache = new WeakMap<
|
||||||
const noKeyframes: NearbyKeyframes = {}
|
const noKeyframes: NearbyKeyframes = {}
|
||||||
|
|
||||||
export function getNearbyKeyframesOfTrack(
|
export function getNearbyKeyframesOfTrack(
|
||||||
track: TrackData | undefined,
|
obj: SheetObject,
|
||||||
|
track: TrackWithId | undefined,
|
||||||
sequencePosition: number,
|
sequencePosition: number,
|
||||||
): NearbyKeyframes {
|
): NearbyKeyframes {
|
||||||
if (!track || track.keyframes.length === 0) return noKeyframes
|
if (!track || track.data.keyframes.length === 0) return noKeyframes
|
||||||
|
|
||||||
const cachedItem = cache.get(track)
|
const cachedItem = cache.get(track.data)
|
||||||
if (cachedItem && cachedItem[0] === sequencePosition) {
|
if (cachedItem && cachedItem[0] === sequencePosition) {
|
||||||
return cachedItem[1]
|
return cachedItem[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculate = (): NearbyKeyframes => {
|
function getKeyframeWithTrackId(idx: number): KeyframeWithTrack | undefined {
|
||||||
const i = track.keyframes.findIndex((kf) => kf.position >= sequencePosition)
|
if (!track) return
|
||||||
|
const found = track.data.keyframes[idx]
|
||||||
if (i === -1)
|
return (
|
||||||
return {
|
found && {
|
||||||
prev: last(track.keyframes),
|
kf: found,
|
||||||
|
track,
|
||||||
|
itemKey: createStudioSheetItemKey.forTrackKeyframe(
|
||||||
|
obj,
|
||||||
|
track.id,
|
||||||
|
found.id,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const k = track.keyframes[i]!
|
const calculate = (): NearbyKeyframes => {
|
||||||
if (k.position === sequencePosition) {
|
const nextOrCurIdx = track.data.keyframes.findIndex(
|
||||||
|
(kf) => kf.position >= sequencePosition,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (nextOrCurIdx === -1) {
|
||||||
return {
|
return {
|
||||||
prev: i > 0 ? track.keyframes[i - 1] : undefined,
|
prev: getKeyframeWithTrackId(track.data.keyframes.length - 1),
|
||||||
cur: k,
|
}
|
||||||
next:
|
}
|
||||||
i === track.keyframes.length - 1 ? undefined : track.keyframes[i + 1],
|
|
||||||
|
const nextOrCur = getKeyframeWithTrackId(nextOrCurIdx)!
|
||||||
|
if (nextOrCur.kf.position === sequencePosition) {
|
||||||
|
return {
|
||||||
|
prev: getKeyframeWithTrackId(nextOrCurIdx - 1),
|
||||||
|
cur: nextOrCur,
|
||||||
|
next: getKeyframeWithTrackId(nextOrCurIdx + 1),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
next: k,
|
next: nextOrCur,
|
||||||
prev: i > 0 ? track.keyframes[i - 1] : undefined,
|
prev: getKeyframeWithTrackId(nextOrCurIdx - 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = calculate()
|
const result = calculate()
|
||||||
cache.set(track, [sequencePosition, result])
|
cache.set(track.data, [sequencePosition, result])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NearbyKeyframes = {
|
export type NearbyKeyframes = {
|
||||||
prev?: Keyframe
|
prev?: KeyframeWithTrack
|
||||||
cur?: Keyframe
|
cur?: KeyframeWithTrack
|
||||||
next?: Keyframe
|
next?: KeyframeWithTrack
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,13 @@ import {
|
||||||
iteratePropType,
|
iteratePropType,
|
||||||
} from '@theatre/shared/propTypes/utils'
|
} from '@theatre/shared/propTypes/utils'
|
||||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||||
|
import {createStudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
||||||
import pointerDeep from '@theatre/shared/utils/pointerDeep'
|
import pointerDeep from '@theatre/shared/utils/pointerDeep'
|
||||||
import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors'
|
import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors'
|
||||||
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
|
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
|
||||||
import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack'
|
import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack'
|
||||||
|
import type {KeyframeWithTrack} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||||
|
|
||||||
interface CommonStuff {
|
interface CommonStuff {
|
||||||
beingScrubbed: boolean
|
beingScrubbed: boolean
|
||||||
|
@ -212,7 +214,11 @@ export function useEditingToolsForCompoundProp<T extends SerializablePrimitive>(
|
||||||
.filter(({track}) => !!track)
|
.filter(({track}) => !!track)
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
nearbies: getNearbyKeyframesOfTrack(s.track, sequencePosition),
|
nearbies: getNearbyKeyframesOfTrack(
|
||||||
|
obj,
|
||||||
|
{id: s.trackId, data: s.track!},
|
||||||
|
sequencePosition,
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const hasCur = nearbyKeyframesInEachTrack.find(
|
const hasCur = nearbyKeyframesInEachTrack.find(
|
||||||
|
@ -223,13 +229,16 @@ export function useEditingToolsForCompoundProp<T extends SerializablePrimitive>(
|
||||||
)
|
)
|
||||||
|
|
||||||
const closestPrev = nearbyKeyframesInEachTrack.reduce<
|
const closestPrev = nearbyKeyframesInEachTrack.reduce<
|
||||||
undefined | number
|
undefined | KeyframeWithTrack
|
||||||
>((acc, s) => {
|
>((acc, s) => {
|
||||||
if (s.nearbies.prev) {
|
if (s.nearbies.prev) {
|
||||||
if (acc === undefined) {
|
if (
|
||||||
return s.nearbies.prev.position
|
acc === undefined ||
|
||||||
|
s.nearbies.prev.kf.position > acc.kf.position
|
||||||
|
) {
|
||||||
|
return s.nearbies.prev
|
||||||
} else {
|
} else {
|
||||||
return Math.max(s.nearbies.prev.position, acc)
|
return acc
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return acc
|
return acc
|
||||||
|
@ -237,53 +246,80 @@ export function useEditingToolsForCompoundProp<T extends SerializablePrimitive>(
|
||||||
}, undefined)
|
}, undefined)
|
||||||
|
|
||||||
const closestNext = nearbyKeyframesInEachTrack.reduce<
|
const closestNext = nearbyKeyframesInEachTrack.reduce<
|
||||||
undefined | number
|
undefined | KeyframeWithTrack
|
||||||
>((acc, s) => {
|
>((acc, s) => {
|
||||||
if (s.nearbies.next) {
|
if (s.nearbies.next) {
|
||||||
if (acc === undefined) {
|
if (
|
||||||
return s.nearbies.next.position
|
acc === undefined ||
|
||||||
|
s.nearbies.next.kf.position < acc.kf.position
|
||||||
|
) {
|
||||||
|
return s.nearbies.next
|
||||||
} else {
|
} else {
|
||||||
return Math.min(s.nearbies.next.position, acc)
|
return acc
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
}, undefined)
|
}, undefined)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (allCur) {
|
||||||
|
getStudio().transaction((api) => {
|
||||||
|
api.unset(pointerToProp)
|
||||||
|
})
|
||||||
|
} else if (hasCur) {
|
||||||
|
getStudio().transaction((api) => {
|
||||||
|
api.set(pointerToProp, val(pointerToProp))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
getStudio().transaction((api) => {
|
||||||
|
api.set(pointerToProp, val(pointerToProp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
cur: {
|
cur: hasCur
|
||||||
type: hasCur ? 'on' : 'off',
|
? {
|
||||||
toggle: () => {
|
type: 'on',
|
||||||
if (allCur) {
|
itemKey:
|
||||||
getStudio().transaction((api) => {
|
createStudioSheetItemKey.forCompoundPropAggregateKeyframe(
|
||||||
api.unset(pointerToProp)
|
obj,
|
||||||
})
|
pathToProp,
|
||||||
} else if (hasCur) {
|
sequencePosition,
|
||||||
getStudio().transaction((api) => {
|
),
|
||||||
api.set(pointerToProp, val(pointerToProp))
|
toggle,
|
||||||
})
|
|
||||||
} else {
|
|
||||||
getStudio().transaction((api) => {
|
|
||||||
api.set(pointerToProp, val(pointerToProp))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
: {
|
||||||
},
|
toggle,
|
||||||
|
type: 'off',
|
||||||
|
},
|
||||||
prev:
|
prev:
|
||||||
closestPrev !== undefined
|
closestPrev !== undefined
|
||||||
? {
|
? {
|
||||||
position: closestPrev,
|
position: closestPrev.kf.position,
|
||||||
|
itemKey:
|
||||||
|
createStudioSheetItemKey.forCompoundPropAggregateKeyframe(
|
||||||
|
obj,
|
||||||
|
pathToProp,
|
||||||
|
closestPrev.kf.position,
|
||||||
|
),
|
||||||
jump: () => {
|
jump: () => {
|
||||||
obj.sheet.getSequence().position = closestPrev
|
obj.sheet.getSequence().position = closestPrev.kf.position
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
next:
|
next:
|
||||||
closestNext !== undefined
|
closestNext !== undefined
|
||||||
? {
|
? {
|
||||||
position: closestNext,
|
position: closestNext.kf.position,
|
||||||
|
itemKey:
|
||||||
|
createStudioSheetItemKey.forCompoundPropAggregateKeyframe(
|
||||||
|
obj,
|
||||||
|
pathToProp,
|
||||||
|
closestNext.kf.position,
|
||||||
|
),
|
||||||
jump: () => {
|
jump: () => {
|
||||||
obj.sheet.getSequence().position = closestNext
|
obj.sheet.getSequence().position = closestNext.kf.position
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
@ -172,7 +172,14 @@ export function useEditingToolsForSimplePropInDetailsPanel<
|
||||||
const sequencePosition = val(
|
const sequencePosition = val(
|
||||||
obj.sheet.getSequence().positionDerivation,
|
obj.sheet.getSequence().positionDerivation,
|
||||||
)
|
)
|
||||||
return getNearbyKeyframesOfTrack(track, sequencePosition)
|
return getNearbyKeyframesOfTrack(
|
||||||
|
obj,
|
||||||
|
track && {
|
||||||
|
data: track,
|
||||||
|
id: sequenceTrackId,
|
||||||
|
},
|
||||||
|
sequencePosition,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[sequenceTrackId],
|
[sequenceTrackId],
|
||||||
)
|
)
|
||||||
|
@ -184,45 +191,54 @@ export function useEditingToolsForSimplePropInDetailsPanel<
|
||||||
} else {
|
} else {
|
||||||
if (nearbyKeyframes.cur) {
|
if (nearbyKeyframes.cur) {
|
||||||
shade = 'Sequenced_OnKeyframe'
|
shade = 'Sequenced_OnKeyframe'
|
||||||
} else if (nearbyKeyframes.prev?.connectedRight === true) {
|
} else if (nearbyKeyframes.prev?.kf.connectedRight === true) {
|
||||||
shade = 'Sequenced_BeingInterpolated'
|
shade = 'Sequenced_BeingInterpolated'
|
||||||
} else {
|
} else {
|
||||||
shade = 'Sequened_NotBeingInterpolated'
|
shade = 'Sequened_NotBeingInterpolated'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (nearbyKeyframes.cur) {
|
||||||
|
getStudio()!.transaction((api) => {
|
||||||
|
api.unset(pointerToProp)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
getStudio()!.transaction((api) => {
|
||||||
|
api.set(pointerToProp, common.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
const controls: NearbyKeyframesControls = {
|
const controls: NearbyKeyframesControls = {
|
||||||
cur: {
|
cur: nearbyKeyframes.cur
|
||||||
type: nearbyKeyframes.cur ? 'on' : 'off',
|
? {
|
||||||
toggle: () => {
|
type: 'on',
|
||||||
if (nearbyKeyframes.cur) {
|
itemKey: nearbyKeyframes.cur.itemKey,
|
||||||
getStudio()!.transaction((api) => {
|
toggle,
|
||||||
api.unset(pointerToProp)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
getStudio()!.transaction((api) => {
|
|
||||||
api.set(pointerToProp, common.value)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
: {
|
||||||
},
|
type: 'off',
|
||||||
|
toggle,
|
||||||
|
},
|
||||||
prev:
|
prev:
|
||||||
nearbyKeyframes.prev !== undefined
|
nearbyKeyframes.prev !== undefined
|
||||||
? {
|
? {
|
||||||
position: nearbyKeyframes.prev.position,
|
itemKey: nearbyKeyframes.prev.itemKey,
|
||||||
|
position: nearbyKeyframes.prev.kf.position,
|
||||||
jump: () => {
|
jump: () => {
|
||||||
obj.sheet.getSequence().position =
|
obj.sheet.getSequence().position =
|
||||||
nearbyKeyframes.prev!.position
|
nearbyKeyframes.prev!.kf.position
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
next:
|
next:
|
||||||
nearbyKeyframes.next !== undefined
|
nearbyKeyframes.next !== undefined
|
||||||
? {
|
? {
|
||||||
position: nearbyKeyframes.next.position,
|
itemKey: nearbyKeyframes.next.itemKey,
|
||||||
|
position: nearbyKeyframes.next.kf.position,
|
||||||
jump: () => {
|
jump: () => {
|
||||||
obj.sheet.getSequence().position =
|
obj.sheet.getSequence().position =
|
||||||
nearbyKeyframes.next!.position
|
nearbyKeyframes.next!.kf.position
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
211
theatre/studio/src/uiComponents/usePresence.tsx
Normal file
211
theatre/studio/src/uiComponents/usePresence.tsx
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||||
|
import type {StrictRecord} from '@theatre/shared/utils/types'
|
||||||
|
import React, {useMemo} from 'react'
|
||||||
|
import {useEffect} from 'react'
|
||||||
|
import {useLogger} from './useLogger'
|
||||||
|
import {Box, prism, valueDerivation} from '@theatre/dataverse'
|
||||||
|
import {Atom} from '@theatre/dataverse'
|
||||||
|
import {useDerivation} from '@theatre/react'
|
||||||
|
import {selectClosestHTMLAncestor} from '@theatre/studio/utils/selectClosestHTMLAncestor'
|
||||||
|
|
||||||
|
/** To mean the presence value */
|
||||||
|
export enum PresenceFlag {
|
||||||
|
/** Self is hovered or what represents "self" is being hovered */
|
||||||
|
Primary = 2,
|
||||||
|
/** Related item is hovered */
|
||||||
|
Secondary = 1,
|
||||||
|
// /** Tutorial */
|
||||||
|
// TutorialEmphasis = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const undefinedD = prism(() => undefined)
|
||||||
|
undefinedD.keepHot() // constant anyway...
|
||||||
|
|
||||||
|
function createPresenceContext(): InternalPresenceContext {
|
||||||
|
const currentUserHoverItemB = new Box<StudioSheetItemKey | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
const currentUserHoverFlagItemsAtom = new Atom(
|
||||||
|
{} as StrictRecord<StudioSheetItemKey, boolean>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// keep as part of presence creation
|
||||||
|
const relationsAtom = new Atom(
|
||||||
|
{} as StrictRecord<
|
||||||
|
StudioSheetItemKey,
|
||||||
|
StrictRecord<
|
||||||
|
StudioSheetItemKey,
|
||||||
|
StrictRecord<string, {flag: PresenceFlag}>
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
)
|
||||||
|
|
||||||
|
let lastRelationId = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
addRelatedFlags(itemKey, relationships) {
|
||||||
|
const relationId = String(++lastRelationId)
|
||||||
|
// "clean up" paths returned from relationships declared
|
||||||
|
const undoAtPaths = relationships.map((rel) => {
|
||||||
|
const presence: {flag: PresenceFlag} = {
|
||||||
|
flag: rel.flag,
|
||||||
|
}
|
||||||
|
const path = [rel.affects, itemKey, relationId]
|
||||||
|
relationsAtom.setIn(path, presence)
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
for (const pathToUndo of undoAtPaths) {
|
||||||
|
relationsAtom.setIn(pathToUndo, undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
usePresenceFlag(itemKey) {
|
||||||
|
const focusD = useMemo(() => {
|
||||||
|
if (!itemKey) return undefinedD
|
||||||
|
// this is the thing being hovered
|
||||||
|
const currentD = currentUserHoverItemB.derivation
|
||||||
|
const primaryFocusDer = valueDerivation(
|
||||||
|
currentUserHoverFlagItemsAtom.pointer[itemKey],
|
||||||
|
)
|
||||||
|
const relationsDer = valueDerivation(relationsAtom.pointer[itemKey])
|
||||||
|
return prism(() => {
|
||||||
|
const primary = primaryFocusDer.getValue()
|
||||||
|
if (primary) {
|
||||||
|
return PresenceFlag.Primary
|
||||||
|
} else {
|
||||||
|
const related = relationsDer.getValue()
|
||||||
|
const current = currentD.getValue()
|
||||||
|
const rels = related && current && related[current]
|
||||||
|
if (rels) {
|
||||||
|
// can this be cached into a derived atom?
|
||||||
|
let best: PresenceFlag | undefined
|
||||||
|
for (const rel of Object.values(rels)) {
|
||||||
|
if (!rel) continue
|
||||||
|
if (best && best >= rel.flag) continue
|
||||||
|
best = rel.flag
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [itemKey])
|
||||||
|
return useDerivation(focusD)
|
||||||
|
},
|
||||||
|
setUserHover(itemKeyOpt) {
|
||||||
|
const prev = currentUserHoverItemB.get()
|
||||||
|
if (prev === itemKeyOpt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (prev) {
|
||||||
|
currentUserHoverFlagItemsAtom.setIn([prev], false)
|
||||||
|
}
|
||||||
|
currentUserHoverItemB.set(itemKeyOpt)
|
||||||
|
if (itemKeyOpt) {
|
||||||
|
currentUserHoverFlagItemsAtom.setIn([itemKeyOpt], true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlagRelationConfig = {
|
||||||
|
affects: StudioSheetItemKey
|
||||||
|
/** adds this flag to affects */
|
||||||
|
flag: PresenceFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalPresenceContext = {
|
||||||
|
usePresenceFlag(
|
||||||
|
itemKey: StudioSheetItemKey | undefined,
|
||||||
|
): PresenceFlag | undefined
|
||||||
|
setUserHover(itemKey: StudioSheetItemKey | undefined): void
|
||||||
|
addRelatedFlags(
|
||||||
|
itemKey: StudioSheetItemKey,
|
||||||
|
config: Array<FlagRelationConfig>,
|
||||||
|
): () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const presenceInternalCtx = React.createContext<InternalPresenceContext>(
|
||||||
|
createPresenceContext(),
|
||||||
|
)
|
||||||
|
export function ProvidePresenceRoot({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const presence = useMemo(() => createPresenceContext(), [])
|
||||||
|
return React.createElement(
|
||||||
|
presenceInternalCtx.Provider,
|
||||||
|
{children, value: presence},
|
||||||
|
children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESENCE_ITEM_DATA_ATTR = 'data-pi-key'
|
||||||
|
|
||||||
|
export default function usePresence(key: StudioSheetItemKey | undefined): {
|
||||||
|
attrs: {[attr: `data-${string}`]: string}
|
||||||
|
flag: PresenceFlag | undefined
|
||||||
|
useRelations(getRelations: () => Array<FlagRelationConfig>, deps: any[]): void
|
||||||
|
} {
|
||||||
|
const presenceInternal = React.useContext(presenceInternalCtx)
|
||||||
|
const flag = presenceInternal.usePresenceFlag(key)
|
||||||
|
|
||||||
|
return {
|
||||||
|
attrs: {
|
||||||
|
[PRESENCE_ITEM_DATA_ATTR]: key as string,
|
||||||
|
},
|
||||||
|
flag,
|
||||||
|
useRelations(getRelations, deps) {
|
||||||
|
useEffect(() => {
|
||||||
|
return key && presenceInternal.addRelatedFlags(key, getRelations())
|
||||||
|
}, [key, ...deps])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePresenceListenersOnRootElement(
|
||||||
|
target: HTMLElement | null | undefined,
|
||||||
|
) {
|
||||||
|
const presence = React.useContext(presenceInternalCtx)
|
||||||
|
const logger = useLogger('PresenceListeners')
|
||||||
|
useEffect(() => {
|
||||||
|
// keep track of current primary hover to make sure we make changes to presence distinct
|
||||||
|
let currentItemKeyUserHover: any
|
||||||
|
if (!target) return
|
||||||
|
const onMouseOver = (event: MouseEvent) => {
|
||||||
|
if (event.target instanceof Node) {
|
||||||
|
const found = selectClosestHTMLAncestor(
|
||||||
|
event.target,
|
||||||
|
`[${PRESENCE_ITEM_DATA_ATTR}]`,
|
||||||
|
)
|
||||||
|
if (found) {
|
||||||
|
const itemKey = found.getAttribute(PRESENCE_ITEM_DATA_ATTR)
|
||||||
|
if (currentItemKeyUserHover !== itemKey) {
|
||||||
|
currentItemKeyUserHover = itemKey
|
||||||
|
presence.setUserHover(
|
||||||
|
(itemKey || undefined) as StudioSheetItemKey | undefined,
|
||||||
|
)
|
||||||
|
logger._debug('Updated current hover', {itemKey})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove hover
|
||||||
|
if (currentItemKeyUserHover != null) {
|
||||||
|
currentItemKeyUserHover = null
|
||||||
|
presence.setUserHover(undefined)
|
||||||
|
logger._debug('Cleared current hover')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target.addEventListener('mouseover', onMouseOver)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
target.removeEventListener('mouseover', onMouseOver)
|
||||||
|
// remove hover
|
||||||
|
if (currentItemKeyUserHover != null) {
|
||||||
|
currentItemKeyUserHover = null
|
||||||
|
logger._debug('Cleared current hover as part of cleanup')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [target, presence])
|
||||||
|
}
|
14
theatre/studio/src/utils/selectClosestHTMLAncestor.ts
Normal file
14
theatre/studio/src/utils/selectClosestHTMLAncestor.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Traverse upwards from the current element to find the first element that matches the selector.
|
||||||
|
*/
|
||||||
|
export function selectClosestHTMLAncestor(
|
||||||
|
start: Element | Node | null,
|
||||||
|
selector: string,
|
||||||
|
): Element | null {
|
||||||
|
if (start == null) return null
|
||||||
|
if (start instanceof Element && start.matches(selector)) {
|
||||||
|
return start
|
||||||
|
} else {
|
||||||
|
return selectClosestHTMLAncestor(start.parentElement, selector)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue