Refactor data-pos common code
This commit is contained in:
parent
030b6d2804
commit
d4060730d7
15 changed files with 148 additions and 192 deletions
|
@ -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, {
|
||||||
|
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
|
tempTransaction?.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
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 = getStudio()!.tempTransaction(({stateEditors}) => {
|
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
|
||||||
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
|
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,19 +88,9 @@ 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 &&
|
newPosition = snapPos
|
||||||
// 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 = newPosition
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
newPosition = snapPos
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -98,12 +98,10 @@ 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 = () => {
|
||||||
|
|
Loading…
Reference in a new issue