Compound prop context menu (#157)

This commit is contained in:
Aria 2022-05-26 01:18:45 +02:00 committed by GitHub
parent cfbb6ab043
commit d83d2b558c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 549 additions and 114 deletions

View file

@ -54,5 +54,71 @@ export function valueInProp<PropConfig extends PropTypeConfig_AllSimples>(
export function isPropConfSequencable( export function isPropConfSequencable(
conf: PropTypeConfig, conf: PropTypeConfig,
): conf is Extract<PropTypeConfig, {interpolate: any}> { ): conf is Extract<PropTypeConfig, {interpolate: any}> {
return Object.prototype.hasOwnProperty.call(conf, 'interpolate') return !isPropConfigComposite(conf) // now all non-compounds are sequencable
}
const compoundPropSequenceabilityCache = new WeakMap<
PropTypeConfig_Compound<{}> | PropTypeConfig_Enum,
boolean
>()
/**
* See {@link compoundHasSimpleDescendantsImpl}
*/
export function compoundHasSimpleDescendants(
conf: PropTypeConfig_Compound<{}> | PropTypeConfig_Enum,
): boolean {
if (!compoundPropSequenceabilityCache.has(conf)) {
compoundPropSequenceabilityCache.set(
conf,
compoundHasSimpleDescendantsImpl(conf),
)
}
return compoundPropSequenceabilityCache.get(conf)!
}
/**
* This basically checks of the compound prop has at least one simple prop in its descendants.
* In other words, if the compound props has no subs, or its subs are only compounds that eventually
* don't have simple subs, this will return false.
*/
function compoundHasSimpleDescendantsImpl(
conf: PropTypeConfig_Compound<{}> | PropTypeConfig_Enum,
): boolean {
if (conf.type === 'enum') {
throw new Error(`Not implemented yet for enums`)
}
for (const key in conf.props) {
const subConf = conf.props[
key as $IntentionalAny as keyof typeof conf.props
] as PropTypeConfig
if (isPropConfigComposite(subConf)) {
if (compoundHasSimpleDescendants(subConf)) {
return true
}
} else {
return true
}
}
return false
}
export function* iteratePropType(
conf: PropTypeConfig,
pathUpToThisPoint: PathToProp,
): Generator<{path: PathToProp; conf: PropTypeConfig}, void, void> {
if (conf.type === 'compound') {
for (const key in conf.props) {
yield* iteratePropType(conf.props[key] as PropTypeConfig, [
...pathUpToThisPoint,
key,
])
}
} else if (conf.type === 'enum') {
throw new Error(`Not implemented yet`)
} else {
return yield {path: pathUpToThisPoint, conf}
}
} }

View file

@ -50,6 +50,9 @@ function cloneDeepSerializableAndPrune<T>(v: T): T | undefined {
} }
} }
/**
* TODO replace with {@link iteratePropType}
*/
function forEachDeepSimplePropOfCompoundProp( function forEachDeepSimplePropOfCompoundProp(
propType: PropTypeConfig_Compound<$IntentionalAny>, propType: PropTypeConfig_Compound<$IntentionalAny>,
path: Array<string | number>, path: Array<string | number>,

View file

@ -39,8 +39,6 @@ const ExtensionPaneWrapper: React.FC<{
} }
const Container = styled(PanelWrapper)` const Container = styled(PanelWrapper)`
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -1,5 +1,6 @@
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes' import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
import {isPropConfigComposite} from '@theatre/shared/propTypes/utils' import {isPropConfigComposite} from '@theatre/shared/propTypes/utils'
import type {$FixMe} from '@theatre/shared/utils/types'
import {getPointerParts} from '@theatre/dataverse' import {getPointerParts} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import last from 'lodash-es/last' import last from 'lodash-es/last'
@ -8,12 +9,13 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {indentationFormula} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor' import {indentationFormula} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import DeterminePropEditorForDetail from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail' import DeterminePropEditorForDetail from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import type {$FixMe} from '@theatre/shared/utils/types'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import {useEditingToolsForCompoundProp} from '@theatre/studio/propEditors/useEditingToolsForCompoundProp'
const Container = styled.div` const Container = styled.div`
--step: 8px; --step: 8px;
@ -42,7 +44,7 @@ const PropName = styled.div`
align-items: center; align-items: center;
user-select: none; user-select: none;
&:hover { &:hover {
/* color: white; */ color: white;
} }
${() => propNameTextCSS}; ${() => propNameTextCSS};
@ -85,18 +87,29 @@ function DetailCompoundPropEditor<
const [propNameContainerRef, propNameContainer] = const [propNameContainerRef, propNameContainer] =
useRefAndState<HTMLDivElement | null>(null) useRefAndState<HTMLDivElement | null>(null)
const tools = useEditingToolsForCompoundProp(
pointerToProp as $FixMe,
obj,
propConfig,
)
const [contextMenu] = useContextMenu(propNameContainer, {
menuItems: tools.contextMenuItems,
})
const lastSubPropIsComposite = compositeSubs.length > 0 const lastSubPropIsComposite = compositeSubs.length > 0
// previous versions of the DetailCompoundPropEditor had a context menu item for "Reset values". // previous versions of the DetailCompoundPropEditor had a context menu item for "Reset values".
return ( return (
<Container> <Container>
{contextMenu}
<Header <Header
// @ts-ignore // @ts-ignore
style={{'--depth': visualIndentation - 1}} style={{'--depth': visualIndentation - 1}}
> >
<Padding> <Padding>
<DefaultOrStaticValueIndicator hasStaticOverride={false} /> {tools.controlIndicators}
<PropName ref={propNameContainerRef}>{propName || 'Props'}</PropName> <PropName ref={propNameContainerRef}>{propName || 'Props'}</PropName>
</Padding> </Padding>
</Header> </Header>

View file

@ -35,6 +35,17 @@ const Children = styled.ul`
list-style: none; list-style: none;
` `
/**
* @remarks
* Right now, we're rendering a hierarchical dom tree that reflects the hierarchy of
* objects, compound props, and their subs. This is not necessary and makes styling complicated.
* Instead of this, we can simply render a list. This should be easy to do, since the view model
* in {@link calculateSequenceEditorTree} already includes all the vertical placement information
* (height and top) we need to render the nodes as a list.
*
* Note that we don't need to change {@link calculateSequenceEditorTree} to be list-based. It can
* retain its hierarchy. It's just the DOM tree that should be list-based.
*/
const RightRow: React.FC<{ const RightRow: React.FC<{
leaf: SequenceEditorTree_Row<unknown> leaf: SequenceEditorTree_Row<unknown>
node: React.ReactElement node: React.ReactElement

View file

@ -12,6 +12,7 @@ import useDrag from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
export const focusRangeStripTheme = { export const focusRangeStripTheme = {
enabled: { enabled: {
@ -175,7 +176,6 @@ const FocusRangeStrip: React.FC<{
}) })
const scaledSpaceToUnitSpace = useVal(layoutP.scaledSpace.toUnitSpace) const scaledSpaceToUnitSpace = useVal(layoutP.scaledSpace.toUnitSpace)
const [isDraggingRef, isDragging] = useRefAndState(false)
const sheet = useVal(layoutP.sheet) const sheet = useVal(layoutP.sheet)
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => { const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
@ -192,7 +192,6 @@ const FocusRangeStrip: React.FC<{
const endPosBeforeDrag = existingRange.range.end const endPosBeforeDrag = existingRange.range.end
let dragHappened = false let dragHappened = false
const sequence = val(layoutP.sheet).getSequence() const sequence = val(layoutP.sheet).getSequence()
isDraggingRef.current = true
return { return {
onDrag(dx) { onDrag(dx) {
@ -234,7 +233,6 @@ const FocusRangeStrip: React.FC<{
} }
}, },
onDragEnd() { onDragEnd() {
isDraggingRef.current = false
if (existingRange) { if (existingRange) {
if (dragHappened && tempTransaction !== undefined) { if (dragHappened && tempTransaction !== undefined) {
tempTransaction.commit() tempTransaction.commit()
@ -250,7 +248,9 @@ const FocusRangeStrip: React.FC<{
} }
}, [sheet, scaledSpaceToUnitSpace]) }, [sheet, scaledSpaceToUnitSpace])
useDrag(rangeStripNode, gestureHandlers) const [isDragging] = useDrag(rangeStripNode, gestureHandlers)
useLockFrameStampPosition(isDragging, -1)
return usePrism(() => { return usePrism(() => {
const existingRange = existingRangeD.getValue() const existingRange = existingRangeD.getValue()

View file

@ -186,30 +186,28 @@ const FocusRangeThumb: React.FC<{
const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, { const snapPos = DopeSnap.checkIfMouseEventSnapToPos(event, {
ignore: hitZoneNode, ignore: hitZoneNode,
}) })
if (snapPos != null) {
if (snapPos == null) {
const deltaPos = scaledSpaceToUnitSpace(dx)
const oldPosPlusDeltaPos = posBeforeDrag + deltaPos
newPosition = oldPosPlusDeltaPos
} else {
newPosition = snapPos newPosition = snapPos
} }
range = existingRangeD.getValue()?.range || defaultRange range = existingRangeD.getValue()?.range || defaultRange
const deltaPos = scaledSpaceToUnitSpace(dx)
const oldPosPlusDeltaPos = posBeforeDrag + deltaPos
// Make sure that the focus range has a minimal width // Make sure that the focus range has a minimal width
if (thumbType === 'start') { if (thumbType === 'start') {
// Prevent the start thumb from going below 0 // Prevent the start thumb from going below 0
newPosition = Math.max( newPosition = Math.max(
Math.min( Math.min(newPosition, range['end'] - minFocusRangeStripWidth),
oldPosPlusDeltaPos,
range['end'] - minFocusRangeStripWidth,
),
0, 0,
) )
} else { } else {
// Prevent the start thumb from going over the length of the sequence // Prevent the start thumb from going over the length of the sequence
newPosition = Math.min( newPosition = Math.min(
Math.max( Math.max(newPosition, range['start'] + minFocusRangeStripWidth),
oldPosPlusDeltaPos,
range['start'] + minFocusRangeStripWidth,
),
sheet.getSequence().length, sheet.getSequence().length,
) )
} }

View file

@ -223,9 +223,12 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
// unsnapped // unsnapped
clamp(posBeforeSeek + deltaPos, 0, sequence.length) clamp(posBeforeSeek + deltaPos, 0, sequence.length)
}, },
onDragEnd() { onDragEnd(dragHappened) {
setIsSeeking(false) setIsSeeking(false)
}, },
onClick(e) {
openPopover(e, thumbRef.current!)
},
} }
}, },
} }
@ -276,9 +279,6 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
<Thumb <Thumb
ref={thumbRef as $IntentionalAny} ref={thumbRef as $IntentionalAny}
{...DopeSnap.includePositionSnapAttrs(posInUnitSpace)} {...DopeSnap.includePositionSnapAttrs(posInUnitSpace)}
onClick={(e) => {
openPopover(e, thumbNode!)
}}
> >
<RoomToClick room={8} /> <RoomToClick room={8} />
<Squinch /> <Squinch />

View file

@ -1,9 +1,16 @@
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import type {VoidFn} from '@theatre/shared/utils/types'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {transparentize} from 'polished' import {transparentize} from 'polished'
import React from 'react' import React from 'react'
import styled, {css} from 'styled-components' import styled, {css} from 'styled-components'
export type NearbyKeyframesControls = {
prev?: Pick<Keyframe, 'position'> & {jump: VoidFn}
cur: {type: 'on'; toggle: VoidFn} | {type: 'off'; toggle: VoidFn}
next?: Pick<Keyframe, 'position'> & {jump: VoidFn}
}
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -158,41 +165,16 @@ namespace Icons {
) )
} }
const NextPrevKeyframeCursors: React.FC<{ const NextPrevKeyframeCursors: React.FC<NearbyKeyframesControls> = (props) => {
prev?: Keyframe
cur?: Keyframe
next?: Keyframe
jumpToPosition: (position: number) => void
toggleKeyframeOnCurrentPosition: () => void
}> = (props) => {
return ( return (
<Container> <Container>
<Prev <Prev available={!!props.prev} onClick={props.prev?.jump}>
available={!!props.prev}
onClick={() => {
if (props.prev) {
props.jumpToPosition(props.prev.position)
}
}}
>
<Icons.Prev /> <Icons.Prev />
</Prev> </Prev>
<CurButton <CurButton isOn={props.cur.type === 'on'} onClick={props.cur.toggle}>
isOn={!!props.cur}
onClick={() => {
props.toggleKeyframeOnCurrentPosition()
}}
>
<Icons.Cur /> <Icons.Cur />
</CurButton> </CurButton>
<Next <Next available={!!props.next} onClick={props.next?.jump}>
available={!!props.next}
onClick={() => {
if (props.next) {
props.jumpToPosition(props.next.position)
}
}}
>
<Icons.Next /> <Icons.Next />
</Next> </Next>
</Container> </Container>

View file

@ -0,0 +1,59 @@
import type {
TrackData,
Keyframe,
} from '@theatre/core/projects/store/types/SheetState_Historic'
import last from 'lodash-es/last'
const cache = new WeakMap<
TrackData,
[seqPosition: number, nearbyKeyframes: NearbyKeyframes]
>()
const noKeyframes: NearbyKeyframes = {}
export function getNearbyKeyframesOfTrack(
track: TrackData | undefined,
sequencePosition: number,
): NearbyKeyframes {
if (!track || track.keyframes.length === 0) return noKeyframes
const cachedItem = cache.get(track)
if (cachedItem && cachedItem[0] === sequencePosition) {
return cachedItem[1]
}
const calculate = (): NearbyKeyframes => {
const i = track.keyframes.findIndex((kf) => kf.position >= sequencePosition)
if (i === -1)
return {
prev: last(track.keyframes),
}
const k = track.keyframes[i]!
if (k.position === sequencePosition) {
return {
prev: i > 0 ? track.keyframes[i - 1] : undefined,
cur: k,
next:
i === track.keyframes.length - 1 ? undefined : track.keyframes[i + 1],
}
} else {
return {
next: k,
prev: i > 0 ? track.keyframes[i - 1] : undefined,
}
}
}
const result = calculate()
cache.set(track, [sequencePosition, result])
return result
}
export type NearbyKeyframes = {
prev?: Keyframe
cur?: Keyframe
next?: Keyframe
}

View file

@ -0,0 +1,316 @@
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import getStudio from '@theatre/studio/getStudio'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import getDeep from '@theatre/shared/utils/getDeep'
import {usePrism} from '@theatre/react'
import type {
$IntentionalAny,
SerializablePrimitive,
} from '@theatre/shared/utils/types'
import {getPointerParts, prism, val} from '@theatre/dataverse'
import type {Pointer} from '@theatre/dataverse'
import get from 'lodash-es/get'
import React from 'react'
import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import type {PropTypeConfig_Compound} from '@theatre/core/propTypes'
import {
compoundHasSimpleDescendants,
isPropConfigComposite,
iteratePropType,
} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import type {IPropPathToTrackIdTree} from '@theatre/core/sheetObjects/SheetObjectTemplate'
import pointerDeep from '@theatre/shared/utils/pointerDeep'
import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack'
interface CommonStuff {
beingScrubbed: boolean
contextMenuItems: Array<IContextMenuItem>
controlIndicators: React.ReactElement
}
/**
* For compounds that have _no_ sequenced track in all of their descendants
*/
interface AllStatic extends CommonStuff {
type: 'AllStatic'
}
/**
* For compounds that have at least one sequenced track in their descendants
*/
interface HasSequences extends CommonStuff {
type: 'HasSequences'
}
type Stuff = AllStatic | HasSequences
export function useEditingToolsForCompoundProp<T extends SerializablePrimitive>(
pointerToProp: Pointer<{}>,
obj: SheetObject,
propConfig: PropTypeConfig_Compound<{}>,
): Stuff {
return usePrism((): Stuff => {
// if the compound has no simple descendants, then there isn't much the user can do with it
if (!compoundHasSimpleDescendants(propConfig)) {
return {
type: 'AllStatic',
beingScrubbed: false,
contextMenuItems: [],
controlIndicators: (
<DefaultOrStaticValueIndicator hasStaticOverride={false} />
),
}
}
const pathToProp = getPointerParts(pointerToProp).path
/**
* TODO This implementation is wrong because {@link stateEditors.studio.ephemeral.projects.stateByProjectId.stateBySheetId.stateByObjectKey.propsBeingScrubbed.flag}
* does not prune empty objects
*/
const someDescendantsBeingScrubbed = !!val(
get(
getStudio()!.atomP.ephemeral.projects.stateByProjectId[
obj.address.projectId
].stateBySheetId[obj.address.sheetId].stateByObjectKey[
obj.address.objectKey
].valuesBeingScrubbed,
getPointerParts(pointerToProp).path,
),
)
const contextMenuItems: IContextMenuItem[] = []
const common: CommonStuff = {
beingScrubbed: someDescendantsBeingScrubbed,
contextMenuItems,
controlIndicators: <></>,
}
const validSequencedTracks = val(
obj.template.getMapOfValidSequenceTracks_forStudio(),
)
const possibleSequenceTrackIds = getDeep(
validSequencedTracks,
pathToProp,
) as undefined | IPropPathToTrackIdTree
const hasOneOrMoreSequencedTracks = !!possibleSequenceTrackIds
const listOfDescendantTrackIds: SequenceTrackId[] = []
let hasOneOrMoreStatics = true
if (hasOneOrMoreSequencedTracks) {
hasOneOrMoreStatics = false
for (const descendant of iteratePropType(propConfig, [])) {
if (isPropConfigComposite(descendant.conf)) continue
const sequencedTrackIdBelongingToDescendant = getDeep(
possibleSequenceTrackIds,
descendant.path,
) as SequenceTrackId | undefined
if (typeof sequencedTrackIdBelongingToDescendant !== 'string') {
hasOneOrMoreStatics = true
} else {
listOfDescendantTrackIds.push(sequencedTrackIdBelongingToDescendant)
}
}
}
if (hasOneOrMoreStatics) {
contextMenuItems.push(
/**
* TODO This is surely confusing for the user if the descendants don't have overrides.
*/
{
label: 'Reset all to default',
callback: () => {
getStudio()!.transaction(({unset}) => {
unset(pointerToProp)
})
},
},
{
label: 'Sequence all',
callback: () => {
getStudio()!.transaction(({stateEditors}) => {
for (const {path, conf} of iteratePropType(
propConfig,
pathToProp,
)) {
if (isPropConfigComposite(conf)) continue
const propAddress = {...obj.address, pathToProp: path}
stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsSequenced(
propAddress,
propConfig,
)
}
})
},
},
)
}
if (hasOneOrMoreSequencedTracks) {
contextMenuItems.push({
label: 'Make all static',
callback: () => {
getStudio()!.transaction(({stateEditors}) => {
for (const {path: subPath, conf} of iteratePropType(
propConfig,
[],
)) {
if (isPropConfigComposite(conf)) continue
const propAddress = {
...obj.address,
pathToProp: [...pathToProp, ...subPath],
}
const pointerToSub = pointerDeep(pointerToProp, subPath)
stateEditors.coreByProject.historic.sheetsById.sequence.setPrimitivePropAsStatic(
{
...propAddress,
value: obj.getValueByPointer(pointerToSub as $IntentionalAny),
},
)
}
})
},
})
}
if (hasOneOrMoreSequencedTracks) {
const sequenceTrackId = possibleSequenceTrackIds
const nearbyKeyframeControls = prism.sub(
'lcr',
(): NearbyKeyframesControls => {
const sequencePosition = val(
obj.sheet.getSequence().positionDerivation,
)
/*
2/10 perf concern:
When displaying a hierarchy like {props: {transform: {position: {x, y, z}}}},
we'd be recalculating this variable for both `position` and `transform`. While
we _could_ be re-using the calculation of `transform` in `position`, I think
it's unlikely that this optimization would matter.
*/
const nearbyKeyframesInEachTrack = listOfDescendantTrackIds
.map((trackId) => ({
trackId,
track: val(
obj.template.project.pointers.historic.sheetsById[
obj.address.sheetId
].sequence.tracksByObject[obj.address.objectKey].trackData[
trackId
],
),
}))
.filter(({track}) => !!track)
.map((s) => ({
...s,
nearbies: getNearbyKeyframesOfTrack(s.track, sequencePosition),
}))
const hasCur = nearbyKeyframesInEachTrack.find(
({nearbies}) => !!nearbies.cur,
)
const allCur = nearbyKeyframesInEachTrack.every(
({nearbies}) => !!nearbies.cur,
)
const closestPrev = nearbyKeyframesInEachTrack.reduce<
undefined | number
>((acc, s) => {
if (s.nearbies.prev) {
if (acc === undefined) {
return s.nearbies.prev.position
} else {
return Math.max(s.nearbies.prev.position, acc)
}
} else {
return acc
}
}, undefined)
const closestNext = nearbyKeyframesInEachTrack.reduce<
undefined | number
>((acc, s) => {
if (s.nearbies.next) {
if (acc === undefined) {
return s.nearbies.next.position
} else {
return Math.min(s.nearbies.next.position, acc)
}
} else {
return acc
}
}, undefined)
return {
cur: {
type: hasCur ? 'on' : 'off',
toggle: () => {
if (allCur) {
getStudio().transaction((api) => {
api.unset(pointerToProp)
})
} else if (hasCur) {
getStudio().transaction((api) => {
api.set(pointerToProp, val(pointerToProp))
})
} else {
getStudio().transaction((api) => {
api.set(pointerToProp, val(pointerToProp))
})
}
},
},
prev:
closestPrev !== undefined
? {
position: closestPrev,
jump: () => {
obj.sheet.getSequence().position = closestPrev
},
}
: undefined,
next:
closestNext !== undefined
? {
position: closestNext,
jump: () => {
obj.sheet.getSequence().position = closestNext
},
}
: undefined,
}
},
[sequenceTrackId],
)
const nextPrevKeyframeCursors = (
<NextPrevKeyframeCursors {...nearbyKeyframeControls} />
)
const ret: HasSequences = {
...common,
type: 'HasSequences',
controlIndicators: nextPrevKeyframeCursors,
}
return ret
} else {
return {
...common,
type: 'AllStatic',
controlIndicators: (
<DefaultOrStaticValueIndicator hasStaticOverride={false} />
),
}
}
}, [])
}

View file

@ -1,10 +1,7 @@
import get from 'lodash-es/get' import get from 'lodash-es/get'
import last from 'lodash-es/last'
import React from 'react' import React from 'react'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {getPointerParts, prism, val} from '@theatre/dataverse' import {getPointerParts, prism, val} from '@theatre/dataverse'
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type Scrub from '@theatre/studio/Scrub' import type Scrub from '@theatre/studio/Scrub'
@ -15,8 +12,10 @@ import type {SerializablePrimitive as SerializablePrimitive} from '@theatre/shar
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes' import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
import {isPropConfSequencable} from '@theatre/shared/propTypes/utils' import {isPropConfSequencable} from '@theatre/shared/propTypes/utils'
import type {SequenceTrackId} from '@theatre/shared/utils/ids' import type {SequenceTrackId} from '@theatre/shared/utils/ids'
import DefaultOrStaticValueIndicator from './DefaultValueIndicator' import DefaultOrStaticValueIndicator from './DefaultValueIndicator'
import type {NearbyKeyframes} from './getNearbyKeyframesOfTrack'
import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack'
import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors'
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors' import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
interface EditingToolsCommon<T> { interface EditingToolsCommon<T> {
@ -170,33 +169,10 @@ export function useEditingToolsForSimplePropInDetailsPanel<
sequenceTrackId sequenceTrackId
], ],
) )
if (!track || track.keyframes.length === 0) return {} const sequencePosition = val(
obj.sheet.getSequence().positionDerivation,
const pos = val(obj.sheet.getSequence().positionDerivation) )
return getNearbyKeyframesOfTrack(track, sequencePosition)
const i = track.keyframes.findIndex((kf) => kf.position >= pos)
if (i === -1)
return {
prev: last(track.keyframes),
}
const k = track.keyframes[i]!
if (k.position === pos) {
return {
prev: i > 0 ? track.keyframes[i - 1] : undefined,
cur: k,
next:
i === track.keyframes.length - 1
? undefined
: track.keyframes[i + 1],
}
} else {
return {
next: k,
prev: i > 0 ? track.keyframes[i - 1] : undefined,
}
}
}, },
[sequenceTrackId], [sequenceTrackId],
) )
@ -215,13 +191,10 @@ export function useEditingToolsForSimplePropInDetailsPanel<
} }
} }
const nextPrevKeyframeCursors = ( const controls: NearbyKeyframesControls = {
<NextPrevKeyframeCursors cur: {
{...nearbyKeyframes} type: nearbyKeyframes.cur ? 'on' : 'off',
jumpToPosition={(position) => { toggle: () => {
obj.sheet.getSequence().position = position
}}
toggleKeyframeOnCurrentPosition={() => {
if (nearbyKeyframes.cur) { if (nearbyKeyframes.cur) {
getStudio()!.transaction((api) => { getStudio()!.transaction((api) => {
api.unset(pointerToProp) api.unset(pointerToProp)
@ -231,8 +204,32 @@ export function useEditingToolsForSimplePropInDetailsPanel<
api.set(pointerToProp, common.value) api.set(pointerToProp, common.value)
}) })
} }
}} },
/> },
prev:
nearbyKeyframes.prev !== undefined
? {
position: nearbyKeyframes.prev.position,
jump: () => {
obj.sheet.getSequence().position =
nearbyKeyframes.prev!.position
},
}
: undefined,
next:
nearbyKeyframes.next !== undefined
? {
position: nearbyKeyframes.next.position,
jump: () => {
obj.sheet.getSequence().position =
nearbyKeyframes.next!.position
},
}
: undefined,
}
const nextPrevKeyframeCursors = (
<NextPrevKeyframeCursors {...controls} />
) )
const ret: EditingToolsSequenced<T> = { const ret: EditingToolsSequenced<T> = {
@ -299,22 +296,6 @@ export function useEditingToolsForSimplePropInDetailsPanel<
}, []) }, [])
} }
type NearbyKeyframes = {
prev?: Keyframe
cur?: Keyframe
next?: Keyframe
}
export const shadeToColor: {[K in Shade]: string} = {
Default: '#222',
Static: '#333',
Static_BeingScrubbed: '#91a100',
Sequenced_OnKeyframe: '#700202',
Sequenced_OnKeyframe_BeingScrubbed: '#c50000',
Sequenced_BeingInterpolated: '#0387a8',
Sequened_NotBeingInterpolated: '#004c5f',
}
type Shade = type Shade =
| 'Default' | 'Default'
| 'Static' | 'Static'

