Simplify useDrag()'s API and implement middle-button dragging in SequenceEditor

This commit is contained in:
Aria Minaei 2022-05-10 15:30:10 +02:00
parent e12d495f29
commit 202b61c48c
17 changed files with 995 additions and 972 deletions

View file

@ -21,52 +21,44 @@ const PanelDragZone: React.FC<
const [ref, node] = useRefAndState<HTMLDivElement>(null as $IntentionalAny) const [ref, node] = useRefAndState<HTMLDivElement>(null as $IntentionalAny)
const dragOpts: Parameters<typeof useDrag>[1] = useMemo(() => { const dragOpts: Parameters<typeof useDrag>[1] = useMemo(() => {
let stuffBeforeDrag = panelStuffRef.current
let tempTransaction: CommitOrDiscard | undefined
let unlock: VoidFn | undefined
return { return {
debugName: 'PanelDragZone', debugName: 'PanelDragZone',
lockCursorTo: 'move', lockCursorTo: 'move',
onDragStart() { onDragStart() {
stuffBeforeDrag = panelStuffRef.current const stuffBeforeDrag = panelStuffRef.current
if (unlock) { let tempTransaction: CommitOrDiscard | undefined
const u = unlock
unlock = undefined
u()
}
unlock = panelStuff.addBoundsHighlightLock()
},
onDrag(dx, dy) {
const newDims: typeof panelStuff['dims'] = {
...stuffBeforeDrag.dims,
top: stuffBeforeDrag.dims.top + dy,
left: stuffBeforeDrag.dims.left + dx,
}
const position = panelDimsToPanelPosition(newDims, {
width: window.innerWidth,
height: window.innerHeight,
})
tempTransaction?.discard() const unlock = panelStuff.addBoundsHighlightLock()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.studio.historic.panelPositions.setPanelPosition({ return {
position, onDrag(dx, dy) {
panelId: stuffBeforeDrag.panelId, const newDims: typeof panelStuff['dims'] = {
}) ...stuffBeforeDrag.dims,
}) top: stuffBeforeDrag.dims.top + dy,
}, left: stuffBeforeDrag.dims.left + dx,
onDragEnd(dragHappened) { }
if (unlock) { const position = panelDimsToPanelPosition(newDims, {
const u = unlock width: window.innerWidth,
unlock = undefined height: window.innerHeight,
u() })
tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.studio.historic.panelPositions.setPanelPosition({
position,
panelId: stuffBeforeDrag.panelId,
})
})
},
onDragEnd(dragHappened) {
unlock()
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
},
} }
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -1,10 +1,10 @@
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import {lighten} from 'polished' import {lighten} from 'polished'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {panelDimsToPanelPosition, usePanel} from './BasePanel' import {panelDimsToPanelPosition, usePanel} from './BasePanel'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
@ -142,96 +142,84 @@ const PanelResizeHandle: React.FC<{
const panelStuff = usePanel() const panelStuff = usePanel()
const panelStuffRef = useRef(panelStuff) const panelStuffRef = useRef(panelStuff)
panelStuffRef.current = panelStuff panelStuffRef.current = panelStuff
const [isDragging, setIsDragging] = useState(false)
const [ref, node] = useRefAndState<HTMLDivElement>(null as $IntentionalAny) const [ref, node] = useRefAndState<HTMLDivElement>(null as $IntentionalAny)
const dragOpts: Parameters<typeof useDrag>[1] = useMemo(() => { const dragOpts: Parameters<typeof useDrag>[1] = useMemo(() => {
let stuffBeforeDrag = panelStuffRef.current
let tempTransaction: CommitOrDiscard | undefined
let unlock: VoidFn | undefined
return { return {
debugName: 'PanelResizeHandle', debugName: 'PanelResizeHandle',
lockCursorTo: cursors[which], lockCursorTo: cursors[which],
onDragStart() { onDragStart() {
stuffBeforeDrag = panelStuffRef.current let tempTransaction: CommitOrDiscard | undefined
setIsDragging(true)
if (unlock) {
const u = unlock
unlock = undefined
u()
}
unlock = panelStuff.addBoundsHighlightLock()
},
onDrag(dx, dy) {
const newDims: typeof panelStuff['dims'] = {
...stuffBeforeDrag.dims,
}
if (which.startsWith('Bottom')) { const stuffBeforeDrag = panelStuffRef.current
newDims.height = Math.max( const unlock = panelStuff.addBoundsHighlightLock()
newDims.height + dy,
stuffBeforeDrag.minDims.height,
)
} else if (which.startsWith('Top')) {
const bottom = newDims.top + newDims.height
const top = Math.min(
bottom - stuffBeforeDrag.minDims.height,
newDims.top + dy,
)
const height = bottom - top
newDims.height = height return {
newDims.top = top onDrag(dx, dy) {
} const newDims: typeof panelStuff['dims'] = {
if (which.endsWith('Left')) { ...stuffBeforeDrag.dims,
const right = newDims.left + newDims.width }
const left = Math.min(
right - stuffBeforeDrag.minDims.width,
newDims.left + dx,
)
const width = right - left
newDims.width = width if (which.startsWith('Bottom')) {
newDims.left = left newDims.height = Math.max(
} else if (which.endsWith('Right')) { newDims.height + dy,
newDims.width = Math.max( stuffBeforeDrag.minDims.height,
newDims.width + dx, )
stuffBeforeDrag.minDims.width, } else if (which.startsWith('Top')) {
) const bottom = newDims.top + newDims.height
} const top = Math.min(
bottom - stuffBeforeDrag.minDims.height,
newDims.top + dy,
)
const height = bottom - top
const position = panelDimsToPanelPosition(newDims, { newDims.height = height
width: window.innerWidth, newDims.top = top
height: window.innerHeight, }
}) if (which.endsWith('Left')) {
const right = newDims.left + newDims.width
const left = Math.min(
right - stuffBeforeDrag.minDims.width,
newDims.left + dx,
)
const width = right - left
tempTransaction?.discard() newDims.width = width
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { newDims.left = left
stateEditors.studio.historic.panelPositions.setPanelPosition({ } else if (which.endsWith('Right')) {
position, newDims.width = Math.max(
panelId: stuffBeforeDrag.panelId, newDims.width + dx,
}) stuffBeforeDrag.minDims.width,
}) )
}, }
onDragEnd(dragHappened) {
if (unlock) { const position = panelDimsToPanelPosition(newDims, {
const u = unlock width: window.innerWidth,
unlock = undefined height: window.innerHeight,
u() })
tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.studio.historic.panelPositions.setPanelPosition({
position,
panelId: stuffBeforeDrag.panelId,
})
})
},
onDragEnd(dragHappened) {
unlock()
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
},
} }
setIsDragging(false)
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
tempTransaction = undefined
}, },
} }
}, [which]) }, [which])
useDrag(node, dragOpts) const [isDragging] = useDrag(node, dragOpts)
const Comp = els[which] const Comp = els[which]
const isOnCorner = which.length <= 6 const isOnCorner = which.length <= 6

View file

@ -8,13 +8,8 @@ import {lighten} from 'polished'
import React from 'react' import React from 'react'
import {useMemo, useRef} from 'react' import {useMemo, useRef} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type {
SequenceEditorPanelLayout,
DopeSheetSelection,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {DOT_SIZE_PX} from './KeyframeDot' import {DOT_SIZE_PX} from './KeyframeDot'
import type KeyframeEditor from './KeyframeEditor' import type KeyframeEditor from './KeyframeEditor'
import type Sequence from '@theatre/core/sequences/Sequence'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover' import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
import CurveEditorPopover from './CurveEditorPopover/CurveEditorPopover' import CurveEditorPopover from './CurveEditorPopover/CurveEditorPopover'
@ -156,84 +151,74 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
propsRef.current = props propsRef.current = props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let tempTransaction: CommitOrDiscard | undefined
let propsAtStartOfDrag: IProps
let selectionDragHandlers:
| ReturnType<DopeSheetSelection['getDragHandlers']>
| undefined
let sequence: Sequence
return { return {
debugName: 'useDragKeyframe', debugName: 'useDragKeyframe',
lockCursorTo: 'ew-resize', lockCursorTo: 'ew-resize',
onDragStart(event) { onDragStart(event) {
const props = propsRef.current const props = propsRef.current
let tempTransaction: CommitOrDiscard | undefined
if (props.selection) { if (props.selection) {
const {selection, leaf} = props const {selection, leaf} = props
const {sheetObject} = leaf const {sheetObject} = leaf
selectionDragHandlers = selection.getDragHandlers({ return selection
...sheetObject.address, .getDragHandlers({
pathToProp: leaf.pathToProp, ...sheetObject.address,
trackId: leaf.trackId, pathToProp: leaf.pathToProp,
keyframeId: props.keyframe.id, trackId: leaf.trackId,
domNode: node!, keyframeId: props.keyframe.id,
positionAtStartOfDrag: domNode: node!,
props.trackData.keyframes[props.index].position, positionAtStartOfDrag:
}) props.trackData.keyframes[props.index].position,
selectionDragHandlers.onDragStart?.(event) })
return .onDragStart(event)
} }
propsAtStartOfDrag = props const propsAtStartOfDrag = props
sequence = val(propsAtStartOfDrag.layoutP.sheet).getSequence() const sequence = val(propsAtStartOfDrag.layoutP.sheet).getSequence()
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) const toUnitSpace = val(
}, propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
onDrag(dx, dy, event) { )
if (selectionDragHandlers) {
selectionDragHandlers.onDrag(dx, dy, event)
return
}
const delta = toUnitSpace(dx)
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes(
{
...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId,
keyframeIds: [
propsAtStartOfDrag.keyframe.id,
propsAtStartOfDrag.trackData.keyframes[
propsAtStartOfDrag.index + 1
].id,
],
translate: delta,
scale: 1,
origin: 0,
snappingFunction: sequence.closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
if (selectionDragHandlers) {
selectionDragHandlers.onDragEnd?.(dragHappened)
selectionDragHandlers = undefined return {
onDrag(dx, dy, event) {
const delta = toUnitSpace(dx)
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.transformKeyframes(
{
...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId,
keyframeIds: [
propsAtStartOfDrag.keyframe.id,
propsAtStartOfDrag.trackData.keyframes[
propsAtStartOfDrag.index + 1
].id,
],
translate: delta,
scale: 1,
origin: 0,
snappingFunction: sequence.closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
},
} }
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -256,15 +256,17 @@ function useKeyframeDrag(
lockCursorTo: 'move', lockCursorTo: 'move',
onDragStart() { onDragStart() {
setFrozen(true) setFrozen(true)
}, return {
onDrag(dx, dy) { onDrag(dx, dy) {
if (!svgNode) return if (!svgNode) return
props.onCurveChange(setHandles(dx, dy)) props.onCurveChange(setHandles(dx, dy))
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
setFrozen(false) setFrozen(false)
props.onCancelCurveChange() props.onCancelCurveChange()
},
}
}, },
}), }),
[svgNode, props.trackData], [svgNode, props.trackData],

View file

@ -1,7 +1,3 @@
import type {
DopeSheetSelection,
SequenceEditorPanelLayout,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
@ -148,14 +144,6 @@ function useDragKeyframe(
propsRef.current = props propsRef.current = props
const useDragOpts = useMemo<UseDragOpts>(() => { const useDragOpts = useMemo<UseDragOpts>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let tempTransaction: CommitOrDiscard | undefined
let propsAtStartOfDrag: IKeyframeDotProps
let selectionDragHandlers:
| ReturnType<DopeSheetSelection['getDragHandlers']>
| undefined
return { return {
debugName: 'KeyframeDot/useDragKeyframe', debugName: 'KeyframeDot/useDragKeyframe',
@ -164,65 +152,61 @@ function useDragKeyframe(
if (props.selection) { if (props.selection) {
const {selection, leaf} = props const {selection, leaf} = props
const {sheetObject} = leaf const {sheetObject} = leaf
selectionDragHandlers = selection.getDragHandlers({ return selection
...sheetObject.address, .getDragHandlers({
pathToProp: leaf.pathToProp, ...sheetObject.address,
trackId: leaf.trackId, pathToProp: leaf.pathToProp,
keyframeId: props.keyframe.id, trackId: leaf.trackId,
domNode: node!, keyframeId: props.keyframe.id,
positionAtStartOfDrag: domNode: node!,
props.trackData.keyframes[props.index].position, positionAtStartOfDrag:
}) props.trackData.keyframes[props.index].position,
selectionDragHandlers.onDragStart?.(event) })
return .onDragStart(event)
} }
propsAtStartOfDrag = props const propsAtStartOfDrag = props
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) const toUnitSpace = val(
}, propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
onDrag(dx, dy, event) {
if (selectionDragHandlers) {
selectionDragHandlers.onDrag(dx, dy, event)
return
}
const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
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,
) )
tempTransaction?.discard() let tempTransaction: CommitOrDiscard | undefined
tempTransaction = undefined
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId,
keyframes: [{...original, position: newPosition}],
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
if (selectionDragHandlers) {
selectionDragHandlers.onDragEnd?.(dragHappened)
selectionDragHandlers = undefined return {
onDrag(dx, dy, event) {
const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
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,
)
tempTransaction?.discard()
tempTransaction = undefined
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.leaf.sheetObject.address,
trackId: propsAtStartOfDrag.leaf.trackId,
keyframes: [{...original, position: newPosition}],
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
},
} }
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -79,27 +79,30 @@ function useCaptureSelection(
} }
val(layoutP.selectionAtom).setState({current: undefined}) val(layoutP.selectionAtom).setState({current: undefined})
},
onDrag(_dx, _dy, event) {
// const state = ref.current!
const rect = containerNode!.getBoundingClientRect()
const posInScaledSpace = event.clientX - rect.left return {
onDrag(_dx, _dy, event) {
// const state = ref.current!
const rect = containerNode!.getBoundingClientRect()
const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)( const posInScaledSpace = event.clientX - rect.left
posInScaledSpace,
)
ref.current = { const posInUnitSpace = val(layoutP.scaledSpace.toUnitSpace)(
positions: [ref.current!.positions[0], posInUnitSpace], posInScaledSpace,
ys: [ref.current!.ys[0], event.clientY - rect.top], )
ref.current = {
positions: [ref.current!.positions[0], posInUnitSpace],
ys: [ref.current!.ys[0], event.clientY - rect.top],
}
const selection = utils.boundsToSelection(layoutP, ref.current)
val(layoutP.selectionAtom).setState({current: selection})
},
onDragEnd(_dragHappened) {
ref.current = null
},
} }
const selection = utils.boundsToSelection(layoutP, ref.current)
val(layoutP.selectionAtom).setState({current: selection})
},
onDragEnd(_dragHappened) {
ref.current = null
}, },
} }
}, [layoutP, containerNode, ref]), }, [layoutP, containerNode, ref]),
@ -183,59 +186,65 @@ namespace utils {
type: 'DopeSheetSelection', type: 'DopeSheetSelection',
byObjectKey: {}, byObjectKey: {},
getDragHandlers(origin) { getDragHandlers(origin) {
let tempTransaction: CommitOrDiscard | undefined
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
return { return {
debugName: 'DopeSheetSelectionView/boundsToSelection', debugName: 'DopeSheetSelectionView/boundsToSelection',
onDragStart() { onDragStart() {
toUnitSpace = val(layoutP.scaledSpace.toUnitSpace) let tempTransaction: CommitOrDiscard | undefined
},
onDrag(dx, _, event) {
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { const toUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
ignore: origin.domNode,
})
let delta: number return {
if (snapPos != null) { onDrag(dx, _, event) {
delta = snapPos - origin.positionAtStartOfDrag if (tempTransaction) {
} else { tempTransaction.discard()
delta = toUnitSpace(dx) tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
const transformKeyframes =
stateEditors.coreByProject.historic.sheetsById.sequence
.transformKeyframes
for (const objectKey of Object.keys(selection.byObjectKey)) {
const {byTrackId} = selection.byObjectKey[objectKey]!
for (const trackId of Object.keys(byTrackId)) {
const {byKeyframeId} = byTrackId[trackId]!
transformKeyframes({
trackId,
keyframeIds: Object.keys(byKeyframeId),
translate: delta,
scale: 1,
origin: 0,
snappingFunction: sheet.getSequence().closestGridPosition,
objectKey,
projectId: origin.projectId,
sheetId: origin.sheetId,
})
} }
}
}) const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {
}, ignore: origin.domNode,
onDragEnd(dragHappened) { })
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard() let delta: number
tempTransaction = undefined if (snapPos != null) {
delta = snapPos - origin.positionAtStartOfDrag
} else {
delta = toUnitSpace(dx)
}
tempTransaction = getStudio()!.tempTransaction(
({stateEditors}) => {
const transformKeyframes =
stateEditors.coreByProject.historic.sheetsById.sequence
.transformKeyframes
for (const objectKey of Object.keys(
selection.byObjectKey,
)) {
const {byTrackId} = selection.byObjectKey[objectKey]!
for (const trackId of Object.keys(byTrackId)) {
const {byKeyframeId} = byTrackId[trackId]!
transformKeyframes({
trackId,
keyframeIds: Object.keys(byKeyframeId),
translate: delta,
scale: 1,
origin: 0,
snappingFunction:
sheet.getSequence().closestGridPosition,
objectKey,
projectId: origin.projectId,
sheetId: origin.sheetId,
})
}
}
},
)
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
},
}
}, },
} }
}, },

