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 {
|
import type {
|
||||||
KeyframeId,
|
KeyframeId,
|
||||||
ObjectAddressKey,
|
ObjectAddressKey,
|
||||||
|
@ -26,6 +27,15 @@ export interface SheetState_Historic {
|
||||||
// Question: What is this? The timeline position of a sequence?
|
// Question: What is this? The timeline position of a sequence?
|
||||||
export type HistoricPositionalSequence = {
|
export type HistoricPositionalSequence = {
|
||||||
type: 'PositionalSequence'
|
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
|
length: number
|
||||||
/**
|
/**
|
||||||
* Given the most common case of tracking a sequence against time (where 1 second = position 1),
|
* 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<
|
tracksByObject: StrictRecord<
|
||||||
ObjectAddressKey,
|
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>
|
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 TrackData = BasicKeyframedTrack
|
||||||
|
|
||||||
export type Keyframe = {
|
export type Keyframe = {
|
||||||
|
@ -56,8 +82,17 @@ export type Keyframe = {
|
||||||
connectedRight: boolean
|
connectedRight: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BasicKeyframedTrack = {
|
type TrackDataCommon<TypeName extends string> = {
|
||||||
type: 'BasicKeyframedTrack'
|
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
|
* {@link Keyframe} is not provided an explicit generic value `T`, because
|
||||||
* a single track can technically have multiple different types for each keyframe.
|
* 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,
|
template.address.objectKey,
|
||||||
)
|
)
|
||||||
this._logger._trace('creating object')
|
this._logger._trace('creating object')
|
||||||
this._internalUtilCtx = {logger: this._logger.downgrade.internal()}
|
this._internalUtilCtx = {logger: this._logger.utilFor.internal()}
|
||||||
this.address = {
|
this.address = {
|
||||||
...template.address,
|
...template.address,
|
||||||
sheetInstanceId: sheet.address.sheetInstanceId,
|
sheetInstanceId: sheet.address.sheetInstanceId,
|
||||||
|
|
|
@ -29,8 +29,24 @@ import {
|
||||||
} from '@theatre/shared/propTypes/utils'
|
} from '@theatre/shared/propTypes/utils'
|
||||||
import getOrderingOfPropTypeConfig from './getOrderingOfPropTypeConfig'
|
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 = {
|
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) {
|
named(name: string, key?: string | number) {
|
||||||
return t(logger.named(name, key))
|
return t(logger.named(name, key))
|
||||||
},
|
},
|
||||||
downgrade: objMap(
|
utilFor: objMap(
|
||||||
logger.downgrade,
|
logger.utilFor,
|
||||||
([audience, downgradeFn]) =>
|
([audience, downgradeFn]) =>
|
||||||
() =>
|
() =>
|
||||||
setupUtilLogger(downgradeFn(), audience, con),
|
setupUtilLogger(downgradeFn(), audience, con),
|
||||||
|
@ -146,7 +146,7 @@ type TestLoggerIncludes = ((string | RegExp) | {not: string | RegExp})[]
|
||||||
|
|
||||||
function setupUtilLogger(
|
function setupUtilLogger(
|
||||||
logger: IUtilLogger,
|
logger: IUtilLogger,
|
||||||
audience: keyof ILogger['downgrade'],
|
audience: keyof ILogger['utilFor'],
|
||||||
con: jest.Mocked<ITheatreConsoleLogger>,
|
con: jest.Mocked<ITheatreConsoleLogger>,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -159,11 +159,11 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1'])
|
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('downgrade', () => {
|
describe('utilFor', () => {
|
||||||
test('.downgrade.public() with defaults', () => {
|
test('.utilFor.public() with defaults', () => {
|
||||||
const h = setup()
|
const h = setup()
|
||||||
|
|
||||||
const publ = h.t().downgrade.public()
|
const publ = h.t().utilFor.public()
|
||||||
|
|
||||||
publ.expectIncluded('error', 'error')
|
publ.expectIncluded('error', 'error')
|
||||||
publ.expectIncluded('warn', 'warn')
|
publ.expectIncluded('warn', 'warn')
|
||||||
|
@ -171,10 +171,10 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
publ.expectExcluded('trace')
|
publ.expectExcluded('trace')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('.downgrade.dev() with defaults', () => {
|
test('.utilFor.dev() with defaults', () => {
|
||||||
const h = setup()
|
const h = setup()
|
||||||
|
|
||||||
const dev = h.t().downgrade.dev()
|
const dev = h.t().utilFor.dev()
|
||||||
|
|
||||||
dev.expectExcluded('error')
|
dev.expectExcluded('error')
|
||||||
dev.expectExcluded('warn')
|
dev.expectExcluded('warn')
|
||||||
|
@ -182,10 +182,10 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
dev.expectExcluded('trace')
|
dev.expectExcluded('trace')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('.downgrade.internal() with defaults', () => {
|
test('.utilFor.internal() with defaults', () => {
|
||||||
const h = setup()
|
const h = setup()
|
||||||
|
|
||||||
const internal = h.t().downgrade.internal()
|
const internal = h.t().utilFor.internal()
|
||||||
|
|
||||||
internal.expectExcluded('error')
|
internal.expectExcluded('error')
|
||||||
internal.expectExcluded('warn')
|
internal.expectExcluded('warn')
|
||||||
|
@ -193,7 +193,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
internal.expectExcluded('trace')
|
internal.expectExcluded('trace')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('.downgrade.internal() can be named', () => {
|
test('.utilFor.internal() can be named', () => {
|
||||||
const h = setup()
|
const h = setup()
|
||||||
|
|
||||||
h.internal.configureLogging({
|
h.internal.configureLogging({
|
||||||
|
@ -201,7 +201,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
min: TheatreLoggerLevel.TRACE,
|
min: TheatreLoggerLevel.TRACE,
|
||||||
})
|
})
|
||||||
|
|
||||||
const internal = h.t().downgrade.internal()
|
const internal = h.t().utilFor.internal()
|
||||||
const appleInternal = internal.named('Apple')
|
const appleInternal = internal.named('Apple')
|
||||||
|
|
||||||
internal.expectIncluded('error', 'error', [{not: 'Apple'}])
|
internal.expectIncluded('error', 'error', [{not: 'Apple'}])
|
||||||
|
@ -215,13 +215,13 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
appleInternal.expectIncluded('trace', 'debug', ['Apple'])
|
appleInternal.expectIncluded('trace', 'debug', ['Apple'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('.downgrade.public() debug/trace warns internal', () => {
|
test('.utilFor.public() debug/trace warns internal', () => {
|
||||||
const h = setup()
|
const h = setup()
|
||||||
{
|
{
|
||||||
h.internal.configureLogging({
|
h.internal.configureLogging({
|
||||||
internal: true,
|
internal: true,
|
||||||
})
|
})
|
||||||
const publ = h.t().downgrade.public()
|
const publ = h.t().utilFor.public()
|
||||||
|
|
||||||
publ.expectIncluded('error', 'error', [{not: 'filtered out'}])
|
publ.expectIncluded('error', 'error', [{not: 'filtered out'}])
|
||||||
publ.expectIncluded('warn', 'warn', [{not: 'filtered out'}])
|
publ.expectIncluded('warn', 'warn', [{not: 'filtered out'}])
|
||||||
|
@ -235,7 +235,7 @@ describeLogger('Theatre internal logger', (setup) => {
|
||||||
h.internal.configureLogging({
|
h.internal.configureLogging({
|
||||||
dev: true,
|
dev: true,
|
||||||
})
|
})
|
||||||
const publ = h.t().downgrade.public()
|
const publ = h.t().utilFor.public()
|
||||||
|
|
||||||
publ.expectIncluded('error', 'error', [{not: /filtered out/}])
|
publ.expectIncluded('error', 'error', [{not: /filtered out/}])
|
||||||
publ.expectIncluded('warn', 'warn', [{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 {
|
export interface ILogger extends _LogFns {
|
||||||
named(name: string, key?: string | number): ILogger
|
named(name: string, key?: string | number): ILogger
|
||||||
lazy: _LazyLogFns
|
lazy: _LazyLogFns
|
||||||
readonly downgrade: {
|
readonly utilFor: {
|
||||||
internal(): IUtilLogger
|
internal(): IUtilLogger
|
||||||
dev(): IUtilLogger
|
dev(): IUtilLogger
|
||||||
public(): IUtilLogger
|
public(): IUtilLogger
|
||||||
|
@ -537,7 +539,7 @@ function createExtLogger(
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
named,
|
named,
|
||||||
downgrade: {
|
utilFor: {
|
||||||
internal() {
|
internal() {
|
||||||
return {
|
return {
|
||||||
debug: logger._debug,
|
debug: logger._debug,
|
||||||
|
@ -545,7 +547,7 @@ function createExtLogger(
|
||||||
warn: logger._warn,
|
warn: logger._warn,
|
||||||
trace: logger._trace,
|
trace: logger._trace,
|
||||||
named(name, key) {
|
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,
|
warn: logger.warnDev,
|
||||||
trace: logger.traceDev,
|
trace: logger.traceDev,
|
||||||
named(name, key) {
|
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)
|
logger._warn(`(public "trace" filtered out) ${message}`, obj)
|
||||||
},
|
},
|
||||||
named(name, key) {
|
named(name, key) {
|
||||||
return logger.named(name, key).downgrade.public()
|
return logger.named(name, key).utilFor.public()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -751,7 +753,7 @@ function _createConsoleLogger(
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
named,
|
named,
|
||||||
downgrade: {
|
utilFor: {
|
||||||
internal() {
|
internal() {
|
||||||
return {
|
return {
|
||||||
debug: logger._debug,
|
debug: logger._debug,
|
||||||
|
@ -759,7 +761,7 @@ function _createConsoleLogger(
|
||||||
warn: logger._warn,
|
warn: logger._warn,
|
||||||
trace: logger._trace,
|
trace: logger._trace,
|
||||||
named(name, key) {
|
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,
|
warn: logger.warnDev,
|
||||||
trace: logger.traceDev,
|
trace: logger.traceDev,
|
||||||
named(name, key) {
|
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)
|
logger._warn(`(public "trace" filtered out) ${message}`, obj)
|
||||||
},
|
},
|
||||||
named(name, key) {
|
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
|
export default internal
|
||||||
.getLogger()
|
.getLogger()
|
||||||
.named('Theatre.js (default logger)')
|
.named('Theatre.js (default logger)')
|
||||||
.downgrade.dev()
|
.utilFor.dev()
|
||||||
|
|
|
@ -4,6 +4,8 @@ import type {
|
||||||
SerializableValue,
|
SerializableValue,
|
||||||
} from '@theatre/shared/utils/types'
|
} from '@theatre/shared/utils/types'
|
||||||
import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids'
|
import type {ObjectAddressKey, ProjectId, SheetId, SheetInstanceId} from './ids'
|
||||||
|
import memoizeFn from './memoizeFn'
|
||||||
|
import type {Nominal} from './Nominal'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the address to a project
|
* Represents the address to a project
|
||||||
|
@ -57,10 +59,12 @@ export interface SheetObjectAddress extends SheetAddress {
|
||||||
|
|
||||||
export type PathToProp = Array<string | number>
|
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 =>
|
export const encodePathToProp = memoizeFn(
|
||||||
JSON.stringify(p)
|
(p: PathToProp): PathToProp_Encoded =>
|
||||||
|
JSON.stringify(p) as PathToProp_Encoded,
|
||||||
|
)
|
||||||
|
|
||||||
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
|
export const decodePathToProp = (s: PathToProp_Encoded): PathToProp =>
|
||||||
JSON.parse(s)
|
JSON.parse(s)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, {useContext, useEffect, useMemo} from 'react'
|
import React, {useContext, useEffect, useMemo} from 'react'
|
||||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
|
||||||
/** See {@link PointerCapturing} */
|
/** See {@link PointerCapturing} */
|
||||||
export type CapturedPointer = {
|
export type CapturedPointer = {
|
||||||
|
@ -35,6 +36,7 @@ type PointerCapturingFn = (forDebugName: string) => InternalPointerCapturing
|
||||||
// const logger = console
|
// const logger = console
|
||||||
|
|
||||||
function _usePointerCapturingContext(): PointerCapturingFn {
|
function _usePointerCapturingContext(): PointerCapturingFn {
|
||||||
|
const logger = useLogger('PointerCapturing')
|
||||||
type CaptureInfo = {
|
type CaptureInfo = {
|
||||||
debugOwnerName: string
|
debugOwnerName: string
|
||||||
debugReason: string
|
debugReason: string
|
||||||
|
@ -51,7 +53,7 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
||||||
}
|
}
|
||||||
const capturing: PointerCapturing = {
|
const capturing: PointerCapturing = {
|
||||||
capturePointer(reason) {
|
capturePointer(reason) {
|
||||||
// logger.log('Capturing pointer', {forDebugName, reason})
|
logger._debug('Capturing pointer', {forDebugName, reason})
|
||||||
if (currentCaptureRef.current != null) {
|
if (currentCaptureRef.current != null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`"${forDebugName}" attempted capturing pointer for "${reason}" while already captured by "${currentCaptureRef.current.debugOwnerName}" for "${currentCaptureRef.current.debugReason}"`,
|
`"${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() {
|
release() {
|
||||||
if (releaseCapture === currentCaptureRef.current) {
|
if (releaseCapture === currentCaptureRef.current) {
|
||||||
// logger.log('Releasing pointer', {
|
logger._debug('Releasing pointer', {
|
||||||
// forDebugName,
|
forDebugName,
|
||||||
// reason,
|
reason,
|
||||||
// })
|
})
|
||||||
updateCapture(null)
|
updateCapture(null)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -89,7 +91,7 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
||||||
capturing,
|
capturing,
|
||||||
forceRelease() {
|
forceRelease() {
|
||||||
if (currentCaptureRef.current === localCapture) {
|
if (currentCaptureRef.current === localCapture) {
|
||||||
// logger.log('Force releasing pointer', currentCaptureRef.current)
|
logger._debug('Force releasing pointer', {localCapture})
|
||||||
updateCapture(null)
|
updateCapture(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -100,7 +102,6 @@ function _usePointerCapturingContext(): PointerCapturingFn {
|
||||||
const PointerCapturingContext = React.createContext<PointerCapturingFn>(
|
const PointerCapturingContext = React.createContext<PointerCapturingFn>(
|
||||||
null as $IntentionalAny,
|
null as $IntentionalAny,
|
||||||
)
|
)
|
||||||
// const ProviderChildren: React.FC<{children?: React.ReactNode}> = function
|
|
||||||
|
|
||||||
const ProviderChildrenMemo: React.FC<{}> = React.memo(({children}) => (
|
const ProviderChildrenMemo: React.FC<{}> = React.memo(({children}) => (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
|
|
@ -14,6 +14,11 @@ import TooltipContext from '@theatre/studio/uiComponents/Popover/TooltipContext'
|
||||||
import {ProvidePointerCapturing} from './PointerCapturing'
|
import {ProvidePointerCapturing} from './PointerCapturing'
|
||||||
import {MountAll} from '@theatre/studio/utils/renderInPortalInContext'
|
import {MountAll} from '@theatre/studio/utils/renderInPortalInContext'
|
||||||
import {PortalLayer, ProvideStyles} from '@theatre/studio/css'
|
import {PortalLayer, ProvideStyles} from '@theatre/studio/css'
|
||||||
|
import {
|
||||||
|
createTheatreInternalLogger,
|
||||||
|
TheatreLoggerLevel,
|
||||||
|
} from '@theatre/shared/logger'
|
||||||
|
import {ProvideLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
|
||||||
const MakeRootHostContainStatic =
|
const MakeRootHostContainStatic =
|
||||||
typeof window !== 'undefined'
|
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() {
|
export default function UIRoot() {
|
||||||
const studio = getStudio()
|
const studio = getStudio()
|
||||||
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
|
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
|
||||||
undefined as $IntentionalAny,
|
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()
|
useKeyboardShortcuts()
|
||||||
|
|
||||||
const visiblityState = useVal(studio.atomP.ahistoric.visibilityState)
|
const visiblityState = useVal(studio.atomP.ahistoric.visibilityState)
|
||||||
|
@ -63,33 +80,35 @@ export default function UIRoot() {
|
||||||
const initialised = val(studio.atomP.ephemeral.initialised)
|
const initialised = val(studio.atomP.ephemeral.initialised)
|
||||||
|
|
||||||
return !initialised ? null : (
|
return !initialised ? null : (
|
||||||
<TooltipContext>
|
<ProvideLogger logger={logger}>
|
||||||
<ProvidePointerCapturing>
|
<TooltipContext>
|
||||||
<MountExtensionComponents />
|
<ProvidePointerCapturing>
|
||||||
<PortalContext.Provider value={portalLayer}>
|
<MountExtensionComponents />
|
||||||
<ProvideStyles
|
<PortalContext.Provider value={portalLayer}>
|
||||||
target={
|
<ProvideStyles
|
||||||
window.__IS_VISUAL_REGRESSION_TESTING === true
|
target={
|
||||||
? undefined
|
window.__IS_VISUAL_REGRESSION_TESTING === true
|
||||||
: getStudio()!.ui.containerShadow
|
? undefined
|
||||||
}
|
: getStudio()!.ui.containerShadow
|
||||||
>
|
}
|
||||||
<>
|
>
|
||||||
<MakeRootHostContainStatic />
|
<>
|
||||||
<Container
|
<MakeRootHostContainStatic />
|
||||||
className={
|
<Container
|
||||||
visiblityState === 'everythingIsHidden' ? 'invisible' : ''
|
className={
|
||||||
}
|
visiblityState === 'everythingIsHidden' ? 'invisible' : ''
|
||||||
>
|
}
|
||||||
<PortalLayer ref={portalLayerRef} />
|
>
|
||||||
{<GlobalToolbar />}
|
<PortalLayer ref={portalLayerRef} />
|
||||||
{<PanelsRoot />}
|
<GlobalToolbar />
|
||||||
</Container>
|
<PanelsRoot />
|
||||||
</>
|
</Container>
|
||||||
</ProvideStyles>
|
</>
|
||||||
</PortalContext.Provider>
|
</ProvideStyles>
|
||||||
</ProvidePointerCapturing>
|
</PortalContext.Provider>
|
||||||
</TooltipContext>
|
</ProvidePointerCapturing>
|
||||||
|
</TooltipContext>
|
||||||
|
</ProvideLogger>
|
||||||
)
|
)
|
||||||
}, [studio, portalLayerRef, portalLayer])
|
}, [studio, portalLayerRef, portalLayer])
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {HiOutlineChevronRight} from 'react-icons/all'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
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};
|
--depth: ${(props) => props.depth};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -17,7 +17,7 @@ export const BaseHeader = styled.div<{isEven: boolean}>`
|
||||||
border-bottom: 1px solid #7695b705;
|
border-bottom: 1px solid #7695b705;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Header = styled(BaseHeader)<{
|
const LeftRowHeader = styled(BaseHeader)<{
|
||||||
isSelectable: boolean
|
isSelectable: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
}>`
|
}>`
|
||||||
|
@ -32,7 +32,7 @@ const Header = styled(BaseHeader)<{
|
||||||
${(props) => props.isSelected && `background: blue`};
|
${(props) => props.isSelected && `background: blue`};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Head_Label = styled.span`
|
const LeftRowHead_Label = styled.span`
|
||||||
${propNameTextCSS};
|
${propNameTextCSS};
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -42,7 +42,7 @@ const Head_Label = styled.span`
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Head_Icon = styled.span<{isCollapsed: boolean}>`
|
const LeftRowHead_Icon = styled.span<{isCollapsed: boolean}>`
|
||||||
width: 12px;
|
width: 12px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
|
@ -59,14 +59,14 @@ const Head_Icon = styled.span<{isCollapsed: boolean}>`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Children = styled.ul`
|
const LeftRowChildren = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AnyCompositeRow: React.FC<{
|
const AnyCompositeRow: React.FC<{
|
||||||
leaf: SequenceEditorTree_Row<unknown>
|
leaf: SequenceEditorTree_Row<string>
|
||||||
label: React.ReactNode
|
label: React.ReactNode
|
||||||
toggleSelect?: VoidFn
|
toggleSelect?: VoidFn
|
||||||
toggleCollapsed: VoidFn
|
toggleCollapsed: VoidFn
|
||||||
|
@ -85,9 +85,9 @@ const AnyCompositeRow: React.FC<{
|
||||||
}) => {
|
}) => {
|
||||||
const hasChildren = Array.isArray(children) && children.length > 0
|
const hasChildren = Array.isArray(children) && children.length > 0
|
||||||
|
|
||||||
return (
|
return leaf.shouldRender ? (
|
||||||
<Container depth={leaf.depth}>
|
<LeftRowContainer depth={leaf.depth}>
|
||||||
<Header
|
<LeftRowHeader
|
||||||
style={{
|
style={{
|
||||||
height: leaf.nodeHeight + 'px',
|
height: leaf.nodeHeight + 'px',
|
||||||
}}
|
}}
|
||||||
|
@ -96,14 +96,14 @@ const AnyCompositeRow: React.FC<{
|
||||||
onClick={toggleSelect}
|
onClick={toggleSelect}
|
||||||
isEven={leaf.n % 2 === 0}
|
isEven={leaf.n % 2 === 0}
|
||||||
>
|
>
|
||||||
<Head_Icon isCollapsed={isCollapsed} onClick={toggleCollapsed}>
|
<LeftRowHead_Icon isCollapsed={isCollapsed} onClick={toggleCollapsed}>
|
||||||
<HiOutlineChevronRight />
|
<HiOutlineChevronRight />
|
||||||
</Head_Icon>
|
</LeftRowHead_Icon>
|
||||||
<Head_Label>{label}</Head_Label>
|
<LeftRowHead_Label>{label}</LeftRowHead_Label>
|
||||||
</Header>
|
</LeftRowHeader>
|
||||||
{hasChildren && <Children>{children}</Children>}
|
{hasChildren && <LeftRowChildren>{children}</LeftRowChildren>}
|
||||||
</Container>
|
</LeftRowContainer>
|
||||||
)
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AnyCompositeRow
|
export default AnyCompositeRow
|
||||||
|
|
|
@ -11,7 +11,7 @@ import styled from 'styled-components'
|
||||||
import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
|
import {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
|
||||||
import {nextPrevCursorsTheme} from '@theatre/studio/propEditors/NextPrevKeyframeCursors'
|
import {nextPrevCursorsTheme} from '@theatre/studio/propEditors/NextPrevKeyframeCursors'
|
||||||
import {graphEditorColors} from '@theatre/studio/panels/SequenceEditorPanel/GraphEditor/GraphEditor'
|
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'
|
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||||
|
|
||||||
const theme = {
|
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
|
isSelected: boolean
|
||||||
isEven: boolean
|
isEven: boolean
|
||||||
}>`
|
}>`
|
||||||
|
@ -34,7 +34,7 @@ const Head = styled(BaseHeader)<{
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
`
|
`
|
||||||
|
|
||||||
const IconContainer = styled.button<{
|
const PrimitivePropRowIconContainer = styled.button<{
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
graphEditorColor: keyof typeof graphEditorColors
|
graphEditorColor: keyof typeof graphEditorColors
|
||||||
}>`
|
}>`
|
||||||
|
@ -73,7 +73,7 @@ const GraphIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const Head_Label = styled.span`
|
const PrimitivePropRowHead_Label = styled.span`
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
${propNameTextCSS};
|
${propNameTextCSS};
|
||||||
`
|
`
|
||||||
|
@ -132,17 +132,17 @@ const PrimitivePropRow: React.FC<{
|
||||||
const isSelectable = true
|
const isSelectable = true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container depth={leaf.depth}>
|
<PrimitivePropRowContainer depth={leaf.depth}>
|
||||||
<Head
|
<PrimitivePropRowHead
|
||||||
isEven={leaf.n % 2 === 0}
|
isEven={leaf.n % 2 === 0}
|
||||||
style={{
|
style={{
|
||||||
height: leaf.nodeHeight + 'px',
|
height: leaf.nodeHeight + 'px',
|
||||||
}}
|
}}
|
||||||
isSelected={isSelected === true}
|
isSelected={isSelected === true}
|
||||||
>
|
>
|
||||||
<Head_Label>{label}</Head_Label>
|
<PrimitivePropRowHead_Label>{label}</PrimitivePropRowHead_Label>
|
||||||
{controlIndicators}
|
{controlIndicators}
|
||||||
<IconContainer
|
<PrimitivePropRowIconContainer
|
||||||
onClick={toggleSelect}
|
onClick={toggleSelect}
|
||||||
isSelected={isSelected === true}
|
isSelected={isSelected === true}
|
||||||
graphEditorColor={possibleColor ?? '1'}
|
graphEditorColor={possibleColor ?? '1'}
|
||||||
|
@ -150,9 +150,9 @@ const PrimitivePropRow: React.FC<{
|
||||||
disabled={!isSelectable}
|
disabled={!isSelectable}
|
||||||
>
|
>
|
||||||
<GraphIcon />
|
<GraphIcon />
|
||||||
</IconContainer>
|
</PrimitivePropRowIconContainer>
|
||||||
</Head>
|
</PrimitivePropRowHead>
|
||||||
</Container>
|
</PrimitivePropRowContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,20 +7,21 @@ import React from 'react'
|
||||||
import AnyCompositeRow from './AnyCompositeRow'
|
import AnyCompositeRow from './AnyCompositeRow'
|
||||||
import PrimitivePropRow from './PrimitivePropRow'
|
import PrimitivePropRow from './PrimitivePropRow'
|
||||||
import {setCollapsedSheetObjectOrCompoundProp} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp'
|
import {setCollapsedSheetObjectOrCompoundProp} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/setCollapsedSheetObjectOrCompoundProp'
|
||||||
|
|
||||||
export const decideRowByPropType = (
|
export const decideRowByPropType = (
|
||||||
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
||||||
): React.ReactElement =>
|
): React.ReactElement => {
|
||||||
leaf.type === 'propWithChildren' ? (
|
const key = 'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]
|
||||||
<PropWithChildrenRow
|
return leaf.shouldRender ? (
|
||||||
leaf={leaf}
|
leaf.type === 'propWithChildren' ? (
|
||||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
<PropWithChildrenRow leaf={leaf} key={key} />
|
||||||
/>
|
) : (
|
||||||
|
<PrimitivePropRow leaf={leaf} key={key} />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<PrimitivePropRow
|
<React.Fragment key={key} />
|
||||||
leaf={leaf}
|
|
||||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const PropWithChildrenRow: React.VFC<{
|
const PropWithChildrenRow: React.VFC<{
|
||||||
leaf: SequenceEditorTree_PropWithChildren
|
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 {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
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 type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
@ -25,7 +25,7 @@ type BasicKeyframedTracksProps = {
|
||||||
trackData: TrackData
|
trackData: TrackData
|
||||||
}
|
}
|
||||||
|
|
||||||
const BasicKeyframedTrack: React.FC<BasicKeyframedTracksProps> = React.memo(
|
const BasicKeyframedTrack: React.VFC<BasicKeyframedTracksProps> = React.memo(
|
||||||
(props) => {
|
(props) => {
|
||||||
const {layoutP, trackData, leaf} = props
|
const {layoutP, trackData, leaf} = props
|
||||||
const [containerRef, containerNode] = useRefAndState<HTMLDivElement | null>(
|
const [containerRef, containerNode] = useRefAndState<HTMLDivElement | null>(
|
||||||
|
@ -57,7 +57,7 @@ const BasicKeyframedTrack: React.FC<BasicKeyframedTracksProps> = React.memo(
|
||||||
)
|
)
|
||||||
|
|
||||||
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
const keyframeEditors = trackData.keyframes.map((kf, index) => (
|
||||||
<KeyframeEditor
|
<SingleKeyframeEditor
|
||||||
keyframe={kf}
|
keyframe={kf}
|
||||||
index={index}
|
index={index}
|
||||||
trackData={trackData}
|
trackData={trackData}
|
||||||
|
@ -89,15 +89,12 @@ function useBasicKeyframedTrackContextMenu(
|
||||||
props: BasicKeyframedTracksProps,
|
props: BasicKeyframedTracksProps,
|
||||||
) {
|
) {
|
||||||
return useContextMenu(node, {
|
return useContextMenu(node, {
|
||||||
|
displayName: 'Keyframe Track',
|
||||||
menuItems: () => {
|
menuItems: () => {
|
||||||
const selectionKeyframes =
|
const selectionKeyframes =
|
||||||
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
||||||
|
|
||||||
if (selectionKeyframes.length > 0) {
|
return [pasteKeyframesContextMenuItem(props, selectionKeyframes)]
|
||||||
return [pasteKeyframesContextMenuItem(props, selectionKeyframes)]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -108,6 +105,7 @@ function pasteKeyframesContextMenuItem(
|
||||||
): IContextMenuItem {
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
||||||
label: 'Paste Keyframes',
|
label: 'Paste Keyframes',
|
||||||
|
enabled: keyframes.length > 0,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const sheet = val(props.layoutP.sheet)
|
const sheet = val(props.layoutP.sheet)
|
||||||
const sequence = sheet.getSequence()
|
const sequence = sheet.getSequence()
|
||||||
|
|
|
@ -4,12 +4,8 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import {lighten} from 'polished'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useMemo, useRef} from 'react'
|
import {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
import {DOT_SIZE_PX} from './KeyframeDot'
|
|
||||||
import type KeyframeEditor from './KeyframeEditor'
|
|
||||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||||
import CurveEditorPopover, {
|
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 {OpenFn} from '@theatre/studio/src/uiComponents/Popover/usePopover'
|
||||||
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||||
import {usePointerCapturing} from '@theatre/studio/UIRoot/PointerCapturing'
|
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 {COLOR_POPOVER_BACK} from './CurveEditorPopover/colors'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import {isKeyframeConnectionInSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
import {isKeyframeConnectionInSelection} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/selections'
|
||||||
|
|
||||||
const CONNECTOR_HEIGHT = DOT_SIZE_PX / 2 + 1
|
const CONNECTOR_HEIGHT = DOT_SIZE_PX / 2 + 1
|
||||||
const CONNECTOR_WIDTH_UNSCALED = 1000
|
const CONNECTOR_WIDTH_UNSCALED = 1000
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import {DOT_SIZE_PX} from './SingleKeyframeDot'
|
||||||
|
|
||||||
const POPOVER_MARGIN = 5
|
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)`
|
const EasingPopover = styled(BasicPopover)`
|
||||||
--popover-outer-stroke: transparent;
|
--popover-outer-stroke: transparent;
|
||||||
--popover-inner-stroke: ${COLOR_POPOVER_BACK};
|
--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 {index, trackData} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
@ -140,28 +94,26 @@ const Connector: React.FC<IProps> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<ConnectorLine
|
||||||
{...themeValues}
|
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
style={{
|
connectorLengthInUnitSpace={connectorLengthInUnitSpace}
|
||||||
// Previously we used scale3d, which had weird fuzzy rendering look in both FF & Chrome
|
isPopoverOpen={isPopoverOpen}
|
||||||
transform: `scaleX(calc(var(--unitSpaceToScaledSpaceMultiplier) * ${
|
isSelected={!!props.selection}
|
||||||
connectorLengthInUnitSpace / CONNECTOR_WIDTH_UNSCALED
|
openPopover={(e) => {
|
||||||
}))`,
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (node) openPopover(e, node)
|
if (node) openPopover(e, node)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{popoverNode}
|
{popoverNode}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</Container>
|
</ConnectorLine>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export default BasicKeyframeConnector
|
||||||
|
|
||||||
export default Connector
|
function useDragKeyframe(
|
||||||
|
node: HTMLDivElement | null,
|
||||||
function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
props: IBasicKeyframeConnectorProps,
|
||||||
|
) {
|
||||||
const propsRef = useRef(props)
|
const propsRef = useRef(props)
|
||||||
propsRef.current = props
|
propsRef.current = props
|
||||||
|
|
||||||
|
@ -240,9 +192,8 @@ function useDragKeyframe(node: HTMLDivElement | null, props: IProps) {
|
||||||
|
|
||||||
useDrag(node, gestureHandlers)
|
useDrag(node, gestureHandlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useConnectorContextMenu(
|
function useConnectorContextMenu(
|
||||||
props: IProps,
|
props: IBasicKeyframeConnectorProps,
|
||||||
node: HTMLDivElement | null,
|
node: HTMLDivElement | null,
|
||||||
cur: Keyframe,
|
cur: Keyframe,
|
||||||
next: Keyframe,
|
next: Keyframe,
|
|
@ -13,7 +13,7 @@ import fuzzy from 'fuzzy'
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import 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 CurveSegmentEditor from './CurveSegmentEditor'
|
||||||
import EasingOption from './EasingOption'
|
import EasingOption from './EasingOption'
|
||||||
import type {CSSCubicBezierArgsString, CubicBezierHandles} from './shared'
|
import type {CSSCubicBezierArgsString, CubicBezierHandles} from './shared'
|
||||||
|
@ -133,7 +133,7 @@ type IProps = {
|
||||||
* Called when user hits enter/escape
|
* Called when user hits enter/escape
|
||||||
*/
|
*/
|
||||||
onRequestClose: (reason: string) => void
|
onRequestClose: (reason: string) => void
|
||||||
} & Parameters<typeof KeyframeEditor>[0]
|
} & ISingleKeyframeEditorProps
|
||||||
|
|
||||||
const CurveEditorPopover: React.FC<IProps> = (props) => {
|
const CurveEditorPopover: React.FC<IProps> = (props) => {
|
||||||
////// `tempTransaction` //////
|
////// `tempTransaction` //////
|
||||||
|
|
|
@ -10,16 +10,18 @@ import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConf
|
||||||
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
|
import type {ISimplePropEditorReactProps} from '@theatre/studio/propEditors/simpleEditors/ISimplePropEditorReactProps'
|
||||||
import {simplePropEditorByPropType} from '@theatre/studio/propEditors/simpleEditors/simplePropEditorByPropType'
|
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']>
|
editingTools: IEditingTools<PropConfigForType<K>['valueType']>
|
||||||
propConfig: PropConfigForType<K>
|
propConfig: PropConfigForType<K>
|
||||||
keyframeValue: PropConfigForType<K>['valueType']
|
keyframeValue: PropConfigForType<K>['valueType']
|
||||||
displayLabel?: string
|
displayLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyframePropEditorContainer = styled.div`
|
const SingleKeyframePropEditorContainer = styled.div`
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
@ -28,7 +30,7 @@ const KeyframePropEditorContainer = styled.div`
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const KeyframePropLabel = styled.span`
|
const SingleKeyframePropLabel = styled.span`
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
@ -50,8 +52,8 @@ const KeyframePropLabel = styled.span`
|
||||||
*
|
*
|
||||||
* @param p - propConfig object for any type of prop.
|
* @param p - propConfig object for any type of prop.
|
||||||
*/
|
*/
|
||||||
export function DeterminePropEditorForKeyframe(
|
export function DeterminePropEditorForSingleKeyframe(
|
||||||
p: IDeterminePropEditorForKeyframeProps<PropTypeConfig['type']>,
|
p: IDeterminePropEditorForSingleKeyframeProps<PropTypeConfig['type']>,
|
||||||
) {
|
) {
|
||||||
const propConfig = p.propConfig
|
const propConfig = p.propConfig
|
||||||
|
|
||||||
|
@ -66,9 +68,9 @@ export function DeterminePropEditorForKeyframe(
|
||||||
const PropEditor = simplePropEditorByPropType[propConfig.type]
|
const PropEditor = simplePropEditorByPropType[propConfig.type]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyframePropEditorContainer>
|
<SingleKeyframePropEditorContainer>
|
||||||
<KeyframePropLabel>{p.displayLabel}</KeyframePropLabel>
|
<SingleKeyframePropLabel>{p.displayLabel}</SingleKeyframePropLabel>
|
||||||
<KeyframeSimplePropEditor
|
<SingleKeyframeSimplePropEditor
|
||||||
SimpleEditorComponent={
|
SimpleEditorComponent={
|
||||||
PropEditor as React.VFC<
|
PropEditor as React.VFC<
|
||||||
ISimplePropEditorReactProps<PropTypeConfig_AllSimples>
|
ISimplePropEditorReactProps<PropTypeConfig_AllSimples>
|
||||||
|
@ -78,7 +80,7 @@ export function DeterminePropEditorForKeyframe(
|
||||||
editingTools={p.editingTools}
|
editingTools={p.editingTools}
|
||||||
keyframeValue={p.keyframeValue}
|
keyframeValue={p.keyframeValue}
|
||||||
/>
|
/>
|
||||||
</KeyframePropEditorContainer>
|
</SingleKeyframePropEditorContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||||
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
|
import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes'
|
||||||
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
|
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
|
||||||
|
|
||||||
export type IKeyframeSimplePropEditorProps<
|
export type ISingleKeyframeSimplePropEditorProps<
|
||||||
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
||||||
> = {
|
> = {
|
||||||
propConfig: TPropTypeConfig
|
propConfig: TPropTypeConfig
|
||||||
|
@ -13,7 +13,7 @@ export type IKeyframeSimplePropEditorProps<
|
||||||
SimpleEditorComponent: React.VFC<ISimplePropEditorReactProps<TPropTypeConfig>>
|
SimpleEditorComponent: React.VFC<ISimplePropEditorReactProps<TPropTypeConfig>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyframeSimplePropEditorContainer = styled.div`
|
const SingleKeyframeSimplePropEditorContainer = styled.div`
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -23,23 +23,23 @@ const KeyframeSimplePropEditorContainer = styled.div`
|
||||||
* Initially used for inline keyframe property editor, this editor is attached to the
|
* Initially used for inline keyframe property editor, this editor is attached to the
|
||||||
* functionality of editing a property for a sequence keyframe.
|
* functionality of editing a property for a sequence keyframe.
|
||||||
*/
|
*/
|
||||||
function KeyframeSimplePropEditor<
|
function SingleKeyframeSimplePropEditor<
|
||||||
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
TPropTypeConfig extends PropTypeConfig_AllSimples,
|
||||||
>({
|
>({
|
||||||
propConfig,
|
propConfig,
|
||||||
editingTools,
|
editingTools,
|
||||||
keyframeValue: value,
|
keyframeValue: value,
|
||||||
SimpleEditorComponent: EditorComponent,
|
SimpleEditorComponent: EditorComponent,
|
||||||
}: IKeyframeSimplePropEditorProps<TPropTypeConfig>) {
|
}: ISingleKeyframeSimplePropEditorProps<TPropTypeConfig>) {
|
||||||
return (
|
return (
|
||||||
<KeyframeSimplePropEditorContainer>
|
<SingleKeyframeSimplePropEditorContainer>
|
||||||
<EditorComponent
|
<EditorComponent
|
||||||
editingTools={editingTools}
|
editingTools={editingTools}
|
||||||
propConfig={propConfig}
|
propConfig={propConfig}
|
||||||
value={value}
|
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 type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import {
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
includeLockFrameStampAttrs,
|
import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||||
useLockFrameStampPosition,
|
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
|
||||||
import {
|
|
||||||
lockedCursorCssVarName,
|
|
||||||
useCssCursorLock,
|
|
||||||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
|
||||||
import SnapCursor from './SnapCursor.svg'
|
|
||||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||||
import type {IKeyframeEditorProps} from './KeyframeEditor'
|
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
||||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||||
|
|
||||||
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover'
|
||||||
import {useTempTransactionEditingTools} from './useTempTransactionEditingTools'
|
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
|
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 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 = {
|
const dotTheme = {
|
||||||
normalColor: '#40AAA4',
|
normalColor: '#40AAA4',
|
||||||
get selectedColor() {
|
get selectedColor() {
|
||||||
|
@ -51,7 +39,7 @@ const dotTheme = {
|
||||||
/** The keyframe diamond ◆ */
|
/** The keyframe diamond ◆ */
|
||||||
const Diamond = styled.div<{isSelected: boolean}>`
|
const Diamond = styled.div<{isSelected: boolean}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${dims(DOT_SIZE_PX)}
|
${absoluteDims(DOT_SIZE_PX)}
|
||||||
|
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
||||||
|
@ -59,56 +47,38 @@ const Diamond = styled.div<{isSelected: boolean}>`
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
${dims(DOT_SIZE_PX)}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const HitZone = styled.div`
|
const HitZone = styled.div`
|
||||||
position: absolute;
|
|
||||||
${dims(HIT_ZONE_SIZE_PX)};
|
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
|
|
||||||
|
${DopeSnapHitZoneUI.CSS}
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor & {
|
#pointer-root.draggingPositionInSequenceEditor & {
|
||||||
pointer-events: auto;
|
${DopeSnapHitZoneUI.CSS_WHEN_SOMETHING_DRAGGING}
|
||||||
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 {
|
&:hover
|
||||||
pointer-events: none !important;
|
+ ${Diamond},
|
||||||
}
|
// notice , "or" in CSS
|
||||||
|
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
||||||
&:hover + ${Diamond}, &.beingDragged + ${Diamond} {
|
+ ${Diamond} {
|
||||||
${dims(DOT_HOVER_SIZE_PX)}
|
${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) */
|
/** 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 [ref, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props)
|
||||||
const [inlineEditorPopover, openEditor] =
|
const [inlineEditorPopover, openEditor] =
|
||||||
useKeyframeInlineEditorPopover(props)
|
useSingleKeyframeInlineEditorPopover(props)
|
||||||
const [isDragging] = useDragForKeyframeDot(node, props, {
|
const [isDragging] = useDragForSingleKeyframeDot(node, props, {
|
||||||
onClickFromDrag(dragStartEvent) {
|
onClickFromDrag(dragStartEvent) {
|
||||||
openEditor(dragStartEvent, ref.current!)
|
openEditor(dragStartEvent, ref.current!)
|
||||||
},
|
},
|
||||||
|
@ -118,9 +88,10 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
||||||
<>
|
<>
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...includeLockFrameStampAttrs(props.keyframe.position)}
|
{...DopeSnapHitZoneUI.reactProps({
|
||||||
{...DopeSnap.includePositionSnapAttrs(props.keyframe.position)}
|
isDragging,
|
||||||
className={isDragging ? 'beingDragged' : ''}
|
position: props.keyframe.position,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<Diamond isSelected={!!props.selection} />
|
<Diamond isSelected={!!props.selection} />
|
||||||
{inlineEditorPopover}
|
{inlineEditorPopover}
|
||||||
|
@ -129,11 +100,12 @@ const KeyframeDot: React.VFC<IKeyframeDotProps> = (props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KeyframeDot
|
export default SingleKeyframeDot
|
||||||
|
|
||||||
function useKeyframeContextMenu(
|
function useSingleKeyframeContextMenu(
|
||||||
target: HTMLDivElement | null,
|
target: HTMLDivElement | null,
|
||||||
props: IKeyframeDotProps,
|
logger: ILogger,
|
||||||
|
props: ISingleKeyframeDotProps,
|
||||||
) {
|
) {
|
||||||
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
|
||||||
props.selection,
|
props.selection,
|
||||||
|
@ -146,20 +118,24 @@ function useKeyframeContextMenu(
|
||||||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||||
|
|
||||||
return useContextMenu(target, {
|
return useContextMenu(target, {
|
||||||
|
displayName: 'Keyframe',
|
||||||
menuItems: () => {
|
menuItems: () => {
|
||||||
return [keyframeSelectionItem, deleteItem]
|
return [keyframeSelectionItem, deleteItem]
|
||||||
},
|
},
|
||||||
|
onOpen() {
|
||||||
|
logger._debug('Show keyframe', props)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The editor that pops up when directly clicking a Keyframe. */
|
/** The editor that pops up when directly clicking a Keyframe. */
|
||||||
function useKeyframeInlineEditorPopover(props: IKeyframeDotProps) {
|
function useSingleKeyframeInlineEditorPopover(props: ISingleKeyframeDotProps) {
|
||||||
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
|
const editingTools = useEditingToolsForKeyframeEditorPopover(props)
|
||||||
const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp)
|
const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp)
|
||||||
|
|
||||||
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
|
return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => (
|
||||||
<BasicPopover showPopoverEdgeTriangle>
|
<BasicPopover showPopoverEdgeTriangle>
|
||||||
<DeterminePropEditorForKeyframe
|
<DeterminePropEditorForSingleKeyframe
|
||||||
propConfig={props.leaf.propConf}
|
propConfig={props.leaf.propConf}
|
||||||
editingTools={editingTools}
|
editingTools={editingTools}
|
||||||
keyframeValue={props.keyframe.value}
|
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
|
const obj = props.leaf.sheetObject
|
||||||
return useTempTransactionEditingTools(({stateEditors}, value) => {
|
return useTempTransactionEditingTools(({stateEditors}, value) => {
|
||||||
const newKeyframe = {...props.keyframe, value}
|
const newKeyframe = {...props.keyframe, value}
|
||||||
|
@ -182,9 +160,9 @@ function useEditingToolsForKeyframeEditorPopover(props: IKeyframeDotProps) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDragForKeyframeDot(
|
function useDragForSingleKeyframeDot(
|
||||||
node: HTMLDivElement | null,
|
node: HTMLDivElement | null,
|
||||||
props: IKeyframeDotProps,
|
props: ISingleKeyframeDotProps,
|
||||||
options: {
|
options: {
|
||||||
/**
|
/**
|
||||||
* hmm: this is a hack so we can actually receive the
|
* hmm: this is a hack so we can actually receive the
|
||||||
|
@ -277,7 +255,7 @@ function useDragForKeyframeDot(
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelectionOrKeyframeContextMenuItem(
|
function deleteSelectionOrKeyframeContextMenuItem(
|
||||||
props: IKeyframeDotProps,
|
props: ISingleKeyframeDotProps,
|
||||||
): IContextMenuItem {
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
||||||
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
||||||
|
@ -300,7 +278,7 @@ function deleteSelectionOrKeyframeContextMenuItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyKeyFrameContextMenuItem(
|
function copyKeyFrameContextMenuItem(
|
||||||
props: IKeyframeDotProps,
|
props: ISingleKeyframeDotProps,
|
||||||
keyframeIds: string[],
|
keyframeIds: string[],
|
||||||
): IContextMenuItem {
|
): IContextMenuItem {
|
||||||
return {
|
return {
|
|
@ -11,16 +11,16 @@ import type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Connector from './Connector'
|
import SingleKeyframeConnector from './BasicKeyframeConnector'
|
||||||
import KeyframeDot from './KeyframeDot'
|
import SingleKeyframeDot from './SingleKeyframeDot'
|
||||||
|
|
||||||
const Container = styled.div`
|
const SingleKeyframeEditorContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
`
|
`
|
||||||
|
|
||||||
const noConnector = <></>
|
const noConnector = <></>
|
||||||
|
|
||||||
export type IKeyframeEditorProps = {
|
export type ISingleKeyframeEditorProps = {
|
||||||
index: number
|
index: number
|
||||||
keyframe: Keyframe
|
keyframe: Keyframe
|
||||||
trackData: TrackData
|
trackData: TrackData
|
||||||
|
@ -29,7 +29,7 @@ export type IKeyframeEditorProps = {
|
||||||
selection: undefined | DopeSheetSelection
|
selection: undefined | DopeSheetSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
const SingleKeyframeEditor: React.VFC<ISingleKeyframeEditorProps> = (props) => {
|
||||||
const {index, trackData} = props
|
const {index, trackData} = props
|
||||||
const cur = trackData.keyframes[index]
|
const cur = trackData.keyframes[index]
|
||||||
const next = trackData.keyframes[index + 1]
|
const next = trackData.keyframes[index + 1]
|
||||||
|
@ -37,7 +37,7 @@ const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
||||||
const connected = cur.connectedRight && !!next
|
const connected = cur.connectedRight && !!next
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<SingleKeyframeEditorContainer
|
||||||
style={{
|
style={{
|
||||||
top: `${props.leaf.nodeHeight / 2}px`,
|
top: `${props.leaf.nodeHeight / 2}px`,
|
||||||
left: `calc(${val(
|
left: `calc(${val(
|
||||||
|
@ -47,10 +47,10 @@ const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
||||||
}px))`,
|
}px))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KeyframeDot {...props} />
|
<SingleKeyframeDot {...props} />
|
||||||
{connected ? <Connector {...props} /> : noConnector}
|
{connected ? <SingleKeyframeConnector {...props} /> : noConnector}
|
||||||
</Container>
|
</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,
|
DopeSheetSelection,
|
||||||
SequenceEditorPanelLayout,
|
SequenceEditorPanelLayout,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import type {SequenceEditorTree_AllRowTypes} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
import type {
|
||||||
|
SequenceEditorTree_AllRowTypes,
|
||||||
|
SequenceEditorTree_PropWithChildren,
|
||||||
|
SequenceEditorTree_SheetObject,
|
||||||
|
} from '@theatre/studio/panels/SequenceEditorPanel/layout/tree'
|
||||||
import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap'
|
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}>`
|
const Container = styled.div<{isShiftDown: boolean}>`
|
||||||
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')};
|
cursor: ${(props) => (props.isShiftDown ? 'cell' : 'default')};
|
||||||
|
@ -54,6 +61,7 @@ function useCaptureSelection(
|
||||||
) {
|
) {
|
||||||
const [ref, state] = useRefAndState<SelectionBounds | null>(null)
|
const [ref, state] = useRefAndState<SelectionBounds | null>(null)
|
||||||
|
|
||||||
|
const logger = useLogger('useCaptureSelection')
|
||||||
useDrag(
|
useDrag(
|
||||||
containerNode,
|
containerNode,
|
||||||
useMemo((): Parameters<typeof useDrag>[1] => {
|
useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
|
@ -96,7 +104,11 @@ function useCaptureSelection(
|
||||||
ys: [ref.current!.ys[0], event.clientY - rect.top],
|
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})
|
val(layoutP.selectionAtom).setState({current: selection})
|
||||||
},
|
},
|
||||||
onDragEnd(_dragHappened) {
|
onDragEnd(_dragHappened) {
|
||||||
|
@ -112,15 +124,70 @@ function useCaptureSelection(
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace utils {
|
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: {
|
const collectorByLeafType: {
|
||||||
[K in SequenceEditorTree_AllRowTypes['type']]?: (
|
[K in SequenceEditorTree_AllRowTypes['type']]?: (
|
||||||
|
logger: IUtilLogger,
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
|
leaf: Extract<SequenceEditorTree_AllRowTypes, {type: K}>,
|
||||||
bounds: Exclude<SelectionBounds, null>,
|
bounds: SelectionBounds,
|
||||||
selection: DopeSheetSelection,
|
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||||
) => void
|
) => 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 {sheetObject, trackId} = leaf
|
||||||
const trackData = val(
|
const trackData = val(
|
||||||
getStudio().atomP.historic.coreByProject[sheetObject.address.projectId]
|
getStudio().atomP.historic.coreByProject[sheetObject.address.projectId]
|
||||||
|
@ -134,10 +201,13 @@ namespace utils {
|
||||||
if (kf.position >= bounds.positions[1]) break
|
if (kf.position >= bounds.positions[1]) break
|
||||||
|
|
||||||
mutableSetDeep(
|
mutableSetDeep(
|
||||||
selection,
|
selectionByObjectKey,
|
||||||
(p) =>
|
(selectionByObjectKeyP) =>
|
||||||
p.byObjectKey[sheetObject.address.objectKey].byTrackId[trackId]
|
// convenience for accessing a deep path which might not actually exist
|
||||||
.byKeyframeId[kf.id],
|
// 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,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -145,24 +215,29 @@ namespace utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectChildren = (
|
const collectChildren = (
|
||||||
|
logger: IUtilLogger,
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
leaf: SequenceEditorTree_AllRowTypes,
|
leaf: SequenceEditorTree_AllRowTypes,
|
||||||
bounds: Exclude<SelectionBounds, null>,
|
bounds: SelectionBounds,
|
||||||
selection: DopeSheetSelection,
|
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||||
) => {
|
) => {
|
||||||
if ('children' in leaf) {
|
if ('children' in leaf) {
|
||||||
for (const sub of leaf.children) {
|
for (const sub of leaf.children) {
|
||||||
collectFromAnyLeaf(layoutP, sub, bounds, selection)
|
collectFromAnyLeaf(logger, layoutP, sub, bounds, selectionByObjectKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectFromAnyLeaf(
|
function collectFromAnyLeaf(
|
||||||
|
logger: IUtilLogger,
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
leaf: SequenceEditorTree_AllRowTypes,
|
leaf: SequenceEditorTree_AllRowTypes,
|
||||||
bounds: Exclude<SelectionBounds, null>,
|
bounds: SelectionBounds,
|
||||||
selection: DopeSheetSelection,
|
selectionByObjectKey: DopeSheetSelection['byObjectKey'],
|
||||||
) {
|
) {
|
||||||
|
// don't collect from non rendered
|
||||||
|
if (!leaf.shouldRender) return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
bounds.ys[0] > leaf.top + leaf.heightIncludingChildren ||
|
bounds.ys[0] > leaf.top + leaf.heightIncludingChildren ||
|
||||||
leaf.top > bounds.ys[1]
|
leaf.top > bounds.ys[1]
|
||||||
|
@ -171,20 +246,39 @@ namespace utils {
|
||||||
}
|
}
|
||||||
const collector = collectorByLeafType[leaf.type]
|
const collector = collectorByLeafType[leaf.type]
|
||||||
if (collector) {
|
if (collector) {
|
||||||
collector(layoutP, leaf as $IntentionalAny, bounds, selection)
|
collector(
|
||||||
|
logger,
|
||||||
|
layoutP,
|
||||||
|
leaf as $IntentionalAny,
|
||||||
|
bounds,
|
||||||
|
selectionByObjectKey,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
collectChildren(layoutP, leaf, bounds, selection)
|
collectChildren(logger, layoutP, leaf, bounds, selectionByObjectKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function boundsToSelection(
|
export function boundsToSelection(
|
||||||
|
logger: ILogger,
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
bounds: Exclude<SelectionBounds, null>,
|
bounds: SelectionBounds,
|
||||||
): DopeSheetSelection {
|
): 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 sheet = val(layoutP.tree.sheet)
|
||||||
const selection: DopeSheetSelection = {
|
return {
|
||||||
type: 'DopeSheetSelection',
|
type: 'DopeSheetSelection',
|
||||||
byObjectKey: {},
|
byObjectKey: selectionByObjectKey,
|
||||||
getDragHandlers(origin) {
|
getDragHandlers(origin) {
|
||||||
return {
|
return {
|
||||||
debugName: 'DopeSheetSelectionView/boundsToSelection',
|
debugName: 'DopeSheetSelectionView/boundsToSelection',
|
||||||
|
@ -204,23 +298,19 @@ namespace utils {
|
||||||
ignore: origin.domNode,
|
ignore: origin.domNode,
|
||||||
})
|
})
|
||||||
|
|
||||||
let delta: number
|
const delta =
|
||||||
if (snapPos != null) {
|
snapPos != null
|
||||||
delta = snapPos - origin.positionAtStartOfDrag
|
? snapPos - origin.positionAtStartOfDrag
|
||||||
} else {
|
: toUnitSpace(dx)
|
||||||
delta = toUnitSpace(dx)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempTransaction = getStudio()!.tempTransaction(
|
tempTransaction = getStudio().tempTransaction(
|
||||||
({stateEditors}) => {
|
({stateEditors}) => {
|
||||||
const transformKeyframes =
|
const transformKeyframes =
|
||||||
stateEditors.coreByProject.historic.sheetsById.sequence
|
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||||
.transformKeyframes
|
.transformKeyframes
|
||||||
|
|
||||||
for (const objectKey of Object.keys(
|
for (const objectKey of Object.keys(selectionByObjectKey)) {
|
||||||
selection.byObjectKey,
|
const {byTrackId} = selectionByObjectKey[objectKey]!
|
||||||
)) {
|
|
||||||
const {byTrackId} = selection.byObjectKey[objectKey]!
|
|
||||||
for (const trackId of Object.keys(byTrackId)) {
|
for (const trackId of Object.keys(byTrackId)) {
|
||||||
const {byKeyframeId} = byTrackId[trackId]!
|
const {byKeyframeId} = byTrackId[trackId]!
|
||||||
transformKeyframes({
|
transformKeyframes({
|
||||||
|
@ -249,13 +339,13 @@ namespace utils {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete() {
|
delete() {
|
||||||
getStudio()!.transaction(({stateEditors}) => {
|
getStudio().transaction(({stateEditors}) => {
|
||||||
const deleteKeyframes =
|
const deleteKeyframes =
|
||||||
stateEditors.coreByProject.historic.sheetsById.sequence
|
stateEditors.coreByProject.historic.sheetsById.sequence
|
||||||
.deleteKeyframes
|
.deleteKeyframes
|
||||||
|
|
||||||
for (const objectKey of Object.keys(selection.byObjectKey)) {
|
for (const objectKey of Object.keys(selectionByObjectKey)) {
|
||||||
const {byTrackId} = selection.byObjectKey[objectKey]!
|
const {byTrackId} = selectionByObjectKey[objectKey]!
|
||||||
for (const trackId of Object.keys(byTrackId)) {
|
for (const trackId of Object.keys(byTrackId)) {
|
||||||
const {byKeyframeId} = byTrackId[trackId]!
|
const {byKeyframeId} = byTrackId[trackId]!
|
||||||
deleteKeyframes({
|
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<{
|
const SelectionRectangle: React.VFC<{
|
||||||
state: Exclude<SelectionBounds, null>
|
state: SelectionBounds
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
}> = ({state, layoutP}) => {
|
}> = ({state, layoutP}) => {
|
||||||
const atom = useValToAtom(state)
|
const atom = useValToAtom(state)
|
||||||
|
|
|
@ -5,13 +5,15 @@ import {usePrism} from '@theatre/react'
|
||||||
import type {Pointer} from '@theatre/dataverse'
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import KeyframedTrack from './BasicKeyframedTrack/BasicKeyframedTrack'
|
|
||||||
import RightRow from './Row'
|
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
|
leaf: SequenceEditorTree_PrimitiveProp
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
}> = ({leaf, layoutP}) => {
|
}> = ({leaf, layoutP}) => {
|
||||||
|
const logger = useLogger('PrimitivePropRow', leaf.pathToProp.join())
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const {sheetObject} = leaf
|
const {sheetObject} = leaf
|
||||||
const {trackId} = leaf
|
const {trackId} = leaf
|
||||||
|
@ -24,7 +26,7 @@ const PrimitivePropRow: React.FC<{
|
||||||
)
|
)
|
||||||
|
|
||||||
if (trackData?.type !== 'BasicKeyframedTrack') {
|
if (trackData?.type !== 'BasicKeyframedTrack') {
|
||||||
console.error(
|
logger.errorDev(
|
||||||
`trackData type ${trackData?.type} is not yet supported on the sequence editor`,
|
`trackData type ${trackData?.type} is not yet supported on the sequence editor`,
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
@ -32,7 +34,11 @@ const PrimitivePropRow: React.FC<{
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const node = (
|
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>
|
return <RightRow leaf={leaf} isCollapsed={false} node={node}></RightRow>
|
||||||
|
|
|
@ -8,15 +8,18 @@ import type {Pointer} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PrimitivePropRow from './PrimitivePropRow'
|
import PrimitivePropRow from './PrimitivePropRow'
|
||||||
import RightRow from './Row'
|
import RightRow from './Row'
|
||||||
|
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||||
|
import {ProvideLogger, useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
|
||||||
export const decideRowByPropType = (
|
export const decideRowByPropType = (
|
||||||
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
leaf: SequenceEditorTree_PropWithChildren | SequenceEditorTree_PrimitiveProp,
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>,
|
layoutP: Pointer<SequenceEditorPanelLayout>,
|
||||||
): React.ReactElement =>
|
): React.ReactElement =>
|
||||||
leaf.type === 'propWithChildren' ? (
|
leaf.type === 'propWithChildren' ? (
|
||||||
<PropWithChildrenRow
|
<RightPropWithChildrenRow
|
||||||
layoutP={layoutP}
|
layoutP={layoutP}
|
||||||
leaf={leaf}
|
viewModel={leaf}
|
||||||
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
key={'prop' + leaf.pathToProp[leaf.pathToProp.length - 1]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -27,19 +30,42 @@ export const decideRowByPropType = (
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const PropWithChildrenRow: React.VFC<{
|
const RightPropWithChildrenRow: React.VFC<{
|
||||||
leaf: SequenceEditorTree_PropWithChildren
|
viewModel: SequenceEditorTree_PropWithChildren
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
}> = ({leaf, layoutP}) => {
|
}> = ({viewModel, layoutP}) => {
|
||||||
|
const logger = useLogger(
|
||||||
|
'RightPropWithChildrenRow',
|
||||||
|
viewModel.pathToProp.join(),
|
||||||
|
)
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const node = <div />
|
const aggregatedKeyframes = collectAggregateKeyframesInPrism(
|
||||||
|
logger.utilFor.internal(),
|
||||||
|
viewModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
const node = (
|
||||||
|
<AggregatedKeyframeTrack
|
||||||
|
layoutP={layoutP}
|
||||||
|
aggregatedKeyframes={aggregatedKeyframes}
|
||||||
|
viewModel={viewModel}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
<ProvideLogger logger={logger}>
|
||||||
{leaf.children.map((propLeaf) =>
|
<RightRow
|
||||||
decideRowByPropType(propLeaf, layoutP),
|
leaf={viewModel}
|
||||||
)}
|
node={node}
|
||||||
</RightRow>
|
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 React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const Container = styled.li<{}>`
|
const RightRowContainer = styled.li<{}>`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -10,7 +10,7 @@ const Container = styled.li<{}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NodeWrapper = styled.div<{isEven: boolean}>`
|
const RightRowNodeWrapper = styled.div<{isEven: boolean}>`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -29,7 +29,7 @@ const NodeWrapper = styled.div<{isEven: boolean}>`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Children = styled.ul`
|
const RightRowChildren = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
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
|
* 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.
|
* retain its hierarchy. It's just the DOM tree that should be list-based.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RightRow: React.FC<{
|
const RightRow: React.FC<{
|
||||||
leaf: SequenceEditorTree_Row<unknown>
|
leaf: SequenceEditorTree_Row<string>
|
||||||
node: React.ReactElement
|
node: React.ReactElement
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
}> = ({leaf, children, node, isCollapsed}) => {
|
}> = ({leaf, children, node, isCollapsed}) => {
|
||||||
const hasChildren = Array.isArray(children) && children.length > 0
|
const hasChildren = Array.isArray(children) && children.length > 0
|
||||||
|
|
||||||
return (
|
return leaf.shouldRender ? (
|
||||||
<Container>
|
<RightRowContainer>
|
||||||
<NodeWrapper
|
<RightRowNodeWrapper
|
||||||
style={{height: leaf.nodeHeight + 'px'}}
|
style={{height: leaf.nodeHeight + 'px'}}
|
||||||
isEven={leaf.n % 2 === 0}
|
isEven={leaf.n % 2 === 0}
|
||||||
>
|
>
|
||||||
{node}
|
{node}
|
||||||
</NodeWrapper>
|
</RightRowNodeWrapper>
|
||||||
{hasChildren && <Children>{children}</Children>}
|
{hasChildren && <RightRowChildren>{children}</RightRowChildren>}
|
||||||
</Container>
|
</RightRowContainer>
|
||||||
)
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RightRow
|
export default RightRow
|
||||||
|
|
|
@ -5,13 +5,32 @@ import type {Pointer} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {decideRowByPropType} from './PropWithChildrenRow'
|
import {decideRowByPropType} from './PropWithChildrenRow'
|
||||||
import RightRow from './Row'
|
import RightRow from './Row'
|
||||||
|
import {useLogger} from '@theatre/studio/uiComponents/useLogger'
|
||||||
|
import {collectAggregateKeyframesInPrism} from './collectAggregateKeyframes'
|
||||||
|
import AggregatedKeyframeTrack from './AggregatedKeyframeTrack/AggregatedKeyframeTrack'
|
||||||
|
|
||||||
const RightSheetObjectRow: React.VFC<{
|
const RightSheetObjectRow: React.VFC<{
|
||||||
leaf: SequenceEditorTree_SheetObject
|
leaf: SequenceEditorTree_SheetObject
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
}> = ({leaf, layoutP}) => {
|
}> = ({leaf, layoutP}) => {
|
||||||
|
const logger = useLogger(
|
||||||
|
`RightSheetObjectRow`,
|
||||||
|
leaf.sheetObject.address.objectKey,
|
||||||
|
)
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const node = <div />
|
const aggregatedKeyframes = collectAggregateKeyframesInPrism(
|
||||||
|
logger.utilFor.internal(),
|
||||||
|
leaf,
|
||||||
|
)
|
||||||
|
|
||||||
|
const node = (
|
||||||
|
<AggregatedKeyframeTrack
|
||||||
|
layoutP={layoutP}
|
||||||
|
aggregatedKeyframes={aggregatedKeyframes}
|
||||||
|
viewModel={leaf}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
<RightRow leaf={leaf} node={node} isCollapsed={leaf.isCollapsed}>
|
||||||
{leaf.children.map((leaf) => decideRowByPropType(leaf, layoutP))}
|
{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
|
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(
|
export function selectedKeyframeConnections(
|
||||||
projectId: ProjectId,
|
projectId: ProjectId,
|
||||||
sheetId: SheetId,
|
sheetId: SheetId,
|
||||||
selection: DopeSheetSelection | undefined,
|
selection: DopeSheetSelection | undefined,
|
||||||
): IDerivation<Array<[Keyframe, Keyframe]>> {
|
): IDerivation<Array<[left: Keyframe, right: Keyframe]>> {
|
||||||
return prism(() => {
|
return prism(() => {
|
||||||
if (selection === undefined) return []
|
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 type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import {useVal} from '@theatre/react'
|
import {useVal} from '@theatre/react'
|
||||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
|
||||||
import getStudio from '@theatre/studio/getStudio'
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
import {
|
import {
|
||||||
lockedCursorCssVarName,
|
lockedCursorCssVarName,
|
||||||
|
@ -11,33 +10,23 @@ import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useCo
|
||||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
import React, {useMemo, useRef} from 'react'
|
import React, {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {
|
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
includeLockFrameStampAttrs,
|
|
||||||
useLockFrameStampPosition,
|
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
|
||||||
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
import type {SequenceMarkerId} from '@theatre/shared/utils/ids'
|
import type {SequenceMarkerId} from '@theatre/shared/utils/ids'
|
||||||
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||||
import SnapCursor from './SnapCursor.svg'
|
|
||||||
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
import useDrag from '@theatre/studio/uiComponents/useDrag'
|
||||||
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
import type {StudioHistoricStateSequenceEditorMarker} from '@theatre/studio/store/types'
|
import type {StudioHistoricStateSequenceEditorMarker} from '@theatre/studio/store/types'
|
||||||
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
|
||||||
import DopeSnap from './DopeSnap'
|
import DopeSnap from './DopeSnap'
|
||||||
|
import {absoluteDims} from '@theatre/studio/utils/absoluteDims'
|
||||||
|
import {DopeSnapHitZoneUI} from './DopeSnapHitZoneUI'
|
||||||
|
|
||||||
const MARKER_SIZE_W_PX = 12
|
const MARKER_SIZE_W_PX = 12
|
||||||
const MARKER_SIZE_H_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_W_PX = MARKER_SIZE_W_PX * 2
|
||||||
const MARKER_HOVER_SIZE_H_PX = MARKER_SIZE_H_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`
|
const MarkerDotContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -48,7 +37,7 @@ const MarkerDotContainer = styled.div`
|
||||||
|
|
||||||
const MarkerVisualDotSVGContainer = styled.div`
|
const MarkerVisualDotSVGContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${dims(MARKER_SIZE_W_PX, MARKER_SIZE_H_PX)}
|
${absoluteDims(MARKER_SIZE_W_PX, MARKER_SIZE_H_PX)}
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -73,53 +62,35 @@ const MarkerVisualDot = React.memo(() => (
|
||||||
))
|
))
|
||||||
|
|
||||||
const HitZone = styled.div`
|
const HitZone = styled.div`
|
||||||
position: absolute;
|
|
||||||
${dims(HIT_ZONE_SIZE_PX)};
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
cursor: ew-resize;
|
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"
|
// "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
|
// ref: https://styled-components.com/docs/basics#pseudoelements-pseudoselectors-and-nesting
|
||||||
#pointer-root.draggingPositionInSequenceEditor:not(.draggingMarker) &,
|
#pointer-root.draggingPositionInSequenceEditor:not(.draggingMarker) &,
|
||||||
#pointer-root.draggingPositionInSequenceEditor &.beingDragged {
|
#pointer-root.draggingPositionInSequenceEditor
|
||||||
|
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS} {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: var(${lockedCursorCssVarName});
|
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
|
&:hover
|
||||||
+ ${MarkerVisualDotSVGContainer},
|
+ ${MarkerVisualDotSVGContainer},
|
||||||
&.beingDragged
|
// notice , "or" in CSS
|
||||||
|
&.${DopeSnapHitZoneUI.BEING_DRAGGED_CLASS}
|
||||||
+ ${MarkerVisualDotSVGContainer} {
|
+ ${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 = {
|
type IMarkerDotProps = {
|
||||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
markerId: SequenceMarkerId
|
markerId: SequenceMarkerId
|
||||||
|
@ -194,14 +165,10 @@ const MarkerDotVisible: React.VFC<IMarkerDotVisibleProps> = ({
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
<HitZone
|
<HitZone
|
||||||
ref={markRef}
|
ref={markRef}
|
||||||
// `data-pos` and `includeLockFrameStampAttrs` are used by FrameStampPositionProvider
|
{...DopeSnapHitZoneUI.reactProps({
|
||||||
// in order to handle snapping the playhead. Adding these props effectively
|
isDragging,
|
||||||
// causes the playhead to "snap" to the marker on mouse over.
|
position: marker.position,
|
||||||
// `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' : ''}
|
|
||||||
/>
|
/>
|
||||||
<MarkerVisualDot />
|
<MarkerVisualDot />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -76,11 +76,13 @@ const Thumb = styled.div`
|
||||||
|
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
|
|
||||||
&.seeking {
|
${Container}.seeking > & {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pointer-root.draggingPositionInSequenceEditor &:not(.seeking) {
|
#pointer-root.draggingPositionInSequenceEditor
|
||||||
|
${Container}:not(.seeking)
|
||||||
|
> & {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: var(${lockedCursorCssVarName});
|
cursor: var(${lockedCursorCssVarName});
|
||||||
}
|
}
|
||||||
|
@ -203,13 +205,13 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
|
|
||||||
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
const gestureHandlers = useMemo((): Parameters<typeof useDrag>[1] => {
|
||||||
return {
|
return {
|
||||||
debugName: 'Playhead',
|
debugName: 'RightOverlay/Playhead',
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
|
||||||
|
|
||||||
const sequence = val(layoutP.sheet).getSequence()
|
const sequence = val(layoutP.sheet).getSequence()
|
||||||
const posBeforeSeek = sequence.position
|
const posBeforeSeek = sequence.position
|
||||||
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
const scaledSpaceToUnitSpace = val(layoutP.scaledSpace.toUnitSpace)
|
||||||
|
|
||||||
|
const setIsSeeking = val(layoutP.seeker.setIsSeeking)
|
||||||
setIsSeeking(true)
|
setIsSeeking(true)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -232,7 +234,7 @@ const Playhead: React.FC<{layoutP: Pointer<SequenceEditorPanelLayout>}> = ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}, [])
|
}, [layoutP, thumbNode])
|
||||||
|
|
||||||
const [isDragging] = useDrag(thumbNode, gestureHandlers)
|
const [isDragging] = useDrag(thumbNode, gestureHandlers)
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,35 @@ import logger from '@theatre/shared/logger'
|
||||||
import {titleBarHeight} from '@theatre/studio/panels/BasePanel/common'
|
import {titleBarHeight} from '@theatre/studio/panels/BasePanel/common'
|
||||||
import type {Studio} from '@theatre/studio/Studio'
|
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
|
nodeHeight: number
|
||||||
|
/** Height of the row + height with children in pixels */
|
||||||
heightIncludingChildren: number
|
heightIncludingChildren: number
|
||||||
|
|
||||||
|
/** Visual indentation */
|
||||||
depth: number
|
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
|
top: number
|
||||||
|
/** Row number (e.g. for correctly styling even / odd alternating styles) */
|
||||||
n: number
|
n: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,26 +101,31 @@ export const calculateSequenceEditorTree = (
|
||||||
prism.ensurePrism()
|
prism.ensurePrism()
|
||||||
let topSoFar = titleBarHeight
|
let topSoFar = titleBarHeight
|
||||||
let nSoFar = 0
|
let nSoFar = 0
|
||||||
|
const rootShouldRender = true
|
||||||
|
|
||||||
const tree: SequenceEditorTree = {
|
const tree: SequenceEditorTree = {
|
||||||
type: 'sheet',
|
type: 'sheet',
|
||||||
sheet,
|
sheet,
|
||||||
children: [],
|
children: [],
|
||||||
|
shouldRender: rootShouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
depth: -1,
|
depth: -1,
|
||||||
n: nSoFar,
|
n: nSoFar,
|
||||||
nodeHeight: 0, // always 0
|
nodeHeight: 0, // always 0
|
||||||
heightIncludingChildren: -1, // will define this later
|
heightIncludingChildren: -1, // will define this later
|
||||||
}
|
}
|
||||||
nSoFar += 1
|
|
||||||
|
|
||||||
const collapsableP =
|
if (rootShouldRender) {
|
||||||
|
nSoFar += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsableItemSetP =
|
||||||
studio.atomP.ahistoric.projects.stateByProjectId[sheet.address.projectId]
|
studio.atomP.ahistoric.projects.stateByProjectId[sheet.address.projectId]
|
||||||
.stateBySheetId[sheet.address.sheetId].sequence.collapsableItems
|
.stateBySheetId[sheet.address.sheetId].sequence.collapsableItems
|
||||||
|
|
||||||
for (const sheetObject of Object.values(val(sheet.objectsP))) {
|
for (const sheetObject of Object.values(val(sheet.objectsP))) {
|
||||||
if (sheetObject) {
|
if (sheetObject) {
|
||||||
addObject(sheetObject, tree.children, tree.depth + 1)
|
addObject(sheetObject, tree.children, tree.depth + 1, rootShouldRender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tree.heightIncludingChildren = topSoFar - tree.top
|
tree.heightIncludingChildren = topSoFar - tree.top
|
||||||
|
@ -106,6 +134,7 @@ export const calculateSequenceEditorTree = (
|
||||||
sheetObject: SheetObject,
|
sheetObject: SheetObject,
|
||||||
arrayOfChildren: Array<SequenceEditorTree_SheetObject>,
|
arrayOfChildren: Array<SequenceEditorTree_SheetObject>,
|
||||||
level: number,
|
level: number,
|
||||||
|
shouldRender: boolean,
|
||||||
) {
|
) {
|
||||||
const trackSetups = val(
|
const trackSetups = val(
|
||||||
sheetObject.template.getMapOfValidSequenceTracks_forStudio(),
|
sheetObject.template.getMapOfValidSequenceTracks_forStudio(),
|
||||||
|
@ -114,36 +143,44 @@ export const calculateSequenceEditorTree = (
|
||||||
if (Object.keys(trackSetups).length === 0) return
|
if (Object.keys(trackSetups).length === 0) return
|
||||||
|
|
||||||
const isCollapsedP =
|
const isCollapsedP =
|
||||||
collapsableP.byId[createStudioSheetItemKey.forSheetObject(sheetObject)]
|
collapsableItemSetP.byId[
|
||||||
.isCollapsed
|
createStudioSheetItemKey.forSheetObject(sheetObject)
|
||||||
|
].isCollapsed
|
||||||
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
||||||
|
|
||||||
const row: SequenceEditorTree_SheetObject = {
|
const row: SequenceEditorTree_SheetObject = {
|
||||||
type: 'sheetObject',
|
type: 'sheetObject',
|
||||||
isCollapsed: isCollapsed,
|
isCollapsed,
|
||||||
|
shouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
children: [],
|
children: [],
|
||||||
depth: level,
|
depth: level,
|
||||||
n: nSoFar,
|
n: nSoFar,
|
||||||
sheetObject: sheetObject,
|
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,
|
heightIncludingChildren: -1,
|
||||||
}
|
}
|
||||||
arrayOfChildren.push(row)
|
arrayOfChildren.push(row)
|
||||||
nSoFar += 1
|
|
||||||
// As we add rows to the tree, top to bottom, we accumulate the pixel
|
if (shouldRender) {
|
||||||
// distance to the top of the tree from the bottom of the current row:
|
nSoFar += 1
|
||||||
topSoFar += row.nodeHeight
|
// As we add rows to the tree, top to bottom, we accumulate the pixel
|
||||||
if (!isCollapsed) {
|
// distance to the top of the tree from the bottom of the current row:
|
||||||
addProps(
|
topSoFar += row.nodeHeight
|
||||||
sheetObject,
|
|
||||||
trackSetups,
|
|
||||||
[],
|
|
||||||
sheetObject.template.config,
|
|
||||||
row.children,
|
|
||||||
level + 1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addProps(
|
||||||
|
sheetObject,
|
||||||
|
trackSetups,
|
||||||
|
[],
|
||||||
|
sheetObject.template.config,
|
||||||
|
row.children,
|
||||||
|
level + 1,
|
||||||
|
shouldRender && !isCollapsed,
|
||||||
|
)
|
||||||
|
|
||||||
row.heightIncludingChildren = topSoFar - row.top
|
row.heightIncludingChildren = topSoFar - row.top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +193,7 @@ export const calculateSequenceEditorTree = (
|
||||||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||||
>,
|
>,
|
||||||
level: number,
|
level: number,
|
||||||
|
shouldRender: boolean,
|
||||||
) {
|
) {
|
||||||
for (const [propKey, setupOrSetups] of Object.entries(trackSetups)) {
|
for (const [propKey, setupOrSetups] of Object.entries(trackSetups)) {
|
||||||
const propConfig = parentPropConfig.props[propKey]
|
const propConfig = parentPropConfig.props[propKey]
|
||||||
|
@ -166,6 +204,7 @@ export const calculateSequenceEditorTree = (
|
||||||
propConfig,
|
propConfig,
|
||||||
arrayOfChildren,
|
arrayOfChildren,
|
||||||
level,
|
level,
|
||||||
|
shouldRender,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +218,7 @@ export const calculateSequenceEditorTree = (
|
||||||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||||
>,
|
>,
|
||||||
level: number,
|
level: number,
|
||||||
|
shouldRender: boolean,
|
||||||
) {
|
) {
|
||||||
if (conf.type === 'compound') {
|
if (conf.type === 'compound') {
|
||||||
const trackMapping =
|
const trackMapping =
|
||||||
|
@ -190,6 +230,7 @@ export const calculateSequenceEditorTree = (
|
||||||
conf,
|
conf,
|
||||||
arrayOfChildren,
|
arrayOfChildren,
|
||||||
level,
|
level,
|
||||||
|
shouldRender,
|
||||||
)
|
)
|
||||||
} else if (conf.type === 'enum') {
|
} else if (conf.type === 'enum') {
|
||||||
logger.warn('Prop type enum is not yet supported in the sequence editor')
|
logger.warn('Prop type enum is not yet supported in the sequence editor')
|
||||||
|
@ -203,6 +244,7 @@ export const calculateSequenceEditorTree = (
|
||||||
conf,
|
conf,
|
||||||
arrayOfChildren,
|
arrayOfChildren,
|
||||||
level,
|
level,
|
||||||
|
shouldRender,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,40 +258,46 @@ export const calculateSequenceEditorTree = (
|
||||||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||||
>,
|
>,
|
||||||
level: number,
|
level: number,
|
||||||
|
shouldRender: boolean,
|
||||||
) {
|
) {
|
||||||
const isCollapsedP =
|
const isCollapsedP =
|
||||||
collapsableP.byId[
|
collapsableItemSetP.byId[
|
||||||
createStudioSheetItemKey.forSheetObjectProp(sheetObject, pathToProp)
|
createStudioSheetItemKey.forSheetObjectProp(sheetObject, pathToProp)
|
||||||
].isCollapsed
|
].isCollapsed
|
||||||
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
const isCollapsed = valueDerivation(isCollapsedP).getValue() ?? false
|
||||||
|
|
||||||
const row: SequenceEditorTree_PropWithChildren = {
|
const row: SequenceEditorTree_PropWithChildren = {
|
||||||
type: 'propWithChildren',
|
type: 'propWithChildren',
|
||||||
isCollapsed: isCollapsed,
|
isCollapsed,
|
||||||
pathToProp,
|
pathToProp,
|
||||||
sheetObject: sheetObject,
|
sheetObject: sheetObject,
|
||||||
|
shouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
children: [],
|
children: [],
|
||||||
nodeHeight: HEIGHT_OF_ANY_TITLE,
|
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||||
heightIncludingChildren: -1,
|
heightIncludingChildren: -1,
|
||||||
depth: level,
|
depth: level,
|
||||||
trackMapping,
|
trackMapping,
|
||||||
n: nSoFar,
|
n: nSoFar,
|
||||||
}
|
}
|
||||||
arrayOfChildren.push(row)
|
arrayOfChildren.push(row)
|
||||||
topSoFar += row.nodeHeight
|
|
||||||
if (!isCollapsed) {
|
|
||||||
nSoFar += 1
|
|
||||||
|
|
||||||
addProps(
|
if (shouldRender) {
|
||||||
sheetObject,
|
topSoFar += row.nodeHeight
|
||||||
trackMapping,
|
nSoFar += 1
|
||||||
pathToProp,
|
|
||||||
conf,
|
|
||||||
row.children,
|
|
||||||
level + 1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addProps(
|
||||||
|
sheetObject,
|
||||||
|
trackMapping,
|
||||||
|
pathToProp,
|
||||||
|
conf,
|
||||||
|
row.children,
|
||||||
|
level + 1,
|
||||||
|
// collapsed shouldn't render child props
|
||||||
|
shouldRender && !isCollapsed,
|
||||||
|
)
|
||||||
|
// }
|
||||||
row.heightIncludingChildren = topSoFar - row.top
|
row.heightIncludingChildren = topSoFar - row.top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +310,7 @@ export const calculateSequenceEditorTree = (
|
||||||
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
SequenceEditorTree_PrimitiveProp | SequenceEditorTree_PropWithChildren
|
||||||
>,
|
>,
|
||||||
level: number,
|
level: number,
|
||||||
|
shouldRender: boolean,
|
||||||
) {
|
) {
|
||||||
const row: SequenceEditorTree_PrimitiveProp = {
|
const row: SequenceEditorTree_PrimitiveProp = {
|
||||||
type: 'primitiveProp',
|
type: 'primitiveProp',
|
||||||
|
@ -269,9 +318,10 @@ export const calculateSequenceEditorTree = (
|
||||||
depth: level,
|
depth: level,
|
||||||
sheetObject: sheetObject,
|
sheetObject: sheetObject,
|
||||||
pathToProp,
|
pathToProp,
|
||||||
|
shouldRender,
|
||||||
top: topSoFar,
|
top: topSoFar,
|
||||||
nodeHeight: HEIGHT_OF_ANY_TITLE,
|
nodeHeight: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||||
heightIncludingChildren: HEIGHT_OF_ANY_TITLE,
|
heightIncludingChildren: shouldRender ? HEIGHT_OF_ANY_TITLE : 0,
|
||||||
trackId,
|
trackId,
|
||||||
n: nSoFar,
|
n: nSoFar,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
BasicKeyframedTrack,
|
||||||
HistoricPositionalSequence,
|
HistoricPositionalSequence,
|
||||||
Keyframe,
|
Keyframe,
|
||||||
SheetState_Historic,
|
SheetState_Historic,
|
||||||
|
@ -609,10 +610,13 @@ namespace stateEditors {
|
||||||
|
|
||||||
const trackId = generateSequenceTrackId()
|
const trackId = generateSequenceTrackId()
|
||||||
|
|
||||||
tracks.trackData[trackId] = {
|
const track: BasicKeyframedTrack = {
|
||||||
type: 'BasicKeyframedTrack',
|
type: 'BasicKeyframedTrack',
|
||||||
|
__debugName: `${p.objectKey}:${pathEncoded}`,
|
||||||
keyframes: [],
|
keyframes: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracks.trackData[trackId] = track
|
||||||
tracks.trackIdByPropPath[pathEncoded] = trackId
|
tracks.trackIdByPropPath[pathEncoded] = trackId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,7 +626,7 @@ namespace stateEditors {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const tracks = _ensureTracksOfObject(p)
|
const tracks = _ensureTracksOfObject(p)
|
||||||
const encodedPropPath = JSON.stringify(p.pathToProp)
|
const encodedPropPath = encodePathToProp(p.pathToProp)
|
||||||
const trackId = tracks.trackIdByPropPath[encodedPropPath]
|
const trackId = tracks.trackIdByPropPath[encodedPropPath]
|
||||||
|
|
||||||
if (typeof trackId !== 'string') return
|
if (typeof trackId !== 'string') return
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||||
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
import useBoundingClientRect from '@theatre/studio/uiComponents/useBoundingClientRect'
|
||||||
import transparentize from 'polished/lib/color/transparentize'
|
import transparentize from 'polished/lib/color/transparentize'
|
||||||
import type {ElementType} from 'react'
|
import type {ElementType} from 'react'
|
||||||
|
import {useMemo} from 'react'
|
||||||
import {useContext} from 'react'
|
import {useContext} from 'react'
|
||||||
import React, {useLayoutEffect, useState} from 'react'
|
import React, {useLayoutEffect, useState} from 'react'
|
||||||
import {createPortal} from 'react-dom'
|
import {createPortal} from 'react-dom'
|
||||||
|
@ -18,6 +19,8 @@ const minWidth = 190
|
||||||
*/
|
*/
|
||||||
const pointerDistanceThreshold = 20
|
const pointerDistanceThreshold = 20
|
||||||
|
|
||||||
|
const SHOW_OPTIONAL_MENU_TITLE = true
|
||||||
|
|
||||||
const MenuContainer = styled.ul`
|
const MenuContainer = styled.ul`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: ${minWidth}px;
|
min-width: ${minWidth}px;
|
||||||
|
@ -33,6 +36,13 @@ const MenuContainer = styled.ul`
|
||||||
${pointerEventsAutoInNormalMode};
|
${pointerEventsAutoInNormalMode};
|
||||||
border-radius: 3px;
|
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: {
|
export type IContextMenuItemCustomNodeRenderFn = (controls: {
|
||||||
closeMenu(): void
|
closeMenu(): void
|
||||||
|
@ -49,8 +59,13 @@ export type IContextMenuItemsValue =
|
||||||
| IContextMenuItem[]
|
| IContextMenuItem[]
|
||||||
| (() => 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<{
|
const ContextMenu: React.FC<{
|
||||||
items: IContextMenuItemsValue
|
items: IContextMenuItemsValue
|
||||||
|
displayName?: string
|
||||||
clickPoint: {clientX: number; clientY: number}
|
clickPoint: {clientX: number; clientY: number}
|
||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
@ -109,10 +124,28 @@ const ContextMenu: React.FC<{
|
||||||
if (ev.key === 'Escape') props.onRequestClose()
|
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(
|
return createPortal(
|
||||||
<MenuContainer ref={setContainer}>
|
<MenuContainer ref={setContainer}>
|
||||||
|
{SHOW_OPTIONAL_MENU_TITLE && props.displayName ? (
|
||||||
|
<MenuTitle>{props.displayName}</MenuTitle>
|
||||||
|
) : null}
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<Item
|
<Item
|
||||||
key={`item-${i}`}
|
key={`item-${i}`}
|
||||||
|
|
|
@ -14,8 +14,8 @@ const ItemContainer = styled.li<{enabled: boolean}>`
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: ${(props) => (props.enabled ? 'auto' : 'none')};
|
color: ${(props) => (props.enabled ? 'white' : '#8f8f8f')};
|
||||||
color: ${(props) => (props.enabled ? 'white' : '#AAA')};
|
cursor: ${(props) => (props.enabled ? 'normal' : 'not-allowed')};
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -28,7 +28,8 @@ const ItemContainer = styled.li<{enabled: boolean}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:after {
|
&: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
|
<ItemContainer
|
||||||
onClick={props.enabled ? props.onClick : noop}
|
onClick={props.enabled ? props.onClick : noop}
|
||||||
enabled={props.enabled}
|
enabled={props.enabled}
|
||||||
|
title={props.enabled ? undefined : 'Disabled'}
|
||||||
>
|
>
|
||||||
<ItemLabel>{props.label}</ItemLabel>
|
<ItemLabel>{props.label}</ItemLabel>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type {VoidFn} from '@theatre/shared/utils/types'
|
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||||
import React from 'react'
|
import React, {useEffect} from 'react'
|
||||||
import ContextMenu from './ContextMenu/ContextMenu'
|
import ContextMenu from './ContextMenu/ContextMenu'
|
||||||
import type {
|
import type {
|
||||||
IContextMenuItemsValue,
|
IContextMenuItemsValue,
|
||||||
|
@ -21,15 +21,24 @@ export default function useContextMenu(
|
||||||
target: HTMLElement | SVGElement | null,
|
target: HTMLElement | SVGElement | null,
|
||||||
opts: IRequestContextMenuOptions & {
|
opts: IRequestContextMenuOptions & {
|
||||||
menuItems: IContextMenuItemsValue
|
menuItems: IContextMenuItemsValue
|
||||||
|
displayName?: string
|
||||||
|
onOpen?: () => void
|
||||||
},
|
},
|
||||||
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
||||||
const [status, close] = useRequestContextMenu(target, opts)
|
const [status, close] = useRequestContextMenu(target, opts)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status.isOpen) {
|
||||||
|
opts.onOpen?.()
|
||||||
|
}
|
||||||
|
}, [status.isOpen, opts.onOpen])
|
||||||
|
|
||||||
const node = !status.isOpen ? (
|
const node = !status.isOpen ? (
|
||||||
emptyNode
|
emptyNode
|
||||||
) : (
|
) : (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
items={opts.menuItems}
|
items={opts.menuItems}
|
||||||
|
displayName={opts.displayName}
|
||||||
clickPoint={status.event}
|
clickPoint={status.event}
|
||||||
onRequestClose={close}
|
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