From d83d2b558cb954ecd004171d0059b7e510eb523e Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 26 May 2022 01:18:45 +0200 Subject: [PATCH] Compound prop context menu (#157) --- theatre/shared/src/propTypes/utils.ts | 68 +++- .../createTransactionPrivateApi.ts | 3 + .../panels/BasePanel/ExtensionPaneWrapper.tsx | 2 - .../DetailCompoundPropEditor.tsx | 21 +- .../DopeSheet/Right/Row.tsx | 11 + .../FocusRangeZone/FocusRangeStrip.tsx | 8 +- .../FocusRangeZone/FocusRangeThumb.tsx | 20 +- .../RightOverlay/Playhead.tsx | 8 +- .../propEditors/NextPrevKeyframeCursors.tsx | 40 +-- .../propEditors/getNearbyKeyframesOfTrack.tsx | 59 ++++ .../useEditingToolsForCompoundProp.tsx | 316 ++++++++++++++++++ .../useEditingToolsForSimpleProp.tsx | 93 ++---- theatre/studio/src/uiComponents/useDrag.ts | 14 +- 13 files changed, 549 insertions(+), 114 deletions(-) create mode 100644 theatre/studio/src/propEditors/getNearbyKeyframesOfTrack.tsx create mode 100644 theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx diff --git a/theatre/shared/src/propTypes/utils.ts b/theatre/shared/src/propTypes/utils.ts index 7ccf8e4..eed0d71 100644 --- a/theatre/shared/src/propTypes/utils.ts +++ b/theatre/shared/src/propTypes/utils.ts @@ -54,5 +54,71 @@ export function valueInProp( export function isPropConfSequencable( conf: PropTypeConfig, ): conf is Extract { - return Object.prototype.hasOwnProperty.call(conf, 'interpolate') + return !isPropConfigComposite(conf) // now all non-compounds are sequencable +} + +const compoundPropSequenceabilityCache = new WeakMap< + PropTypeConfig_Compound<{}> | PropTypeConfig_Enum, + boolean +>() + +/** + * See {@link compoundHasSimpleDescendantsImpl} + */ +export function compoundHasSimpleDescendants( + conf: PropTypeConfig_Compound<{}> | PropTypeConfig_Enum, +): boolean { + if (!compoundPropSequenceabilityCache.has(conf)) { + compoundPropSequenceabilityCache.set( + conf, + compoundHasSimpleDescendantsImpl(conf), + ) + } + + return compoundPropSequenceabilityCache.get(conf)! +} + +/** + * This basically checks of the compound prop has at least one simple prop in its descendants. + * In other words, if the compound props has no subs, or its subs are only compounds that eventually + * don't have simple subs, this will return false. + */ +function compoundHasSimpleDescendantsImpl( + conf: PropTypeConfig_Compound<{}> | PropTypeConfig_Enum, +): boolean { + if (conf.type === 'enum') { + throw new Error(`Not implemented yet for enums`) + } + + for (const key in conf.props) { + const subConf = conf.props[ + key as $IntentionalAny as keyof typeof conf.props + ] as PropTypeConfig + if (isPropConfigComposite(subConf)) { + if (compoundHasSimpleDescendants(subConf)) { + return true + } + } else { + return true + } + } + return false +} + +export function* iteratePropType( + conf: PropTypeConfig, + pathUpToThisPoint: PathToProp, +): Generator<{path: PathToProp; conf: PropTypeConfig}, void, void> { + if (conf.type === 'compound') { + for (const key in conf.props) { + yield* iteratePropType(conf.props[key] as PropTypeConfig, [ + ...pathUpToThisPoint, + key, + ]) + } + } else if (conf.type === 'enum') { + throw new Error(`Not implemented yet`) + } else { + return yield {path: pathUpToThisPoint, conf} + } } diff --git a/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts index 778ad44..b2fe39a 100644 --- a/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts +++ b/theatre/studio/src/StudioStore/createTransactionPrivateApi.ts @@ -50,6 +50,9 @@ function cloneDeepSerializableAndPrune(v: T): T | undefined { } } +/** + * TODO replace with {@link iteratePropType} + */ function forEachDeepSimplePropOfCompoundProp( propType: PropTypeConfig_Compound<$IntentionalAny>, path: Array, diff --git a/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx b/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx index b6cbdbe..31101b2 100644 --- a/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx +++ b/theatre/studio/src/panels/BasePanel/ExtensionPaneWrapper.tsx @@ -39,8 +39,6 @@ const ExtensionPaneWrapper: React.FC<{ } const Container = styled(PanelWrapper)` - overflow: hidden; - display: flex; flex-direction: column; diff --git a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx index b42e7f6..da2986a 100644 --- a/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/DeterminePropEditorForDetail/DetailCompoundPropEditor.tsx @@ -1,5 +1,6 @@ import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import {isPropConfigComposite} from '@theatre/shared/propTypes/utils' +import type {$FixMe} from '@theatre/shared/utils/types' import {getPointerParts} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse' import last from 'lodash-es/last' @@ -8,12 +9,13 @@ import React from 'react' import styled from 'styled-components' import {indentationFormula} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS' -import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import useRefAndState from '@theatre/studio/utils/useRefAndState' import DeterminePropEditorForDetail from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' -import type {$FixMe} from '@theatre/shared/utils/types' + +import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' +import {useEditingToolsForCompoundProp} from '@theatre/studio/propEditors/useEditingToolsForCompoundProp' const Container = styled.div` --step: 8px; @@ -42,7 +44,7 @@ const PropName = styled.div` align-items: center; user-select: none; &:hover { - /* color: white; */ + color: white; } ${() => propNameTextCSS}; @@ -85,18 +87,29 @@ function DetailCompoundPropEditor< const [propNameContainerRef, propNameContainer] = useRefAndState(null) + const tools = useEditingToolsForCompoundProp( + pointerToProp as $FixMe, + obj, + propConfig, + ) + + const [contextMenu] = useContextMenu(propNameContainer, { + menuItems: tools.contextMenuItems, + }) + const lastSubPropIsComposite = compositeSubs.length > 0 // previous versions of the DetailCompoundPropEditor had a context menu item for "Reset values". return ( + {contextMenu}
- + {tools.controlIndicators} {propName || 'Props'}
diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx index 22f1873..f7d00c0 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/Row.tsx @@ -35,6 +35,17 @@ const Children = styled.ul` list-style: none; ` +/** + * @remarks + * Right now, we're rendering a hierarchical dom tree that reflects the hierarchy of + * objects, compound props, and their subs. This is not necessary and makes styling complicated. + * Instead of this, we can simply render a list. This should be easy to do, since the view model + * in {@link calculateSequenceEditorTree} already includes all the vertical placement information + * (height and top) we need to render the nodes as a list. + * + * Note that we don't need to change {@link calculateSequenceEditorTree} to be list-based. It can + * retain its hierarchy. It's just the DOM tree that should be list-based. + */ const RightRow: React.FC<{ leaf: SequenceEditorTree_Row node: React.ReactElement diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeStrip.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeStrip.tsx index e089751..3dc4c36 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeStrip.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeStrip.tsx @@ -12,6 +12,7 @@ import useDrag from '@theatre/studio/uiComponents/useDrag' import useRefAndState from '@theatre/studio/utils/useRefAndState' import React, {useMemo} from 'react' import styled from 'styled-components' +import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' export const focusRangeStripTheme = { enabled: { @@ -175,7 +176,6 @@ const FocusRangeStrip: React.FC<{ }) const scaledSpaceToUnitSpace = useVal(layoutP.scaledSpace.toUnitSpace) - const [isDraggingRef, isDragging] = useRefAndState(false) const sheet = useVal(layoutP.sheet) const gestureHandlers = useMemo((): Parameters[1] => { @@ -192,7 +192,6 @@ const FocusRangeStrip: React.FC<{ const endPosBeforeDrag = existingRange.range.end let dragHappened = false const sequence = val(layoutP.sheet).getSequence() - isDraggingRef.current = true return { onDrag(dx) { @@ -234,7 +233,6 @@ const FocusRangeStrip: React.FC<{ } }, onDragEnd() { - isDraggingRef.current = false if (existingRange) { if (dragHappened && tempTransaction !== undefined) { tempTransaction.commit() @@ -250,7 +248,9 @@ const FocusRangeStrip: React.FC<{ } }, [sheet, scaledSpaceToUnitSpace]) - useDrag(rangeStripNode, gestureHandlers) + const [isDragging] = useDrag(rangeStripNode, gestureHandlers) + + useLockFrameStampPosition(isDragging, -1) return usePrism(() => { const existingRange = existingRangeD.getValue() diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx index 6ad3cb7..4f68908 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx @@ -186,30 +186,28 @@ const FocusRangeThumb: React.FC<{ const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { ignore: hitZoneNode, }) - if (snapPos != null) { + + if (snapPos == null) { + const deltaPos = scaledSpaceToUnitSpace(dx) + const oldPosPlusDeltaPos = posBeforeDrag + deltaPos + newPosition = oldPosPlusDeltaPos + } else { newPosition = snapPos } range = existingRangeD.getValue()?.range || defaultRange - const deltaPos = scaledSpaceToUnitSpace(dx) - const oldPosPlusDeltaPos = posBeforeDrag + deltaPos + // Make sure that the focus range has a minimal width if (thumbType === 'start') { // Prevent the start thumb from going below 0 newPosition = Math.max( - Math.min( - oldPosPlusDeltaPos, - range['end'] - minFocusRangeStripWidth, - ), + Math.min(newPosition, range['end'] - minFocusRangeStripWidth), 0, ) } else { // Prevent the start thumb from going over the length of the sequence newPosition = Math.min( - Math.max( - oldPosPlusDeltaPos, - range['start'] + minFocusRangeStripWidth, - ), + Math.max(newPosition, range['start'] + minFocusRangeStripWidth), sheet.getSequence().length, ) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx index 8a2b10b..f637899 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx @@ -223,9 +223,12 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ // unsnapped clamp(posBeforeSeek + deltaPos, 0, sequence.length) }, - onDragEnd() { + onDragEnd(dragHappened) { setIsSeeking(false) }, + onClick(e) { + openPopover(e, thumbRef.current!) + }, } }, } @@ -276,9 +279,6 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ { - openPopover(e, thumbNode!) - }} > diff --git a/theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx b/theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx index 0784b0a..44137eb 100644 --- a/theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx +++ b/theatre/studio/src/propEditors/NextPrevKeyframeCursors.tsx @@ -1,9 +1,16 @@ import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' +import type {VoidFn} from '@theatre/shared/utils/types' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {transparentize} from 'polished' import React from 'react' import styled, {css} from 'styled-components' +export type NearbyKeyframesControls = { + prev?: Pick & {jump: VoidFn} + cur: {type: 'on'; toggle: VoidFn} | {type: 'off'; toggle: VoidFn} + next?: Pick & {jump: VoidFn} +} + const Container = styled.div` display: flex; justify-content: center; @@ -158,41 +165,16 @@ namespace Icons { ) } -const NextPrevKeyframeCursors: React.FC<{ - prev?: Keyframe - cur?: Keyframe - next?: Keyframe - jumpToPosition: (position: number) => void - toggleKeyframeOnCurrentPosition: () => void -}> = (props) => { +const NextPrevKeyframeCursors: React.FC = (props) => { return ( - { - if (props.prev) { - props.jumpToPosition(props.prev.position) - } - }} - > + - { - props.toggleKeyframeOnCurrentPosition() - }} - > + - { - if (props.next) { - props.jumpToPosition(props.next.position) - } - }} - > + diff --git a/theatre/studio/src/propEditors/getNearbyKeyframesOfTrack.tsx b/theatre/studio/src/propEditors/getNearbyKeyframesOfTrack.tsx new file mode 100644 index 0000000..ac186dc --- /dev/null +++ b/theatre/studio/src/propEditors/getNearbyKeyframesOfTrack.tsx @@ -0,0 +1,59 @@ +import type { + TrackData, + Keyframe, +} from '@theatre/core/projects/store/types/SheetState_Historic' +import last from 'lodash-es/last' + +const cache = new WeakMap< + TrackData, + [seqPosition: number, nearbyKeyframes: NearbyKeyframes] +>() + +const noKeyframes: NearbyKeyframes = {} + +export function getNearbyKeyframesOfTrack( + track: TrackData | undefined, + sequencePosition: number, +): NearbyKeyframes { + if (!track || track.keyframes.length === 0) return noKeyframes + + const cachedItem = cache.get(track) + if (cachedItem && cachedItem[0] === sequencePosition) { + return cachedItem[1] + } + + const calculate = (): NearbyKeyframes => { + const i = track.keyframes.findIndex((kf) => kf.position >= sequencePosition) + + if (i === -1) + return { + prev: last(track.keyframes), + } + + const k = track.keyframes[i]! + if (k.position === sequencePosition) { + return { + prev: i > 0 ? track.keyframes[i - 1] : undefined, + cur: k, + next: + i === track.keyframes.length - 1 ? undefined : track.keyframes[i + 1], + } + } else { + return { + next: k, + prev: i > 0 ? track.keyframes[i - 1] : undefined, + } + } + } + + const result = calculate() + cache.set(track, [sequencePosition, result]) + + return result +} + +export type NearbyKeyframes = { + prev?: Keyframe + cur?: Keyframe + next?: Keyframe +} diff --git a/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx new file mode 100644 index 0000000..57f25cb --- /dev/null +++ b/theatre/studio/src/propEditors/useEditingToolsForCompoundProp.tsx @@ -0,0 +1,316 @@ +import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import getStudio from '@theatre/studio/getStudio' +import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' +import getDeep from '@theatre/shared/utils/getDeep' +import {usePrism} from '@theatre/react' +import type { + $IntentionalAny, + SerializablePrimitive, +} from '@theatre/shared/utils/types' +import {getPointerParts, prism, val} from '@theatre/dataverse' +import type {Pointer} from '@theatre/dataverse' +import get from 'lodash-es/get' +import React from 'react' +import DefaultOrStaticValueIndicator from './DefaultValueIndicator' +import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' +import { + compoundHasSimpleDescendants, + isPropConfigComposite, + iteratePropType, +} from '@theatre/shared/propTypes/utils' +import type {SequenceTrackId} from '@theatre/shared/utils/ids' +import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate' +import pointerDeep from '@theatre/shared/utils/pointerDeep' +import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors' +import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' +import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack' + +interface CommonStuff { + beingScrubbed: boolean + contextMenuItems: Array + controlIndicators: React.ReactElement +} + +/** + * For compounds that have _no_ sequenced track in all of their descendants + */ +interface AllStatic extends CommonStuff { + type: 'AllStatic' +} + +/** + * For compounds that have at least one sequenced track in their descendants + */ +interface HasSequences extends CommonStuff { + type: 'HasSequences' +} + +type Stuff = AllStatic | HasSequences + +export function useEditingToolsForCompoundProp( + pointerToProp: Pointer<{}>, + obj: SheetObject, + propConfig: PropTypeConfig_Compound<{}>, +): Stuff { + return usePrism((): Stuff => { + // if the compound has no simple descendants, then there isn't much the user can do with it + if (!compoundHasSimpleDescendants(propConfig)) { + return { + type: 'AllStatic', + beingScrubbed: false, + contextMenuItems: [], + controlIndicators: ( + + ), + } + } + + const pathToProp = getPointerParts(pointerToProp).path + + /** + * TODO This implementation is wrong because {@link stateEditors.studio.ephemeral.projects.stateByProjectId.stateBySheetId.stateByObjectKey.propsBeingScrubbed.flag} + * does not prune empty objects + */ + const someDescendantsBeingScrubbed = !!val( + get( + getStudio()!.atomP.ephemeral.projects.stateByProjectId[ + obj.address.projectId + ].stateBySheetId[obj.address.sheetId].stateByObjectKey[ + obj.address.objectKey + ].valuesBeingScrubbed, + getPointerParts(pointerToProp).path, + ), + ) + + const contextMenuItems: IContextMenuItem[] = [] + + const common: CommonStuff = { + beingScrubbed: someDescendantsBeingScrubbed, + contextMenuItems, + controlIndicators: <>, + } + + const validSequencedTracks = val( + obj.template.getMapOfValidSequenceTracks_forStudio(), + ) + + const possibleSequenceTrackIds = getDeep( + validSequencedTracks, + pathToProp, + ) as undefined | IPropPathToTrackIdTree + + const hasOneOrMoreSequencedTracks = !!possibleSequenceTrackIds + const listOfDescendantTrackIds: SequenceTrackId[] = [] + + let hasOneOrMoreStatics = true + if (hasOneOrMoreSequencedTracks) { + hasOneOrMoreStatics = false + for (const descendant of iteratePropType(propConfig, [])) { + if (isPropConfigComposite(descendant.conf)) continue + const sequencedTrackIdBelongingToDescendant = getDeep( + possibleSequenceTrackIds, + descendant.path, + ) as SequenceTrackId | undefined + if (typeof sequencedTrackIdBelongingToDescendant !== 'string') { + hasOneOrMoreStatics = true + } else { + listOfDescendantTrackIds.push(sequencedTrackIdBelongingToDescendant) + } + } + } + + if (hasOneOrMoreStatics) { + contextMenuItems.push( + /** + * TODO This is surely confusing for the user if the descendants don't have overrides. + */ + { + label: 'Reset all to default', + callback: () => { + getStudio()!.transaction(({unset}) => { + unset(pointerToProp) + }) + }, + }, + { + label: 'Sequence all', + callback: () => { + getStudio()!.transaction(({stateEditors}) => { + for (const {path, conf} of iteratePropType( + propConfig, + pathToProp, + )) { + if (isPropConfigComposite(conf)) continue + const propAddress = {...obj.address, pathToProp: path} + + stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsSequenced( + propAddress, + propConfig, + ) + } + }) + }, + }, + ) + } + + if (hasOneOrMoreSequencedTracks) { + contextMenuItems.push({ + label: 'Make all static', + callback: () => { + getStudio()!.transaction(({stateEditors}) => { + for (const {path: subPath, conf} of iteratePropType( + propConfig, + [], + )) { + if (isPropConfigComposite(conf)) continue + const propAddress = { + ...obj.address, + pathToProp: [...pathToProp, ...subPath], + } + const pointerToSub = pointerDeep(pointerToProp, subPath) + + stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsStatic( + { + ...propAddress, + value: obj.getValueByPointer(pointerToSub as $IntentionalAny), + }, + ) + } + }) + }, + }) + } + + if (hasOneOrMoreSequencedTracks) { + const sequenceTrackId = possibleSequenceTrackIds + const nearbyKeyframeControls = prism.sub( + 'lcr', + (): NearbyKeyframesControls => { + const sequencePosition = val( + obj.sheet.getSequence().positionDerivation, + ) + + /* + 2/10 perf concern: + When displaying a hierarchy like {props: {transform: {position: {x, y, z}}}}, + we'd be recalculating this variable for both `position` and `transform`. While + we _could_ be re-using the calculation of `transform` in `position`, I think + it's unlikely that this optimization would matter. + */ + const nearbyKeyframesInEachTrack = listOfDescendantTrackIds + .map((trackId) => ({ + trackId, + track: val( + obj.template.project.pointers.historic.sheetsById[ + obj.address.sheetId + ].sequence.tracksByObject[obj.address.objectKey].trackData[ + trackId + ], + ), + })) + .filter(({track}) => !!track) + .map((s) => ({ + ...s, + nearbies: getNearbyKeyframesOfTrack(s.track, sequencePosition), + })) + + const hasCur = nearbyKeyframesInEachTrack.find( + ({nearbies}) => !!nearbies.cur, + ) + const allCur = nearbyKeyframesInEachTrack.every( + ({nearbies}) => !!nearbies.cur, + ) + + const closestPrev = nearbyKeyframesInEachTrack.reduce< + undefined | number + >((acc, s) => { + if (s.nearbies.prev) { + if (acc === undefined) { + return s.nearbies.prev.position + } else { + return Math.max(s.nearbies.prev.position, acc) + } + } else { + return acc + } + }, undefined) + + const closestNext = nearbyKeyframesInEachTrack.reduce< + undefined | number + >((acc, s) => { + if (s.nearbies.next) { + if (acc === undefined) { + return s.nearbies.next.position + } else { + return Math.min(s.nearbies.next.position, acc) + } + } else { + return acc + } + }, undefined) + + return { + cur: { + type: hasCur ? 'on' : 'off', + 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)) + }) + } + }, + }, + prev: + closestPrev !== undefined + ? { + position: closestPrev, + jump: () => { + obj.sheet.getSequence().position = closestPrev + }, + } + : undefined, + next: + closestNext !== undefined + ? { + position: closestNext, + jump: () => { + obj.sheet.getSequence().position = closestNext + }, + } + : undefined, + } + }, + [sequenceTrackId], + ) + + const nextPrevKeyframeCursors = ( + + ) + + const ret: HasSequences = { + ...common, + type: 'HasSequences', + controlIndicators: nextPrevKeyframeCursors, + } + + return ret + } else { + return { + ...common, + type: 'AllStatic', + controlIndicators: ( + + ), + } + } + }, []) +} diff --git a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx index 79cb0d6..b490011 100644 --- a/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx +++ b/theatre/studio/src/propEditors/useEditingToolsForSimpleProp.tsx @@ -1,10 +1,7 @@ import get from 'lodash-es/get' -import last from 'lodash-es/last' import React from 'react' - import type {Pointer} from '@theatre/dataverse' import {getPointerParts, prism, val} from '@theatre/dataverse' -import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import getStudio from '@theatre/studio/getStudio' import type Scrub from '@theatre/studio/Scrub' @@ -15,8 +12,10 @@ import type {SerializablePrimitive as SerializablePrimitive} from '@theatre/shar import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes' import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' import type {SequenceTrackId} from '@theatre/shared/utils/ids' - import DefaultOrStaticValueIndicator from './DefaultValueIndicator' +import type {NearbyKeyframes} from './getNearbyKeyframesOfTrack' +import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack' +import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' interface EditingToolsCommon { @@ -170,33 +169,10 @@ export function useEditingToolsForSimplePropInDetailsPanel< sequenceTrackId ], ) - if (!track || track.keyframes.length === 0) return {} - - const pos = val(obj.sheet.getSequence().positionDerivation) - - const i = track.keyframes.findIndex((kf) => kf.position >= pos) - - if (i === -1) - return { - prev: last(track.keyframes), - } - - const k = track.keyframes[i]! - if (k.position === pos) { - return { - prev: i > 0 ? track.keyframes[i - 1] : undefined, - cur: k, - next: - i === track.keyframes.length - 1 - ? undefined - : track.keyframes[i + 1], - } - } else { - return { - next: k, - prev: i > 0 ? track.keyframes[i - 1] : undefined, - } - } + const sequencePosition = val( + obj.sheet.getSequence().positionDerivation, + ) + return getNearbyKeyframesOfTrack(track, sequencePosition) }, [sequenceTrackId], ) @@ -215,13 +191,10 @@ export function useEditingToolsForSimplePropInDetailsPanel< } } - const nextPrevKeyframeCursors = ( - { - obj.sheet.getSequence().position = position - }} - toggleKeyframeOnCurrentPosition={() => { + const controls: NearbyKeyframesControls = { + cur: { + type: nearbyKeyframes.cur ? 'on' : 'off', + toggle: () => { if (nearbyKeyframes.cur) { getStudio()!.transaction((api) => { api.unset(pointerToProp) @@ -231,8 +204,32 @@ export function useEditingToolsForSimplePropInDetailsPanel< api.set(pointerToProp, common.value) }) } - }} - /> + }, + }, + prev: + nearbyKeyframes.prev !== undefined + ? { + position: nearbyKeyframes.prev.position, + jump: () => { + obj.sheet.getSequence().position = + nearbyKeyframes.prev!.position + }, + } + : undefined, + next: + nearbyKeyframes.next !== undefined + ? { + position: nearbyKeyframes.next.position, + jump: () => { + obj.sheet.getSequence().position = + nearbyKeyframes.next!.position + }, + } + : undefined, + } + + const nextPrevKeyframeCursors = ( + ) const ret: EditingToolsSequenced = { @@ -299,22 +296,6 @@ export function useEditingToolsForSimplePropInDetailsPanel< }, []) } -type NearbyKeyframes = { - prev?: Keyframe - cur?: Keyframe - next?: Keyframe -} - -export const shadeToColor: {[K in Shade]: string} = { - Default: '#222', - Static: '#333', - Static_BeingScrubbed: '#91a100', - Sequenced_OnKeyframe: '#700202', - Sequenced_OnKeyframe_BeingScrubbed: '#c50000', - Sequenced_BeingInterpolated: '#0387a8', - Sequened_NotBeingInterpolated: '#004c5f', -} - type Shade = | 'Default' | 'Static' diff --git a/theatre/studio/src/uiComponents/useDrag.ts b/theatre/studio/src/uiComponents/useDrag.ts index 5018e1a..d9822a0 100644 --- a/theatre/studio/src/uiComponents/useDrag.ts +++ b/theatre/studio/src/uiComponents/useDrag.ts @@ -34,7 +34,9 @@ type OnDragCallback = ( dyFromLastEvent: number, ) => void -type OnDragEndCallback = (dragHappened: boolean) => void +type OnClickCallback = (mouseUpEvent: MouseEvent) => void + +type OnDragEndCallback = (dragHappened: boolean, event?: MouseEvent) => void export type UseDragOpts = { /** @@ -88,6 +90,7 @@ export type UseDragOpts = { */ onDragEnd?: OnDragEndCallback onDrag: OnDragCallback + onClick?: OnClickCallback } // which mouse button to use the drag event @@ -170,7 +173,8 @@ export default function useDrag( const callbacksRef = useRef<{ onDrag: OnDragCallback onDragEnd: OnDragEndCallback - }>({onDrag: noop, onDragEnd: noop}) + onClick: OnClickCallback + }>({onDrag: noop, onDragEnd: noop, onClick: noop}) const capturedPointerRef = useRef() // needed to have a state on the react lifecycle which can be updated @@ -239,13 +243,16 @@ export default function useDrag( } } - const dragEndHandler = () => { + const dragEndHandler = (e: MouseEvent) => { removeDragListeners() if (!stateRef.current.domDragStarted) return const dragHappened = stateRef.current.detection.detected stateRef.current = {domDragStarted: false} if (opts.shouldPointerLock && !isSafari) document.exitPointerLock() callbacksRef.current.onDragEnd(dragHappened) + if (!dragHappened) { + callbacksRef.current.onClick(e) + } ensureIsDraggingUpToDateForReactLifecycle() } @@ -296,6 +303,7 @@ export default function useDrag( callbacksRef.current.onDrag = returnOfOnDragStart.onDrag callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop + callbacksRef.current.onClick = returnOfOnDragStart.onClick ?? noop // need to capture pointer after we know the provided handler wants to handle drag start capturedPointerRef.current = capturePointer('Drag start')