View file

@ -1,4 +1,3 @@
import type Sequence from '@theatre/core/sequences/Sequence'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
@ -50,7 +49,7 @@ const HorizontallyScrollableArea: React.FC<{
) )
useHandlePanAndZoom(layoutP, containerNode) useHandlePanAndZoom(layoutP, containerNode)
useDragHandlers(layoutP, containerNode) useDragPlayheadHandlers(layoutP, containerNode)
useUpdateScrollFromClippedSpaceRange(layoutP, containerNode) useUpdateScrollFromClippedSpaceRange(layoutP, containerNode)
return ( return (
@ -70,31 +69,13 @@ const HorizontallyScrollableArea: React.FC<{
export default HorizontallyScrollableArea export default HorizontallyScrollableArea
function useDragHandlers( function useDragPlayheadHandlers(
layoutP: Pointer<SequenceEditorPanelLayout>, layoutP: Pointer<SequenceEditorPanelLayout>,
containerEl: HTMLDivElement | null, containerEl: HTMLDivElement | null,
) { ) {
const handlers = useMemo((): Parameters<typeof useDrag>[1] => { const handlers = useMemo((): Parameters<typeof useDrag>[1] => {
let posBeforeSeek = 0
let sequence: Sequence
let scaledSpaceToUnitSpace: typeof layoutP.scaledSpace.toUnitSpace.$$__pointer_type
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
return { return {
debugName: 'HorizontallyScrollableArea', debugName: 'HorizontallyScrollableArea',
onDrag(dx: number, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx)
const unsnappedPos = clamp(posBeforeSeek + deltaPos, 0, sequence.length)
let newPosition = unsnappedPos
const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {})
if (snapPos != null) {
newPosition = snapPos
}
sequence.position = newPosition
},
onDragStart(event) { onDragStart(event) {
if (event.target instanceof HTMLInputElement) { if (event.target instanceof HTMLInputElement) {
// editing some value // editing some value
@ -124,16 +105,38 @@ function useDragHandlers(
Infinity, Infinity,
) )
sequence = val(layoutP.sheet).getSequence() const setIsSeeking = val(layoutP.seeker.setIsSeeking)
const sequence = val(layoutP.sheet).getSequence()
sequence.position = initialPositionInUnitSpace sequence.position = initialPositionInUnitSpace
posBeforeSeek = initialPositionInUnitSpace const posBeforeSeek = initialPositionInUnitSpace
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace) const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
setIsSeeking(true) setIsSeeking(true)
},
onDragEnd() { return {
setIsSeeking(false) onDrag(dx: number, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx)
const unsnappedPos = clamp(
posBeforeSeek + deltaPos,
0,
sequence.length,
)
let newPosition = unsnappedPos
const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {})
if (snapPos != null) {
newPosition = snapPos
}
sequence.position = newPosition
},
onDragEnd() {
setIsSeeking(false)
},
}
}, },
} }
}, [layoutP, containerEl]) }, [layoutP, containerEl])
@ -233,6 +236,39 @@ function useHandlePanAndZoom(
node.removeEventListener('wheel', receiveWheelEvent, listenerOptions) node.removeEventListener('wheel', receiveWheelEvent, listenerOptions)
} }
}, [node, layoutP]) }, [node, layoutP])
useDrag(
node,
useMemo<Parameters<typeof useDrag>[1]>(() => {
return {
onDragStart(e) {
const oldRange = val(layoutP.clippedSpace.range)
const setRange = val(layoutP.clippedSpace.setRange)
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
e.preventDefault()
e.stopPropagation()
return {
onDrag(dx, dy, _, __, deltaYFromLastEvent) {
receiveVerticalWheelEvent({deltaY: -deltaYFromLastEvent})
const delta = -scaledSpaceToUnitSpace(dx)
const newRange = mapValues(
oldRange,
(originalPos) => originalPos + delta,
)
setRange(newRange)
},
}
},
debugName: 'HorizontallyScrollableArea Middle Button Drag',
buttons: [1],
lockCursorTo: 'grab',
}
}, [layoutP]),
)
} }
function normalize(value: number, [min, max]: [min: number, max: number]) { function normalize(value: number, [min, max]: [min: number, max: number]) {

View file

@ -1,7 +1,7 @@
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef} 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 {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
@ -10,7 +10,6 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
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 {
includeLockFrameStampAttrs, includeLockFrameStampAttrs,
@ -219,58 +218,56 @@ function useDragBulge(
): [isDragging: boolean] { ): [isDragging: boolean] {
const propsRef = useRef(props) const propsRef = useRef(props)
propsRef.current = props propsRef.current = props
const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, -1)
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let tempTransaction: CommitOrDiscard | undefined
let propsAtStartOfDrag: IProps
let sheet: Sheet
let initialLength: number
return { return {
debugName: 'LengthIndicator/useDragBulge', debugName: 'LengthIndicator/useDragBulge',
lockCursorTo: 'ew-resize', lockCursorTo: 'ew-resize',
onDragStart(event) { onDragStart(event) {
setIsDragging(true) let tempTransaction: CommitOrDiscard | undefined
propsAtStartOfDrag = propsRef.current
sheet = val(propsRef.current.layoutP.sheet)
initialLength = sheet.getSequence().length
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) const propsAtStartOfDrag = propsRef.current
}, const sheet = val(propsRef.current.layoutP.sheet)
onDrag(dx, dy, event) { const initialLength = sheet.getSequence().length
const delta = toUnitSpace(dx)
if (tempTransaction) { const toUnitSpace = val(
tempTransaction.discard() propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
tempTransaction = undefined )
return {
onDrag(dx, dy, event) {
const delta = toUnitSpace(dx)
if (tempTransaction) {
tempTransaction.discard()
tempTransaction = undefined
}
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.setLength(
{
...sheet.address,
length: initialLength + delta,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
},
} }
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.setLength({
...sheet.address,
length: initialLength + delta,
})
})
},
onDragEnd(dragHappened) {
setIsDragging(false)
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
tempTransaction = undefined
}, },
} }
}, []) }, [])
useDrag(node, gestureHandlers) const [isDragging] = useDrag(node, gestureHandlers)
useLockFrameStampPosition(isDragging, -1)
return [isDragging] return [isDragging]
} }

