diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx index 1eb275c..5279cd3 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeDot.tsx @@ -14,7 +14,7 @@ import {lighten} from 'polished' import React, {useMemo, useRef} from 'react' import styled from 'styled-components' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' -import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' +import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import { lockedCursorCssVarName, useCssCursorLock, @@ -22,6 +22,7 @@ import { import SnapCursor from './SnapCursor.svg' import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack' import type {IKeyframeEditorProps} from './KeyframeEditor' +import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' export const DOT_SIZE_PX = 6 const HIT_ZONE_SIZE_PX = 12 @@ -106,11 +107,8 @@ const KeyframeDot: React.VFC = (props) => { <> @@ -190,31 +188,19 @@ function useDragKeyframe( const original = propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index] - const deltaPos = toUnitSpace(dx) - const newPosBeforeSnapping = Math.max(original.position + deltaPos, 0) + const newPosition = Math.max( + // check if our event hoversover a [data-pos] element + DopeSnap.checkIfMouseEventSnapToPos(event, { + ignore: node, + }) ?? + // if we don't find snapping target, check the distance dragged + original position + original.position + toUnitSpace(dx), + // sanitize to minimum of zero + 0, + ) - let newPosition = newPosBeforeSnapping - - const snapTarget = event - .composedPath() - .find( - (el): el is Element => - el instanceof Element && - el !== node && - el.hasAttribute('data-pos'), - ) - - if (snapTarget) { - const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) - if (isFinite(snapPos)) { - newPosition = snapPos - } - } - - if (tempTransaction) { - tempTransaction.discard() - tempTransaction = undefined - } + tempTransaction?.discard() + tempTransaction = undefined tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx index d396195..6ac5ea9 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/DopeSheetSelectionView.tsx @@ -16,6 +16,7 @@ import type { SequenceEditorPanelLayout, } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorTree_AllRowTypes} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' +import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' const Container = styled.div<{isShiftDown: boolean}>` cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')}; @@ -191,26 +192,20 @@ namespace utils { toUnitSpace = val(layoutP.scaledSpace.toUnitSpace) }, onDrag(dx, _, event) { - let delta = toUnitSpace(dx) if (tempTransaction) { tempTransaction.discard() tempTransaction = undefined } - const snapTarget = event - .composedPath() - .find( - (el): el is Element => - el instanceof Element && - el !== origin.domNode && - el.hasAttribute('data-pos'), - ) + const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { + ignore: origin.domNode, + }) - if (snapTarget) { - const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) - if (isFinite(snapPos)) { - delta = snapPos - origin.positionAtStartOfDrag - } + let delta: number + if (snapPos != null) { + delta = snapPos - origin.positionAtStartOfDrag + } else { + delta = toUnitSpace(dx) } tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { @@ -238,15 +233,8 @@ namespace utils { }) }, onDragEnd(dragHappened) { - if (dragHappened) { - if (tempTransaction) { - tempTransaction.commit() - } - } else { - if (tempTransaction) { - tempTransaction.discard() - } - } + if (dragHappened) tempTransaction?.commit() + else tempTransaction?.discard() tempTransaction = undefined }, } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx index c853df2..096fef6 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx @@ -12,6 +12,7 @@ import {useReceiveVerticalWheelEvent} from '@theatre/studio/panels/SequenceEdito import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler' import type {IRange} from '@theatre/shared/utils/types' +import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' const Container = styled.div` position: absolute; @@ -87,19 +88,9 @@ function useDragHandlers( let newPosition = unsnappedPos - const snapTarget = event.composedPath().find( - (el): el is Element => - el instanceof Element && - // el !== thumbNode && - el.hasAttribute('data-pos'), - ) - - if (snapTarget) { - const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) - - if (isFinite(snapPos)) { - newPosition = snapPos - } + const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {}) + if (snapPos != null) { + newPosition = snapPos } sequence.position = newPosition diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx index d3b3c91..b0a6236 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator.tsx @@ -13,7 +13,7 @@ import getStudio from '@theatre/studio/getStudio' import type Sheet from '@theatre/core/sheets/Sheet' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import { - attributeNameThatLocksFramestamp, + includeLockFrameStampAttrs, useLockFrameStampPosition, } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {GoChevronLeft, GoChevronRight} from 'react-icons/all' @@ -130,6 +130,10 @@ type IProps = { layoutP: Pointer } +/** + * This appears at the end of the sequence where you can adjust the length of the sequence. + * Kinda looks like `< >` at the top bar at end of the sequence editor. + */ const LengthIndicator: React.FC = ({layoutP}) => { const [nodeRef, node] = useRefAndState(null) const [isDragging] = useDragBulge(node, {layoutP}) @@ -186,7 +190,7 @@ const LengthIndicator: React.FC = ({layoutP}) => { onClick={(e) => { openPopover(e, node!) }} - {...{[attributeNameThatLocksFramestamp]: 'hide'}} + {...includeLockFrameStampAttrs('hide')} > diff --git a/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx b/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx index a9f8bbd..9100d36 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx @@ -147,6 +147,14 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => { * This attribute is used so that when the cursor hovers over a keyframe, * the framestamp snaps to the position of that keyframe. * + * Use as a spread in a React element. + * + * @example + * ```tsx + *
+ * ``` + * + * @remarks * Elements that need this behavior must set a data attribute like so: *
* Setting this attribute to "hide" hides the stamp. @@ -157,9 +165,13 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => { * `pointer-events` on an element that should lock the framestamp. * * See {@link FrameStampPositionProvider} + * */ -export const attributeNameThatLocksFramestamp = - 'data-theatre-lock-framestamp-to' +export const includeLockFrameStampAttrs = (value: number | 'hide') => ({ + [ATTR_LOCK_FRAMESTAMP]: value === 'hide' ? value : value.toFixed(3), +}) + +const ATTR_LOCK_FRAMESTAMP = 'data-theatre-lock-framestamp-to' const pointerPositionInUnitSpace = ( layoutP: Pointer, @@ -175,8 +187,8 @@ const pointerPositionInUnitSpace = ( for (const el of mousePos.composedPath()) { if (!(el instanceof HTMLElement || el instanceof SVGElement)) break - if (el.hasAttribute(attributeNameThatLocksFramestamp)) { - const val = el.getAttribute(attributeNameThatLocksFramestamp) + if (el.hasAttribute(ATTR_LOCK_FRAMESTAMP)) { + const val = el.getAttribute(ATTR_LOCK_FRAMESTAMP) if (typeof val !== 'string') continue if (val === 'hide') return [-1, FrameStampPositionType.hidden] const double = parseFloat(val) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx index bf19f54..aabeb9b 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotNonScalar.tsx @@ -11,12 +11,13 @@ import styled from 'styled-components' import type KeyframeEditor from './KeyframeEditor' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' -import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' +import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import { lockedCursorCssVarName, useCssCursorLock, } from '@theatre/studio/uiComponents/PointerEventsHandler' +import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' export const dotSize = 6 @@ -78,10 +79,8 @@ const GraphEditorDotNonScalar: React.VFC = (props) => { cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`, cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`, }} - {...{ - [attributeNameThatLocksFramestamp]: cur.position.toFixed(3), - }} - data-pos={cur.position.toFixed(3)} + {...includeLockFrameStampAttrs(cur.position)} + {...DopeSnap.includePositionSnapAttrs(cur.position)} className={isDragging ? 'beingDragged' : ''} /> = (props) => { cx: `calc(var(--unitSpaceToScaledSpaceMultiplier) * ${cur.position} * 1px)`, cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`, }} - {...{ - [attributeNameThatLocksFramestamp]: cur.position.toFixed(3), - }} - data-pos={cur.position.toFixed(3)} + {...includeLockFrameStampAttrs(cur.position)} + {...DopeSnap.includePositionSnapAttrs(cur.position)} className={isDragging ? 'beingDragged' : ''} /> diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/DopeSnap.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/DopeSnap.tsx new file mode 100644 index 0000000..8e798cd --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/DopeSnap.tsx @@ -0,0 +1,47 @@ +// Pretty much same code as for keyframe and similar for playhead. +// Consider if we should unify the implementations. +// - See "useLockFrameStampPosition" +// - Also see "pointerPositionInUnitSpace" for a related impl (for different problem) +const POSITION_SNAP_ATTR = 'data-pos' + +/** + * Uses `[data-pos]` attribute to understand potential snap targets. + */ +const DopeSnap = { + checkIfMouseEventSnapToPos( + event: MouseEvent, + options?: {ignore?: Element | null}, + ): number | null { + const snapTarget = event + .composedPath() + .find( + (el): el is Element => + el instanceof Element && + el !== options?.ignore && + el.hasAttribute(POSITION_SNAP_ATTR), + ) + + if (snapTarget) { + const snapPos = parseFloat(snapTarget.getAttribute(POSITION_SNAP_ATTR)!) + if (isFinite(snapPos)) { + return snapPos + } + } + + return null + }, + + /** + * Use as a spread in a React element + * + * @example + * ```tsx + *
+ * ``` + */ + includePositionSnapAttrs(position: number) { + return {[POSITION_SNAP_ATTR]: position} + }, +} + +export default DopeSnap diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx index a711e76..93888a2 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FocusRangeZone/FocusRangeThumb.tsx @@ -18,11 +18,12 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState' import React, {useMemo} from 'react' import styled from 'styled-components' import { - attributeNameThatLocksFramestamp, + includeLockFrameStampAttrs, useLockFrameStampPosition, } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {focusRangeStripTheme, RangeStrip} from './FocusRangeStrip' import type Sheet from '@theatre/core/sheets/Sheet' +import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' const TheDiv = styled.div<{enabled: boolean; type: 'start' | 'end'}>` position: absolute; @@ -199,30 +200,17 @@ const FocusRangeThumb: React.FC<{ ) }, onDrag(dx, _, event) { - range = existingRangeD.getValue()?.range || defaultRange - - const deltaPos = scaledSpaceToUnitSpace(dx) let newPosition: number - const oldPosPlusDeltaPos = posBeforeDrag + deltaPos - - // Enable snapping - const snapTarget = event - .composedPath() - .find( - (el): el is Element => - el instanceof Element && - el !== hitZoneNode && - el.hasAttribute('data-pos'), - ) - - if (snapTarget) { - const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) - - if (isFinite(snapPos)) { - newPosition = snapPos - } + const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { + ignore: hitZoneNode, + }) + if (snapPos != null) { + 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 @@ -263,11 +251,8 @@ const FocusRangeThumb: React.FC<{ }) }, onDragEnd(dragHappened) { - if (dragHappened && tempTransaction !== undefined) { - tempTransaction.commit() - } else if (tempTransaction) { - tempTransaction.discard() - } + if (dragHappened) tempTransaction?.commit() + else tempTransaction?.discard() }, } }, [layoutP]) @@ -305,10 +290,8 @@ const FocusRangeThumb: React.FC<{ return ( = ({ {contextMenu} @@ -227,7 +225,7 @@ function useDragMarker( const original = markerAtStartOfDrag const newPosition = Math.max( // check if our event hoversover a [data-pos] element - POSITION_SNAPPING.checkIfMouseEventSnapToPos(event, { + DopeSnap.checkIfMouseEventSnapToPos(event, { ignore: node, }) ?? // if we don't find snapping target, check the distance dragged + original position @@ -267,39 +265,3 @@ function useDragMarker( return [isDragging] } - -// Pretty much same code as for keyframe and similar for playhead. -// Consider if we should unify the implementations. -// - See "useLockFrameStampPosition" -// - Also see "pointerPositionInUnitSpace" for a related impl (for different problem) -const POSITION_SNAPPING = { - /** - * Used to indicate that when hovering over this element, we should enable - * snapping to the given position. - */ - attributeNameForPosition: 'data-pos', - checkIfMouseEventSnapToPos( - event: MouseEvent, - options?: {ignore?: HTMLElement | null}, - ): number | null { - const snapTarget = event - .composedPath() - .find( - (el): el is Element => - el instanceof Element && - el !== options?.ignore && - el.hasAttribute(POSITION_SNAPPING.attributeNameForPosition), - ) - - if (snapTarget) { - const snapPos = parseFloat( - snapTarget.getAttribute(POSITION_SNAPPING.attributeNameForPosition)!, - ) - if (isFinite(snapPos)) { - return snapPos - } - } - - return null - }, -} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx index eeb3f1d..94017cd 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/Playhead.tsx @@ -12,7 +12,7 @@ import React, {useMemo} from 'react' import styled from 'styled-components' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import { - attributeNameThatLocksFramestamp, + includeLockFrameStampAttrs, useLockFrameStampPosition, } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' @@ -27,6 +27,7 @@ import { import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import getStudio from '@theatre/studio/getStudio' import {generateSequenceMarkerId} from '@theatre/shared/utils/ids' +import DopeSnap from './DopeSnap' const Container = styled.div<{isVisible: boolean}>` --thumbColor: #00e0ff; @@ -218,27 +219,13 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ }, onDrag(dx, _, event) { const deltaPos = scaledSpaceToUnitSpace(dx) - const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length) - let newPosition = unsnappedPos - - const snapTarget = event - .composedPath() - .find( - (el): el is Element => - el instanceof Element && - el !== thumbNode && - el.hasAttribute('data-pos'), - ) - - if (snapTarget) { - const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) - if (isFinite(snapPos)) { - newPosition = snapPos - } - } - - sequence.position = newPosition + sequence.position = + DopeSnap.checkIfMouseEventSnapToPos(event, { + ignore: thumbNode, + }) ?? + // unsnapped + clamp(posBeforeSeek + deltaPos, 0, sequence.length) }, onDragEnd() { setIsSeeking(false) @@ -286,11 +273,11 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ className={`${isSeeking && 'seeking'} ${ isPlayheadAttachedToFocusRange && 'playheadattachedtofocusrange' }`} - {...{[attributeNameThatLocksFramestamp]: 'hide'}} + {...includeLockFrameStampAttrs('hide')} > { openPopover(e, thumbNode!) }} @@ -305,7 +292,7 @@ const Playhead: React.FC<{layoutP: Pointer}> = ({ diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/TopStrip.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/TopStrip.tsx index 490d0cd..1f5741d 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/TopStrip.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/TopStrip.tsx @@ -4,7 +4,7 @@ import React from 'react' import styled from 'styled-components' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid' -import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' +import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import FocusRangeZone from './FocusRangeZone/FocusRangeZone' @@ -34,7 +34,7 @@ const TopStrip: React.FC<{layoutP: Pointer}> = ({ return ( <> - + diff --git a/theatre/studio/src/uiComponents/useDrag.ts b/theatre/studio/src/uiComponents/useDrag.ts index 745a79a..01be584 100644 --- a/theatre/studio/src/uiComponents/useDrag.ts +++ b/theatre/studio/src/uiComponents/useDrag.ts @@ -98,12 +98,10 @@ export default function useDrag( } const dragEndHandler = () => { - if (modeRef.current === 'dragging') { - removeDragListeners() - modeRef.current = 'notDragging' + removeDragListeners() + modeRef.current = 'notDragging' - optsRef.current.onDragEnd?.(stateRef.current.dragHappened) - } + optsRef.current.onDragEnd?.(stateRef.current.dragHappened) } const addDragListeners = () => {