feature/2022-05/display aggregate keyframes in sequence editor (#170)
Co-authored-by: Fülöp <fulopkovacs@users.noreply.github.com> Co-authored-by: Fülöp Kovács <kovacs.fulop@gmail.com>
This commit is contained in:
parent
d83d2b558c
commit
e8c440f357
41 changed files with 1410 additions and 462 deletions
|
@ -1,3 +1,4 @@
|
|||
import type {PathToProp_Encoded} from '@theatre/shared/utils/addresses'
|
||||
import type {
|
||||
KeyframeId,
|
||||
ObjectAddressKey,
|
||||
|
@ -26,6 +27,15 @@ export interface SheetState_Historic {
|
|||
// Question: What is this? The timeline position of a sequence?
|
||||
export type HistoricPositionalSequence = {
|
||||
type: 'PositionalSequence'
|
||||
/**
|
||||
* This is the length of the sequence in unit position. If the sequence
|
||||
* is interpreted in seconds, then a length=2 means the sequence is two
|
||||
* seconds long.
|
||||
*
|
||||
* Note that if there are keyframes sitting after sequence.length, they don't
|
||||
* get truncated, but calling sequence.play() will play until it reaches the
|
||||
* length of the sequence.
|
||||
*/
|
||||
length: number
|
||||
/**
|
||||
* Given the most common case of tracking a sequence against time (where 1 second = position 1),
|
||||
|
@ -37,12 +47,28 @@ export type HistoricPositionalSequence = {
|
|||
tracksByObject: StrictRecord<
|
||||
ObjectAddressKey,
|
||||
{
|
||||
trackIdByPropPath: StrictRecord<string, SequenceTrackId>
|
||||
// I think this prop path has to be to a basic keyframe track (simple prop)
|
||||
// at least until we have other kinds of "TrackData".
|
||||
// Explicitly, this does not include prop paths for compound props (those
|
||||
// are sequenced by sequenecing their simple descendant props)
|
||||
trackIdByPropPath: StrictRecord<PathToProp_Encoded, SequenceTrackId>
|
||||
|
||||
/**
|
||||
* A flat record of SequenceTrackId to TrackData. It's better
|
||||
* that only its sub-props are observed (say via val(pointer(...))),
|
||||
* rather than the object as a whole.
|
||||
*/
|
||||
trackData: StrictRecord<SequenceTrackId, TrackData>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently just {@link BasicKeyframedTrack}.
|
||||
*
|
||||
* Future: Other types of tracks can be added in, such as `MixedTrack` which would
|
||||
* look like `[keyframes, expression, moreKeyframes, anotherExpression, …]`.
|
||||
*/
|
||||
export type TrackData = BasicKeyframedTrack
|
||||
|
||||
export type Keyframe = {
|
||||
|
@ -56,8 +82,17 @@ export type Keyframe = {
|
|||
connectedRight: boolean
|
||||
}
|
||||
|
||||
export type BasicKeyframedTrack = {
|
||||
type: 'BasicKeyframedTrack'
|
||||
type TrackDataCommon<TypeName extends string> = {
|
||||
type: TypeName
|
||||
/**
|
||||
* Initial name of the track for debugging purposes. In the future, let's
|
||||
* strip this value from `studio.createContentOfSaveFile()` Could also be
|
||||
* useful for users who manually edit the project state.
|
||||
*/
|
||||
__debugName?: string
|
||||
}
|
||||
|
||||
export type BasicKeyframedTrack = TrackDataCommon<'BasicKeyframedTrack'> & {
|
||||
/**
|
||||
* {@link Keyframe} is not provided an explicit generic value `T`, because
|
||||
* a single track can technically have multiple different types for each keyframe.
|
||||
|
|
|
@ -64,7 +64,7 @@ export default class SheetObject implements IdentityDerivationProvider {
|
|||
template.address.objectKey,
|
||||
)
|
||||
this._logger._trace('creating object')
|
||||
this._internalUtilCtx = {logger: this._logger.downgrade.internal()}
|
||||
this._internalUtilCtx = {logger: this._logger.utilFor.internal()}
|
||||
this.address = {
|
||||
...template.address,
|
||||
sheetInstanceId: sheet.address.sheetInstanceId,
|
||||
|
|
|
@ -29,8 +29,24 @@ import {
|
|||
} from '@theatre/shared/propTypes/utils'
|
||||
import getOrderingOfPropTypeConfig from './getOrderingOfPropTypeConfig'
|
||||
|
||||
/**
|
||||
* Given an object like: `{transform: {type: 'absolute', position: {x: 0}}}`,
|
||||
* if both `transform.type` and `transform.position.x` are sequenced, this
|
||||
* type would look like:
|
||||
*
|
||||
* ```ts
|
||||
* {
|
||||
* transform: {
|
||||
* type: 'SDFJSDFJ', // track id of transform.type
|
||||
* position: {
|
||||
* x: 'NCXNS' // track id of transform.position.x
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type IPropPathToTrackIdTree = {
|
||||
[key in string]?: SequenceTrackId | IPropPathToTrackIdTree
|
||||
[propName in string]?: SequenceTrackId | IPropPathToTrackIdTree
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -79,8 +79,8 @@ function setupFn(options: ITheatreInternalLoggerOptions) {
|
|||
named(name: string, key?: string | number) {
|
||||
return t(logger.named(name, key))
|
||||
},
|
||||
downgrade: objMap(
|
||||
logger.downgrade,
|
||||
utilFor: objMap(
|
||||
logger.utilFor,
|
||||
([audience, downgradeFn]) =>
|
||||
() =>
|
||||
setupUtilLogger(downgradeFn(), audience, con),
|
||||
|
@ -146,7 +146,7 @@ type TestLoggerIncludes = ((string | RegExp) | {not: string | RegExp})[]
|
|||
|
||||
function setupUtilLogger(
|
||||
logger: IUtilLogger,
|
||||
audience: keyof ILogger['downgrade'],
|
||||
audience: keyof ILogger['utilFor'],
|
||||
con: jest.Mocked<ITheatreConsoleLogger>,
|
||||
) {
|
||||
return {
|
||||
|
|
|
@ -159,11 +159,11 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1'])
|
||||
})
|
||||
})
|
||||
describe('downgrade', () => {
|
||||
test('.downgrade.public() with defaults', () => {
|
||||
describe('utilFor', () => {
|
||||
test('.utilFor.public() with defaults', () => {
|
||||
const h = setup()
|
||||
|
||||
const publ = h.t().downgrade.public()
|
||||
const publ = h.t().utilFor.public()
|
||||
|
||||
publ.expectIncluded('error', 'error')
|
||||
publ.expectIncluded('warn', 'warn')
|
||||
|
@ -171,10 +171,10 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
publ.expectExcluded('trace')
|
||||
})
|
||||
|
||||
test('.downgrade.dev() with defaults', () => {
|
||||
test('.utilFor.dev() with defaults', () => {
|
||||
const h = setup()
|
||||
|
||||
const dev = h.t().downgrade.dev()
|
||||
const dev = h.t().utilFor.dev()
|
||||
|
||||
dev.expectExcluded('error')
|
||||
dev.expectExcluded('warn')
|
||||
|
@ -182,10 +182,10 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
dev.expectExcluded('trace')
|
||||
})
|
||||
|
||||
test('.downgrade.internal() with defaults', () => {
|
||||
test('.utilFor.internal() with defaults', () => {
|
||||
const h = setup()
|
||||
|
||||
const internal = h.t().downgrade.internal()
|
||||
const internal = h.t().utilFor.internal()
|
||||
|
||||
internal.expectExcluded('error')
|
||||
internal.expectExcluded('warn')
|
||||
|
@ -193,7 +193,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
internal.expectExcluded('trace')
|
||||
})
|
||||
|
||||
test('.downgrade.internal() can be named', () => {
|
||||
test('.utilFor.internal() can be named', () => {
|
||||
const h = setup()
|
||||
|
||||
h.internal.configureLogging({
|
||||
|
@ -201,7 +201,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
min: TheatreLoggerLevel.TRACE,
|
||||
})
|
||||
|
||||
const internal = h.t().downgrade.internal()
|
||||
const internal = h.t().utilFor.internal()
|
||||
const appleInternal = internal.named('Apple')
|
||||
|
||||
internal.expectIncluded('error', 'error', [{not: 'Apple'}])
|
||||
|
@ -215,13 +215,13 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
appleInternal.expectIncluded('trace', 'debug', ['Apple'])
|
||||
})
|
||||
|
||||
test('.downgrade.public() debug/trace warns internal', () => {
|
||||
test('.utilFor.public() debug/trace warns internal', () => {
|
||||
const h = setup()
|
||||
{
|
||||
h.internal.configureLogging({
|
||||
internal: true,
|
||||
})
|
||||
const publ = h.t().downgrade.public()
|
||||
const publ = h.t().utilFor.public()
|
||||
|
||||
publ.expectIncluded('error', 'error', [{not: 'filtered out'}])
|
||||
publ.expectIncluded('warn', 'warn', [{not: 'filtered out'}])
|
||||
|
@ -235,7 +235,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
|||
h.internal.configureLogging({
|
||||
dev: true,
|
||||
})
|
||||
const publ = h.t().downgrade.public()
|
||||
const publ = h.t().utilFor.public()
|
||||
|
||||
publ.expectIncluded('error', 'error', [{not: /filtered out/}])
|
||||
publ.expectIncluded('warn', 'warn', [{not: /filtered out/}])
|
||||
|
|
|
@ -68,11 +68,13 @@ export type _LazyLogFns = Readonly<
|
|||
}
|
||||
>
|
||||
|
||||
/** Internal library logger */
|
||||
/** Internal library logger
|
||||
* TODO document these fns
|
||||
*/
|
||||
export interface ILogger extends _LogFns {
|
||||
named(name: string, key?: string | number): ILogger
|
||||
lazy: _LazyLogFns
|
||||
readonly downgrade: {
|
||||
readonly utilFor: {
|
||||
internal(): IUtilLogger
|
||||
dev(): IUtilLogger
|
||||
public(): IUtilLogger
|
||||
|
@ -537,7 +539,7 @@ function createExtLogger(
|
|||
},
|
||||
//
|
||||
named,
|
||||
downgrade: {
|
||||
utilFor: {
|
||||
internal() {
|
||||
return {
|
||||
debug: logger._debug,
|
||||
|
@ -545,7 +547,7 @@ function createExtLogger(
|
|||
warn: logger._warn,
|
||||
trace: logger._trace,
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.internal()
|
||||
return logger.named(name, key).utilFor.internal()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -556,7 +558,7 @@ function createExtLogger(
|
|||
warn: logger.warnDev,
|
||||
trace: logger.traceDev,
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.dev()
|
||||
return logger.named(name, key).utilFor.dev()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -571,7 +573,7 @@ function createExtLogger(
|
|||
logger._warn(`(public "trace" filtered out) ${message}`, obj)
|
||||
},
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.public()
|
||||
return logger.named(name, key).utilFor.public()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -751,7 +753,7 @@ function _createConsoleLogger(
|
|||
},
|
||||
//
|
||||
named,
|
||||
downgrade: {
|
||||
utilFor: {
|
||||
internal() {
|
||||
return {
|
||||
debug: logger._debug,
|
||||
|
@ -759,7 +761,7 @@ function _createConsoleLogger(
|
|||
warn: logger._warn,
|
||||
trace: logger._trace,
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.internal()
|
||||
return logger.named(name, key).utilFor.internal()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -770,7 +772,7 @@ function _createConsoleLogger(
|
|||
warn: logger.warnDev,
|
||||
trace: logger.traceDev,
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.dev()
|
||||
return logger.named(name, key).utilFor.dev()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -785,7 +787,7 @@ function _createConsoleLogger(
|
|||
logger._warn(`(public "trace" filtered out) ${message}`, obj)
|
||||
},
|
||||
named(name, key) {
|
||||
return logger.named(name, key).downgrade.public()
|
||||
return logger.named(name, key).utilFor.public()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
@ -34,4 +34,4 @@ internal.configureLogging({
|
|||
export default internal
|
||||
.getLogger()
|
||||
.named('Theatre.js (default logger)')
|
||||
.downgrade.dev()
|
||||
.utilFor.dev()
|
||||
|
|
|
@ -4,6 +4,8 @@ import type {
|
|||
SerializableValue,
|
||||
} from '@theatre/shared/utils/types'
|
||||
import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids'
|
||||
import memoizeFn from './memoizeFn'
|
||||
import type {Nominal} from './Nominal'
|
||||
|
||||
/**
|
||||
* Represents the address to a project
|
||||
|
@ -57,10 +59,12 @@ export interface SheetObjectAddress extends SheetAddress {
|
|||
|
||||
export type PathToProp = Array<string | number>
|
||||
|
||||
export type PathToProp_Encoded = string
|
||||
export type PathToProp_Encoded = Nominal<'PathToProp_Encoded'>
|
||||
|
||||
export const encodePathToProp = (p: PathToProp): PathToProp_Encoded =>
|
||||
JSON.stringify(p)
|
||||
export const encodePathToProp = memoizeFn(
|
||||
(p: PathToProp): PathToProp_Encoded =>
|
||||
JSON.stringify(p) as PathToProp_Encoded,
|
||||
)
|
||||
|
||||
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
|
||||
JSON.parse(s)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, {useContext, useEffect, useMemo} from 'react'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
/** See {@link PointerCapturing} */
|
||||
export type CapturedPointer = {
|
||||
|
@ -35,6 +36,7 @@ type PointerCapturingFn = (forDebugName: string) => InternalPointerCapturing
|
|||
// const logger = console
|
||||
|
||||
function _usePointerCapturingContext(): PointerCapturingFn {
|
||||
const logger = useLogger('PointerCapturing')
|
||||
type CaptureInfo = {
|
||||
debugOwnerName: string
|
||||
debugReason: string
|
||||
|
@ -51,7 +53,7 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
|||
}
|
||||
const capturing: PointerCapturing = {
|
||||
capturePointer(reason) {
|
||||
// logger.log('Capturing pointer', {forDebugName, reason})
|
||||
logger._debug('Capturing pointer', {forDebugName, reason})
|
||||
if (currentCaptureRef.current != null) {
|
||||
throw new Error(
|
||||
`"${forDebugName}" attempted capturing pointer for "${reason}" while already captured by "${currentCaptureRef.current.debugOwnerName}" for "${currentCaptureRef.current.debugReason}"`,
|
||||
|
@ -69,10 +71,10 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
|||
},
|
||||
release() {
|
||||
if (releaseCapture === currentCaptureRef.current) {
|
||||
// logger.log('Releasing pointer', {
|
||||
// forDebugName,
|
||||
// reason,
|
||||
// })
|
||||
logger._debug('Releasing pointer', {
|
||||
forDebugName,
|
||||
reason,
|
||||
})
|
||||
updateCapture(null)
|
||||
return true
|
||||
}
|
||||
|
@ -89,7 +91,7 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
|||
capturing,
|
||||
forceRelease() {
|
||||
if (currentCaptureRef.current === localCapture) {
|
||||
// logger.log('Force releasing pointer', currentCaptureRef.current)
|
||||
logger._debug('Force releasing pointer', {localCapture})
|
||||
updateCapture(null)
|
||||
}
|
||||
},
|
||||
|
@ -100,7 +102,6 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
|||
const PointerCapturingContext = React.createContext<PointerCapturingFn>(
|
||||
null as $IntentionalAny,
|
||||
)
|
||||
// const ProviderChildren: React.FC<{children?: React.ReactNode}> = function
|
||||
|
||||
const ProviderChildrenMemo: React.FC<{}> = React.memo(({children}) => (
|
||||
<>{children}</>
|
||||
|
|
|
@ -14,6 +14,11 @@ import TooltipContext from '@theatre/studio/uiComponents/Popover/TooltipContext'
|
|||
import {ProvidePointerCapturing} from './PointerCapturing'
|
||||
import {MountAll} from '@theatre/studio/utils/renderInPortalInContext'
|
||||
import {PortalLayer, ProvideStyles} from '@theatre/studio/css'
|
||||
import {
|
||||
createTheatreInternalLogger,
|
||||
TheatreLoggerLevel,
|
||||
} from '@theatre/shared/logger'
|
||||
import {ProvideLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
const MakeRootHostContainStatic =
|
||||
typeof window !== 'undefined'
|
||||
|
@ -39,12 +44,24 @@ const Container = styled(PointerEventsHandler)`
|
|||
}
|
||||
`
|
||||
|
||||
const INTERNAL_LOGGING = /Playground.+Theatre\.js/.test(
|
||||
(typeof document !== 'undefined' ? document?.title : null) ?? '',
|
||||
)
|
||||
|
||||
export default function UIRoot() {
|
||||
const studio = getStudio()
|
||||
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
|
||||
undefined as $IntentionalAny,
|
||||
)
|
||||
|
||||
const uiRootLogger = createTheatreInternalLogger()
|
||||
uiRootLogger.configureLogging({
|
||||
min: TheatreLoggerLevel.DEBUG,
|
||||
dev: INTERNAL_LOGGING,
|
||||
internal: INTERNAL_LOGGING,
|
||||
})
|
||||
const logger = uiRootLogger.getLogger().named('Theatre UIRoot')
|
||||
|
||||
useKeyboardShortcuts()
|
||||
|
||||
const visiblityState = useVal(studio.atomP.ahistoric.visibilityState)
|
||||
|
@ -63,33 +80,35 @@ export default function UIRoot() {
|
|||
const initialised = val(studio.atomP.ephemeral.initialised)
|
||||
|
||||
return !initialised ? null : (
|
||||
<TooltipContext>
|
||||
<ProvidePointerCapturing>
|
||||
<MountExtensionComponents />
|
||||
<PortalContext.Provider value={portalLayer}>
|
||||
<ProvideStyles
|
||||
target={
|
||||
window.__IS_VISUAL_REGRESSION_TESTING === true
|
||||
? undefined
|
||||
: getStudio()!.ui.containerShadow
|
||||
}
|
||||
>
|
||||
<>
|
||||
<MakeRootHostContainStatic />
|
||||
<Container
|
||||
className={
|
||||
visiblityState === 'everythingIsHidden' ? 'invisible' : ''
|
||||
}
|
||||
>
|
||||
<PortalLayer ref={portalLayerRef} />
|
||||
{<GlobalToolbar />}
|
||||
{<PanelsRoot />}
|
||||
</Container>
|
||||
</>
|
||||
</ProvideStyles>
|
||||
</PortalContext.Provider>
|
||||
</ProvidePointerCapturing>
|
||||
</TooltipContext>
|
||||
<ProvideLogger logger={logger}>
|
||||
<TooltipContext>
|
||||
<ProvidePointerCapturing>
|
||||
<MountExtensionComponents />
|
||||
<PortalContext.Provider value={portalLayer}>
|
||||
<ProvideStyles
|
||||
target={
|
||||
window.__IS_VISUAL_REGRESSION_TESTING === true
|
||||
? undefined
|
||||
: getStudio()!.ui.containerShadow
|
||||
}
|
||||
>
|
||||
<>
|
||||
<MakeRootHostContainStatic />
|
||||
<Container
|
||||
className={
|
||||
visiblityState === 'everythingIsHidden' ? 'invisible' : ''
|
||||
}
|
||||
>
|
||||
<PortalLayer ref={portalLayerRef} />
|
||||
<GlobalToolbar />
|
||||
<PanelsRoot />
|
||||
</Container>
|
||||
</>
|
||||
</ProvideStyles>
|
||||
</PortalContext.Provider>
|
||||
</ProvidePointerCapturing>
|
||||
</TooltipContext>
|
||||
</ProvideLogger>
|
||||
)
|
||||
}, [studio, portalLayerRef, portalLayer])
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import {HiOutlineChevronRight} from 'react-icons/all'
|
|||
import styled from 'styled-components'
|
||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||
|
||||
export const Container = styled.li<{depth: number}>`
|
||||
export const LeftRowContainer = styled.li<{depth: number}>`
|
||||
--depth: ${(props) => props.depth};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -17,7 +17,7 @@ export const BaseHeader = styled.div<{isEven: boolean}>`
|
|||
border-bottom: 1px solid #7695b705;
|
||||
`
|
||||
|
||||
const Header = styled(BaseHeader)<{
|
||||
const LeftRowHeader = styled(BaseHeader)<{
|
||||
isSelectable: boolean
|
||||
isSelected: boolean
|
||||
}>`
|
||||
|
@ -32,7 +32,7 @@ const Header = styled(BaseHeader)<{
|
|||
${(props) => props.isSelected && `background: blue`};
|
||||
`
|
||||
|
||||
const Head_Label = styled.span`
|
||||
const LeftRowHead_Label = styled.span`
|
||||
${propNameTextCSS};
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -42,7 +42,7 @@ const Head_Label = styled.span`
|
|||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const Head_Icon = styled.span<{isCollapsed: boolean}>`
|
||||
const LeftRowHead_Icon = styled.span<{isCollapsed: boolean}>`
|
||||
width: 12px;
|
||||
padding: 8px;
|
||||
font-size: 9px;
|
||||
|
@ -59,14 +59,14 @@ const Head_Icon = styled.span<{isCollapsed: boolean}>`
|
|||
}
|
||||
`
|
||||
|
||||
const Children = styled.ul`
|
||||
const LeftRowChildren = styled.ul`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
`
|
||||
|
||||
const AnyCompositeRow: React.FC<{
|
||||
leaf: SequenceEditorTree_Row<unknown>
|
||||
leaf: SequenceEditorTree_Row<string>
|
||||
label: React.ReactNode
|
||||
toggleSelect?: VoidFn
|
||||
toggleCollapsed: VoidFn
|
||||
|
@ -85,9 +85,9 @@ const AnyCompositeRow: React.FC<{
|
|||
}) => {
|
||||
const hasChildren = Array.isArray(children) && children.length > 0
|
||||
|
||||
return (
|
||||
<Container depth={leaf.depth}>
|
||||
<Header
|
||||
return leaf.shouldRender ? (
|
||||
<LeftRowContainer depth={leaf.depth}>
|
||||
<LeftRowHeader
|
||||
style={{
|
||||
height: leaf.nodeHeight + 'px',
|
||||
}}
|
||||
|
@ -96,14 +96,14 @@ const AnyCompositeRow: React.FC<{
|
|||
onClick={toggleSelect}
|
||||
isEven={leaf.n % 2 === 0}
|
||||
>
|
||||
<Head_Icon isCollapsed={isCollapsed} onClick={toggleCollapsed}>
|
||||
<LeftRowHead_Icon isCollapsed={isCollapsed} onClick={toggleCollapsed}>
|
||||
<HiOutlineChevronRight />
|
||||
</Head_Icon>
|
||||
<Head_Label>{label}</Head_Label>
|
||||
</Header>
|
||||
{hasChildren && <Children>{children}</Children>}
|
||||
</Container>
|
||||
)
|
||||
</LeftRowHead_Icon>
|
||||
<LeftRowHead_Label>{label}</LeftRowHead_Label>
|
||||
</LeftRowHeader>
|
||||
{hasChildren && <LeftRowChildren>{children}</LeftRowChildren>}
|
||||
</LeftRowContainer>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default AnyCompositeRow
|
||||
|
|
|
@ -11,7 +11,7 @@ import styled from 'styled-components'
|
|||
import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
|
||||
import {nextPrevCursorsTheme} from '@theatre/studio/propEditors/NextPrevKeyframeCursors'
|
||||
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
|
||||
import {BaseHeader, Container as BaseContainer} from './AnyCompositeRow'
|
||||
import {BaseHeader, LeftRowContainer as BaseContainer} from './AnyCompositeRow'
|
||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||
|
||||
const theme = {
|
||||
|
@ -20,9 +20,9 @@ const theme = {
|
|||
},
|
||||
}
|
||||
|
||||
const Container = styled(BaseContainer)<{}>``
|
||||
const PrimitivePropRowContainer = styled(BaseContainer)<{}>``
|
||||
|
||||
const Head = styled(BaseHeader)<{
|
||||
const PrimitivePropRowHead = styled(BaseHeader)<{
|
||||
isSelected: boolean
|
||||
isEven: boolean
|
||||
}>`
|
||||
|
@ -34,7 +34,7 @@ const Head = styled(BaseHeader)<{
|
|||
box-sizing: border-box;
|
||||
`
|
||||
|
||||
const IconContainer = styled.button<{
|
||||
const PrimitivePropRowIconContainer = styled.button<{
|
||||
isSelected: boolean
|
||||
graphEditorColor: keyof typeof graphEditorColors
|
||||
}>`
|
||||
|
@ -73,7 +73,7 @@ const GraphIcon = () => (
|
|||
</svg>
|
||||
)
|
||||
|
||||
const Head_Label = styled.span`
|
||||
const PrimitivePropRowHead_Label = styled.span`
|
||||
margin-right: 4px;
|
||||
${propNameTextCSS};
|
||||
`
|
||||
|
@ -132,17 +132,17 @@ const PrimitivePropRow: React.FC<{
|
|||
const isSelectable = true
|
||||
|
||||
return (
|
||||
<Container depth={leaf.depth}>
|
||||
<Head
|
||||
<PrimitivePropRowContainer depth={leaf.depth}>
|
||||
<PrimitivePropRowHead
|
||||
isEven={leaf.n % 2 === 0}
|
||||
style={{
|
||||
height: leaf.nodeHeight + 'px',
|
||||
}}
|
||||
isSelected={isSelected === true}
|
||||
>
|
||||
<Head_Label>{label}</Head_Label>
|
||||
<PrimitivePropRowHead_Label>{label}</PrimitivePropRowHead_Label>
|
||||
{controlIndicators}
|
||||
<IconContainer
|
||||
<PrimitivePropRowIconContainer
|
||||
onClick={toggleSelect}
|
||||
isSelected={isSelected === true}
|
||||
graphEditorColor={possibleColor ?? '1'}
|
||||
|
@ -150,9 +150,9 @@ const PrimitivePropRow: React.FC<{
|
|||
disabled={!isSelectable}
|
||||
>
|
||||
<GraphIcon />
|
||||
</IconContainer>
|
||||
</Head>
|
||||
</Container>
|
||||
</PrimitivePropRowIconContainer>
|
||||
</PrimitivePropRowHead>
|
||||
</PrimitivePropRowContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,20 +7,21 @@ import React from 'react'
|
|||
import AnyCompositeRow from './AnyCompositeRow'
|
||||
import PrimitivePropRow from './PrimitivePropRow'
|
||||
import {setCollapsedSheetObjectOrCompoundProp} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp'
|
||||
|
||||
export const decideRowByPropType = (
|
||||
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
||||
): React.ReactElement =>
|
||||
leaf.type === 'propWithChildren' ? (
|
||||
<PropWithChildrenRow
|
||||
leaf={leaf}
|
||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
||||
/>
|
||||
): React.ReactElement => {
|
||||
const key = 'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]
|
||||
return leaf.shouldRender ? (
|
||||
leaf.type === 'propWithChildren' ? (
|
||||
<PropWithChildrenRow leaf={leaf} key={key} />
|
||||
) : (
|
||||
<PrimitivePropRow leaf={leaf} key={key} />
|
||||
)
|
||||
) : (
|
||||
<PrimitivePropRow
|
||||
leaf={leaf}
|
||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
||||
/>
|
||||
<React.Fragment key={key} />
|
||||
)
|
||||
}
|
||||
|
||||
const PropWithChildrenRow: React.VFC<{
|
||||
leaf: SequenceEditorTree_PropWithChildren
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
import type {
|
||||
Keyframe,
|
||||
TrackData,
|
||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import type {
|
||||
DopeSheetSelection,
|
||||
SequenceEditorPanelLayout,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import type {
|
||||
SequenceEditorTree_PropWithChildren,
|
||||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import {ConnectorLine} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||
import {AggregateKeyframePositionIsSelected} from './AggregatedKeyframeTrack'
|
||||
import type {KeyframeWithTrack} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
|
||||
const AggregateKeyframeEditorContainer = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
const noConnector = <></>
|
||||
|
||||
export type IAggregateKeyframesAtPosition = {
|
||||
position: number
|
||||
/** all tracks have a keyframe for this position (otherwise, false means 'partial') */
|
||||
allHere: boolean
|
||||
selected: AggregateKeyframePositionIsSelected | undefined
|
||||
keyframes: {
|
||||
kf: Keyframe
|
||||
track: {
|
||||
id: SequenceTrackId
|
||||
data: TrackData
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
export type IAggregateKeyframeEditorProps = {
|
||||
index: number
|
||||
aggregateKeyframes: IAggregateKeyframesAtPosition[]
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
viewModel:
|
||||
| SequenceEditorTree_PropWithChildren
|
||||
| SequenceEditorTree_SheetObject
|
||||
selection: undefined | DopeSheetSelection
|
||||
}
|
||||
|
||||
const AggregateKeyframeEditor: React.VFC<IAggregateKeyframeEditorProps> = (
|
||||
props,
|
||||
) => {
|
||||
const {index, aggregateKeyframes} = props
|
||||
const cur = aggregateKeyframes[index]
|
||||
const next = aggregateKeyframes[index + 1]
|
||||
const connected =
|
||||
next && cur.keyframes.length === next.keyframes.length
|
||||
? // all keyframes are same in the next position
|
||||
cur.keyframes.every(
|
||||
({track}, ind) => next.keyframes[ind].track === track,
|
||||
) && {
|
||||
length: next.position - cur.position,
|
||||
selected:
|
||||
cur.selected === AggregateKeyframePositionIsSelected.AllSelected &&
|
||||
next.selected === AggregateKeyframePositionIsSelected.AllSelected,
|
||||
}
|
||||
: null
|
||||
|
||||
return (
|
||||
<AggregateKeyframeEditorContainer
|
||||
style={{
|
||||
top: `${props.viewModel.nodeHeight / 2}px`,
|
||||
left: `calc(${val(
|
||||
props.layoutP.scaledSpace.leftPadding,
|
||||
)}px + calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||
cur.position
|
||||
}px))`,
|
||||
}}
|
||||
>
|
||||
<AggregateKeyframeDot
|
||||
keyframes={cur.keyframes}
|
||||
position={cur.position}
|
||||
theme={{
|
||||
isSelected: cur.selected,
|
||||
}}
|
||||
isAllHere={cur.allHere}
|
||||
/>
|
||||
{connected ? (
|
||||
<ConnectorLine
|
||||
/* TEMP: Disabled until interactivity */
|
||||
mvpIsInteractiveDisabled={true}
|
||||
connectorLengthInUnitSpace={connected.length}
|
||||
isPopoverOpen={false}
|
||||
// if all keyframe aggregates are selected
|
||||
isSelected={connected.selected}
|
||||
/>
|
||||
) : (
|
||||
noConnector
|
||||
)}
|
||||
</AggregateKeyframeEditorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const DOT_SIZE_PX = 16
|
||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||
|
||||
/** The keyframe diamond ◆ */
|
||||
const DotContainer = styled.div`
|
||||
position: absolute;
|
||||
${absoluteDims(DOT_SIZE_PX)}
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const HitZone = styled.div`
|
||||
z-index: 2;
|
||||
/* TEMP: Disabled until interactivity */
|
||||
/* cursor: ew-resize; */
|
||||
|
||||
${DopeSnapHitZoneUI.CSS}
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||
}
|
||||
|
||||
/* TEMP: Disabled until interactivity */
|
||||
/* &:hover + ${DotContainer}, */
|
||||
#pointer-root.draggingPositionInSequenceEditor &:hover + ${DotContainer},
|
||||
// notice "," css "or"
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} + ${DotContainer} {
|
||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||
}
|
||||
`
|
||||
|
||||
const AggregateKeyframeDot = React.forwardRef(AggregateKeyframeDot_ref)
|
||||
function AggregateKeyframeDot_ref(
|
||||
props: React.PropsWithChildren<{
|
||||
theme: IDotThemeValues
|
||||
isAllHere: boolean
|
||||
position: number
|
||||
keyframes: KeyframeWithTrack[]
|
||||
}>,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<HitZone
|
||||
ref={ref}
|
||||
{...DopeSnapHitZoneUI.reactProps({
|
||||
isDragging: false,
|
||||
position: props.position,
|
||||
})}
|
||||
/>
|
||||
<DotContainer>
|
||||
{props.isAllHere ? (
|
||||
<AggregateDotAllHereSvg {...props.theme} />
|
||||
) : (
|
||||
<AggregateDotSomeHereSvg {...props.theme} />
|
||||
)}
|
||||
</DotContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type IDotThemeValues = {
|
||||
isSelected: AggregateKeyframePositionIsSelected | undefined
|
||||
}
|
||||
|
||||
const SELECTED_COLOR = '#b8e4e2'
|
||||
const DEFAULT_PRIMARY_COLOR = '#40AAA4'
|
||||
const DEFAULT_SECONDARY_COLOR = '#45747C'
|
||||
const selectionColorAll = (theme: IDotThemeValues) =>
|
||||
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
||||
? SELECTED_COLOR
|
||||
: theme.isSelected ===
|
||||
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
||||
? DEFAULT_PRIMARY_COLOR
|
||||
: DEFAULT_SECONDARY_COLOR
|
||||
const selectionColorSome = (theme: IDotThemeValues) =>
|
||||
theme.isSelected === AggregateKeyframePositionIsSelected.AllSelected
|
||||
? SELECTED_COLOR
|
||||
: theme.isSelected ===
|
||||
AggregateKeyframePositionIsSelected.AtLeastOneUnselected
|
||||
? DEFAULT_PRIMARY_COLOR
|
||||
: DEFAULT_SECONDARY_COLOR
|
||||
|
||||
const AggregateDotAllHereSvg = (theme: IDotThemeValues) => (
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="4.46443"
|
||||
y="10.0078"
|
||||
width="5"
|
||||
height="5"
|
||||
transform="rotate(-45 4.46443 10.0078)"
|
||||
fill="#212327" // background knockout fill
|
||||
stroke={selectionColorSome(theme)}
|
||||
/>
|
||||
<rect
|
||||
x="3.75732"
|
||||
y="6.01953"
|
||||
width="6"
|
||||
height="6"
|
||||
transform="rotate(-45 3.75732 6.01953)"
|
||||
fill={selectionColorAll(theme)}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
// when the aggregate keyframes are sparse across tracks at this position
|
||||
const AggregateDotSomeHereSvg = (theme: IDotThemeValues) => (
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="4.46443"
|
||||
y="8"
|
||||
width="5"
|
||||
height="5"
|
||||
transform="rotate(-45 4.46443 8)"
|
||||
fill="#23262B"
|
||||
stroke={selectionColorAll(theme)}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default AggregateKeyframeEditor
|
|
@ -0,0 +1,188 @@
|
|||
import type {
|
||||
DopeSheetSelection,
|
||||
SequenceEditorPanelLayout,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import type {
|
||||
SequenceEditorTree_PropWithChildren,
|
||||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import type {IAggregateKeyframesAtPosition} from './AggregateKeyframeEditor'
|
||||
import AggregateKeyframeEditor from './AggregateKeyframeEditor'
|
||||
import type {AggregatedKeyframes} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/collectAggregateKeyframes'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
const AggregatedKeyframeTrackContainer = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
type IAggregatedKeyframeTracksProps = {
|
||||
viewModel:
|
||||
| SequenceEditorTree_PropWithChildren
|
||||
| SequenceEditorTree_SheetObject
|
||||
aggregatedKeyframes: AggregatedKeyframes
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}
|
||||
|
||||
type _AggSelection = {
|
||||
selectedPositions: Map<number, AggregateKeyframePositionIsSelected>
|
||||
selection: DopeSheetSelection | undefined
|
||||
}
|
||||
|
||||
const EMPTY_SELECTION: _AggSelection = Object.freeze({
|
||||
selectedPositions: new Map(),
|
||||
selection: undefined,
|
||||
})
|
||||
|
||||
function AggregatedKeyframeTrack_memo(props: IAggregatedKeyframeTracksProps) {
|
||||
const {layoutP, aggregatedKeyframes, viewModel} = props
|
||||
const logger = useLogger('AggregatedKeyframeTrack')
|
||||
const [containerRef, containerNode] = useRefAndState<HTMLDivElement | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const {selectedPositions, selection} = useCollectedSelectedPositions(
|
||||
layoutP,
|
||||
viewModel,
|
||||
aggregatedKeyframes,
|
||||
)
|
||||
|
||||
const [contextMenu, _, isOpen] = useAggregatedKeyframeTrackContextMenu(
|
||||
containerNode,
|
||||
props,
|
||||
() => logger._debug('see aggregatedKeyframes', props.aggregatedKeyframes),
|
||||
)
|
||||
|
||||
const posKfs: IAggregateKeyframesAtPosition[] = [
|
||||
...aggregatedKeyframes.byPosition.entries(),
|
||||
]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([position, keyframes]) => ({
|
||||
position,
|
||||
keyframes,
|
||||
selected: selectedPositions.get(position),
|
||||
allHere: keyframes.length === aggregatedKeyframes.tracks.length,
|
||||
}))
|
||||
|
||||
const keyframeEditors = posKfs.map(({position, keyframes}, index) => (
|
||||
<AggregateKeyframeEditor
|
||||
index={index}
|
||||
layoutP={layoutP}
|
||||
viewModel={viewModel}
|
||||
aggregateKeyframes={posKfs}
|
||||
key={'agg-' + position}
|
||||
selection={
|
||||
selectedPositions.has(position) === true ? selection : undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
|
||||
return (
|
||||
<AggregatedKeyframeTrackContainer
|
||||
ref={containerRef}
|
||||
style={{
|
||||
background: isOpen ? '#444850 ' : 'unset',
|
||||
}}
|
||||
>
|
||||
{keyframeEditors}
|
||||
{contextMenu}
|
||||
</AggregatedKeyframeTrackContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const AggregatedKeyframeTrack = React.memo(AggregatedKeyframeTrack_memo)
|
||||
export default AggregatedKeyframeTrack
|
||||
|
||||
export enum AggregateKeyframePositionIsSelected {
|
||||
AllSelected,
|
||||
AtLeastOneUnselected,
|
||||
NoneSelected,
|
||||
}
|
||||
|
||||
const {AllSelected, AtLeastOneUnselected, NoneSelected} =
|
||||
AggregateKeyframePositionIsSelected
|
||||
|
||||
/** Helper to put together the selected positions */
|
||||
function useCollectedSelectedPositions(
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
viewModel:
|
||||
| SequenceEditorTree_PropWithChildren
|
||||
| SequenceEditorTree_SheetObject,
|
||||
aggregatedKeyframes: AggregatedKeyframes,
|
||||
): _AggSelection {
|
||||
return usePrism(() => {
|
||||
const selectionAtom = val(layoutP.selectionAtom)
|
||||
const sheetObjectSelection = val(
|
||||
selectionAtom.pointer.current.byObjectKey[
|
||||
viewModel.sheetObject.address.objectKey
|
||||
],
|
||||
)
|
||||
if (!sheetObjectSelection) return EMPTY_SELECTION
|
||||
|
||||
const selectedAtPositions = new Map<
|
||||
number,
|
||||
AggregateKeyframePositionIsSelected
|
||||
>()
|
||||
|
||||
for (const [position, kfsWithTrack] of aggregatedKeyframes.byPosition) {
|
||||
let positionIsSelected: undefined | AggregateKeyframePositionIsSelected =
|
||||
undefined
|
||||
for (const kfWithTrack of kfsWithTrack) {
|
||||
const kfIsSelected =
|
||||
sheetObjectSelection.byTrackId[kfWithTrack.track.id]?.byKeyframeId?.[
|
||||
kfWithTrack.kf.id
|
||||
] === true
|
||||
// -1/10: This sux
|
||||
// undefined = have not encountered
|
||||
if (positionIsSelected === undefined) {
|
||||
// first item
|
||||
if (kfIsSelected) {
|
||||
positionIsSelected = AllSelected
|
||||
} else {
|
||||
positionIsSelected = NoneSelected
|
||||
}
|
||||
} else if (kfIsSelected) {
|
||||
if (positionIsSelected === NoneSelected) {
|
||||
positionIsSelected = AtLeastOneUnselected
|
||||
}
|
||||
} else {
|
||||
if (positionIsSelected === AllSelected) {
|
||||
positionIsSelected = AtLeastOneUnselected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positionIsSelected != null) {
|
||||
selectedAtPositions.set(position, positionIsSelected)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedPositions: selectedAtPositions,
|
||||
selection: val(selectionAtom.pointer.current),
|
||||
}
|
||||
}, [layoutP, aggregatedKeyframes])
|
||||
}
|
||||
|
||||
function useAggregatedKeyframeTrackContextMenu(
|
||||
node: HTMLDivElement | null,
|
||||
props: IAggregatedKeyframeTracksProps,
|
||||
debugOnOpen: () => void,
|
||||
) {
|
||||
return useContextMenu(node, {
|
||||
onOpen: debugOnOpen,
|
||||
displayName: 'Aggregate Keyframe Track',
|
||||
menuItems: () => {
|
||||
return []
|
||||
},
|
||||
})
|
||||
}
|
|
@ -7,7 +7,7 @@ import type {Pointer} from '@theatre/dataverse'
|
|||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
||||
import SingleKeyframeEditor from './KeyframeEditor/SingleKeyframeEditor'
|
||||
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
|
@ -25,7 +25,7 @@ type BasicKeyframedTracksProps = {
|
|||
trackData: TrackData
|
||||
}
|
||||
|
||||
const BasicKeyframedTrack: React.FC<BasicKeyframedTracksProps> = React.memo(
|
||||
const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
||||
(props) => {
|
||||
const {layoutP, trackData, leaf} = props
|
||||
const [containerRef, containerNode] = useRefAndState<HTMLDivElement | null>(
|
||||
|
@ -57,7 +57,7 @@ const BasicKeyframedTrack: React.FC<BasicKeyframedTracksProps> = React.memo(
|
|||
)
|
||||
|
||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||
<KeyframeEditor
|
||||
<SingleKeyframeEditor
|
||||
keyframe={kf}
|
||||
index={index}
|
||||
trackData={trackData}
|
||||
|
@ -89,15 +89,12 @@ function useBasicKeyframedTrackContextMenu(
|
|||
props: BasicKeyframedTracksProps,
|
||||
) {
|
||||
return useContextMenu(node, {
|
||||
displayName: 'Keyframe Track',
|
||||
menuItems: () => {
|
||||
const selectionKeyframes =
|
||||
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
||||
|
||||
if (selectionKeyframes.length > 0) {
|
||||
return [pasteKeyframesContextMenuItem(props, selectionKeyframes)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return [pasteKeyframesContextMenuItem(props, selectionKeyframes)]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -108,6 +105,7 @@ function pasteKeyframesContextMenuItem(
|
|||
): IContextMenuItem {
|
||||
return {
|
||||
label: 'Paste Keyframes',
|
||||
enabled: keyframes.length > 0,
|
||||
callback: () => {
|
||||
const sheet = val(props.layoutP.sheet)
|
||||
const sequence = sheet.getSequence()
|
||||
|
|
|
@ -4,12 +4,8 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
|||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {lighten} from 'polished'
|
||||
import React from 'react'
|
||||
import {useMemo, useRef} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {DOT_SIZE_PX} from './KeyframeDot'
|
||||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||
import CurveEditorPopover, {
|
||||
|
@ -19,72 +15,30 @@ import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceE
|
|||
import type {OpenFn} from '@theatre/studio/src/uiComponents/Popover/usePopover'
|
||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
||||
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
||||
import type {IConnectorThemeValues} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||
import {ConnectorLine} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/keyframeRowUI/ConnectorLine'
|
||||
import {COLOR_POPOVER_BACK} from './CurveEditorPopover/colors'
|
||||
import {useVal} from '@theatre/react'
|
||||
import {isKeyframeConnectionInSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||
|
||||
const CONNECTOR_HEIGHT = DOT_SIZE_PX / 2 + 1
|
||||
const CONNECTOR_WIDTH_UNSCALED = 1000
|
||||
import styled from 'styled-components'
|
||||
import {DOT_SIZE_PX} from './SingleKeyframeDot'
|
||||
|
||||
const POPOVER_MARGIN = 5
|
||||
|
||||
type IConnectorThemeValues = {
|
||||
isPopoverOpen: boolean
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
export const CONNECTOR_THEME = {
|
||||
normalColor: `#365b59`, // (greenish-blueish)ish
|
||||
popoverOpenColor: `#817720`, // orangey yellowish
|
||||
barColor: (values: IConnectorThemeValues) => {
|
||||
const base = values.isPopoverOpen
|
||||
? CONNECTOR_THEME.popoverOpenColor
|
||||
: CONNECTOR_THEME.normalColor
|
||||
return values.isSelected ? lighten(0.2, base) : base
|
||||
},
|
||||
hoverColor: (values: IConnectorThemeValues) => {
|
||||
const base = values.isPopoverOpen
|
||||
? CONNECTOR_THEME.popoverOpenColor
|
||||
: CONNECTOR_THEME.normalColor
|
||||
return values.isSelected ? lighten(0.4, base) : lighten(0.1, base)
|
||||
},
|
||||
}
|
||||
|
||||
const Container = styled.div<IConnectorThemeValues>`
|
||||
position: absolute;
|
||||
background: ${CONNECTOR_THEME.barColor};
|
||||
height: ${CONNECTOR_HEIGHT}px;
|
||||
width: ${CONNECTOR_WIDTH_UNSCALED}px;
|
||||
|
||||
left: 0;
|
||||
top: -${CONNECTOR_HEIGHT / 2}px;
|
||||
transform-origin: top left;
|
||||
z-index: 0;
|
||||
cursor: ew-resize;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: ' ';
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${CONNECTOR_THEME.hoverColor};
|
||||
}
|
||||
`
|
||||
|
||||
const EasingPopover = styled(BasicPopover)`
|
||||
--popover-outer-stroke: transparent;
|
||||
--popover-inner-stroke: ${COLOR_POPOVER_BACK};
|
||||
`
|
||||
|
||||
type IProps = Parameters<typeof KeyframeEditor>[0]
|
||||
type IBasicKeyframeConnectorProps = ISingleKeyframeEditorProps
|
||||
|
||||
const Connector: React.FC<IProps> = (props) => {
|
||||
const BasicKeyframeConnector: React.VFC<IBasicKeyframeConnectorProps> = (
|
||||
props,
|
||||
) => {
|
||||
const {index, trackData} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
|
@ -140,28 +94,26 @@ const Connector: React.FC<IProps> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...themeValues}
|
||||
<ConnectorLine
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
// Previously we used scale3d, which had weird fuzzy rendering look in both FF & Chrome
|
||||
transform: `scaleX(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||
connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED
|
||||
}))`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
connectorLengthInUnitSpace={connectorLengthInUnitSpace}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isSelected={!!props.selection}
|
||||
openPopover={(e) => {
|
||||
if (node) openPopover(e, node)
|
||||
}}
|
||||
>
|
||||
{popoverNode}
|
||||
{contextMenu}
|
||||
</Container>
|
||||
</ConnectorLine>
|
||||
)
|
||||
}
|
||||
export default BasicKeyframeConnector
|
||||
|
||||
export default Connector
|
||||
|
||||
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||
function useDragKeyframe(
|
||||
node: HTMLDivElement | null,
|
||||
props: IBasicKeyframeConnectorProps,
|
||||
) {
|
||||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
|
||||
|
@ -240,9 +192,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
|||
|
||||
useDrag(node, gestureHandlers)
|
||||
}
|
||||
|
||||
function useConnectorContextMenu(
|
||||
props: IProps,
|
||||
props: IBasicKeyframeConnectorProps,
|
||||
node: HTMLDivElement | null,
|
||||
cur: Keyframe,
|
||||
next: Keyframe,
|
|
@ -13,7 +13,7 @@ import fuzzy from 'fuzzy'
|
|||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import type KeyframeEditor from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor'
|
||||
import type {ISingleKeyframeEditorProps} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeEditor'
|
||||
import CurveSegmentEditor from './CurveSegmentEditor'
|
||||
import EasingOption from './EasingOption'
|
||||
import type {CSSCubicBezierArgsString, CubicBezierHandles} from './shared'
|
||||
|
@ -133,7 +133,7 @@ type IProps = {
|
|||
* Called when user hits enter/escape
|
||||
*/
|
||||
onRequestClose: (reason: string) => void
|
||||
} & Parameters<typeof KeyframeEditor>[0]
|
||||
} & ISingleKeyframeEditorProps
|
||||
|
||||
const CurveEditorPopover: React.FC<IProps> = (props) => {
|
||||
////// `tempTransaction` //////
|
||||
|
|
|
@ -10,16 +10,18 @@ import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConf
|
|||
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
|
||||
import {simplePropEditorByPropType} from '@theatre/studio/propEditors/simpleEditors/simplePropEditorByPropType'
|
||||
|
||||
import KeyframeSimplePropEditor from './DeterminePropEditorForKeyframe/KeyframeSimplePropEditor'
|
||||
import SingleKeyframeSimplePropEditor from './DeterminePropEditorForSingleKeyframe/SingleKeyframeSimplePropEditor'
|
||||
|
||||
type IDeterminePropEditorForKeyframeProps<K extends PropTypeConfig['type']> = {
|
||||
type IDeterminePropEditorForSingleKeyframeProps<
|
||||
K extends PropTypeConfig['type'],
|
||||
> = {
|
||||
editingTools: IEditingTools<PropConfigForType<K>['valueType']>
|
||||
propConfig: PropConfigForType<K>
|
||||
keyframeValue: PropConfigForType<K>['valueType']
|
||||
displayLabel?: string
|
||||
}
|
||||
|
||||
const KeyframePropEditorContainer = styled.div`
|
||||
const SingleKeyframePropEditorContainer = styled.div`
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
@ -28,7 +30,7 @@ const KeyframePropEditorContainer = styled.div`
|
|||
min-width: 100px;
|
||||
}
|
||||
`
|
||||
const KeyframePropLabel = styled.span`
|
||||
const SingleKeyframePropLabel = styled.span`
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
|
@ -50,8 +52,8 @@ const KeyframePropLabel = styled.span`
|
|||
*
|
||||
* @param p - propConfig object for any type of prop.
|
||||
*/
|
||||
export function DeterminePropEditorForKeyframe(
|
||||
p: IDeterminePropEditorForKeyframeProps<PropTypeConfig['type']>,
|
||||
export function DeterminePropEditorForSingleKeyframe(
|
||||
p: IDeterminePropEditorForSingleKeyframeProps<PropTypeConfig['type']>,
|
||||
) {
|
||||
const propConfig = p.propConfig
|
||||
|
||||
|
@ -66,9 +68,9 @@ export function DeterminePropEditorForKeyframe(
|
|||
const PropEditor = simplePropEditorByPropType[propConfig.type]
|
||||
|
||||
return (
|
||||
<KeyframePropEditorContainer>
|
||||
<KeyframePropLabel>{p.displayLabel}</KeyframePropLabel>
|
||||
<KeyframeSimplePropEditor
|
||||
<SingleKeyframePropEditorContainer>
|
||||
<SingleKeyframePropLabel>{p.displayLabel}</SingleKeyframePropLabel>
|
||||
<SingleKeyframeSimplePropEditor
|
||||
SimpleEditorComponent={
|
||||
PropEditor as React.VFC<
|
||||
ISimplePropEditorReactProps<PropTypeConfig_AllSimples>
|
||||
|
@ -78,7 +80,7 @@ export function DeterminePropEditorForKeyframe(
|
|||
editingTools={p.editingTools}
|
||||
keyframeValue={p.keyframeValue}
|
||||
/>
|
||||
</KeyframePropEditorContainer>
|
||||
</SingleKeyframePropEditorContainer>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import styled from 'styled-components'
|
|||
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
|
||||
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
|
||||
|
||||
export type IKeyframeSimplePropEditorProps<
|
||||
export type ISingleKeyframeSimplePropEditorProps<
|
||||
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
||||
> = {
|
||||
propConfig: TPropTypeConfig
|
||||
|
@ -13,7 +13,7 @@ export type IKeyframeSimplePropEditorProps<
|
|||
SimpleEditorComponent: React.VFC<ISimplePropEditorReactProps<TPropTypeConfig>>
|
||||
}
|
||||
|
||||
const KeyframeSimplePropEditorContainer = styled.div`
|
||||
const SingleKeyframeSimplePropEditorContainer = styled.div`
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -23,23 +23,23 @@ const KeyframeSimplePropEditorContainer = styled.div`
|
|||
* Initially used for inline keyframe property editor, this editor is attached to the
|
||||
* functionality of editing a property for a sequence keyframe.
|
||||
*/
|
||||
function KeyframeSimplePropEditor<
|
||||
function SingleKeyframeSimplePropEditor<
|
||||
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
||||
>({
|
||||
propConfig,
|
||||
editingTools,
|
||||
keyframeValue: value,
|
||||
SimpleEditorComponent: EditorComponent,
|
||||
}: IKeyframeSimplePropEditorProps<TPropTypeConfig>) {
|
||||
}: ISingleKeyframeSimplePropEditorProps<TPropTypeConfig>) {
|
||||
return (
|
||||
<KeyframeSimplePropEditorContainer>
|
||||
<SingleKeyframeSimplePropEditorContainer>
|
||||
<EditorComponent
|
||||
editingTools={editingTools}
|
||||
propConfig={propConfig}
|
||||
value={value}
|
||||
/>
|
||||
</KeyframeSimplePropEditorContainer>
|
||||
</SingleKeyframeSimplePropEditorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyframeSimplePropEditor
|
||||
export default SingleKeyframeSimplePropEditor
|
|
@ -11,36 +11,24 @@ import useDrag from '@theatre/studio/uiComponents/useDrag'
|
|||
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {
|
||||
includeLockFrameStampAttrs,
|
||||
useLockFrameStampPosition,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {
|
||||
lockedCursorCssVarName,
|
||||
useCssCursorLock,
|
||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import SnapCursor from './SnapCursor.svg'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||
import type {IKeyframeEditorProps} from './KeyframeEditor'
|
||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
|
||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
|
||||
import {DeterminePropEditorForKeyframe} from './DeterminePropEditorForKeyframe'
|
||||
import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe'
|
||||
import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import {DopeSnapHitZoneUI} from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnapHitZoneUI'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
import type {ILogger} from '@theatre/shared/logger'
|
||||
|
||||
export const DOT_SIZE_PX = 6
|
||||
const HIT_ZONE_SIZE_PX = 12
|
||||
const SNAP_CURSOR_SIZE_PX = 34
|
||||
const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 5
|
||||
|
||||
const dims = (size: number) => `
|
||||
left: ${-size / 2}px;
|
||||
top: ${-size / 2}px;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
`
|
||||
|
||||
const dotTheme = {
|
||||
normalColor: '#40AAA4',
|
||||
get selectedColor() {
|
||||
|
@ -51,7 +39,7 @@ const dotTheme = {
|
|||
/** The keyframe diamond ◆ */
|
||||
const Diamond = styled.div<{isSelected: boolean}>`
|
||||
position: absolute;
|
||||
${dims(DOT_SIZE_PX)}
|
||||
${absoluteDims(DOT_SIZE_PX)}
|
||||
|
||||
background: ${(props) =>
|
||||
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
||||
|
@ -59,56 +47,38 @@ const Diamond = styled.div<{isSelected: boolean}>`
|
|||
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
${dims(DOT_SIZE_PX)}
|
||||
`
|
||||
|
||||
const HitZone = styled.div`
|
||||
position: absolute;
|
||||
${dims(HIT_ZONE_SIZE_PX)};
|
||||
|
||||
z-index: 1;
|
||||
|
||||
cursor: ew-resize;
|
||||
|
||||
${DopeSnapHitZoneUI.CSS}
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor & {
|
||||
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
|
||||
}
|
||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||
}
|
||||
|
||||
&.beingDragged {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
&:hover + ${Diamond}, &.beingDragged + ${Diamond} {
|
||||
${dims(DOT_HOVER_SIZE_PX)}
|
||||
&:hover
|
||||
+ ${Diamond},
|
||||
// notice , "or" in CSS
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
||||
+ ${Diamond} {
|
||||
${absoluteDims(DOT_HOVER_SIZE_PX)}
|
||||
}
|
||||
`
|
||||
|
||||
type IKeyframeDotProps = IKeyframeEditorProps
|
||||
type ISingleKeyframeDotProps = ISingleKeyframeEditorProps
|
||||
|
||||
/** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
|
||||
const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
||||
const SingleKeyframeDot: React.VFC<ISingleKeyframeDotProps> = (props) => {
|
||||
const logger = useLogger('SingleKeyframeDot')
|
||||
const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||
|
||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
|
||||
const [inlineEditorPopover, openEditor] =
|
||||
useKeyframeInlineEditorPopover(props)
|
||||
const [isDragging] = useDragForKeyframeDot(node, props, {
|
||||
useSingleKeyframeInlineEditorPopover(props)
|
||||
const [isDragging] = useDragForSingleKeyframeDot(node, props, {
|
||||
onClickFromDrag(dragStartEvent) {
|
||||
openEditor(dragStartEvent, ref.current!)
|
||||
},
|
||||
|
@ -118,9 +88,10 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
|||
<>
|
||||
<HitZone
|
||||
ref={ref}
|
||||
{...includeLockFrameStampAttrs(props.keyframe.position)}
|
||||
{...DopeSnap.includePositionSnapAttrs(props.keyframe.position)}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
{...DopeSnapHitZoneUI.reactProps({
|
||||
isDragging,
|
||||
position: props.keyframe.position,
|
||||
})}
|
||||
/>
|
||||
<Diamond isSelected={!!props.selection} />
|
||||
{inlineEditorPopover}
|
||||
|
@ -129,11 +100,12 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default KeyframeDot
|
||||
export default SingleKeyframeDot
|
||||
|
||||
function useKeyframeContextMenu(
|
||||
function useSingleKeyframeContextMenu(
|
||||
target: HTMLDivElement | null,
|
||||
props: IKeyframeDotProps,
|
||||
logger: ILogger,
|
||||
props: ISingleKeyframeDotProps,
|
||||
) {
|
||||
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
||||
props.selection,
|
||||
|
@ -146,20 +118,24 @@ function useKeyframeContextMenu(
|
|||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||
|
||||
return useContextMenu(target, {
|
||||
displayName: 'Keyframe',
|
||||
menuItems: () => {
|
||||
return [keyframeSelectionItem, deleteItem]
|
||||
},
|
||||
onOpen() {
|
||||
logger._debug('Show keyframe', props)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** The editor that pops up when directly clicking a Keyframe. */
|
||||
function useKeyframeInlineEditorPopover(props: IKeyframeDotProps) {
|
||||
function useSingleKeyframeInlineEditorPopover(props: ISingleKeyframeDotProps) {
|
||||
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
|
||||
const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp)
|
||||
|
||||
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
|
||||
<BasicPopover showPopoverEdgeTriangle>
|
||||
<DeterminePropEditorForKeyframe
|
||||
<DeterminePropEditorForSingleKeyframe
|
||||
propConfig={props.leaf.propConf}
|
||||
editingTools={editingTools}
|
||||
keyframeValue={props.keyframe.value}
|
||||
|
@ -169,7 +145,9 @@ function useKeyframeInlineEditorPopover(props: IKeyframeDotProps) {
|
|||
))
|
||||
}
|
||||
|
||||
function useEditingToolsForKeyframeEditorPopover(props: IKeyframeDotProps) {
|
||||
function useEditingToolsForKeyframeEditorPopover(
|
||||
props: ISingleKeyframeDotProps,
|
||||
) {
|
||||
const obj = props.leaf.sheetObject
|
||||
return useTempTransactionEditingTools(({stateEditors}, value) => {
|
||||
const newKeyframe = {...props.keyframe, value}
|
||||
|
@ -182,9 +160,9 @@ function useEditingToolsForKeyframeEditorPopover(props: IKeyframeDotProps) {
|
|||
})
|
||||
}
|
||||
|
||||
function useDragForKeyframeDot(
|
||||
function useDragForSingleKeyframeDot(
|
||||
node: HTMLDivElement | null,
|
||||
props: IKeyframeDotProps,
|
||||
props: ISingleKeyframeDotProps,
|
||||
options: {
|
||||
/**
|
||||
* hmm: this is a hack so we can actually receive the
|
||||
|
@ -277,7 +255,7 @@ function useDragForKeyframeDot(
|
|||
}
|
||||
|
||||
function deleteSelectionOrKeyframeContextMenuItem(
|
||||
props: IKeyframeDotProps,
|
||||
props: ISingleKeyframeDotProps,
|
||||
): IContextMenuItem {
|
||||
return {
|
||||
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
||||
|
@ -300,7 +278,7 @@ function deleteSelectionOrKeyframeContextMenuItem(
|
|||
}
|
||||
|
||||
function copyKeyFrameContextMenuItem(
|
||||
props: IKeyframeDotProps,
|
||||
props: ISingleKeyframeDotProps,
|
||||
keyframeIds: string[],
|
||||
): IContextMenuItem {
|
||||
return {
|
|
@ -11,16 +11,16 @@ import type {Pointer} from '@theatre/dataverse'
|
|||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Connector from './Connector'
|
||||
import KeyframeDot from './KeyframeDot'
|
||||
import SingleKeyframeConnector from './BasicKeyframeConnector'
|
||||
import SingleKeyframeDot from './SingleKeyframeDot'
|
||||
|
||||
const Container = styled.div`
|
||||
const SingleKeyframeEditorContainer = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
const noConnector = <></>
|
||||
|
||||
export type IKeyframeEditorProps = {
|
||||
export type ISingleKeyframeEditorProps = {
|
||||
index: number
|
||||
keyframe: Keyframe
|
||||
trackData: TrackData
|
||||
|
@ -29,7 +29,7 @@ export type IKeyframeEditorProps = {
|
|||
selection: undefined | DopeSheetSelection
|
||||
}
|
||||
|
||||
const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
||||
const SingleKeyframeEditor: React.VFC<ISingleKeyframeEditorProps> = (props) => {
|
||||
const {index, trackData} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
|
@ -37,7 +37,7 @@ const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
|||
const connected = cur.connectedRight && !!next
|
||||
|
||||
return (
|
||||
<Container
|
||||
<SingleKeyframeEditorContainer
|
||||
style={{
|
||||
top: `${props.leaf.nodeHeight / 2}px`,
|
||||
left: `calc(${val(
|
||||
|
@ -47,10 +47,10 @@ const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
|||
}px))`,
|
||||
}}
|
||||
>
|
||||
<KeyframeDot {...props} />
|
||||
{connected ? <Connector {...props} /> : noConnector}
|
||||
</Container>
|
||||
<SingleKeyframeDot {...props} />
|
||||
{connected ? <SingleKeyframeConnector {...props} /> : noConnector}
|
||||
</SingleKeyframeEditorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyframeEditor
|
||||
export default SingleKeyframeEditor
|
|
@ -1,6 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 358 B |
|
@ -15,8 +15,15 @@ import type {
|
|||
DopeSheetSelection,
|
||||
SequenceEditorPanelLayout,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||
import type {SequenceEditorTree_AllRowTypes} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import type {
|
||||
SequenceEditorTree_AllRowTypes,
|
||||
SequenceEditorTree_PropWithChildren,
|
||||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||
import type {ILogger, IUtilLogger} from '@theatre/shared/logger'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
const Container = styled.div<{isShiftDown: boolean}>`
|
||||
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')};
|
||||
|
@ -54,6 +61,7 @@ function useCaptureSelection(
|
|||
) {
|
||||
const [ref, state] = useRefAndState<SelectionBounds | null>(null)
|
||||
|
||||
const logger = useLogger('useCaptureSelection')
|
||||
useDrag(
|
||||
containerNode,
|
||||
useMemo((): Parameters<typeof useDrag>[1] => {
|
||||
|
@ -96,7 +104,11 @@ function useCaptureSelection(
|
|||
ys: [ref.current!.ys[0], event.clientY - rect.top],
|
||||
}
|
||||
|
||||
const selection = utils.boundsToSelection(layoutP, ref.current)
|
||||
const selection = utils.boundsToSelection(
|
||||
logger,
|
||||
layoutP,
|
||||
ref.current,
|
||||
)
|
||||
val(layoutP.selectionAtom).setState({current: selection})
|
||||
},
|
||||
onDragEnd(_dragHappened) {
|
||||
|
@ -112,15 +124,70 @@ function useCaptureSelection(
|
|||
}
|
||||
|
||||
namespace utils {
|
||||
const collectForAggregatedChildren = (
|
||||
logger: IUtilLogger,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
leaf: SequenceEditorTree_SheetObject | SequenceEditorTree_PropWithChildren,
|
||||
bounds: SelectionBounds,
|
||||
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||
) => {
|
||||
const sheetObject = leaf.sheetObject
|
||||
const aggregatedKeyframes = collectAggregateKeyframesInPrism(logger, leaf)
|
||||
|
||||
const bottom = leaf.top + leaf.nodeHeight
|
||||
if (bottom > bounds.ys[0]) {
|
||||
for (const [position, keyframes] of aggregatedKeyframes.byPosition) {
|
||||
if (position <= bounds.positions[0]) continue
|
||||
if (position >= bounds.positions[1]) break
|
||||
|
||||
// yes selected
|
||||
|
||||
for (const keyframeWithTrack of keyframes) {
|
||||
mutableSetDeep(
|
||||
selectionByObjectKey,
|
||||
(selectionByObjectKeyP) =>
|
||||
// convenience for accessing a deep path which might not actually exist
|
||||
// through the use of pointer proxy (so we don't have to deal with undeifned )
|
||||
selectionByObjectKeyP[sheetObject.address.objectKey].byTrackId[
|
||||
keyframeWithTrack.track.id
|
||||
].byKeyframeId[keyframeWithTrack.kf.id],
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey)
|
||||
}
|
||||
|
||||
const collectorByLeafType: {
|
||||
[K in SequenceEditorTree_AllRowTypes['type']]?: (
|
||||
logger: IUtilLogger,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
|
||||
bounds: Exclude<SelectionBounds, null>,
|
||||
selection: DopeSheetSelection,
|
||||
bounds: SelectionBounds,
|
||||
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||
) => void
|
||||
} = {
|
||||
primitiveProp(layoutP, leaf, bounds, selection) {
|
||||
propWithChildren(logger, layoutP, leaf, bounds, selectionByObjectKey) {
|
||||
collectForAggregatedChildren(
|
||||
logger,
|
||||
layoutP,
|
||||
leaf,
|
||||
bounds,
|
||||
selectionByObjectKey,
|
||||
)
|
||||
},
|
||||
sheetObject(logger, layoutP, leaf, bounds, selectionByObjectKey) {
|
||||
collectForAggregatedChildren(
|
||||
logger,
|
||||
layoutP,
|
||||
leaf,
|
||||
bounds,
|
||||
selectionByObjectKey,
|
||||
)
|
||||
},
|
||||
primitiveProp(logger, layoutP, leaf, bounds, selectionByObjectKey) {
|
||||
const {sheetObject, trackId} = leaf
|
||||
const trackData = val(
|
||||
getStudio().atomP.historic.coreByProject[sheetObject.address.projectId]
|
||||
|
@ -134,10 +201,13 @@ namespace utils {
|
|||
if (kf.position >= bounds.positions[1]) break
|
||||
|
||||
mutableSetDeep(
|
||||
selection,
|
||||
(p) =>
|
||||
p.byObjectKey[sheetObject.address.objectKey].byTrackId[trackId]
|
||||
.byKeyframeId[kf.id],
|
||||
selectionByObjectKey,
|
||||
(selectionByObjectKeyP) =>
|
||||
// convenience for accessing a deep path which might not actually exist
|
||||
// through the use of pointer proxy (so we don't have to deal with undeifned )
|
||||
selectionByObjectKeyP[sheetObject.address.objectKey].byTrackId[
|
||||
trackId
|
||||
].byKeyframeId[kf.id],
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
@ -145,24 +215,29 @@ namespace utils {
|
|||
}
|
||||
|
||||
const collectChildren = (
|
||||
logger: IUtilLogger,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
leaf: SequenceEditorTree_AllRowTypes,
|
||||
bounds: Exclude<SelectionBounds, null>,
|
||||
selection: DopeSheetSelection,
|
||||
bounds: SelectionBounds,
|
||||
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||
) => {
|
||||
if ('children' in leaf) {
|
||||
for (const sub of leaf.children) {
|
||||
collectFromAnyLeaf(layoutP, sub, bounds, selection)
|
||||
collectFromAnyLeaf(logger, layoutP, sub, bounds, selectionByObjectKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectFromAnyLeaf(
|
||||
logger: IUtilLogger,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
leaf: SequenceEditorTree_AllRowTypes,
|
||||
bounds: Exclude<SelectionBounds, null>,
|
||||
selection: DopeSheetSelection,
|
||||
bounds: SelectionBounds,
|
||||
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||
) {
|
||||
// don't collect from non rendered
|
||||
if (!leaf.shouldRender) return
|
||||
|
||||
if (
|
||||
bounds.ys[0] > leaf.top + leaf.heightIncludingChildren ||
|
||||
leaf.top > bounds.ys[1]
|
||||
|
@ -171,20 +246,39 @@ namespace utils {
|
|||
}
|
||||
const collector = collectorByLeafType[leaf.type]
|
||||
if (collector) {
|
||||
collector(layoutP, leaf as $IntentionalAny, bounds, selection)
|
||||
collector(
|
||||
logger,
|
||||
layoutP,
|
||||
leaf as $IntentionalAny,
|
||||
bounds,
|
||||
selectionByObjectKey,
|
||||
)
|
||||
} else {
|
||||
collectChildren(layoutP, leaf, bounds, selection)
|
||||
collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function boundsToSelection(
|
||||
logger: ILogger,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
bounds: Exclude<SelectionBounds, null>,
|
||||
bounds: SelectionBounds,
|
||||
): DopeSheetSelection {
|
||||
const selectionByObjectKey: DopeSheetSelection['byObjectKey'] = {}
|
||||
bounds = sortBounds(bounds)
|
||||
|
||||
const tree = val(layoutP.tree)
|
||||
collectFromAnyLeaf(
|
||||
logger.utilFor.internal(),
|
||||
layoutP,
|
||||
tree,
|
||||
bounds,
|
||||
selectionByObjectKey,
|
||||
)
|
||||
|
||||
const sheet = val(layoutP.tree.sheet)
|
||||
const selection: DopeSheetSelection = {
|
||||
return {
|
||||
type: 'DopeSheetSelection',
|
||||
byObjectKey: {},
|
||||
byObjectKey: selectionByObjectKey,
|
||||
getDragHandlers(origin) {
|
||||
return {
|
||||
debugName: 'DopeSheetSelectionView/boundsToSelection',
|
||||
|
@ -204,23 +298,19 @@ namespace utils {
|
|||
ignore: origin.domNode,
|
||||
})
|
||||
|
||||
let delta: number
|
||||
if (snapPos != null) {
|
||||
delta = snapPos - origin.positionAtStartOfDrag
|
||||
} else {
|
||||
delta = toUnitSpace(dx)
|
||||
}
|
||||
const delta =
|
||||
snapPos != null
|
||||
? snapPos - origin.positionAtStartOfDrag
|
||||
: toUnitSpace(dx)
|
||||
|
||||
tempTransaction = getStudio()!.tempTransaction(
|
||||
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 objectKey of Object.keys(selectionByObjectKey)) {
|
||||
const {byTrackId} = selectionByObjectKey[objectKey]!
|
||||
for (const trackId of Object.keys(byTrackId)) {
|
||||
const {byKeyframeId} = byTrackId[trackId]!
|
||||
transformKeyframes({
|
||||
|
@ -249,13 +339,13 @@ namespace utils {
|
|||
}
|
||||
},
|
||||
delete() {
|
||||
getStudio()!.transaction(({stateEditors}) => {
|
||||
getStudio().transaction(({stateEditors}) => {
|
||||
const deleteKeyframes =
|
||||
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||
.deleteKeyframes
|
||||
|
||||
for (const objectKey of Object.keys(selection.byObjectKey)) {
|
||||
const {byTrackId} = selection.byObjectKey[objectKey]!
|
||||
for (const objectKey of Object.keys(selectionByObjectKey)) {
|
||||
const {byTrackId} = selectionByObjectKey[objectKey]!
|
||||
for (const trackId of Object.keys(byTrackId)) {
|
||||
const {byKeyframeId} = byTrackId[trackId]!
|
||||
deleteKeyframes({
|
||||
|
@ -269,13 +359,6 @@ namespace utils {
|
|||
})
|
||||
},
|
||||
}
|
||||
|
||||
bounds = sortBounds(bounds)
|
||||
|
||||
const tree = val(layoutP.tree)
|
||||
collectFromAnyLeaf(layoutP, tree, bounds, selection)
|
||||
|
||||
return selection
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,8 +378,8 @@ const sortBounds = (b: SelectionBounds): SelectionBounds => {
|
|||
}
|
||||
}
|
||||
|
||||
const SelectionRectangle: React.FC<{
|
||||
state: Exclude<SelectionBounds, null>
|
||||
const SelectionRectangle: React.VFC<{
|
||||
state: SelectionBounds
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({state, layoutP}) => {
|
||||
const atom = useValToAtom(state)
|
||||
|
|
|
@ -5,13 +5,15 @@ import {usePrism} from '@theatre/react'
|
|||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import React from 'react'
|
||||
import KeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
|
||||
import RightRow from './Row'
|
||||
import BasicKeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
const PrimitivePropRow: React.FC<{
|
||||
const PrimitivePropRow: React.VFC<{
|
||||
leaf: SequenceEditorTree_PrimitiveProp
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({leaf, layoutP}) => {
|
||||
const logger = useLogger('PrimitivePropRow', leaf.pathToProp.join())
|
||||
return usePrism(() => {
|
||||
const {sheetObject} = leaf
|
||||
const {trackId} = leaf
|
||||
|
@ -24,7 +26,7 @@ const PrimitivePropRow: React.FC<{
|
|||
)
|
||||
|
||||
if (trackData?.type !== 'BasicKeyframedTrack') {
|
||||
console.error(
|
||||
logger.errorDev(
|
||||
`trackData type ${trackData?.type} is not yet supported on the sequence editor`,
|
||||
)
|
||||
return (
|
||||
|
@ -32,7 +34,11 @@ const PrimitivePropRow: React.FC<{
|
|||
)
|
||||
} else {
|
||||
const node = (
|
||||
<KeyframedTrack layoutP={layoutP} trackData={trackData} leaf={leaf} />
|
||||
<BasicKeyframedTrack
|
||||
layoutP={layoutP}
|
||||
trackData={trackData}
|
||||
leaf={leaf}
|
||||
/>
|
||||
)
|
||||
|
||||
return <RightRow leaf={leaf} isCollapsed={false} node={node}></RightRow>
|
||||
|
|
|
@ -8,15 +8,18 @@ import type {Pointer} from '@theatre/dataverse'
|
|||
import React from 'react'
|
||||
import PrimitivePropRow from './PrimitivePropRow'
|
||||
import RightRow from './Row'
|
||||
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||
import {ProvideLogger, useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
|
||||
export const decideRowByPropType = (
|
||||
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||
): React.ReactElement =>
|
||||
leaf.type === 'propWithChildren' ? (
|
||||
<PropWithChildrenRow
|
||||
<RightPropWithChildrenRow
|
||||
layoutP={layoutP}
|
||||
leaf={leaf}
|
||||
viewModel={leaf}
|
||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
||||
/>
|
||||
) : (
|
||||
|
@ -27,19 +30,42 @@ export const decideRowByPropType = (
|
|||
/>
|
||||
)
|
||||
|
||||
const PropWithChildrenRow: React.VFC<{
|
||||
leaf: SequenceEditorTree_PropWithChildren
|
||||
const RightPropWithChildrenRow: React.VFC<{
|
||||
viewModel: SequenceEditorTree_PropWithChildren
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({leaf, layoutP}) => {
|
||||
}> = ({viewModel, layoutP}) => {
|
||||
const logger = useLogger(
|
||||
'RightPropWithChildrenRow',
|
||||
viewModel.pathToProp.join(),
|
||||
)
|
||||
return usePrism(() => {
|
||||
const node = <div />
|
||||
const aggregatedKeyframes = collectAggregateKeyframesInPrism(
|
||||
logger.utilFor.internal(),
|
||||
viewModel,
|
||||
)
|
||||
|
||||
const node = (
|
||||
<AggregatedKeyframeTrack
|
||||
layoutP={layoutP}
|
||||
aggregatedKeyframes={aggregatedKeyframes}
|
||||
viewModel={viewModel}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
||||
{leaf.children.map((propLeaf) =>
|
||||
decideRowByPropType(propLeaf, layoutP),
|
||||
)}
|
||||
</RightRow>
|
||||
<ProvideLogger logger={logger}>
|
||||
<RightRow
|
||||
leaf={viewModel}
|
||||
node={node}
|
||||
isCollapsed={viewModel.isCollapsed}
|
||||
>
|
||||
{viewModel.children.map((propLeaf) =>
|
||||
decideRowByPropType(propLeaf, layoutP),
|
||||
)}
|
||||
</RightRow>
|
||||
</ProvideLogger>
|
||||
)
|
||||
}, [leaf, layoutP])
|
||||
}, [viewModel, layoutP])
|
||||
}
|
||||
|
||||
export default RightPropWithChildrenRow
|
||||
|
|
|
@ -2,7 +2,7 @@ import type {SequenceEditorTree_Row} from '@theatre/studio/panels/SequenceEditor
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Container = styled.li<{}>`
|
||||
const RightRowContainer = styled.li<{}>`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
@ -10,7 +10,7 @@ const Container = styled.li<{}>`
|
|||
position: relative;
|
||||
`
|
||||
|
||||
const NodeWrapper = styled.div<{isEven: boolean}>`
|
||||
const RightRowNodeWrapper = styled.div<{isEven: boolean}>`
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
@ -29,7 +29,7 @@ const NodeWrapper = styled.div<{isEven: boolean}>`
|
|||
}
|
||||
`
|
||||
|
||||
const Children = styled.ul`
|
||||
const RightRowChildren = styled.ul`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
@ -46,24 +46,25 @@ const Children = styled.ul`
|
|||
* 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<{
|
||||
leaf: SequenceEditorTree_Row<unknown>
|
||||
leaf: SequenceEditorTree_Row<string>
|
||||
node: React.ReactElement
|
||||
isCollapsed: boolean
|
||||
}> = ({leaf, children, node, isCollapsed}) => {
|
||||
const hasChildren = Array.isArray(children) && children.length > 0
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<NodeWrapper
|
||||
return leaf.shouldRender ? (
|
||||
<RightRowContainer>
|
||||
<RightRowNodeWrapper
|
||||
style={{height: leaf.nodeHeight + 'px'}}
|
||||
isEven={leaf.n % 2 === 0}
|
||||
>
|
||||
{node}
|
||||
</NodeWrapper>
|
||||
{hasChildren && <Children>{children}</Children>}
|
||||
</Container>
|
||||
)
|
||||
</RightRowNodeWrapper>
|
||||
{hasChildren && <RightRowChildren>{children}</RightRowChildren>}
|
||||
</RightRowContainer>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default RightRow
|
||||
|
|
|
@ -5,13 +5,32 @@ import type {Pointer} from '@theatre/dataverse'
|
|||
import React from 'react'
|
||||
import {decideRowByPropType} from './PropWithChildrenRow'
|
||||
import RightRow from './Row'
|
||||
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||
|
||||
const RightSheetObjectRow: React.VFC<{
|
||||
leaf: SequenceEditorTree_SheetObject
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
}> = ({leaf, layoutP}) => {
|
||||
const logger = useLogger(
|
||||
`RightSheetObjectRow`,
|
||||
leaf.sheetObject.address.objectKey,
|
||||
)
|
||||
return usePrism(() => {
|
||||
const node = <div />
|
||||
const aggregatedKeyframes = collectAggregateKeyframesInPrism(
|
||||
logger.utilFor.internal(),
|
||||
leaf,
|
||||
)
|
||||
|
||||
const node = (
|
||||
<AggregatedKeyframeTrack
|
||||
layoutP={layoutP}
|
||||
aggregatedKeyframes={aggregatedKeyframes}
|
||||
viewModel={leaf}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
||||
{leaf.children.map((leaf) => decideRowByPropType(leaf, layoutP))}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import type {
|
||||
SequenceEditorTree_PropWithChildren,
|
||||
SequenceEditorTree_SheetObject,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import type {
|
||||
Keyframe,
|
||||
TrackData,
|
||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import type {IUtilLogger} from '@theatre/shared/logger'
|
||||
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
||||
|
||||
/**
|
||||
* An index over a series of keyframes that have been collected from different tracks.
|
||||
*
|
||||
* Usually constructed via {@link collectAggregateKeyframesInPrism}.
|
||||
*/
|
||||
export type AggregatedKeyframes = {
|
||||
byPosition: Map<number, KeyframeWithTrack[]>
|
||||
tracks: TrackWithId[]
|
||||
}
|
||||
|
||||
export type TrackWithId = {
|
||||
id: SequenceTrackId
|
||||
data: TrackData
|
||||
}
|
||||
|
||||
export type KeyframeWithTrack = {
|
||||
kf: Keyframe
|
||||
track: TrackWithId
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect {@link AggregatedKeyframes} information from the given tree row with children.
|
||||
*
|
||||
* Must be called within a `prism` context.
|
||||
*
|
||||
* Implementation progress 2/10:
|
||||
* - This currently does a lot of duplicate work for each compound rows' compound rows.
|
||||
* - This appears to have O(N) complexity with N being the number of "things" in the
|
||||
* tree, thus we don't see an immediate need to cache it further.
|
||||
* - If concerned, consider making a playground with a lot of objects to test this kind of thing.
|
||||
*
|
||||
* Note that we do not need to filter to only tracks that should be displayed, because we
|
||||
* do not do anything counting or iterating over all tracks.
|
||||
*
|
||||
* Furthermore, we _could_ have been traversing the tree of the sheet and producing
|
||||
* an aggreagte from that, but _that_ aggregate would not take into account
|
||||
* things like filters in the `SequenceEditorPanel`, where the filter would exclude
|
||||
* certain objects and props from the tree.
|
||||
*
|
||||
*/
|
||||
export function collectAggregateKeyframesInPrism(
|
||||
logger: IUtilLogger,
|
||||
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_SheetObject,
|
||||
): AggregatedKeyframes {
|
||||
const sheetObject = leaf.sheetObject
|
||||
|
||||
const projectId = sheetObject.address.projectId
|
||||
|
||||
const sheetObjectTracksP =
|
||||
getStudio().atomP.historic.coreByProject[projectId].sheetsById[
|
||||
sheetObject.address.sheetId
|
||||
].sequence.tracksByObject[sheetObject.address.objectKey]
|
||||
|
||||
const aggregatedKeyframes: AggregatedKeyframes[] = []
|
||||
const childSimpleTracks: TrackWithId[] = []
|
||||
for (const childLeaf of leaf.children) {
|
||||
if (childLeaf.type === 'primitiveProp') {
|
||||
const trackId = val(
|
||||
sheetObjectTracksP.trackIdByPropPath[
|
||||
encodePathToProp(childLeaf.pathToProp)
|
||||
],
|
||||
)
|
||||
if (!trackId) {
|
||||
logger.trace('missing track id?', {childLeaf})
|
||||
continue
|
||||
}
|
||||
|
||||
const trackData = val(sheetObjectTracksP.trackData[trackId])
|
||||
if (!trackData) {
|
||||
logger.trace('missing track data?', {trackId, childLeaf})
|
||||
continue
|
||||
}
|
||||
|
||||
childSimpleTracks.push({id: trackId, data: trackData})
|
||||
} else if (childLeaf.type === 'propWithChildren') {
|
||||
aggregatedKeyframes.push(
|
||||
collectAggregateKeyframesInPrism(
|
||||
logger.named('propWithChildren', childLeaf.pathToProp.join()),
|
||||
childLeaf,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
const _exhaustive: never = childLeaf
|
||||
logger.error('unexpected kind of prop', {childLeaf})
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace('see collected of children', {
|
||||
aggregatedKeyframes,
|
||||
childSimpleTracks,
|
||||
})
|
||||
|
||||
const tracks = aggregatedKeyframes
|
||||
.flatMap((a) => a.tracks)
|
||||
.concat(childSimpleTracks)
|
||||
|
||||
const byPosition = new Map<number, KeyframeWithTrack[]>()
|
||||
|
||||
for (const track of tracks) {
|
||||
const kfs = track.data.keyframes
|
||||
for (let i = 0; i < kfs.length; i++) {
|
||||
const kf = kfs[i]
|
||||
let existing = byPosition.get(kf.position)
|
||||
if (!existing) {
|
||||
existing = []
|
||||
byPosition.set(kf.position, existing)
|
||||
}
|
||||
existing.push({kf, track})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
byPosition,
|
||||
tracks,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import {lighten} from 'polished'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {DOT_SIZE_PX} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeDot'
|
||||
|
||||
const CONNECTOR_HEIGHT = DOT_SIZE_PX / 2 + 1
|
||||
const CONNECTOR_WIDTH_UNSCALED = 1000
|
||||
|
||||
export type IConnectorThemeValues = {
|
||||
isPopoverOpen: boolean
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
export const CONNECTOR_THEME = {
|
||||
normalColor: `#365b59`, // (greenish-blueish)ish
|
||||
popoverOpenColor: `#817720`, // orangey yellowish
|
||||
barColor: (values: IConnectorThemeValues) => {
|
||||
const base = values.isPopoverOpen
|
||||
? CONNECTOR_THEME.popoverOpenColor
|
||||
: CONNECTOR_THEME.normalColor
|
||||
return values.isSelected ? lighten(0.2, base) : base
|
||||
},
|
||||
hoverColor: (values: IConnectorThemeValues) => {
|
||||
const base = values.isPopoverOpen
|
||||
? CONNECTOR_THEME.popoverOpenColor
|
||||
: CONNECTOR_THEME.normalColor
|
||||
return values.isSelected ? lighten(0.4, base) : lighten(0.1, base)
|
||||
},
|
||||
}
|
||||
|
||||
const Container = styled.div<IConnectorThemeValues>`
|
||||
position: absolute;
|
||||
background: ${CONNECTOR_THEME.barColor};
|
||||
height: ${CONNECTOR_HEIGHT}px;
|
||||
width: ${CONNECTOR_WIDTH_UNSCALED}px;
|
||||
|
||||
left: 0;
|
||||
top: -${CONNECTOR_HEIGHT / 2}px;
|
||||
transform-origin: top left;
|
||||
z-index: 0;
|
||||
cursor: ew-resize;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: ' ';
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${CONNECTOR_THEME.hoverColor};
|
||||
}
|
||||
`
|
||||
|
||||
type IConnectorLineProps = React.PropsWithChildren<{
|
||||
isPopoverOpen: boolean
|
||||
/** TEMP: Remove once interactivity is added for aggregate? */
|
||||
mvpIsInteractiveDisabled?: boolean
|
||||
openPopover?: (event: React.MouseEvent) => void
|
||||
isSelected: boolean
|
||||
connectorLengthInUnitSpace: number
|
||||
}>
|
||||
|
||||
export const ConnectorLine = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
IConnectorLineProps
|
||||
>((props, ref) => {
|
||||
const themeValues: IConnectorThemeValues = {
|
||||
isPopoverOpen: props.isPopoverOpen,
|
||||
isSelected: props.isSelected,
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...themeValues}
|
||||
ref={ref}
|
||||
style={{
|
||||
// Previously we used scale3d, which had weird fuzzy rendering look in both FF & Chrome
|
||||
transform: `scaleX(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
||||
props.connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED
|
||||
}))`,
|
||||
pointerEvents: props.mvpIsInteractiveDisabled ? 'none' : undefined,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
props.openPopover?.(e)
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Container>
|
||||
)
|
||||
})
|
|
@ -25,11 +25,16 @@ export function isKeyframeConnectionInSelection(
|
|||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all the selected keyframes
|
||||
* that are connected to one another. Useful for changing
|
||||
* the tweening in between keyframes.
|
||||
*/
|
||||
export function selectedKeyframeConnections(
|
||||
projectId: ProjectId,
|
||||
sheetId: SheetId,
|
||||
selection: DopeSheetSelection | undefined,
|
||||
): IDerivation<Array<[Keyframe, Keyframe]>> {
|
||||
): IDerivation<Array<[left: Keyframe, right: Keyframe]>> {
|
||||
return prism(() => {
|
||||
if (selection === undefined) return []
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {lockedCursorCssVarName} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import {css} from 'styled-components'
|
||||
import SnapCursor from './SnapCursor.svg'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import DopeSnap from './DopeSnap'
|
||||
import {includeLockFrameStampAttrs} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
|
||||
const HIT_ZONE_SIZE_PX = 12
|
||||
const SNAP_CURSOR_SIZE_PX = 34
|
||||
const BEING_DRAGGED_CLASS = 'beingDragged'
|
||||
|
||||
/**
|
||||
* Helper CSS for consistent display of the `⸢⸤⸣⸥` thing
|
||||
*/
|
||||
export const DopeSnapHitZoneUI = {
|
||||
BEING_DRAGGED_CLASS,
|
||||
CSS: css`
|
||||
position: absolute;
|
||||
${absoluteDims(HIT_ZONE_SIZE_PX)};
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
&.${BEING_DRAGGED_CLASS} {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`,
|
||||
CSS_WHEN_SOMETHING_DRAGGING: css`
|
||||
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
|
||||
}
|
||||
`,
|
||||
/** Intrinsic element props for `<HitZone/>`s */
|
||||
reactProps(config: {position: number; isDragging: boolean}) {
|
||||
return {
|
||||
// `data-pos` and `includeLockFrameStampAttrs` 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.
|
||||
...includeLockFrameStampAttrs(config.position),
|
||||
...DopeSnap.includePositionSnapAttrs(config.position),
|
||||
className: config.isDragging ? DopeSnapHitZoneUI.BEING_DRAGGED_CLASS : '',
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
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,
|
||||
|
@ -11,33 +10,23 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
|||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import React, {useMemo, useRef} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
includeLockFrameStampAttrs,
|
||||
useLockFrameStampPosition,
|
||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {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 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'
|
||||
import DopeSnap from './DopeSnap'
|
||||
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||
import {DopeSnapHitZoneUI} from './DopeSnapHitZoneUI'
|
||||
|
||||
const MARKER_SIZE_W_PX = 12
|
||||
const MARKER_SIZE_H_PX = 12
|
||||
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 MarkerDotContainer = styled.div`
|
||||
position: absolute;
|
||||
|
@ -48,7 +37,7 @@ const MarkerDotContainer = styled.div`
|
|||
|
||||
const MarkerVisualDotSVGContainer = styled.div`
|
||||
position: absolute;
|
||||
${dims(MARKER_SIZE_W_PX, MARKER_SIZE_H_PX)}
|
||||
${absoluteDims(MARKER_SIZE_W_PX, MARKER_SIZE_H_PX)}
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
|
@ -73,53 +62,35 @@ const MarkerVisualDot = React.memo(() => (
|
|||
))
|
||||
|
||||
const HitZone = styled.div`
|
||||
position: absolute;
|
||||
${dims(HIT_ZONE_SIZE_PX)};
|
||||
z-index: 1;
|
||||
|
||||
cursor: ew-resize;
|
||||
|
||||
${pointerEventsAutoInNormalMode};
|
||||
${DopeSnapHitZoneUI.CSS}
|
||||
|
||||
// :not dragging marker to ensure that markers don't snap to other markers
|
||||
// this works because only one marker track (so this technique is not used by keyframes...)
|
||||
#pointer-root.draggingPositionInSequenceEditor:not(.draggingMarker) & {
|
||||
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||
}
|
||||
|
||||
// "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-root.draggingPositionInSequenceEditor
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} {
|
||||
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
|
||||
+ ${MarkerVisualDotSVGContainer},
|
||||
&.beingDragged
|
||||
// notice , "or" in CSS
|
||||
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
||||
+ ${MarkerVisualDotSVGContainer} {
|
||||
${dims(MARKER_HOVER_SIZE_W_PX, MARKER_HOVER_SIZE_H_PX)}
|
||||
${absoluteDims(MARKER_HOVER_SIZE_W_PX, MARKER_HOVER_SIZE_H_PX)}
|
||||
}
|
||||
`
|
||||
|
||||
type IMarkerDotProps = {
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
markerId: SequenceMarkerId
|
||||
|
@ -194,14 +165,10 @@ const MarkerDotVisible: React.VFC<IMarkerDotVisibleProps> = ({
|
|||
{contextMenu}
|
||||
<HitZone
|
||||
ref={markRef}
|
||||
// `data-pos` and `includeLockFrameStampAttrs` 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.
|
||||
{...includeLockFrameStampAttrs(marker.position)}
|
||||
{...DopeSnap.includePositionSnapAttrs(marker.position)}
|
||||
className={isDragging ? 'beingDragged' : ''}
|
||||
{...DopeSnapHitZoneUI.reactProps({
|
||||
isDragging,
|
||||
position: marker.position,
|
||||
})}
|
||||
/>
|
||||
<MarkerVisualDot />
|
||||
</>
|
||||
|
|
|
@ -76,11 +76,13 @@ const Thumb = styled.div`
|
|||
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
&.seeking {
|
||||
${Container}.seeking > & {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
||||
#pointer-root.draggingPositionInSequenceEditor
|
||||
${Container}:not(.seeking)
|
||||
> & {
|
||||
pointer-events: auto;
|
||||
cursor: var(${lockedCursorCssVarName});
|
||||
}
|
||||
|
@ -203,13 +205,13 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
|
||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||
return {
|
||||
debugName: 'Playhead',
|
||||
debugName: 'RightOverlay/Playhead',
|
||||
onDragStart() {
|
||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||
|
||||
const sequence = val(layoutP.sheet).getSequence()
|
||||
const posBeforeSeek = sequence.position
|
||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||
|
||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||
setIsSeeking(true)
|
||||
|
||||
return {
|
||||
|
@ -232,7 +234,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
|||
}
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
}, [layoutP, thumbNode])
|
||||
|
||||
const [isDragging] = useDrag(thumbNode, gestureHandlers)
|
||||
|
||||
|
|
|
@ -16,12 +16,35 @@ import logger from '@theatre/shared/logger'
|
|||
import {titleBarHeight} from '@theatre/studio/panels/BasePanel/common'
|
||||
import type {Studio} from '@theatre/studio/Studio'
|
||||
|
||||
export type SequenceEditorTree_Row<Type> = {
|
||||
type: Type
|
||||
/**
|
||||
* Base "view model" for each row with common
|
||||
* required information such as row heights & depth.
|
||||
*/
|
||||
export type SequenceEditorTree_Row<TypeName extends string> = {
|
||||
/** type of this row, e.g. `"sheet"` or `"sheetObject"` */
|
||||
type: TypeName
|
||||
/** Height of just the row in pixels */
|
||||
nodeHeight: number
|
||||
/** Height of the row + height with children in pixels */
|
||||
heightIncludingChildren: number
|
||||
|
||||
/** Visual indentation */
|
||||
depth: number
|
||||
/**
|
||||
* This is a part of the tree, but it is not rendered at all,
|
||||
* and it doesn't contribute to height.
|
||||
*
|
||||
* In the future, if we have a filtering mechanism like "show only position props",
|
||||
* this would not be the place to make false, that node should just not be included
|
||||
* in the tree at all, so it doesn't affect aggregate keyframes.
|
||||
*/
|
||||
shouldRender: boolean
|
||||
/**
|
||||
* Distance in pixels from the top of this row to the row container's top
|
||||
* This can be used to help figure out what's being box selected (marquee).
|
||||
*/
|
||||
top: number
|
||||
/** Row number (e.g. for correctly styling even / odd alternating styles) */
|
||||
n: number
|
||||
}
|
||||
|
||||
|
@ -78,26 +101,31 @@ export const calculateSequenceEditorTree = (
|
|||
prism.ensurePrism()
|
||||
let topSoFar = titleBarHeight
|
||||
let nSoFar = 0
|
||||
const rootShouldRender = true
|
||||
|
||||
const tree: SequenceEditorTree = {
|
||||
type: 'sheet',
|
||||
sheet,
|
||||
children: [],
|
||||
shouldRender: rootShouldRender,
|
||||
top: topSoFar,
|
||||
depth: -1,
|
||||
n: nSoFar,
|
||||
nodeHeight: 0, // always 0
|
||||
heightIncludingChildren: -1, // will define this later
|
||||
}
|
||||
nSoFar += 1
|
||||
|
||||
const collapsableP =
|
||||
if (rootShouldRender) {
|
||||
nSoFar += 1
|
||||
}
|
||||
|
||||
const collapsableItemSetP =
|
||||
studio.atomP.ahistoric.projects.stateByProjectId[sheet.address.projectId]
|
||||
.stateBySheetId[sheet.address.sheetId].sequence.collapsableItems
|
||||
|
||||
for (const sheetObject of Object.values(val(sheet.objectsP))) {
|
||||
if (sheetObject) {
|
||||
addObject(sheetObject, tree.children, tree.depth + 1)
|
||||
addObject(sheetObject, tree.children, tree.depth + 1, rootShouldRender)
|
||||
}
|
||||
}
|
||||
tree.heightIncludingChildren = topSoFar - tree.top
|
||||
|
@ -106,6 +134,7 @@ export const calculateSequenceEditorTree = (
|
|||
sheetObject: SheetObject,
|
||||
arrayOfChildren: Array<SequenceEditorTree_SheetObject>,
|
||||
level: number,
|
||||
shouldRender: boolean,
|
||||
) {
|
||||
const trackSetups = val(
|
||||
sheetObject.template.getMapOfValidSequenceTracks_forStudio(),
|
||||
|
@ -114,36 +143,44 @@ export const calculateSequenceEditorTree = (
|
|||
if (Object.keys(trackSetups).length === 0) return
|
||||
|
||||
const isCollapsedP =
|
||||
collapsableP.byId[createStudioSheetItemKey.forSheetObject(sheetObject)]
|
||||
.isCollapsed
|
||||
collapsableItemSetP.byId[
|
||||
createStudioSheetItemKey.forSheetObject(sheetObject)
|
||||
].isCollapsed
|
||||
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
||||
|
||||
const row: SequenceEditorTree_SheetObject = {
|
||||
type: 'sheetObject',
|
||||
isCollapsed: isCollapsed,
|
||||
isCollapsed,
|
||||
shouldRender,
|
||||
top: topSoFar,
|
||||
children: [],
|
||||
depth: level,
|
||||
n: nSoFar,
|
||||
sheetObject: sheetObject,
|
||||
nodeHeight: HEIGHT_OF_ANY_TITLE,
|
||||
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||
// Question: Why -1? Is this relevant for "shouldRender"?
|
||||
// Perhaps this is to indicate this does not have a valid value.
|
||||
heightIncludingChildren: -1,
|
||||
}
|
||||
arrayOfChildren.push(row)
|
||||
nSoFar += 1
|
||||
// As we add rows to the tree, top to bottom, we accumulate the pixel
|
||||
// distance to the top of the tree from the bottom of the current row:
|
||||
topSoFar += row.nodeHeight
|
||||
if (!isCollapsed) {
|
||||
addProps(
|
||||
sheetObject,
|
||||
trackSetups,
|
||||
[],
|
||||
sheetObject.template.config,
|
||||
row.children,
|
||||
level + 1,
|
||||
)
|
||||
|
||||
if (shouldRender) {
|
||||
nSoFar += 1
|
||||
// As we add rows to the tree, top to bottom, we accumulate the pixel
|
||||
// distance to the top of the tree from the bottom of the current row:
|
||||
topSoFar += row.nodeHeight
|
||||
}
|
||||
|
||||
addProps(
|
||||
sheetObject,
|
||||
trackSetups,
|
||||
[],
|
||||
sheetObject.template.config,
|
||||
row.children,
|
||||
level + 1,
|
||||
shouldRender && !isCollapsed,
|
||||
)
|
||||
|
||||
row.heightIncludingChildren = topSoFar - row.top
|
||||
}
|
||||
|
||||
|
@ -156,6 +193,7 @@ export const calculateSequenceEditorTree = (
|
|||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||
>,
|
||||
level: number,
|
||||
shouldRender: boolean,
|
||||
) {
|
||||
for (const [propKey, setupOrSetups] of Object.entries(trackSetups)) {
|
||||
const propConfig = parentPropConfig.props[propKey]
|
||||
|
@ -166,6 +204,7 @@ export const calculateSequenceEditorTree = (
|
|||
propConfig,
|
||||
arrayOfChildren,
|
||||
level,
|
||||
shouldRender,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +218,7 @@ export const calculateSequenceEditorTree = (
|
|||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||
>,
|
||||
level: number,
|
||||
shouldRender: boolean,
|
||||
) {
|
||||
if (conf.type === 'compound') {
|
||||
const trackMapping =
|
||||
|
@ -190,6 +230,7 @@ export const calculateSequenceEditorTree = (
|
|||
conf,
|
||||
arrayOfChildren,
|
||||
level,
|
||||
shouldRender,
|
||||
)
|
||||
} else if (conf.type === 'enum') {
|
||||
logger.warn('Prop type enum is not yet supported in the sequence editor')
|
||||
|
@ -203,6 +244,7 @@ export const calculateSequenceEditorTree = (
|
|||
conf,
|
||||
arrayOfChildren,
|
||||
level,
|
||||
shouldRender,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -216,40 +258,46 @@ export const calculateSequenceEditorTree = (
|
|||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||
>,
|
||||
level: number,
|
||||
shouldRender: boolean,
|
||||
) {
|
||||
const isCollapsedP =
|
||||
collapsableP.byId[
|
||||
collapsableItemSetP.byId[
|
||||
createStudioSheetItemKey.forSheetObjectProp(sheetObject, pathToProp)
|
||||
].isCollapsed
|
||||
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
||||
|
||||
const row: SequenceEditorTree_PropWithChildren = {
|
||||
type: 'propWithChildren',
|
||||
isCollapsed: isCollapsed,
|
||||
isCollapsed,
|
||||
pathToProp,
|
||||
sheetObject: sheetObject,
|
||||
shouldRender,
|
||||
top: topSoFar,
|
||||
children: [],
|
||||
nodeHeight: HEIGHT_OF_ANY_TITLE,
|
||||
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||
heightIncludingChildren: -1,
|
||||
depth: level,
|
||||
trackMapping,
|
||||
n: nSoFar,
|
||||
}
|
||||
arrayOfChildren.push(row)
|
||||
topSoFar += row.nodeHeight
|
||||
if (!isCollapsed) {
|
||||
nSoFar += 1
|
||||
|
||||
addProps(
|
||||
sheetObject,
|
||||
trackMapping,
|
||||
pathToProp,
|
||||
conf,
|
||||
row.children,
|
||||
level + 1,
|
||||
)
|
||||
if (shouldRender) {
|
||||
topSoFar += row.nodeHeight
|
||||
nSoFar += 1
|
||||
}
|
||||
|
||||
addProps(
|
||||
sheetObject,
|
||||
trackMapping,
|
||||
pathToProp,
|
||||
conf,
|
||||
row.children,
|
||||
level + 1,
|
||||
// collapsed shouldn't render child props
|
||||
shouldRender && !isCollapsed,
|
||||
)
|
||||
// }
|
||||
row.heightIncludingChildren = topSoFar - row.top
|
||||
}
|
||||
|
||||
|
@ -262,6 +310,7 @@ export const calculateSequenceEditorTree = (
|
|||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||
>,
|
||||
level: number,
|
||||
shouldRender: boolean,
|
||||
) {
|
||||
const row: SequenceEditorTree_PrimitiveProp = {
|
||||
type: 'primitiveProp',
|
||||
|
@ -269,9 +318,10 @@ export const calculateSequenceEditorTree = (
|
|||
depth: level,
|
||||
sheetObject: sheetObject,
|
||||
pathToProp,
|
||||
shouldRender,
|
||||
top: topSoFar,
|
||||
nodeHeight: HEIGHT_OF_ANY_TITLE,
|
||||
heightIncludingChildren: HEIGHT_OF_ANY_TITLE,
|
||||
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||
heightIncludingChildren: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||
trackId,
|
||||
n: nSoFar,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
BasicKeyframedTrack,
|
||||
HistoricPositionalSequence,
|
||||
Keyframe,
|
||||
SheetState_Historic,
|
||||
|
@ -609,10 +610,13 @@ namespace stateEditors {
|
|||
|
||||
const trackId = generateSequenceTrackId()
|
||||
|
||||
tracks.trackData[trackId] = {
|
||||
const track: BasicKeyframedTrack = {
|
||||
type: 'BasicKeyframedTrack',
|
||||
__debugName: `${p.objectKey}:${pathEncoded}`,
|
||||
keyframes: [],
|
||||
}
|
||||
|
||||
tracks.trackData[trackId] = track
|
||||
tracks.trackIdByPropPath[pathEncoded] = trackId
|
||||
}
|
||||
|
||||
|
@ -622,7 +626,7 @@ namespace stateEditors {
|
|||
},
|
||||
) {
|
||||
const tracks = _ensureTracksOfObject(p)
|
||||
const encodedPropPath = JSON.stringify(p.pathToProp)
|
||||
const encodedPropPath = encodePathToProp(p.pathToProp)
|
||||
const trackId = tracks.trackIdByPropPath[encodedPropPath]
|
||||
|
||||
if (typeof trackId !== 'string') return
|
||||
|
|
|
@ -2,6 +2,7 @@ import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
|||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||
import transparentize from 'polished/lib/color/transparentize'
|
||||
import type {ElementType} from 'react'
|
||||
import {useMemo} from 'react'
|
||||
import {useContext} from 'react'
|
||||
import React, {useLayoutEffect, useState} from 'react'
|
||||
import {createPortal} from 'react-dom'
|
||||
|
@ -18,6 +19,8 @@ const minWidth = 190
|
|||
*/
|
||||
const pointerDistanceThreshold = 20
|
||||
|
||||
const SHOW_OPTIONAL_MENU_TITLE = true
|
||||
|
||||
const MenuContainer = styled.ul`
|
||||
position: absolute;
|
||||
min-width: ${minWidth}px;
|
||||
|
@ -33,6 +36,13 @@ const MenuContainer = styled.ul`
|
|||
${pointerEventsAutoInNormalMode};
|
||||
border-radius: 3px;
|
||||
`
|
||||
const MenuTitle = styled.div`
|
||||
padding: 4px 10px;
|
||||
border-bottom: 1px solid #6262626d;
|
||||
color: #adadadb3;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
export type IContextMenuItemCustomNodeRenderFn = (controls: {
|
||||
closeMenu(): void
|
||||
|
@ -49,8 +59,13 @@ export type IContextMenuItemsValue =
|
|||
| IContextMenuItem[]
|
||||
| (() => IContextMenuItem[])
|
||||
|
||||
/**
|
||||
* TODO let's make sure that triggering a context menu would close
|
||||
* the other open context menu (if one _is_ open).
|
||||
*/
|
||||
const ContextMenu: React.FC<{
|
||||
items: IContextMenuItemsValue
|
||||
displayName?: string
|
||||
clickPoint: {clientX: number; clientY: number}
|
||||
onRequestClose: () => void
|
||||
}> = (props) => {
|
||||
|
@ -109,10 +124,28 @@ const ContextMenu: React.FC<{
|
|||
if (ev.key === 'Escape') props.onRequestClose()
|
||||
})
|
||||
|
||||
const items = Array.isArray(props.items) ? props.items : props.items()
|
||||
const items = useMemo(() => {
|
||||
const itemsArr = Array.isArray(props.items) ? props.items : props.items()
|
||||
if (itemsArr.length > 0) return itemsArr
|
||||
else
|
||||
return [
|
||||
{
|
||||
/**
|
||||
* TODO Need design for this
|
||||
*/
|
||||
label: props.displayName
|
||||
? `No actions for ${props.displayName}`
|
||||
: `No actions found`,
|
||||
enabled: false,
|
||||
},
|
||||
]
|
||||
}, [props.items])
|
||||
|
||||
return createPortal(
|
||||
<MenuContainer ref={setContainer}>
|
||||
{SHOW_OPTIONAL_MENU_TITLE && props.displayName ? (
|
||||
<MenuTitle>{props.displayName}</MenuTitle>
|
||||
) : null}
|
||||
{items.map((item, i) => (
|
||||
<Item
|
||||
key={`item-${i}`}
|
||||
|
|
|
@ -14,8 +14,8 @@ const ItemContainer = styled.li<{enabled: boolean}>`
|
|||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
pointer-events: ${(props) => (props.enabled ? 'auto' : 'none')};
|
||||
color: ${(props) => (props.enabled ? 'white' : '#AAA')};
|
||||
color: ${(props) => (props.enabled ? 'white' : '#8f8f8f')};
|
||||
cursor: ${(props) => (props.enabled ? 'normal' : 'not-allowed')};
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
|
@ -28,7 +28,8 @@ const ItemContainer = styled.li<{enabled: boolean}>`
|
|||
}
|
||||
|
||||
&:hover:after {
|
||||
background-color: rgba(63, 174, 191, 0.75);
|
||||
background-color: ${(props) =>
|
||||
props.enabled ? 'rgba(63, 174, 191, 0.75)' : 'initial'};
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -43,6 +44,7 @@ const Item: React.FC<{
|
|||
<ItemContainer
|
||||
onClick={props.enabled ? props.onClick : noop}
|
||||
enabled={props.enabled}
|
||||
title={props.enabled ? undefined : 'Disabled'}
|
||||
>
|
||||
<ItemLabel>{props.label}</ItemLabel>
|
||||
</ItemContainer>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||
import React from 'react'
|
||||
import React, {useEffect} from 'react'
|
||||
import ContextMenu from './ContextMenu/ContextMenu'
|
||||
import type {
|
||||
IContextMenuItemsValue,
|
||||
|
@ -21,15 +21,24 @@ export default function useContextMenu(
|
|||
target: HTMLElement | SVGElement | null,
|
||||
opts: IRequestContextMenuOptions & {
|
||||
menuItems: IContextMenuItemsValue
|
||||
displayName?: string
|
||||
onOpen?: () => void
|
||||
},
|
||||
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
||||
const [status, close] = useRequestContextMenu(target, opts)
|
||||
|
||||
useEffect(() => {
|
||||
if (status.isOpen) {
|
||||
opts.onOpen?.()
|
||||
}
|
||||
}, [status.isOpen, opts.onOpen])
|
||||
|
||||
const node = !status.isOpen ? (
|
||||
emptyNode
|
||||
) : (
|
||||
<ContextMenu
|
||||
items={opts.menuItems}
|
||||
displayName={opts.displayName}
|
||||
clickPoint={status.event}
|
||||
onRequestClose={close}
|
||||
/>
|
||||
|
|
24
theatre/studio/src/uiComponents/useLogger.tsx
Normal file
24
theatre/studio/src/uiComponents/useLogger.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type {ILogger} from '@theatre/shared/logger'
|
||||
import React, {useContext, useMemo} from 'react'
|
||||
|
||||
const loggerContext = React.createContext<ILogger>(null!)
|
||||
export function ProvideLogger(
|
||||
props: React.PropsWithChildren<{logger: ILogger}>,
|
||||
) {
|
||||
return (
|
||||
<loggerContext.Provider value={props.logger}>
|
||||
{props.children}
|
||||
</loggerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useLogger(name?: string, key?: number | string) {
|
||||
const parentLogger = useContext(loggerContext)
|
||||
return useMemo(() => {
|
||||
if (name) {
|
||||
return parentLogger.named(name, key)
|
||||
} else {
|
||||
return parentLogger
|
||||
}
|
||||
}, [parentLogger, name, key])
|
||||
}
|
6
theatre/studio/src/utils/absoluteDims.tsx
Normal file
6
theatre/studio/src/utils/absoluteDims.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const absoluteDims = (w: number, h = w) => `
|
||||
left: ${w * -0.5}px;
|
||||
top: ${h * -0.5}px;
|
||||
width: ${w}px;
|
||||
height: ${h}px;
|
||||
`
|
Loading…
Reference in a new issue