View file

@ -1,10 +1,8 @@
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {VoidFn} from '@theatre/shared/utils/types'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {clamp} from 'lodash-es' import {clamp} from 'lodash-es'
import React, {useMemo, useRef} from 'react' import React, {useMemo, useRef} from 'react'
@ -125,115 +123,130 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
propsRef.current = props propsRef.current = props
const handlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const handlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
let scaledToUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let verticalToExtremumSpace: SequenceEditorPanelLayout['graphEditorVerticalSpace']['toExtremumSpace']
let propsAtStartOfDrag: IProps
let tempTransaction: CommitOrDiscard | undefined
let unlockExtremums: VoidFn | undefined
return { return {
debugName: 'CurveHandler/useOurDrags', debugName: 'CurveHandler/useOurDrags',
lockCursorTo: 'move', lockCursorTo: 'move',
onDragStart() { onDragStart() {
propsAtStartOfDrag = propsRef.current let tempTransaction: CommitOrDiscard | undefined
scaledToUnitSpace = val( const propsAtStartOfDrag = propsRef.current
const scaledToUnitSpace = val(
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace, propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
) )
verticalToExtremumSpace = val( const verticalToExtremumSpace = val(
propsAtStartOfDrag.layoutP.graphEditorVerticalSpace.toExtremumSpace, propsAtStartOfDrag.layoutP.graphEditorVerticalSpace.toExtremumSpace,
) )
unlockExtremums = propsAtStartOfDrag.extremumSpace.lock() const unlockExtremums = propsAtStartOfDrag.extremumSpace.lock()
},
onDrag(dxInScaledSpace, dy) { return {
if (tempTransaction) { onDrag(dxInScaledSpace, dy) {
tempTransaction.discard() if (tempTransaction) {
tempTransaction = undefined tempTransaction.discard()
tempTransaction = undefined
}
const {index, trackData} = propsAtStartOfDrag
const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1]
const dPosInUnitSpace = scaledToUnitSpace(dxInScaledSpace)
let dPosInKeyframeDiffSpace =
dPosInUnitSpace / (next.position - cur.position)
const dyInVerticalSpace = -dy
const dYInExtremumSpace = verticalToExtremumSpace(dyInVerticalSpace)
const dYInValueSpace =
propsAtStartOfDrag.extremumSpace.deltaToValueSpace(
dYInExtremumSpace,
)
const curValue = props.isScalar ? (cur.value as number) : 0
const nextValue = props.isScalar ? (next.value as number) : 1
const dyInKeyframeDiffSpace =
dYInValueSpace / (nextValue - curValue)
if (propsAtStartOfDrag.which === 'left') {
const handleX = clamp(
cur.handles[2] + dPosInKeyframeDiffSpace,
0,
1,
)
const handleY = cur.handles[3] + dyInKeyframeDiffSpace
tempTransaction = getStudio()!.tempTransaction(
({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
trackId: propsAtStartOfDrag.trackId,
keyframes: [
{
...cur,
handles: [
cur.handles[0],
cur.handles[1],
handleX,
handleY,
],
},
],
},
)
},
)
} else {
const handleX = clamp(
next.handles[0] + dPosInKeyframeDiffSpace,
0,
1,
)
const handleY = next.handles[1] + dyInKeyframeDiffSpace
tempTransaction = getStudio()!.tempTransaction(
({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
keyframes: [
{
...next,
handles: [
handleX,
handleY,
next.handles[2],
next.handles[3],
],
},
],
},
)
},
)
}
},
onDragEnd(dragHappened) {
unlockExtremums()
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
},
} }
const {index, trackData} = propsAtStartOfDrag
const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1]
const dPosInUnitSpace = scaledToUnitSpace(dxInScaledSpace)
let dPosInKeyframeDiffSpace =
dPosInUnitSpace / (next.position - cur.position)
const dyInVerticalSpace = -dy
const dYInExtremumSpace = verticalToExtremumSpace(dyInVerticalSpace)
const dYInValueSpace =
propsAtStartOfDrag.extremumSpace.deltaToValueSpace(dYInExtremumSpace)
const curValue = props.isScalar ? (cur.value as number) : 0
const nextValue = props.isScalar ? (next.value as number) : 1
const dyInKeyframeDiffSpace = dYInValueSpace / (nextValue - curValue)
if (propsAtStartOfDrag.which === 'left') {
const handleX = clamp(cur.handles[2] + dPosInKeyframeDiffSpace, 0, 1)
const handleY = cur.handles[3] + dyInKeyframeDiffSpace
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
trackId: propsAtStartOfDrag.trackId,
keyframes: [
{
...cur,
handles: [cur.handles[0], cur.handles[1], handleX, handleY],
},
],
},
)
})
} else {
const handleX = clamp(next.handles[0] + dPosInKeyframeDiffSpace, 0, 1)
const handleY = next.handles[1] + dyInKeyframeDiffSpace
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
keyframes: [
{
...next,
handles: [
handleX,
handleY,
next.handles[2],
next.handles[3],
],
},
],
},
)
})
}
},
onDragEnd(dragHappened) {
if (unlockExtremums) {
const unlock = unlockExtremums
unlockExtremums = undefined
unlock()
}
if (dragHappened) {
if (tempTransaction) {
tempTransaction.commit()
}
} else {
if (tempTransaction) {
tempTransaction.discard()
}
}
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -1,10 +1,8 @@
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {VoidFn} from '@theatre/shared/utils/types'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -107,68 +105,62 @@ function useDragKeyframe(
propsRef.current = _props propsRef.current = _props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let propsAtStartOfDrag: IProps
let tempTransaction: CommitOrDiscard | undefined
let unlockExtremums: VoidFn | undefined
return { return {
debugName: 'GraphEditorDotNonScalar/useDragKeyframe', debugName: 'GraphEditorDotNonScalar/useDragKeyframe',
lockCursorTo: 'ew-resize', lockCursorTo: 'ew-resize',
onDragStart(event) { onDragStart(event) {
setIsDragging(true) setIsDragging(true)
const propsAtStartOfDrag = propsRef.current
propsAtStartOfDrag = propsRef.current const toUnitSpace = val(
propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
)
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) const unlockExtremums = propsAtStartOfDrag.extremumSpace.lock()
let tempTransaction: CommitOrDiscard | undefined
unlockExtremums = propsAtStartOfDrag.extremumSpace.lock() return {
}, onDrag(dx, dy) {
onDrag(dx, dy) { const original =
const original = propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
const deltaPos = toUnitSpace(dx) const deltaPos = toUnitSpace(dx)
const updatedKeyframes: Keyframe[] = [] const updatedKeyframes: Keyframe[] = []
const cur: Keyframe = { const cur: Keyframe = {
...original, ...original,
position: original.position + deltaPos, position: original.position + deltaPos,
value: original.value, value: original.value,
handles: [...original.handles], handles: [...original.handles],
}
updatedKeyframes.push(cur)
tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId,
keyframes: updatedKeyframes,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
setIsDragging(false)
unlockExtremums()
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
},
} }
updatedKeyframes.push(cur)
tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{
...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId,
keyframes: updatedKeyframes,
snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
setIsDragging(false)
if (unlockExtremums) {
const unlock = unlockExtremums
unlockExtremums = undefined
unlock()
}
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -1,10 +1,8 @@
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import type {VoidFn} from '@theatre/shared/utils/types'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -108,121 +106,122 @@ function useDragKeyframe(
propsRef.current = _props propsRef.current = _props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let propsAtStartOfDrag: IProps
let tempTransaction: CommitOrDiscard | undefined
let verticalToExtremumSpace: SequenceEditorPanelLayout['graphEditorVerticalSpace']['toExtremumSpace']
let unlockExtremums: VoidFn | undefined
let keepSpeeds = false
return { return {
debugName: 'GraphEditorDotScalar/useDragKeyframe', debugName: 'GraphEditorDotScalar/useDragKeyframe',
lockCursorTo: 'move', lockCursorTo: 'move',
onDragStart(event) { onDragStart(event) {
setIsDragging(true) setIsDragging(true)
keepSpeeds = !!event.altKey const keepSpeeds = !!event.altKey
propsAtStartOfDrag = propsRef.current const propsAtStartOfDrag = propsRef.current
toUnitSpace = val(propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace) const toUnitSpace = val(
verticalToExtremumSpace = val( propsAtStartOfDrag.layoutP.scaledSpace.toUnitSpace,
)
const verticalToExtremumSpace = val(
propsAtStartOfDrag.layoutP.graphEditorVerticalSpace.toExtremumSpace, propsAtStartOfDrag.layoutP.graphEditorVerticalSpace.toExtremumSpace,
) )
unlockExtremums = propsAtStartOfDrag.extremumSpace.lock() const unlockExtremums = propsAtStartOfDrag.extremumSpace.lock()
}, let tempTransaction: CommitOrDiscard | undefined
onDrag(dx, dy) {
const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
const deltaPos = toUnitSpace(dx) return {
const dyInVerticalSpace = -dy onDrag(dx, dy) {
const dYInExtremumSpace = verticalToExtremumSpace(dyInVerticalSpace) const original =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index]
const dYInValueSpace = const deltaPos = toUnitSpace(dx)
propsAtStartOfDrag.extremumSpace.deltaToValueSpace(dYInExtremumSpace) const dyInVerticalSpace = -dy
const dYInExtremumSpace = verticalToExtremumSpace(dyInVerticalSpace)
const updatedKeyframes: Keyframe[] = [] const dYInValueSpace =
propsAtStartOfDrag.extremumSpace.deltaToValueSpace(
dYInExtremumSpace,
)
const cur: Keyframe = { const updatedKeyframes: Keyframe[] = []
...original,
position: original.position + deltaPos,
value: (original.value as number) + dYInValueSpace,
handles: [...original.handles],
}
updatedKeyframes.push(cur) const cur: Keyframe = {
...original,
if (keepSpeeds) { position: original.position + deltaPos,
const prev = value: (original.value as number) + dYInValueSpace,
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index - 1] handles: [...original.handles],
if (
prev &&
Math.abs((original.value as number) - (prev.value as number)) > 0
) {
const newPrev: Keyframe = {
...prev,
handles: [...prev.handles],
} }
updatedKeyframes.push(newPrev)
newPrev.handles[3] = preserveRightHandle(
prev.handles[3],
prev.value as number,
prev.value as number,
original.value as number,
cur.value as number,
)
}
const next =
propsAtStartOfDrag.trackData.keyframes[propsAtStartOfDrag.index + 1]
if ( updatedKeyframes.push(cur)
next &&
Math.abs((original.value as number) - (next.value as number)) > 0 if (keepSpeeds) {
) { const prev =
const newNext: Keyframe = { propsAtStartOfDrag.trackData.keyframes[
...next, propsAtStartOfDrag.index - 1
handles: [...next.handles], ]
if (
prev &&
Math.abs((original.value as number) - (prev.value as number)) >
0
) {
const newPrev: Keyframe = {
...prev,
handles: [...prev.handles],
}
updatedKeyframes.push(newPrev)
newPrev.handles[3] = preserveRightHandle(
prev.handles[3],
prev.value as number,
prev.value as number,
original.value as number,
cur.value as number,
)
}
const next =
propsAtStartOfDrag.trackData.keyframes[
propsAtStartOfDrag.index + 1
]
if (
next &&
Math.abs((original.value as number) - (next.value as number)) >
0
) {
const newNext: Keyframe = {
...next,
handles: [...next.handles],
}
updatedKeyframes.push(newNext)
newNext.handles[1] = preserveLeftHandle(
newNext.handles[1],
newNext.value as number,
newNext.value as number,
original.value as number,
cur.value as number,
)
}
} }
updatedKeyframes.push(newNext)
newNext.handles[1] = preserveLeftHandle(
newNext.handles[1],
newNext.value as number,
newNext.value as number,
original.value as number,
cur.value as number,
)
}
}
tempTransaction?.discard() tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes( stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes(
{ {
...propsAtStartOfDrag.sheetObject.address, ...propsAtStartOfDrag.sheetObject.address,
trackId: propsAtStartOfDrag.trackId, trackId: propsAtStartOfDrag.trackId,
keyframes: updatedKeyframes, keyframes: updatedKeyframes,
snappingFunction: val( snappingFunction: val(
propsAtStartOfDrag.layoutP.sheet, propsAtStartOfDrag.layoutP.sheet,
).getSequence().closestGridPosition, ).getSequence().closestGridPosition,
}, },
) )
}) })
}, },
onDragEnd(dragHappened) { onDragEnd(dragHappened) {
setIsDragging(false) setIsDragging(false)
if (unlockExtremums) { unlockExtremums()
const unlock = unlockExtremums if (dragHappened) {
unlockExtremums = undefined tempTransaction?.commit()
unlock() } else {
tempTransaction?.discard()
}
},
} }
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
tempTransaction = undefined
}, },
} }
}, []) }, [])

