Initial marker features
- Add comments about cursor lock, framestamp lock - Add PointableSet, Mark Ids, dataverse usage fix - Add marker dragging, fix PointerCapturing Co-authored-by: Cole Lawrence <cole@colelawrence.com> Co-authored-by: Aria <aria.minaei@gmail.com>
This commit is contained in:
parent
a3b1938d43
commit
030b6d2804
22 changed files with 722 additions and 73 deletions
|
@ -28,6 +28,7 @@ export type HistoricPositionalSequence = {
|
|||
type: 'PositionalSequence'
|
||||
length: number
|
||||
/**
|
||||
* Given the most common case of tracking a sequence against time (where 1 second = position 1),
|
||||
* If set to, say, 30, then the keyframe editor will try to snap all keyframes
|
||||
* to a 30fps grid
|
||||
*/
|
||||
|
|
94
theatre/shared/src/utils/PointableSet.ts
Normal file
94
theatre/shared/src/utils/PointableSet.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import type {StrictRecord} from './types'
|
||||
|
||||
/**
|
||||
* Consistent way to maintain an unordered structure which plays well with dataverse pointers and
|
||||
* data change tracking:
|
||||
* - paths to items are stable;
|
||||
* - changing one item does not change `allIds`, so it can be mapped over in React without
|
||||
* unnecessary re-renders
|
||||
*/
|
||||
export type PointableSet<Id extends string, V> = {
|
||||
/**
|
||||
* Usually accessed through pointers before accessing the actual value, so we can separate
|
||||
* changes to individual items from changes to the entire list of items.
|
||||
*/
|
||||
byId: StrictRecord<Id, V>
|
||||
/**
|
||||
* Separate list of ids allows us to recognize changes to set item memberships, without being triggered changed when
|
||||
* the contents of its items have changed.
|
||||
*/
|
||||
allIds: StrictRecord<Id, true>
|
||||
}
|
||||
|
||||
export const pointableSetUtil = {
|
||||
create<Id extends string, V>(
|
||||
values?: Iterable<[Id, V]>,
|
||||
): PointableSet<Id, V> {
|
||||
const set: PointableSet<Id, V> = {byId: {}, allIds: {}}
|
||||
if (values) {
|
||||
for (const [id, value] of values) {
|
||||
set.byId[id] = value
|
||||
set.allIds[id] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
},
|
||||
shallowCopy<Id extends string, V>(
|
||||
existing: PointableSet<Id, V> | undefined,
|
||||
): PointableSet<Id, V> {
|
||||
return {
|
||||
byId: {...existing?.byId},
|
||||
allIds: {...existing?.allIds},
|
||||
}
|
||||
},
|
||||
add<Id extends string, V>(
|
||||
existing: PointableSet<Id, V> | undefined,
|
||||
id: Id,
|
||||
value: V,
|
||||
): PointableSet<Id, V> {
|
||||
return {
|
||||
byId: {...existing?.byId, [id]: value},
|
||||
allIds: {...existing?.allIds, [id]: true},
|
||||
}
|
||||
},
|
||||
merge<Id extends string, V>(
|
||||
sets: PointableSet<Id, V>[],
|
||||
): PointableSet<Id, V> {
|
||||
const target = pointableSetUtil.create<Id, V>()
|
||||
for (let i = 0; i < sets.length; i++) {
|
||||
target.byId = {...target.byId, ...sets[i].byId}
|
||||
target.allIds = {...target.allIds, ...sets[i].allIds}
|
||||
}
|
||||
return target
|
||||
},
|
||||
remove<Id extends string, V>(
|
||||
existing: PointableSet<Id, V>,
|
||||
id: Id,
|
||||
): PointableSet<Id, V> {
|
||||
const set = pointableSetUtil.shallowCopy(existing)
|
||||
delete set.allIds[id]
|
||||
delete set.byId[id]
|
||||
return set
|
||||
},
|
||||
/**
|
||||
* Note: this is not very performant (it's not crazy slow or anything)
|
||||
* it's just that it's not able to re-use object classes in v8 due to
|
||||
* excessive `delete obj[key]` instances.
|
||||
*
|
||||
* `Map`s would be faster, but they aren't used for synchronized JSON state stuff.
|
||||
* See {@link StrictRecord} for related conversation.
|
||||
*/
|
||||
filter<Id extends string, V>(
|
||||
existing: PointableSet<Id, V>,
|
||||
predicate: (value: V | undefined) => boolean | undefined | null,
|
||||
): PointableSet<Id, V> {
|
||||
const set = pointableSetUtil.shallowCopy(existing)
|
||||
for (const [id, value] of Object.entries(set.byId)) {
|
||||
if (!predicate(value)) {
|
||||
delete set.allIds[id]
|
||||
delete set.byId[id]
|
||||
}
|
||||
}
|
||||
return set
|
||||
},
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import {nanoid as generateNonSecure} from 'nanoid/non-secure'
|
||||
import type {Nominal} from './Nominal'
|
||||
import type {$IntentionalAny} from './types'
|
||||
|
||||
export type KeyframeId = Nominal<'KeyframeId'>
|
||||
|
||||
|
@ -9,7 +8,7 @@ export function generateKeyframeId(): KeyframeId {
|
|||
}
|
||||
|
||||
export function asKeyframeId(s: string): KeyframeId {
|
||||
return s as $IntentionalAny
|
||||
return s as KeyframeId
|
||||
}
|
||||
|
||||
export type ProjectId = Nominal<'ProjectId'>
|
||||
|
@ -17,14 +16,19 @@ export type SheetId = Nominal<'SheetId'>
|
|||
export type SheetInstanceId = Nominal<'SheetInstanceId'>
|
||||
export type PaneInstanceId = Nominal<'PaneInstanceId'>
|
||||
export type SequenceTrackId = Nominal<'SequenceTrackId'>
|
||||
export type SequenceMarkerId = Nominal<'SequenceMarkerId'>
|
||||
export type ObjectAddressKey = Nominal<'ObjectAddressKey'>
|
||||
/** UI panels can contain a {@link PaneInstanceId} or something else. */
|
||||
export type UIPanelId = Nominal<'UIPanelId'>
|
||||
|
||||
export function generateSequenceTrackId(): SequenceTrackId {
|
||||
return generateNonSecure(10) as $IntentionalAny as SequenceTrackId
|
||||
return generateNonSecure(10) as SequenceTrackId
|
||||
}
|
||||
|
||||
export function asSequenceTrackId(s: string): SequenceTrackId {
|
||||
return s as $IntentionalAny
|
||||
return s as SequenceTrackId
|
||||
}
|
||||
|
||||
export function generateSequenceMarkerId(): SequenceMarkerId {
|
||||
return generateNonSecure(10) as SequenceMarkerId
|
||||
}
|
||||
|
|
|
@ -68,7 +68,8 @@ export type DeepPartialOfSerializableValue<T extends SerializableValue> =
|
|||
* This is equivalent to `Partial<Record<Key, V>>` being used to describe a sort of Map
|
||||
* where the keys might not have values.
|
||||
*
|
||||
* We do not use `Map`s, because they add comlpexity with converting to `JSON.stringify` + pointer types
|
||||
* We do not use `Map`s or `Set`s, because they add complexity with converting to
|
||||
* `JSON.stringify` + pointer types.
|
||||
*/
|
||||
export type StrictRecord<Key extends string, V> = {[K in Key]?: V}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, {useContext, useMemo} from 'react'
|
||||
import React, {useContext, useEffect, useMemo} from 'react'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
|
||||
/** See {@link PointerCapturing} */
|
||||
export type CapturedPointer = {
|
||||
release(): void
|
||||
/** Double check that you still have the current capture and weren't forcibly released */
|
||||
isCapturing(): boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,41 +25,73 @@ export type PointerCapturing = {
|
|||
capturePointer(debugReason: string): CapturedPointer
|
||||
}
|
||||
|
||||
type PointerCapturingFn = (forDebugName: string) => PointerCapturing
|
||||
type InternalPointerCapturing = {
|
||||
capturing: PointerCapturing
|
||||
forceRelease(): void
|
||||
}
|
||||
|
||||
type PointerCapturingFn = (forDebugName: string) => InternalPointerCapturing
|
||||
|
||||
// const logger = console
|
||||
|
||||
function _usePointerCapturingContext(): PointerCapturingFn {
|
||||
let [currentCapture, setCurrentCapture] = React.useState<null | {
|
||||
type CaptureInfo = {
|
||||
debugOwnerName: string
|
||||
debugReason: string
|
||||
}>(null)
|
||||
}
|
||||
let currentCaptureRef = React.useRef<null | CaptureInfo>(null)
|
||||
|
||||
return (forDebugName) => {
|
||||
return {
|
||||
/** keep track of the captures being made by this user of {@link usePointerCapturing} */
|
||||
let localCapture: CaptureInfo | null
|
||||
const updateCapture = (to: CaptureInfo | null): CaptureInfo | null => {
|
||||
localCapture = to
|
||||
currentCaptureRef.current = to
|
||||
return to
|
||||
}
|
||||
const capturing: PointerCapturing = {
|
||||
capturePointer(reason) {
|
||||
// logger.log('Capturing pointer', {forDebugName, reason})
|
||||
if (currentCapture != null) {
|
||||
if (currentCaptureRef.current != null) {
|
||||
throw new Error(
|
||||
`"${forDebugName}" attempted capturing pointer for "${reason}" while already captured by "${currentCapture.debugOwnerName}" for "${currentCapture.debugReason}"`,
|
||||
`"${forDebugName}" attempted capturing pointer for "${reason}" while already captured by "${currentCaptureRef.current.debugOwnerName}" for "${currentCaptureRef.current.debugReason}"`,
|
||||
)
|
||||
}
|
||||
|
||||
setCurrentCapture({debugOwnerName: forDebugName, debugReason: reason})
|
||||
const releaseCapture = updateCapture({
|
||||
debugOwnerName: forDebugName,
|
||||
debugReason: reason,
|
||||
})
|
||||
|
||||
const releaseCapture = currentCapture
|
||||
return {
|
||||
isCapturing() {
|
||||
return releaseCapture === currentCaptureRef.current
|
||||
},
|
||||
release() {
|
||||
if (releaseCapture === currentCapture) {
|
||||
if (releaseCapture === currentCaptureRef.current) {
|
||||
// logger.log('Releasing pointer', {
|
||||
// forDebugName,
|
||||
// reason,
|
||||
// })
|
||||
setCurrentCapture(null)
|
||||
updateCapture(null)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
},
|
||||
isPointerBeingCaptured() {
|
||||
return currentCapture != null
|
||||
return currentCaptureRef.current != null
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
capturing,
|
||||
forceRelease() {
|
||||
if (currentCaptureRef.current === localCapture) {
|
||||
// logger.log('Force releasing pointer', currentCaptureRef.current)
|
||||
updateCapture(null)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +130,16 @@ export function ProvidePointerCapturing(props: {
|
|||
|
||||
export function usePointerCapturing(forDebugName: string): PointerCapturing {
|
||||
const pointerCapturingFn = useContext(PointerCapturingContext)
|
||||
return useMemo(() => {
|
||||
const control = useMemo(() => {
|
||||
return pointerCapturingFn(forDebugName)
|
||||
}, [forDebugName, pointerCapturingFn])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// force release on unmount
|
||||
control.forceRelease()
|
||||
}
|
||||
}, [forDebugName, pointerCapturingFn])
|
||||
|
||||
return control.capturing
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import {lighten} from 'polished'
|
||||
import {css} from 'styled-components'
|
||||
|
||||
/**
|
||||
* This CSS string is used to correctly set pointer-events on an element
|
||||
* when the pointer is dragging something.
|
||||
* Naming explanation: "NormalMode" as opposed to dragging mode.
|
||||
*
|
||||
* @see PointerEventsHandler - the place that sets `.normal` on #pointer-root
|
||||
*/
|
||||
export const pointerEventsAutoInNormalMode = css`
|
||||
#pointer-root & {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -11,12 +11,12 @@ import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
|||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {lighten} from 'polished'
|
||||
import React, {useMemo, useRef, useState} from 'react'
|
||||
import React, {useMemo, useRef} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import SnapCursor from './SnapCursor.svg'
|
||||
|
@ -42,7 +42,8 @@ const dotTheme = {
|
|||
},
|
||||
}
|
||||
|
||||
const Square = styled.div<{isSelected: boolean}>`
|
||||
/** The keyframe diamond ◆ */
|
||||
const Diamond = styled.div<{isSelected: boolean}>`
|
||||
position: absolute;
|
||||
${dims(DOT_SIZE_PX)}
|
||||
|
||||
|
@ -65,8 +66,11 @@ const HitZone = styled.div`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
|
||||
// ⸢⸤⸣⸥ thing
|
||||
// This box extends the hitzone so the user does not
|
||||
// accidentally leave the hitzone
|
||||
&:hover:after {
|
||||
position: absolute;
|
||||
top: calc(50% - ${SNAP_CURSOR_SIZE_PX / 2}px);
|
||||
|
@ -84,7 +88,7 @@ const HitZone = styled.div`
|
|||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:hover + ${Square}, &.beingDragged + ${Square} {
|
||||
&:hover + ${Diamond}, &.beingDragged + ${Diamond} {
|
||||
${dims(DOT_HOVER_SIZE_PX)}
|
||||
}
|
||||
`
|
||||
|
@ -109,7 +113,7 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
|||
}}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
/>
|
||||
<Square isSelected={!!props.selection} />
|
||||
<Diamond isSelected={!!props.selection} />
|
||||
{contextMenu}
|
||||
</>
|
||||
)
|
||||
|
@ -142,9 +146,6 @@ function useDragKeyframe(
|
|||
node: HTMLDivElement | null,
|
||||
props: IKeyframeDotProps,
|
||||
): [isDragging: boolean] {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||
|
||||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
|
||||
|
@ -158,10 +159,9 @@ function useDragKeyframe(
|
|||
| undefined
|
||||
|
||||
return {
|
||||
debugName: 'Dot/useDragKeyframe',
|
||||
debugName: 'KeyframeDot/useDragKeyframe',
|
||||
|
||||
onDragStart(event) {
|
||||
setIsDragging(true)
|
||||
const props = propsRef.current
|
||||
if (props.selection) {
|
||||
const {selection, leaf} = props
|
||||
|
@ -229,29 +229,21 @@ function useDragKeyframe(
|
|||
})
|
||||
},
|
||||
onDragEnd(dragHappened) {
|
||||
setIsDragging(false)
|
||||
|
||||
if (selectionDragHandlers) {
|
||||
selectionDragHandlers.onDragEnd?.(dragHappened)
|
||||
|
||||
selectionDragHandlers = undefined
|
||||
}
|
||||
if (dragHappened) {
|
||||
if (tempTransaction) {
|
||||
tempTransaction.commit()
|
||||
}
|
||||
} else {
|
||||
if (tempTransaction) {
|
||||
tempTransaction.discard()
|
||||
}
|
||||
}
|
||||
if (dragHappened) tempTransaction?.commit()
|
||||
else tempTransaction?.discard()
|
||||
tempTransaction = undefined
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
useDrag(node, useDragOpts)
|
||||
const [isDragging] = useDrag(node, useDragOpts)
|
||||
|
||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||
|
||||
return [isDragging]
|
||||
|
|
|
@ -40,6 +40,12 @@ type LockItem = {
|
|||
|
||||
let lastLockId = 0
|
||||
|
||||
/**
|
||||
* Provides snapping positions to "stamps".
|
||||
*
|
||||
* One example of a stamp includes the "Keyframe Dot" which show a `⌜⌞⌝⌟` kinda UI
|
||||
* around the dot when dragged over.
|
||||
*/
|
||||
const FrameStampPositionProvider: React.FC<{
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({children, layoutP}) => {
|
||||
|
@ -144,6 +150,13 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => {
|
|||
* Elements that need this behavior must set a data attribute like so:
|
||||
* <div data-theatre-lock-framestamp-to="120.55" />
|
||||
* Setting this attribute to "hide" hides the stamp.
|
||||
*
|
||||
* @see lockedCursorCssVarName - CSS variable used to set the cursor on an element that
|
||||
* should lock the framestamp. Look for usages.
|
||||
* @see pointerEventsAutoInNormalMode - CSS snippet used to correctly set
|
||||
* `pointer-events` on an element that should lock the framestamp.
|
||||
*
|
||||
* See {@link FrameStampPositionProvider}
|
||||
*/
|
||||
export const attributeNameThatLocksFramestamp =
|
||||
'data-theatre-lock-framestamp-to'
|
||||
|
|
|
@ -14,7 +14,7 @@ import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPa
|
|||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
|
@ -45,7 +45,7 @@ const HitZone = styled.circle`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPa
|
|||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
|
||||
|
@ -45,7 +45,7 @@ const HitZone = styled.circle`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/TopStrip'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||
|
@ -57,7 +57,7 @@ const TheDiv = styled.div<{enabled: boolean; type: 'start' | 'end'}>`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {useVal} from '@theatre/react'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import React, {useMemo, useRef} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
attributeNameThatLocksFramestamp,
|
||||
useLockFrameStampPosition,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import type {SequenceMarkerId} from '@theatre/shared/utils/ids'
|
||||
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||
import SnapCursor from './SnapCursor.svg'
|
||||
import MarkerDotVisual from './MarkerDotVisual.svg'
|
||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import type {StudioHistoricStateSequenceEditorMarker} from '@theatre/studio/store/types'
|
||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||
|
||||
const MARKER_SIZE_W_PX = 10
|
||||
const MARKER_SIZE_H_PX = 8
|
||||
const HIT_ZONE_SIZE_PX = 12
|
||||
const SNAP_CURSOR_SIZE_PX = 34
|
||||
const MARKER_HOVER_SIZE_W_PX = MARKER_SIZE_W_PX * 2
|
||||
const MARKER_HOVER_SIZE_H_PX = MARKER_SIZE_H_PX * 2
|
||||
const dims = (w: number, h = w) => `
|
||||
left: ${w * -0.5}px;
|
||||
top: ${h * -0.5}px;
|
||||
width: ${w}px;
|
||||
height: ${h}px;
|
||||
`
|
||||
|
||||
const MarkerVisualDot = styled.div`
|
||||
position: absolute;
|
||||
${dims(MARKER_SIZE_W_PX, MARKER_SIZE_H_PX)}
|
||||
|
||||
background-image: url(${MarkerDotVisual});
|
||||
background-size: contain;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const HitZone = styled.div`
|
||||
position: absolute;
|
||||
${dims(HIT_ZONE_SIZE_PX)};
|
||||
z-index: 1;
|
||||
|
||||
cursor: ew-resize;
|
||||
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
// "All instances of this component <Mark/> inside #pointer-root when it has the .draggingPositionInSequenceEditor class"
|
||||
// ref: https://styled-components.com/docs/basics#pseudoelements-pseudoselectors-and-nesting
|
||||
#pointer-root.draggingPositionInSequenceEditor:not(.draggingMarker) &,
|
||||
#pointer-root.draggingPositionInSequenceEditor &.beingDragged {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor:not(.draggingMarker) & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
|
||||
// ⸢⸤⸣⸥ thing
|
||||
// This box extends the hitzone so the user does not
|
||||
// accidentally leave the hitzone
|
||||
&:hover:after {
|
||||
position: absolute;
|
||||
top: calc(50% - ${SNAP_CURSOR_SIZE_PX / 2}px);
|
||||
left: calc(50% - ${SNAP_CURSOR_SIZE_PX / 2}px);
|
||||
width: ${SNAP_CURSOR_SIZE_PX}px;
|
||||
height: ${SNAP_CURSOR_SIZE_PX}px;
|
||||
display: block;
|
||||
content: ' ';
|
||||
background: url(${SnapCursor}) no-repeat 100% 100%;
|
||||
// This icon might also fit: GiConvergenceTarget
|
||||
}
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:hover + ${MarkerVisualDot}, &.beingDragged + ${MarkerVisualDot} {
|
||||
${dims(MARKER_HOVER_SIZE_W_PX, MARKER_HOVER_SIZE_H_PX)}
|
||||
}
|
||||
`
|
||||
type IMarkerDotProps = {
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
markerId: SequenceMarkerId
|
||||
}
|
||||
|
||||
const MarkerDot: React.VFC<IMarkerDotProps> = ({layoutP, markerId}) => {
|
||||
const sheetAddress = useVal(layoutP.sheet.address)
|
||||
const marker = useVal(
|
||||
getStudio().atomP.historic.projects.stateByProjectId[sheetAddress.projectId]
|
||||
.stateBySheetId[sheetAddress.sheetId].sequenceEditor.markerSet.byId[
|
||||
markerId
|
||||
],
|
||||
)
|
||||
if (!marker) {
|
||||
// 1/10 maybe this is normal if React tries to re-render this with
|
||||
// out of date data. (e.g. Suspense / Transition stuff?)
|
||||
return null
|
||||
}
|
||||
|
||||
return <MarkerDotDefined marker={marker} layoutP={layoutP} />
|
||||
}
|
||||
|
||||
export default MarkerDot
|
||||
|
||||
type IMarkerDotDefinedProps = {
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
marker: StudioHistoricStateSequenceEditorMarker
|
||||
}
|
||||
|
||||
const MarkerDotDefined: React.VFC<IMarkerDotDefinedProps> = ({
|
||||
layoutP,
|
||||
marker,
|
||||
}) => {
|
||||
const sheetAddress = useVal(layoutP.sheet.address)
|
||||
const clippedSpaceFromUnitSpace = useVal(layoutP.clippedSpace.fromUnitSpace)
|
||||
|
||||
const [markRef, markNode] = useRefAndState<HTMLDivElement | null>(null)
|
||||
|
||||
const [contextMenu] = useMarkerContextMenu(markNode, {
|
||||
sheetAddress,
|
||||
markerId: marker.id,
|
||||
})
|
||||
|
||||
const [isDragging] = useDragMarker(markNode, {
|
||||
layoutP,
|
||||
marker,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: zIndexes.marker,
|
||||
transform: `translateX(${clippedSpaceFromUnitSpace(
|
||||
marker.position,
|
||||
)}px)`,
|
||||
// below the sequence ruler "top bar"
|
||||
top: '18px',
|
||||
}}
|
||||
>
|
||||
{contextMenu}
|
||||
<HitZone
|
||||
ref={markRef}
|
||||
// `data-pos` and `attributeNameThatLocksFramestamp` are used by FrameStampPositionProvider
|
||||
// in order to handle snapping the playhead. Adding these props effectively
|
||||
// causes the playhead to "snap" to the marker on mouse over.
|
||||
// `pointerEventsAutoInNormalMode` and `lockedCursorCssVarName` in the CSS above are also
|
||||
// used to make this behave correctly.
|
||||
{...{
|
||||
[attributeNameThatLocksFramestamp]: marker.position.toFixed(3),
|
||||
[POSITION_SNAPPING.attributeNameForPosition]:
|
||||
marker.position.toFixed(3),
|
||||
}}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
/>
|
||||
<MarkerVisualDot />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function useMarkerContextMenu(
|
||||
node: HTMLElement | null,
|
||||
options: {
|
||||
sheetAddress: SheetAddress
|
||||
markerId: SequenceMarkerId
|
||||
},
|
||||
) {
|
||||
return useContextMenu(node, {
|
||||
menuItems() {
|
||||
return [
|
||||
{
|
||||
label: 'Remove marker',
|
||||
callback: () => {
|
||||
getStudio().transaction(({stateEditors}) => {
|
||||
stateEditors.studio.historic.projects.stateByProjectId.stateBySheetId.sequenceEditor.removeMarker(
|
||||
{
|
||||
sheetAddress: options.sheetAddress,
|
||||
markerId: options.markerId,
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function useDragMarker(
|
||||
node: HTMLDivElement | null,
|
||||
props: {
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
marker: StudioHistoricStateSequenceEditorMarker
|
||||
},
|
||||
): [isDragging: boolean] {
|
||||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
|
||||
const useDragOpts = useMemo<UseDragOpts>(() => {
|
||||
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
||||
let tempTransaction: CommitOrDiscard | undefined
|
||||
let markerAtStartOfDrag: StudioHistoricStateSequenceEditorMarker
|
||||
|
||||
return {
|
||||
debugName: `MarkerDot/useDragMarker (${props.marker.id})`,
|
||||
|
||||
onDragStart(_event) {
|
||||
markerAtStartOfDrag = propsRef.current.marker
|
||||
toUnitSpace = val(props.layoutP.scaledSpace.toUnitSpace)
|
||||
},
|
||||
onDrag(dx, _dy, event) {
|
||||
const original = markerAtStartOfDrag
|
||||
const newPosition = Math.max(
|
||||
// check if our event hoversover a [data-pos] element
|
||||
POSITION_SNAPPING.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 = 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()
|
||||
tempTransaction = undefined
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isDragging] = useDrag(node, useDragOpts)
|
||||
|
||||
useLockFrameStampPosition(isDragging, props.marker.position)
|
||||
useCssCursorLock(
|
||||
isDragging,
|
||||
'draggingPositionInSequenceEditor draggingMarker',
|
||||
'ew-resize',
|
||||
)
|
||||
|
||||
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
|
||||
},
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_220_60402" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="3" width="10" height="6">
|
||||
<rect width="8.56043" height="5.43915" transform="matrix(-1 -8.74228e-08 -8.74228e-08 1 9.28027 3.28027)" fill="#C4C4C4"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_220_60402)">
|
||||
<rect width="6" height="6" transform="matrix(-0.707107 -0.707107 -0.707107 0.707107 9.28027 2.65527)" fill="#E0C917"/>
|
||||
</g>
|
||||
<rect width="8.02659" height="2.00315" transform="matrix(-1 -8.74228e-08 -8.74228e-08 1 9.01318 1.99707)" fill="#E0C917"/>
|
||||
</svg>
|
After Width: | Height: | Size: 625 B |
|
@ -0,0 +1,27 @@
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {useVal} from '@theatre/react'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import React from 'react'
|
||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import MarkerDot from './MarkerDot'
|
||||
|
||||
const Markers: React.VFC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||
layoutP,
|
||||
}) => {
|
||||
const sheetAddress = useVal(layoutP.sheet.address)
|
||||
const markerSetP =
|
||||
getStudio().atomP.historic.projects.stateByProjectId[sheetAddress.projectId]
|
||||
.stateBySheetId[sheetAddress.sheetId].sequenceEditor.markerSet
|
||||
const markerAllIds = useVal(markerSetP.allIds)
|
||||
|
||||
return (
|
||||
<>
|
||||
{markerAllIds &&
|
||||
Object.keys(markerAllIds).map((markerId) => (
|
||||
<MarkerDot key={markerId} layoutP={layoutP} markerId={markerId} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Markers
|
|
@ -21,9 +21,12 @@ import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
|||
import PlayheadPositionPopover from './PlayheadPositionPopover'
|
||||
import {getIsPlayheadAttachedToFocusRange} from '@theatre/studio/UIRoot/useKeyboardShortcuts'
|
||||
import {
|
||||
lockedCursorCssPropName,
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {generateSequenceMarkerId} from '@theatre/shared/utils/ids'
|
||||
|
||||
const Container = styled.div<{isVisible: boolean}>`
|
||||
--thumbColor: #00e0ff;
|
||||
|
@ -49,7 +52,7 @@ const Rod = styled.div`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||
/* pointer-events: auto; */
|
||||
/* cursor: var(${lockedCursorCssPropName}); */
|
||||
/* cursor: var(${lockedCursorCssVarName}); */
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
|
@ -79,7 +82,7 @@ const Thumb = styled.div`
|
|||
|
||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssPropName});
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
||||
${Container}.playheadattachedtofocusrange > & {
|
||||
|
@ -250,6 +253,11 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
// hide the frame stamp when seeking
|
||||
useLockFrameStampPosition(useVal(layoutP.seeker.isSeeking) || isDragging, -1)
|
||||
|
||||
const [contextMenu] = usePlayheadContextMenu(thumbNode, {
|
||||
// pass in a pointer to ensure we aren't spending retrieval on every render
|
||||
layoutP,
|
||||
})
|
||||
|
||||
return usePrism(() => {
|
||||
const isSeeking = val(layoutP.seeker.isSeeking)
|
||||
|
||||
|
@ -270,6 +278,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{contextMenu}
|
||||
{popoverNode}
|
||||
<Container
|
||||
isVisible={isVisible}
|
||||
|
@ -306,3 +315,37 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
}
|
||||
|
||||
export default Playhead
|
||||
|
||||
function usePlayheadContextMenu(
|
||||
node: HTMLElement | null,
|
||||
options: {layoutP: Pointer<SequenceEditorPanelLayout>},
|
||||
) {
|
||||
return useContextMenu(node, {
|
||||
menuItems() {
|
||||
return [
|
||||
{
|
||||
label: 'Place marker',
|
||||
callback: () => {
|
||||
getStudio().transaction(({stateEditors}) => {
|
||||
// only retrieve val on callback to reduce unnecessary work on every use
|
||||
const sheet = val(options.layoutP.sheet)
|
||||
const sheetSequence = sheet.getSequence()
|
||||
stateEditors.studio.historic.projects.stateByProjectId.stateBySheetId.sequenceEditor.replaceMarkers(
|
||||
{
|
||||
sheetAddress: sheet.address,
|
||||
markers: [
|
||||
{
|
||||
id: generateSequenceMarkerId(),
|
||||
position: sheetSequence.position,
|
||||
},
|
||||
],
|
||||
snappingFunction: sheetSequence.closestGridPosition,
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import HorizontalScrollbar from './HorizontalScrollbar'
|
|||
import Playhead from './Playhead'
|
||||
import TopStrip from './TopStrip'
|
||||
import FocusRangeCurtains from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/FocusRangeCurtains'
|
||||
import Markers from './Markers'
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
|
@ -34,6 +35,7 @@ const RightOverlay: React.FC<{
|
|||
<HorizontalScrollbar layoutP={layoutP} />
|
||||
<FrameStamp layoutP={layoutP} />
|
||||
<TopStrip layoutP={layoutP} />
|
||||
<Markers layoutP={layoutP} />
|
||||
<LengthIndicator layoutP={layoutP} />
|
||||
<FocusRangeCurtains layoutP={layoutP} />
|
||||
</Container>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 7V1H7" stroke="#74FFDE" stroke-width="0.25" />
|
||||
<path d="M7 33H1L1 27" stroke="#74FFDE" stroke-width="0.25" />
|
||||
<path d="M33 27V33H27" stroke="#74FFDE" stroke-width="0.25" />
|
||||
<path d="M27 1L33 1V7" stroke="#74FFDE" stroke-width="0.25" />
|
||||
</svg>
|
After Width: | Height: | Size: 358 B |
|
@ -53,6 +53,7 @@ export const zIndexes = (() => {
|
|||
lengthIndicatorStrip: 0,
|
||||
playhead: 0,
|
||||
currentFrameStamp: 0,
|
||||
marker: 0,
|
||||
horizontalScrollbar: 0,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
||||
import type {
|
||||
KeyframeId,
|
||||
SequenceMarkerId,
|
||||
SequenceTrackId,
|
||||
UIPanelId,
|
||||
} from '@theatre/shared/utils/ids'
|
||||
|
@ -40,6 +41,7 @@ import type {
|
|||
OutlineSelectionState,
|
||||
PanelPosition,
|
||||
StudioAhistoricState,
|
||||
StudioHistoricStateSequenceEditorMarker,
|
||||
} from './types'
|
||||
import {clamp, uniq} from 'lodash-es'
|
||||
import {
|
||||
|
@ -52,6 +54,7 @@ import {
|
|||
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
||||
import type SheetObjectTemplate from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
||||
import type {PropTypeConfig} from '@theatre/core/propTypes'
|
||||
import {pointableSetUtil} from '@theatre/shared/utils/PointableSet'
|
||||
|
||||
export const setDrafts__onlyMeantToBeCalledByTransaction = (
|
||||
drafts: undefined | Drafts,
|
||||
|
@ -250,6 +253,85 @@ namespace stateEditors {
|
|||
])
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureMarkers(sheetAddress: SheetAddress) {
|
||||
const sequenceEditor =
|
||||
stateEditors.studio.historic.projects.stateByProjectId.stateBySheetId._ensure(
|
||||
sheetAddress,
|
||||
).sequenceEditor
|
||||
|
||||
if (!sequenceEditor.markerSet) {
|
||||
sequenceEditor.markerSet = pointableSetUtil.create()
|
||||
}
|
||||
|
||||
return sequenceEditor.markerSet
|
||||
}
|
||||
|
||||
export function replaceMarkers(p: {
|
||||
sheetAddress: SheetAddress
|
||||
markers: Array<StudioHistoricStateSequenceEditorMarker>
|
||||
snappingFunction: (p: number) => number
|
||||
}) {
|
||||
const currentMarkerSet = _ensureMarkers(p.sheetAddress)
|
||||
|
||||
const sanitizedMarkers = p.markers
|
||||
.filter((marker) => {
|
||||
if (!isFinite(marker.position)) return false
|
||||
|
||||
return true // marker looks valid
|
||||
})
|
||||
.map((marker) => ({
|
||||
...marker,
|
||||
position: p.snappingFunction(marker.position),
|
||||
}))
|
||||
|
||||
const newMarkersById = keyBy(sanitizedMarkers, 'id')
|
||||
|
||||
/** Usually starts as the "unselected" markers */
|
||||
let markersThatArentBeingReplaced = pointableSetUtil.filter(
|
||||
currentMarkerSet,
|
||||
(marker) => marker && !newMarkersById[marker.id],
|
||||
)
|
||||
|
||||
const markersThatArentBeingReplacedByPosition = keyBy(
|
||||
Object.values(markersThatArentBeingReplaced.byId),
|
||||
'position',
|
||||
)
|
||||
|
||||
// If the new transformed markers overlap with any existing markers,
|
||||
// we remove the overlapped markers
|
||||
sanitizedMarkers.forEach(({position}) => {
|
||||
const existingMarkerAtThisPosition =
|
||||
markersThatArentBeingReplacedByPosition[position]
|
||||
if (existingMarkerAtThisPosition) {
|
||||
markersThatArentBeingReplaced = pointableSetUtil.remove(
|
||||
markersThatArentBeingReplaced,
|
||||
existingMarkerAtThisPosition.id,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Object.assign(
|
||||
currentMarkerSet,
|
||||
pointableSetUtil.merge([
|
||||
markersThatArentBeingReplaced,
|
||||
pointableSetUtil.create(
|
||||
sanitizedMarkers.map((marker) => [marker.id, marker]),
|
||||
),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
export function removeMarker(options: {
|
||||
sheetAddress: SheetAddress
|
||||
markerId: SequenceMarkerId
|
||||
}) {
|
||||
const currentMarkerSet = _ensureMarkers(options.sheetAddress)
|
||||
Object.assign(
|
||||
currentMarkerSet,
|
||||
pointableSetUtil.remove(currentMarkerSet, options.markerId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -574,6 +656,9 @@ namespace stateEditors {
|
|||
)
|
||||
if (indexOfLeftKeyframe === -1) {
|
||||
keyframes.unshift({
|
||||
// generating the keyframe within the `setKeyframeAtPosition` makes it impossible for us
|
||||
// to make this business logic deterministic, which is important to guarantee for collaborative
|
||||
// editing.
|
||||
id: generateKeyframeId(),
|
||||
position,
|
||||
connectedRight: true,
|
||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
|||
WithoutSheetInstance,
|
||||
} from '@theatre/shared/utils/addresses'
|
||||
import type {StrictRecord} from '@theatre/shared/utils/types'
|
||||
import type {PointableSet} from '@theatre/shared/utils/PointableSet'
|
||||
import type Project from '@theatre/core/projects/Project'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||
|
@ -15,6 +16,7 @@ import type {
|
|||
ObjectAddressKey,
|
||||
PaneInstanceId,
|
||||
ProjectId,
|
||||
SequenceMarkerId,
|
||||
SheetId,
|
||||
SheetInstanceId,
|
||||
UIPanelId,
|
||||
|
@ -69,6 +71,22 @@ export type PaneInstanceDescriptor = {
|
|||
paneClass: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker allows you to mark notable positions in your sequence.
|
||||
*
|
||||
* See root {@link StudioHistoricState}
|
||||
*/
|
||||
export type StudioHistoricStateSequenceEditorMarker = {
|
||||
id: SequenceMarkerId
|
||||
/**
|
||||
* The position this marker takes in the sequence.
|
||||
*
|
||||
* Usually, this value is measured in seconds, but the unit could be varied based on the kind of
|
||||
* unit you're using for mapping to the position (e.g. Position=1 = 10px of scrolling)
|
||||
*/
|
||||
position: number
|
||||
}
|
||||
|
||||
/**
|
||||
* See parent {@link StudioHistoricStateProject}.
|
||||
* See root {@link StudioHistoricState}
|
||||
|
@ -76,6 +94,10 @@ export type PaneInstanceDescriptor = {
|
|||
export type StudioHistoricStateProjectSheet = {
|
||||
selectedInstanceId: undefined | SheetInstanceId
|
||||
sequenceEditor: {
|
||||
markerSet?: PointableSet<
|
||||
SequenceMarkerId,
|
||||
StudioHistoricStateSequenceEditorMarker
|
||||
>
|
||||
selectedPropsByObject: StrictRecord<
|
||||
ObjectAddressKey,
|
||||
StrictRecord<PathToProp_Encoded, keyof typeof graphEditorColors>
|
||||
|
@ -88,16 +110,6 @@ export type StudioHistoricStateProject = {
|
|||
stateBySheetId: StrictRecord<SheetId, StudioHistoricStateProjectSheet>
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link StudioHistoricState} includes both studio and project data, and
|
||||
* contains data changed for an undo/redo history.
|
||||
*
|
||||
* ## Internally
|
||||
*
|
||||
* We use immer `Draft`s to encapsulate this whole state to then be operated
|
||||
* on by each transaction. The derived values from the store will also include
|
||||
* the application of the "temp transactions" stack.
|
||||
*/
|
||||
export type StudioHistoricState = {
|
||||
projects: {
|
||||
stateByProjectId: StrictRecord<ProjectId, StudioHistoricStateProject>
|
||||
|
|
|
@ -12,12 +12,14 @@ import styled from 'styled-components'
|
|||
const elementId = 'pointer-root'
|
||||
|
||||
/**
|
||||
* When the cursor is locked, this css prop is added to #pointer-root
|
||||
* When the cursor is locked, this css var is added to #pointer-root
|
||||
* whose value will be the locked cursor (e.g. ew-resize).
|
||||
*
|
||||
* Look up references of this constant for examples of how it is used.
|
||||
*
|
||||
* See {@link useCssCursorLock} - code that locks the cursor
|
||||
*/
|
||||
export const lockedCursorCssPropName = '--lockedCursor'
|
||||
export const lockedCursorCssVarName = '--lockedCursor'
|
||||
|
||||
const Container = styled.div`
|
||||
pointer-events: auto;
|
||||
|
@ -73,7 +75,7 @@ const PointerEventsHandler: React.FC<{
|
|||
style={{
|
||||
cursor: lockedCursor,
|
||||
// @ts-ignore
|
||||
[lockedCursorCssPropName]: lockedCursor,
|
||||
[lockedCursorCssVarName]: lockedCursor,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
@ -94,6 +96,8 @@ const PointerEventsHandler: React.FC<{
|
|||
* but then "unlocking" that style will again reveal the existing styles.
|
||||
*
|
||||
* It behaves a bit like a stack.
|
||||
*
|
||||
* See {@link lockedCursorCssVarName}
|
||||
*/
|
||||
export const useCssCursorLock = (
|
||||
/** Whether to enable the provided cursor style */
|
||||
|
|
|
@ -80,10 +80,10 @@ export default function useDrag(
|
|||
}
|
||||
}>({dragHappened: false, startPos: {x: 0, y: 0}})
|
||||
|
||||
const capturedPointerRef = useRef<undefined | CapturedPointer>()
|
||||
useLayoutEffect(() => {
|
||||
if (!target) return
|
||||
|
||||
let capturedPointer: undefined | CapturedPointer
|
||||
const getDistances = (event: MouseEvent): [number, number] => {
|
||||
const {startPos} = stateRef.current
|
||||
return [event.screenX - startPos.x, event.screenY - startPos.y]
|
||||
|
@ -98,11 +98,12 @@ export default function useDrag(
|
|||
}
|
||||
|
||||
const dragEndHandler = () => {
|
||||
removeDragListeners()
|
||||
modeRef.current = 'notDragging'
|
||||
if (modeRef.current === 'dragging') {
|
||||
removeDragListeners()
|
||||
modeRef.current = 'notDragging'
|
||||
|
||||
optsRef.current.onDragEnd &&
|
||||
optsRef.current.onDragEnd(stateRef.current.dragHappened)
|
||||
optsRef.current.onDragEnd?.(stateRef.current.dragHappened)
|
||||
}
|
||||
}
|
||||
|
||||
const addDragListeners = () => {
|
||||
|
@ -111,7 +112,7 @@ export default function useDrag(
|
|||
}
|
||||
|
||||
const removeDragListeners = () => {
|
||||
capturedPointer?.release()
|
||||
capturedPointerRef.current?.release()
|
||||
document.removeEventListener('mousemove', dragHandler)
|
||||
document.removeEventListener('mouseup', dragEndHandler)
|
||||
}
|
||||
|
@ -132,7 +133,7 @@ export default function useDrag(
|
|||
|
||||
const dragStartHandler = (event: MouseEvent) => {
|
||||
// defensively release
|
||||
capturedPointer?.release()
|
||||
capturedPointerRef.current?.release()
|
||||
|
||||
const opts = optsRef.current
|
||||
if (opts.disabled === true) return
|
||||
|
@ -147,7 +148,7 @@ export default function useDrag(
|
|||
}
|
||||
|
||||
// need to capture pointer after we know the provided handler wants to handle drag start
|
||||
capturedPointer = capturePointer('Drag start')
|
||||
capturedPointerRef.current = capturePointer('Drag start')
|
||||
|
||||
if (!opts.dontBlockMouseDown) {
|
||||
event.stopPropagation()
|
||||
|
@ -176,8 +177,7 @@ export default function useDrag(
|
|||
target.removeEventListener('click', preventUnwantedClick as $FixMe)
|
||||
|
||||
if (modeRef.current !== 'notDragging') {
|
||||
optsRef.current.onDragEnd &&
|
||||
optsRef.current.onDragEnd(modeRef.current === 'dragging')
|
||||
optsRef.current.onDragEnd?.(modeRef.current === 'dragging')
|
||||
}
|
||||
modeRef.current = 'notDragging'
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue