Refactor data-pos common code

This commit is contained in:
vezwork 2022-05-06 14:06:53 -04:00 committed by Cole Lawrence
parent 030b6d2804
commit d4060730d7
15 changed files with 148 additions and 192 deletions

View file

@ -14,7 +14,7 @@ import {lighten} from 'polished'
import React, {useMemo, useRef} from 'react' import React, {useMemo, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' 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 { import {
lockedCursorCssVarName, lockedCursorCssVarName,
useCssCursorLock, useCssCursorLock,
@ -22,6 +22,7 @@ import {
import SnapCursor from './SnapCursor.svg' import SnapCursor from './SnapCursor.svg'
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack' import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
import type {IKeyframeEditorProps} from './KeyframeEditor' import type {IKeyframeEditorProps} from './KeyframeEditor'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
export const DOT_SIZE_PX = 6 export const DOT_SIZE_PX = 6
const HIT_ZONE_SIZE_PX = 12 const HIT_ZONE_SIZE_PX = 12
@ -106,11 +107,8 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
<> <>
<HitZone <HitZone
ref={ref} ref={ref}
data-pos={props.keyframe.position.toFixed(3)} {...includeLockFrameStampAttrs(props.keyframe.position)}
{...{ {...DopeSnap.includePositionSnapAttrs(props.keyframe.position)}
[attributeNameThatLocksFramestamp]:
props.keyframe.position.toFixed(3),
}}
className={isDragging ? 'beingDragged' : ''} className={isDragging ? 'beingDragged' : ''}
/> />
<Diamond isSelected={!!props.selection} /> <Diamond isSelected={!!props.selection} />
@ -190,31 +188,19 @@ function useDragKeyframe(
const original = const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index] propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
const deltaPos = toUnitSpace(dx) const newPosition = Math.max(
const newPosBeforeSnapping = Math.max(original.position + deltaPos, 0) // check if our event hoversover a [data-pos] element
DopeSnap.checkIfMouseEventSnapToPos(event, {
let newPosition = newPosBeforeSnapping ignore: node,
}) ??
const snapTarget = event // if we don't find snapping target, check the distance dragged + original position
.composedPath() original.position + toUnitSpace(dx),
.find( // sanitize to minimum of zero
(el): el is Element => 0,
el instanceof Element &&
el !== node &&
el.hasAttribute('data-pos'),
) )
if (snapTarget) { tempTransaction?.discard()
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
if (isFinite(snapPos)) {
newPosition = snapPos
}
}
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{ {

View file

@ -16,6 +16,7 @@ import type {
SequenceEditorPanelLayout, SequenceEditorPanelLayout,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import type {SequenceEditorTree_AllRowTypes} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree' 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}>` const Container = styled.div<{isShiftDown: boolean}>`
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')}; cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')};
@ -191,26 +192,20 @@ namespace utils {
toUnitSpace = val(layoutP.scaledSpace.toUnitSpace) toUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
}, },
onDrag(dx, _, event) { onDrag(dx, _, event) {
let delta = toUnitSpace(dx)
if (tempTransaction) { if (tempTransaction) {
tempTransaction.discard() tempTransaction.discard()
tempTransaction = undefined tempTransaction = undefined
} }
const snapTarget = event const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {
.composedPath() ignore: origin.domNode,
.find( })
(el): el is Element =>
el instanceof Element &&
el !== origin.domNode &&
el.hasAttribute('data-pos'),
)
if (snapTarget) { let delta: number
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!) if (snapPos != null) {
if (isFinite(snapPos)) {
delta = snapPos - origin.positionAtStartOfDrag delta = snapPos - origin.positionAtStartOfDrag
} } else {
delta = toUnitSpace(dx)
} }
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
@ -238,15 +233,8 @@ namespace utils {
}) })
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
if (dragHappened) { if (dragHappened) tempTransaction?.commit()
if (tempTransaction) { else tempTransaction?.discard()
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
tempTransaction = undefined tempTransaction = undefined
}, },
} }

View file