View file

@ -1,7 +1,7 @@
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {prism, val} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse'
import {usePrism, useVal} from '@theatre/react' import {usePrism, useVal} from '@theatre/react'
import type {$IntentionalAny, IRange} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
@ -179,81 +179,73 @@ const FocusRangeStrip: React.FC<{
const sheet = useVal(layoutP.sheet) const sheet = useVal(layoutP.sheet)
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => { const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
let sequence = sheet.getSequence()
let startPosBeforeDrag: number,
endPosBeforeDrag: number,
tempTransaction: CommitOrDiscard | undefined
let dragHappened = false
let existingRange: {enabled: boolean; range: IRange<number>} | undefined
let target: HTMLDivElement | undefined
let newStartPosition: number, newEndPosition: number let newStartPosition: number, newEndPosition: number
return { return {
debugName: 'FocusRangeStrip', debugName: 'FocusRangeStrip',
onDragStart(event) { onDragStart(event) {
existingRange = existingRangeD.getValue() let tempTransaction: CommitOrDiscard | undefined
let existingRange = existingRangeD.getValue()
if (!existingRange) return false
if (existingRange) { const startPosBeforeDrag = existingRange.range.start
startPosBeforeDrag = existingRange.range.start const endPosBeforeDrag = existingRange.range.end
endPosBeforeDrag = existingRange.range.end let dragHappened = false
dragHappened = false const sequence = val(layoutP.sheet).getSequence()
sequence = val(layoutP.sheet).getSequence() isDraggingRef.current = true
target = event.target as HTMLDivElement
isDraggingRef.current = true
} else {
return false
}
},
onDrag(dx) {
existingRange = existingRangeD.getValue()
if (existingRange) {
dragHappened = true
const deltaPos = scaledSpaceToUnitSpace(dx)
const start = startPosBeforeDrag + deltaPos return {
let end = endPosBeforeDrag + deltaPos onDrag(dx) {
existingRange = existingRangeD.getValue()
if (existingRange) {
dragHappened = true
const deltaPos = scaledSpaceToUnitSpace(dx)
if (end < start) { const start = startPosBeforeDrag + deltaPos
end = start let end = endPosBeforeDrag + deltaPos
}
;[newStartPosition, newEndPosition] = clampRange( if (end < start) {
[start, end], end = start
[0, sequence.length], }
).map((pos) => sequence.closestGridPosition(pos))
if (tempTransaction) { ;[newStartPosition, newEndPosition] = clampRange(
tempTransaction.discard() [start, end],
} [0, sequence.length],
).map((pos) => sequence.closestGridPosition(pos))
tempTransaction = getStudio().tempTransaction(({stateEditors}) => { if (tempTransaction) {
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set( tempTransaction.discard()
{ }
...sheet.address,
range: { tempTransaction = getStudio().tempTransaction(
start: newStartPosition, ({stateEditors}) => {
end: newEndPosition, stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set(
{
...sheet.address,
range: {
start: newStartPosition,
end: newEndPosition,
},
enabled: existingRange?.enabled ?? true,
},
)
}, },
enabled: existingRange?.enabled ?? true, )
}, }
) },
}) onDragEnd() {
} isDraggingRef.current = false
}, if (existingRange) {
onDragEnd() { if (dragHappened && tempTransaction !== undefined) {
isDraggingRef.current = false tempTransaction.commit()
if (existingRange) { } else if (tempTransaction) {
if (dragHappened && tempTransaction !== undefined) { tempTransaction.discard()
tempTransaction.commit() }
} else if (tempTransaction) { }
tempTransaction.discard() },
}
tempTransaction = undefined
}
if (target !== undefined) {
target = undefined
} }
}, },
lockCursorTo: 'grabbing', lockCursorTo: 'grabbing',
} }
}, [sheet, scaledSpaceToUnitSpace]) }, [sheet, scaledSpaceToUnitSpace])