View file

@ -34,7 +34,9 @@ type OnDragCallback = (
dyFromLastEvent: number, dyFromLastEvent: number,
) => void ) => void
type OnDragEndCallback = (dragHappened: boolean) => void type OnClickCallback = (mouseUpEvent: MouseEvent) => void
type OnDragEndCallback = (dragHappened: boolean, event?: MouseEvent) => void
export type UseDragOpts = { export type UseDragOpts = {
/** /**
@ -88,6 +90,7 @@ export type UseDragOpts = {
*/ */
onDragEnd?: OnDragEndCallback onDragEnd?: OnDragEndCallback
onDrag: OnDragCallback onDrag: OnDragCallback
onClick?: OnClickCallback
} }
// which mouse button to use the drag event // which mouse button to use the drag event
@ -170,7 +173,8 @@ export default function useDrag(
const callbacksRef = useRef<{ const callbacksRef = useRef<{
onDrag: OnDragCallback onDrag: OnDragCallback
onDragEnd: OnDragEndCallback onDragEnd: OnDragEndCallback
}>({onDrag: noop, onDragEnd: noop}) onClick: OnClickCallback
}>({onDrag: noop, onDragEnd: noop, onClick: noop})
const capturedPointerRef = useRef<undefined | CapturedPointer>() const capturedPointerRef = useRef<undefined | CapturedPointer>()
// needed to have a state on the react lifecycle which can be updated // needed to have a state on the react lifecycle which can be updated
@ -239,13 +243,16 @@ export default function useDrag(
} }
} }
const dragEndHandler = () => { const dragEndHandler = (e: MouseEvent) => {
removeDragListeners() removeDragListeners()
if (!stateRef.current.domDragStarted) return if (!stateRef.current.domDragStarted) return
const dragHappened = stateRef.current.detection.detected const dragHappened = stateRef.current.detection.detected
stateRef.current = {domDragStarted: false} stateRef.current = {domDragStarted: false}
if (opts.shouldPointerLock && !isSafari) document.exitPointerLock() if (opts.shouldPointerLock && !isSafari) document.exitPointerLock()
callbacksRef.current.onDragEnd(dragHappened) callbacksRef.current.onDragEnd(dragHappened)
if (!dragHappened) {
callbacksRef.current.onClick(e)
}
ensureIsDraggingUpToDateForReactLifecycle() ensureIsDraggingUpToDateForReactLifecycle()
} }
@ -296,6 +303,7 @@ export default function useDrag(
callbacksRef.current.onDrag = returnOfOnDragStart.onDrag callbacksRef.current.onDrag = returnOfOnDragStart.onDrag
callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop callbacksRef.current.onDragEnd = returnOfOnDragStart.onDragEnd ?? noop
callbacksRef.current.onClick = returnOfOnDragStart.onClick ?? 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
capturedPointerRef.current = capturePointer('Drag start') capturedPointerRef.current = capturePointer('Drag start')