@ -12,6 +12,7 @@ import {useReceiveVerticalWheelEvent} from '@theatre/studio/panels/SequenceEdito
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler' import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
import type {IRange} from '@theatre/shared/utils/types' import type {IRange} from '@theatre/shared/utils/types'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
const Container = styled.div` const Container = styled.div`
position: absolute; position: absolute;
@ -87,20 +88,10 @@ function useDragHandlers(
let newPosition = unsnappedPos let newPosition = unsnappedPos
const snapTarget = event.composedPath().find( const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {})
(el): el is Element => if (snapPos != null) {
el instanceof Element &&
// el !== thumbNode &&
el.hasAttribute('data-pos'),
)
if (snapTarget) {
const snapPos = parseFloat(snapTarget.getAttribute('data-pos')!)
if (isFinite(snapPos)) {
newPosition = snapPos newPosition = snapPos
} }
}
sequence.position = newPosition sequence.position = newPosition
}, },

View file

@ -13,7 +13,7 @@ import getStudio from '@theatre/studio/getStudio'
import type Sheet from '@theatre/core/sheets/Sheet' import type Sheet from '@theatre/core/sheets/Sheet'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import { import {
attributeNameThatLocksFramestamp, includeLockFrameStampAttrs,
useLockFrameStampPosition, useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {GoChevronLeft, GoChevronRight} from 'react-icons/all' import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
@ -130,6 +130,10 @@ type IProps = {
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
} }
/**
* 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<IProps> = ({layoutP}) => { const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null) const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
const [isDragging] = useDragBulge(node, {layoutP}) const [isDragging] = useDragBulge(node, {layoutP})
@ -186,7 +190,7 @@ const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
onClick={(e) => { onClick={(e) => {
openPopover(e, node!) openPopover(e, node!)
}} }}
{...{[attributeNameThatLocksFramestamp]: 'hide'}} {...includeLockFrameStampAttrs('hide')}
> >
<GoChevronLeft /> <GoChevronLeft />
<GoChevronRight /> <GoChevronRight />

View file

@ -147,6 +147,14 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => {
* This attribute is used so that when the cursor hovers over a keyframe, * This attribute is used so that when the cursor hovers over a keyframe,
* the framestamp snaps to the position of that keyframe. * the framestamp snaps to the position of that keyframe.
* *
* Use as a spread in a React element.
*
* @example
* ```tsx
* <div {...includeLockFrameStampAttrs(10)}/>
* ```
*
* @remarks
* Elements that need this behavior must set a data attribute like so: * Elements that need this behavior must set a data attribute like so:
* <div data-theatre-lock-framestamp-to="120.55" /> * <div data-theatre-lock-framestamp-to="120.55" />
* Setting this attribute to "hide" hides the stamp. * 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. * `pointer-events` on an element that should lock the framestamp.
* *
* See {@link FrameStampPositionProvider} * See {@link FrameStampPositionProvider}
*
*/ */
export const attributeNameThatLocksFramestamp = export const includeLockFrameStampAttrs = (value: number | 'hide') => ({
'data-theatre-lock-framestamp-to' [ATTR_LOCK_FRAMESTAMP]: value === 'hide' ? value : value.toFixed(3),
})
const ATTR_LOCK_FRAMESTAMP = 'data-theatre-lock-framestamp-to'
const pointerPositionInUnitSpace = ( const pointerPositionInUnitSpace = (
layoutP: Pointer<SequenceEditorPanelLayout>, layoutP: Pointer<SequenceEditorPanelLayout>,
@ -175,8 +187,8 @@ const pointerPositionInUnitSpace = (
for (const el of mousePos.composedPath()) { for (const el of mousePos.composedPath()) {
if (!(el instanceof HTMLElement || el instanceof SVGElement)) break if (!(el instanceof HTMLElement || el instanceof SVGElement)) break
if (el.hasAttribute(attributeNameThatLocksFramestamp)) { if (el.hasAttribute(ATTR_LOCK_FRAMESTAMP)) {
const val = el.getAttribute(attributeNameThatLocksFramestamp) const val = el.getAttribute(ATTR_LOCK_FRAMESTAMP)
if (typeof val !== 'string') continue if (typeof val !== 'string') continue
if (val === 'hide') return [-1, FrameStampPositionType.hidden] if (val === 'hide') return [-1, FrameStampPositionType.hidden]
const double = parseFloat(val) const double = parseFloat(val)

View file

@ -11,12 +11,13 @@ import styled from 'styled-components'
import type KeyframeEditor from './KeyframeEditor' import type KeyframeEditor from './KeyframeEditor'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' 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 {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import { import {
lockedCursorCssVarName, lockedCursorCssVarName,
useCssCursorLock, useCssCursorLock,
} from '@theatre/studio/uiComponents/PointerEventsHandler' } from '@theatre/studio/uiComponents/PointerEventsHandler'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
export const dotSize = 6 export const dotSize = 6
@ -78,10 +79,8 @@ 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)`,
}} }}
{...{ {...includeLockFrameStampAttrs(cur.position)}
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3), {...DopeSnap.includePositionSnapAttrs(cur.position)}
}}
data-pos={cur.position.toFixed(3)}
className={isDragging ? 'beingDragged' : ''} className={isDragging ? 'beingDragged' : ''}
/> />
<Circle <Circle

View file

@ -11,12 +11,13 @@ import styled from 'styled-components'
import type KeyframeEditor from './KeyframeEditor' import type KeyframeEditor from './KeyframeEditor'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' 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 {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import { import {
lockedCursorCssVarName, lockedCursorCssVarName,
useCssCursorLock, useCssCursorLock,
} from '@theatre/studio/uiComponents/PointerEventsHandler' } from '@theatre/studio/uiComponents/PointerEventsHandler'
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
export const dotSize = 6 export const dotSize = 6
@ -79,10 +80,8 @@ const GraphEditorDotScalar: 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)`,
}} }}
{...{ {...includeLockFrameStampAttrs(cur.position)}
[attributeNameThatLocksFramestamp]: cur.position.toFixed(3), {...DopeSnap.includePositionSnapAttrs(cur.position)}
}}
data-pos={cur.position.toFixed(3)}
className={isDragging ? 'beingDragged' : ''} className={isDragging ? 'beingDragged' : ''}
/> />
<Circle <Circle

View file

@ -6,7 +6,7 @@ import React, {useCallback} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {VscTriangleUp} from 'react-icons/all' import {VscTriangleUp} from 'react-icons/all'
import {attributeNameThatLocksFramestamp} from './FrameStampPositionProvider' import {includeLockFrameStampAttrs} from './FrameStampPositionProvider'
const Container = styled.button` const Container = styled.button`
outline: none; outline: none;
@ -65,7 +65,7 @@ const GraphEditorToggle: React.FC<{
onClick={toggle} onClick={toggle}
title={'Toggle graph editor'} title={'Toggle graph editor'}
className={isOpen ? 'open' : ''} className={isOpen ? 'open' : ''}
{...{[attributeNameThatLocksFramestamp]: 'hide'}} {...includeLockFrameStampAttrs('hide')}
> >
<VscTriangleUp /> <VscTriangleUp />
</Container> </Container>

View file

@ -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
* <div {...DopeSnap.includePositionSnapAttrs(10)}/>
* ```
*/
includePositionSnapAttrs(position: number) {
return {[POSITION_SNAP_ATTR]: position}
},
}
export default DopeSnap

View file

@ -18,11 +18,12 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { import {
attributeNameThatLocksFramestamp, includeLockFrameStampAttrs,
useLockFrameStampPosition, useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {focusRangeStripTheme, RangeStrip} from './FocusRangeStrip' import {focusRangeStripTheme, RangeStrip} from './FocusRangeStrip'
import type Sheet from '@theatre/core/sheets/Sheet' 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'}>` const TheDiv = styled.div<{enabled: boolean; type: 'start' | 'end'}>`
position: absolute; position: absolute;
@ -199,30 +200,17 @@ const FocusRangeThumb: React.FC<{
) )
}, },
onDrag(dx, _, event) { onDrag(dx, _, event) {
range = existingRangeD.getValue()?.range || defaultRange
const deltaPos = scaledSpaceToUnitSpace(dx)
let newPosition: number let newPosition: number
const oldPosPlusDeltaPos = posBeforeDrag + deltaPos const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {
ignore: hitZoneNode,
// Enable snapping })
const snapTarget = event if (snapPos != null) {
.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 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 // Make sure that the focus range has a minimal width
if (thumbType === 'start') { if (thumbType === 'start') {
// Prevent the start thumb from going below 0 // Prevent the start thumb from going below 0
@ -263,11 +251,8 @@ const FocusRangeThumb: React.FC<{
}) })
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
if (dragHappened && tempTransaction !== undefined) { if (dragHappened) tempTransaction?.commit()
tempTransaction.commit() else tempTransaction?.discard()
} else if (tempTransaction) {
tempTransaction.discard()
}
}, },
} }
}, [layoutP]) }, [layoutP])
@ -305,10 +290,8 @@ const FocusRangeThumb: React.FC<{
return ( return (
<TheDiv <TheDiv
ref={hitZoneRef as $IntentionalAny} ref={hitZoneRef as $IntentionalAny}
data-pos={position.toFixed(3)} {...DopeSnap.includePositionSnapAttrs(position)}
{...{ {...includeLockFrameStampAttrs(position)}
[attributeNameThatLocksFramestamp]: position.toFixed(3),
}}
className={`${isDragging && 'dragging'} ${enabled && 'enabled'}`} className={`${isDragging && 'dragging'} ${enabled && 'enabled'}`}
enabled={enabled} enabled={enabled}
type={thumbType} type={thumbType}

View file

@ -9,7 +9,7 @@ import {position} from 'polished'
import React, {useCallback, useMemo, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const Container = styled.div` const Container = styled.div`
@ -241,7 +241,7 @@ const HorizontalScrollbar: React.FC<{
return ( return (
<Container <Container
style={{bottom: bottom + 8 + 'px'}} style={{bottom: bottom + 8 + 'px'}}
{...{[attributeNameThatLocksFramestamp]: 'hide'}} {...includeLockFrameStampAttrs('hide')}
> >
<TimeThread> <TimeThread>
<DraggableArea <DraggableArea

View file

@ -12,7 +12,7 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import React, {useMemo, useRef} from 'react' import React, {useMemo, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { import {
attributeNameThatLocksFramestamp, includeLockFrameStampAttrs,
useLockFrameStampPosition, useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
@ -25,6 +25,7 @@ import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import type {StudioHistoricStateSequenceEditorMarker} from '@theatre/studio/store/types' import type {StudioHistoricStateSequenceEditorMarker} from '@theatre/studio/store/types'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import DopeSnap from './DopeSnap'
const MARKER_SIZE_W_PX = 10 const MARKER_SIZE_W_PX = 10
const MARKER_SIZE_H_PX = 8 const MARKER_SIZE_H_PX = 8
@ -156,16 +157,13 @@ const MarkerDotDefined: React.VFC<IMarkerDotDefinedProps> = ({
{contextMenu} {contextMenu}
<HitZone <HitZone
ref={markRef} ref={markRef}
// `data-pos` and `attributeNameThatLocksFramestamp` are used by FrameStampPositionProvider // `data-pos` and `includeLockFrameStampAttrs` are used by FrameStampPositionProvider
// in order to handle snapping the playhead. Adding these props effectively // in order to handle snapping the playhead. Adding these props effectively
// causes the playhead to "snap" to the marker on mouse over. // causes the playhead to "snap" to the marker on mouse over.
// `pointerEventsAutoInNormalMode` and `lockedCursorCssVarName` in the CSS above are also // `pointerEventsAutoInNormalMode` and `lockedCursorCssVarName` in the CSS above are also
// used to make this behave correctly. // used to make this behave correctly.
{...{ {...includeLockFrameStampAttrs(marker.position)}
[attributeNameThatLocksFramestamp]: marker.position.toFixed(3), {...DopeSnap.includePositionSnapAttrs(marker.position)}
[POSITION_SNAPPING.attributeNameForPosition]:
marker.position.toFixed(3),
}}
className={isDragging ? 'beingDragged' : ''} className={isDragging ? 'beingDragged' : ''}
/> />
<MarkerVisualDot /> <MarkerVisualDot />
@ -227,7 +225,7 @@ function useDragMarker(
const original = markerAtStartOfDrag const original = markerAtStartOfDrag
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
POSITION_SNAPPING.checkIfMouseEventSnapToPos(event, { DopeSnap.checkIfMouseEventSnapToPos(event, {
ignore: node, ignore: node,
}) ?? }) ??
// if we don't find snapping target, check the distance dragged + original position // if we don't find snapping target, check the distance dragged + original position
@ -267,39 +265,3 @@ function useDragMarker(
return [isDragging] 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
},
}

View file

@ -12,7 +12,7 @@ import React, {useMemo} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import { import {
attributeNameThatLocksFramestamp, includeLockFrameStampAttrs,
useLockFrameStampPosition, useLockFrameStampPosition,
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' } from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
@ -27,6 +27,7 @@ import {
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import {generateSequenceMarkerId} from '@theatre/shared/utils/ids' import {generateSequenceMarkerId} from '@theatre/shared/utils/ids'
import DopeSnap from './DopeSnap'
const Container = styled.div<{isVisible: boolean}>` const Container = styled.div<{isVisible: boolean}>`
--thumbColor: #00e0ff; --thumbColor: #00e0ff;
@ -218,27 +219,13 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
}, },
onDrag(dx, _, event) { onDrag(dx, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx) const deltaPos = scaledSpaceToUnitSpace(dx)
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
let newPosition = unsnappedPos sequence.position =
DopeSnap.checkIfMouseEventSnapToPos(event, {
const snapTarget = event ignore: thumbNode,
.composedPath() }) ??
.find( // unsnapped
(el): el is Element => clamp(posBeforeSeek + deltaPos, 0, sequence.length)
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
}, },
onDragEnd() { onDragEnd() {
setIsSeeking(false) setIsSeeking(false)
@ -286,11 +273,11 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
className={`${isSeeking && 'seeking'} ${ className={`${isSeeking && 'seeking'} ${
isPlayheadAttachedToFocusRange && 'playheadattachedtofocusrange' isPlayheadAttachedToFocusRange && 'playheadattachedtofocusrange'
}`} }`}
{...{[attributeNameThatLocksFramestamp]: 'hide'}} {...includeLockFrameStampAttrs('hide')}
> >
<Thumb <Thumb
ref={thumbRef as $IntentionalAny} ref={thumbRef as $IntentionalAny}
data-pos={posInUnitSpace.toFixed(3)} {...DopeSnap.includePositionSnapAttrs(posInUnitSpace)}
onClick={(e) => { onClick={(e) => {
openPopover(e, thumbNode!) openPopover(e, thumbNode!)
}} }}
@ -305,7 +292,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
</Thumb> </Thumb>
<Rod <Rod
data-pos={posInUnitSpace.toFixed(3)} {...DopeSnap.includePositionSnapAttrs(posInUnitSpace)}
className={isSeeking ? 'seeking' : ''} className={isSeeking ? 'seeking' : ''}
/> />
</Container> </Container>

View file

@ -4,7 +4,7 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import StampsGrid from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid' 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 {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import FocusRangeZone from './FocusRangeZone/FocusRangeZone' import FocusRangeZone from './FocusRangeZone/FocusRangeZone'
@ -34,7 +34,7 @@ const TopStrip: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
return ( return (
<> <>
<Container {...{[attributeNameThatLocksFramestamp]: 'hide'}}> <Container {...includeLockFrameStampAttrs('hide')}>
<StampsGrid layoutP={layoutP} width={width} height={topStripHeight} /> <StampsGrid layoutP={layoutP} width={width} height={topStripHeight} />
<FocusRangeZone layoutP={layoutP} /> <FocusRangeZone layoutP={layoutP} />
</Container> </Container>

View file

@ -98,13 +98,11 @@ export default function useDrag(
} }
const dragEndHandler = () => { const dragEndHandler = () => {
if (modeRef.current === 'dragging') {
removeDragListeners() removeDragListeners()
modeRef.current = 'notDragging' modeRef.current = 'notDragging'
optsRef.current.onDragEnd?.(stateRef.current.dragHappened) optsRef.current.onDragEnd?.(stateRef.current.dragHappened)
} }
}
const addDragListeners = () => { const addDragListeners = () => {
document.addEventListener('mousemove', dragHandler) document.addEventListener('mousemove', dragHandler)