View file

@ -22,7 +22,6 @@ import {
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 DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' 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'}>`
@ -172,87 +171,84 @@ const FocusRangeThumb: React.FC<{
) )
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => { const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
let defaultRange: IRange
let range: IRange
let focusRangeEnabled: boolean
let posBeforeDrag: number
let tempTransaction: CommitOrDiscard | undefined
let minFocusRangeStripWidth: number
let sheet: Sheet
let scaledSpaceToUnitSpace: (s: number) => number
return { return {
debugName: 'FocusRangeThumb', debugName: 'FocusRangeThumb',
onDragStart() { onDragStart() {
sheet = val(layoutP.sheet) let tempTransaction: CommitOrDiscard | undefined
let range: IRange
const sheet = val(layoutP.sheet)
const sequence = sheet.getSequence() const sequence = sheet.getSequence()
defaultRange = {start: 0, end: sequence.length} const defaultRange = {start: 0, end: sequence.length}
let existingRange = existingRangeD.getValue() || { let existingRange = existingRangeD.getValue() || {
range: defaultRange, range: defaultRange,
enabled: false, enabled: false,
} }
focusRangeEnabled = existingRange.enabled const focusRangeEnabled = existingRange.enabled
posBeforeDrag = existingRange.range[thumbType] const posBeforeDrag = existingRange.range[thumbType]
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace) const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
minFocusRangeStripWidth = scaledSpaceToUnitSpace( const minFocusRangeStripWidth = scaledSpaceToUnitSpace(
focusRangeStripTheme.rangeStripMinWidth, focusRangeStripTheme.rangeStripMinWidth,
) )
},
onDrag(dx, _, event) { return {
let newPosition: number onDrag(dx, _, event) {
const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { let newPosition: number
ignore: hitZoneNode, const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {
}) ignore: hitZoneNode,
if (snapPos != null) { })
newPosition = snapPos 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
newPosition = Math.max(
Math.min(
oldPosPlusDeltaPos,
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,
),
sheet.getSequence().length,
)
}
const newPositionInFrame = sheet
.getSequence()
.closestGridPosition(newPosition)
if (tempTransaction !== undefined) {
tempTransaction.discard()
}
tempTransaction = getStudio().tempTransaction(({stateEditors}) => {
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set(
{
...sheet.address,
range: {...range, [thumbType]: newPositionInFrame},
enabled: focusRangeEnabled,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
},
} }
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,
),
0,
)
} else {
// Prevent the start thumb from going over the length of the sequence
newPosition = Math.min(
Math.max(
oldPosPlusDeltaPos,
range['start'] + minFocusRangeStripWidth,
),
sheet.getSequence().length,
)
}
const newPositionInFrame = sheet
.getSequence()
.closestGridPosition(newPosition)
if (tempTransaction !== undefined) {
tempTransaction.discard()
}
tempTransaction = getStudio().tempTransaction(({stateEditors}) => {
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set(
{
...sheet.address,
range: {...range, [thumbType]: newPositionInFrame},
enabled: focusRangeEnabled,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
}, },
} }
}, [layoutP]) }, [layoutP])

View file

@ -1,9 +1,7 @@
import type Sequence from '@theatre/core/sequences/Sequence'
import type Sheet from '@theatre/core/sheets/Sheet'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {prism, val} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse'
import {usePrism} from '@theatre/react' import {usePrism} from '@theatre/react'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import { import {
panelDimsToPanelPosition, panelDimsToPanelPosition,
@ -102,152 +100,150 @@ function usePanelDragZoneGestureHandlers(
const focusRangeCreationGestureHandlers = (): Parameters< const focusRangeCreationGestureHandlers = (): Parameters<
typeof useDrag typeof useDrag
>[1] => { >[1] => {
let startPosInUnitSpace: number,
tempTransaction: CommitOrDiscard | undefined
let clippedSpaceToUnitSpace: (s: number) => number
let scaledSpaceToUnitSpace: (s: number) => number
let sequence: Sequence
let sheet: Sheet
let minFocusRangeStripWidth: number
return { return {
debugName: 'FocusRangeZone/focusRangeCreationGestureHandlers', debugName: 'FocusRangeZone/focusRangeCreationGestureHandlers',
onDragStart(event) { onDragStart(event) {
clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace) let tempTransaction: CommitOrDiscard | undefined
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
sheet = val(layoutP.sheet) const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
sequence = sheet.getSequence() const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
const sheet = val(layoutP.sheet)
const sequence = sheet.getSequence()
const targetElement: HTMLElement = event.target as HTMLElement const targetElement: HTMLElement = event.target as HTMLElement
const rect = targetElement!.getBoundingClientRect() const rect = targetElement!.getBoundingClientRect()
startPosInUnitSpace = clippedSpaceToUnitSpace( const startPosInUnitSpace = clippedSpaceToUnitSpace(
event.clientX - rect.left, event.clientX - rect.left,
) )
minFocusRangeStripWidth = scaledSpaceToUnitSpace( const minFocusRangeStripWidth = scaledSpaceToUnitSpace(
focusRangeStripTheme.rangeStripMinWidth, focusRangeStripTheme.rangeStripMinWidth,
) )
},
onDrag(dx) {
const deltaPos = scaledSpaceToUnitSpace(dx)
let start = startPosInUnitSpace return {
let end = startPosInUnitSpace + deltaPos onDrag(dx) {
const deltaPos = scaledSpaceToUnitSpace(dx)
;[start, end] = [ let start = startPosInUnitSpace
clamp(start, 0, sequence.length), let end = startPosInUnitSpace + deltaPos
clamp(end, 0, sequence.length),
].map((pos) => sequence.closestGridPosition(pos))
if (end < start) { ;[start, end] = [
;[start, end] = [ clamp(start, 0, sequence.length),
Math.max(Math.min(end, start - minFocusRangeStripWidth), 0), clamp(end, 0, sequence.length),
start, ].map((pos) => sequence.closestGridPosition(pos))
]
} else if (dx > 0) { if (end < start) {
end = Math.min( ;[start, end] = [
Math.max(end, start + minFocusRangeStripWidth), Math.max(Math.min(end, start - minFocusRangeStripWidth), 0),
sequence.length, start,
) ]
} else if (dx > 0) {
end = Math.min(
Math.max(end, start + minFocusRangeStripWidth),
sequence.length,
)
}
if (tempTransaction) {
tempTransaction.discard()
}
tempTransaction = getStudio().tempTransaction(
({stateEditors}) => {
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set(
{
...sheet.address,
range: {start, end},
enabled: true,
},
)
},
)
},
onDragEnd(dragHappened) {
if (dragHappened && tempTransaction !== undefined) {
tempTransaction.commit()
} else if (tempTransaction) {
tempTransaction.discard()
}
},
} }
if (tempTransaction) {
tempTransaction.discard()
}
tempTransaction = getStudio().tempTransaction(({stateEditors}) => {
stateEditors.studio.ahistoric.projects.stateByProjectId.stateBySheetId.sequence.focusRange.set(
{
...sheet.address,
range: {start, end},
enabled: true,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened && tempTransaction !== undefined) {
tempTransaction.commit()
} else if (tempTransaction) {
tempTransaction.discard()
}
tempTransaction = undefined
}, },
lockCursorTo: 'ew-resize', lockCursorTo: 'ew-resize',
} }
} }
const panelMoveGestureHandlers = (): Parameters<typeof useDrag>[1] => { const panelMoveGestureHandlers = (): Parameters<typeof useDrag>[1] => {
let stuffBeforeDrag = panelStuffRef.current
let tempTransaction: CommitOrDiscard | undefined
let unlock: VoidFn | undefined
return { return {
debugName: 'FocusRangeZone/panelMoveGestureHandlers', debugName: 'FocusRangeZone/panelMoveGestureHandlers',
onDragStart() { onDragStart() {
stuffBeforeDrag = panelStuffRef.current let tempTransaction: CommitOrDiscard | undefined
if (unlock) { const stuffBeforeDrag = panelStuffRef.current
const u = unlock
unlock = undefined
u()
}
unlock = panelStuffRef.current.addBoundsHighlightLock()
},
onDrag(dx, dy) {
const newDims: typeof panelStuffRef.current['dims'] = {
...stuffBeforeDrag.dims,
top: stuffBeforeDrag.dims.top + dy,
left: stuffBeforeDrag.dims.left + dx,
}
const position = panelDimsToPanelPosition(newDims, {
width: window.innerWidth,
height: window.innerHeight,
})
tempTransaction?.discard() const unlock = panelStuffRef.current.addBoundsHighlightLock()
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.studio.historic.panelPositions.setPanelPosition({ return {
position, onDrag(dx, dy) {
panelId: stuffBeforeDrag.panelId, const newDims: typeof panelStuffRef.current['dims'] = {
}) ...stuffBeforeDrag.dims,
}) top: stuffBeforeDrag.dims.top + dy,
}, left: stuffBeforeDrag.dims.left + dx,
onDragEnd(dragHappened) { }
if (unlock) { const position = panelDimsToPanelPosition(newDims, {
const u = unlock width: window.innerWidth,
unlock = undefined height: window.innerHeight,
u() })
tempTransaction?.discard()
tempTransaction = getStudio()!.tempTransaction(
({stateEditors}) => {
stateEditors.studio.historic.panelPositions.setPanelPosition({
position,
panelId: stuffBeforeDrag.panelId,
})
},
)
},
onDragEnd(dragHappened) {
unlock()
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
},
} }
if (dragHappened) {
tempTransaction?.commit()
} else {
tempTransaction?.discard()
}
tempTransaction = undefined
}, },
lockCursorTo: 'move', lockCursorTo: 'move',
} }
} }
let currentGestureHandlers: undefined | Parameters<typeof useDrag>[1]
return { return {
debugName: 'FocusRangeZone', debugName: 'FocusRangeZone',
onDragStart(event) { onDragStart(event) {
if (event.shiftKey) { const [_mode, currentGestureHandlers] = event.shiftKey
setMode('creating') ? [
currentGestureHandlers = focusRangeCreationGestureHandlers() 'creating' as 'creating',
} else { focusRangeCreationGestureHandlers().onDragStart(event),
setMode('moving-panel') ]
currentGestureHandlers = panelMoveGestureHandlers() : [
'moving-panel' as 'moving-panel',
panelMoveGestureHandlers().onDragStart(event),
]
setMode(_mode)
if (currentGestureHandlers === false) return false
return {
onDrag(dx, dy, event, ddx, ddy) {
currentGestureHandlers.onDrag(dx, dy, event, ddx, ddy)
},
onDragEnd(dragHappened) {
setMode('none')
currentGestureHandlers.onDragEnd?.(dragHappened)
},
} }
currentGestureHandlers.onDragStart!(event)
},
onDrag(dx, dy, event) {
currentGestureHandlers!.onDrag(dx, dy, event)
},
onDragEnd(dragHappened) {
setMode('none')
currentGestureHandlers!.onDragEnd!(dragHappened)
}, },
} }
}, [layoutP, panelStuffRef]) }, [layoutP, panelStuffRef])

View file

@ -247,46 +247,44 @@ function useDragMarker(
propsRef.current = props propsRef.current = props
const useDragOpts = useMemo<UseDragOpts>(() => { const useDragOpts = useMemo<UseDragOpts>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let tempTransaction: CommitOrDiscard | undefined
let markerAtStartOfDrag: StudioHistoricStateSequenceEditorMarker
return { return {
debugName: `MarkerDot/useDragMarker (${props.marker.id})`, debugName: `MarkerDot/useDragMarker (${props.marker.id})`,
onDragStart(_event) { onDragStart(_event) {
markerAtStartOfDrag = propsRef.current.marker const markerAtStartOfDrag = propsRef.current.marker
toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace) const toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace)
}, let tempTransaction: CommitOrDiscard | undefined
onDrag(dx, _dy, event) {
const original = markerAtStartOfDrag
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,
)
tempTransaction?.discard() return {
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => { onDrag(dx, _dy, event) {
stateEditors.studio.historic.projects.stateByProjectId.stateBySheetId.sequenceEditor.replaceMarkers( const original = markerAtStartOfDrag
{ const newPosition = Math.max(
sheetAddress: val(props.layoutP.sheet.address), // check if our event hoversover a [data-pos] element
markers: [{...original, position: newPosition}], DopeSnap.checkIfMouseEventSnapToPos(event, {
snappingFunction: val(props.layoutP.sheet).getSequence() ignore: node,
.closestGridPosition, }) ??
}, // if we don't find snapping target, check the distance dragged + original position
) original.position + toUnitSpace(dx),
}) // sanitize to minimum of zero
}, 0,
onDragEnd(dragHappened) { )
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard() tempTransaction?.discard()
tempTransaction = undefined tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
stateEditors.studio.historic.projects.stateByProjectId.stateBySheetId.sequenceEditor.replaceMarkers(
{
sheetAddress: val(props.layoutP.sheet.address),
markers: [{...original, position: newPosition}],
snappingFunction: val(props.layoutP.sheet).getSequence()
.closestGridPosition,
},
)
})
},
onDragEnd(dragHappened) {
if (dragHappened) tempTransaction?.commit()
else tempTransaction?.discard()
},
}
}, },
} }
}, []) }, [])

View file

@ -1,4 +1,3 @@
import type Sequence from '@theatre/core/sequences/Sequence'
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import RoomToClick from '@theatre/studio/uiComponents/RoomToClick' import RoomToClick from '@theatre/studio/uiComponents/RoomToClick'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
@ -203,32 +202,31 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
) )
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => { const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
let posBeforeSeek = 0
let sequence: Sequence
let scaledSpaceToUnitSpace: typeof layoutP.scaledSpace.toUnitSpace.$$__pointer_type
return { return {
debugName: 'Playhead', debugName: 'Playhead',
onDragStart() { onDragStart() {
sequence = val(layoutP.sheet).getSequence() const setIsSeeking = val(layoutP.seeker.setIsSeeking)
posBeforeSeek = sequence.position
scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
setIsSeeking(true)
},
onDrag(dx, _, event) {
const deltaPos = scaledSpaceToUnitSpace(dx)
sequence.position = const sequence = val(layoutP.sheet).getSequence()
DopeSnap.checkIfMouseEventSnapToPos(event, { const posBeforeSeek = sequence.position
ignore: thumbNode, const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
}) ?? setIsSeeking(true)
// unsnapped
clamp(posBeforeSeek + deltaPos, 0, sequence.length) return {
}, onDrag(dx, _, event) {
onDragEnd() { const deltaPos = scaledSpaceToUnitSpace(dx)
setIsSeeking(false)
sequence.position =
DopeSnap.checkIfMouseEventSnapToPos(event, {
ignore: thumbNode,
}) ??
// unsnapped
clamp(posBeforeSeek + deltaPos, 0, sequence.length)
},
onDragEnd() {
setIsSeeking(false)
},
}
}, },
} }
}, []) }, [])

View file

@ -4,6 +4,28 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {useCssCursorLock} from './PointerEventsHandler' import {useCssCursorLock} from './PointerEventsHandler'
import type {CapturedPointer} from '@theatre/studio/UIRoot/PointerCapturing' import type {CapturedPointer} from '@theatre/studio/UIRoot/PointerCapturing'
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing' import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
import noop from '@theatre/shared/utils/noop'
export enum MouseButton {
Left = 0,
Middle = 1,
// Not including Right because it _might_ interfere with chord clicking.
// So we'll wait for chord-clicking to land before exploring right-button gestures
}
/**
* dx, dy: delta x/y from the start of the drag
*/
type OnDragCallback = (
// deltaX/Y are counted from the start of the drag
deltaX: number,
deltaY: number,
event: MouseEvent,
dxFromLastEvent: number,
dyFromLastEvent: number,
) => void
type OnDragEndCallback = (dragHappened: boolean) => void
export type UseDragOpts = { export type UseDragOpts = {
/** /**
@ -35,22 +57,23 @@ export type UseDragOpts = {
* onDragStart can be undefined, in which case, we always handle useDrag, * onDragStart can be undefined, in which case, we always handle useDrag,
* but when defined, we can allow the handler to return false to indicate ignore this dragging * but when defined, we can allow the handler to return false to indicate ignore this dragging
*/ */
onDragStart?: (event: MouseEvent) => void | false onDragStart: (event: MouseEvent) =>
/** | false
* Called at the end of the drag gesture. | {
* `dragHappened` will be `true` if the user actually moved the pointer /**
* (if onDrag isn't called, then this will be false becuase the user hasn't moved the pointer) * Called at the end of the drag gesture.
*/ * `dragHappened` will be `true` if the user actually moved the pointer
onDragEnd?: (dragHappened: boolean) => void * (if onDrag isn't called, then this will be false becuase the user hasn't moved the pointer)
/** */
* This will be called 0 times if the gesture ends up being a click, onDragEnd?: OnDragEndCallback
* or 1 or more times if it ends up being a drag gesture. onDrag: OnDragCallback
* }
* `dx`: the delta x
* `dy`: the delta y // which mouse button to use the drag event
* `event`: the mouse event buttons?:
*/ | [MouseButton]
onDrag: (dx: number, dy: number, event: MouseEvent) => void | [MouseButton, MouseButton]
| [MouseButton | MouseButton | MouseButton]
} }
export default function useDrag( export default function useDrag(
@ -80,9 +103,15 @@ export default function useDrag(
} }
}>({dragHappened: false, startPos: {x: 0, y: 0}}) }>({dragHappened: false, startPos: {x: 0, y: 0}})
const callbacksRef = useRef<{
onDrag: OnDragCallback
onDragEnd: OnDragEndCallback
}>({onDrag: noop, onDragEnd: noop})
const capturedPointerRef = useRef<undefined | CapturedPointer>() const capturedPointerRef = useRef<undefined | CapturedPointer>()
useLayoutEffect(() => { useLayoutEffect(() => {
if (!target) return if (!target) return
let lastDeltas = [0, 0]
const getDistances = (event: MouseEvent): [number, number] => { const getDistances = (event: MouseEvent): [number, number] => {
const {startPos} = stateRef.current const {startPos} = stateRef.current
@ -94,14 +123,26 @@ export default function useDrag(
modeRef.current = 'dragging' modeRef.current = 'dragging'
const deltas = getDistances(event) const deltas = getDistances(event)
optsRef.current.onDrag(deltas[0], deltas[1], event) const [deltaFromLastX, deltaFromLastY] = [
deltas[0] - lastDeltas[0],
deltas[1] - lastDeltas[1],
]
lastDeltas = deltas
callbacksRef.current.onDrag(
deltas[0],
deltas[1],
event,
deltaFromLastX,
deltaFromLastY,
)
} }
const dragEndHandler = () => { const dragEndHandler = () => {
removeDragListeners() removeDragListeners()
modeRef.current = 'notDragging' modeRef.current = 'notDragging'
optsRef.current.onDragEnd?.(stateRef.current.dragHappened) callbacksRef.current.onDragEnd(stateRef.current.dragHappened)
} }
const addDragListeners = () => { const addDragListeners = () => {
@ -136,13 +177,18 @@ export default function useDrag(
const opts = optsRef.current const opts = optsRef.current
if (opts.disabled === true) return if (opts.disabled === true) return
if (event.button !== 0) return const acceptedButtons: MouseButton[] = opts.buttons ?? [MouseButton.Left]
// onDragStart can be undefined, in which case, we always handle useDrag, if (!acceptedButtons.includes(event.button)) return
// but when defined, we can allow the handler to return false to indicate ignore this dragging
if (opts.onDragStart != null) { const returnOfOnDragStart = opts.onDragStart(event)
const shouldIgnore = opts.onDragStart(event) === false
if (shouldIgnore) return if (returnOfOnDragStart === false) {
// we should ignore the gesture
return
} else {
callbacksRef.current.onDrag = returnOfOnDragStart.onDrag
callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop
} }
// need to capture pointer after we know the provided handler wants to handle drag start // need to capture pointer after we know the provided handler wants to handle drag start
@ -175,7 +221,7 @@ export default function useDrag(
target.removeEventListener('click', preventUnwantedClick as $FixMe) target.removeEventListener('click', preventUnwantedClick as $FixMe)
if (modeRef.current !== 'notDragging') { if (modeRef.current !== 'notDragging') {
optsRef.current.onDragEnd?.(modeRef.current === 'dragging') callbacksRef.current.onDragEnd?.(modeRef.current === 'dragging')
} }
modeRef.current = 'notDragging' modeRef.current = 'notDragging'
} }