Skip to content

Commit 4342cca

Browse files
committed
individual timeline display and track selection
1 parent 8ceb49e commit 4342cca

8 files changed

Lines changed: 120 additions & 23 deletions

File tree

apps/desktop/src/renderer/engine.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MidiInput, MidiInputPanel, MidiGlobalPanel } from '@hedron-gl/midi-inpu
55
import { GamepadInput, GamepadInputPanel, GamepadGlobalPanel } from '@hedron-gl/gamepad-input'
66
import { LFOInput, LFOInputPanel } from '@hedron-gl/lfo-input'
77
import { AudioInput, AudioInputPanel, AudioGlobalPanel } from '@hedron-gl/audio-input'
8-
import { TimelineInput, TimelineGlobalPanel } from '@hedron-gl/timeline'
8+
import { TimelineInput, TimelineGlobalPanel, TimelineInputPanel } from '@hedron-gl/timeline'
99
import { SceneControlPlugin, SceneControlGlobalPanel } from '@hedron-gl/scene-control'
1010

1111
export const performanceMonitor = new Stats()
@@ -36,6 +36,7 @@ export const pluginViews = {
3636
lfo: LFOInputPanel,
3737
audio: AudioInputPanel,
3838
gamepad: GamepadInputPanel,
39+
['timeline-track']: TimelineInputPanel,
3940
},
4041
globalPanel: {
4142
['audio-input']: AudioGlobalPanel,

apps/desktop/src/renderer/handlers/fileHandlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export const handleSaveProjectDialog = async (options?: { saveAs?: boolean }) =>
9393
openedControlGroups,
9494
selectedNodes,
9595
selectedInputs,
96+
selectedTimelineTrackId,
9697
selectedSceneId,
9798
selectedSketches,
9899
} = appState
@@ -111,6 +112,7 @@ export const handleSaveProjectDialog = async (options?: { saveAs?: boolean }) =>
111112
selectedSketches,
112113
selectedNodes,
113114
selectedInputs,
115+
selectedTimelineTrackId,
114116
openedControlGroups,
115117
},
116118
}

packages/app-store/src/appStore.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface ProjectData {
3939
selectedSketches: { [sceneId: string]: string | null }
4040
selectedNodes: { [sketchId: string]: string | null }
4141
selectedInputs: { [inputId: string]: string | null }
42+
selectedTimelineTrackId: string | null
4243
openedControlGroups: { [sketchId: string]: Record<number, boolean> }
4344
}
4445
}
@@ -48,6 +49,7 @@ export interface AppState {
4849
selectedSketches: ProjectData['app']['selectedSketches'] // TODO: should be part of ProjectData
4950
selectedNodes: ProjectData['app']['selectedNodes']
5051
selectedInputs: ProjectData['app']['selectedInputs']
52+
selectedTimelineTrackId: ProjectData['app']['selectedTimelineTrackId']
5153
openedControlGroups: ProjectData['app']['openedControlGroups']
5254
sketchesDir: string | null
5355
globalDialogId: DialogId | null
@@ -56,6 +58,7 @@ export interface AppState {
5658
sketchesServerBuildResult: BuildResult | null
5759
setSelectedNode: (sketchID: string, nodeId: string | null) => void
5860
setSelectedInput: (nodeId: string, inputId: string | null) => void
61+
setSelectedTimelineTrackId: (trackId: string | null) => void
5962
setOpenedControlGroup: (sketchId: string, groupIndex: number, isOpen: boolean) => void
6063
setSelectedSceneId: (id: string | null) => void
6164
setSelectedSketch: (sceneId: string, sketchId: string | null) => void
@@ -93,6 +96,7 @@ export const createAppStore = () =>
9396
currentSavePath: null,
9497
selectedNodes: {},
9598
selectedInputs: {},
99+
selectedTimelineTrackId: null,
96100
openedControlGroups: {},
97101
saveList: [],
98102
sketchesServerBuildResult: null,
@@ -150,6 +154,11 @@ export const createAppStore = () =>
150154
state.selectedInputs[nodeId] = inputId
151155
})
152156
},
157+
setSelectedTimelineTrackId: (trackId) => {
158+
set((state) => {
159+
state.selectedTimelineTrackId = trackId
160+
})
161+
},
153162
setOpenedControlGroup: (sketchId: string, groupIndex: number, isOpen: boolean) => {
154163
set((state) => {
155164
if (!state.openedControlGroups[sketchId]) {
@@ -211,6 +220,13 @@ export const createAppStore = () =>
211220
}
212221
}
213222

223+
if (
224+
state.selectedTimelineTrackId &&
225+
!validNodeIds.has(state.selectedTimelineTrackId)
226+
) {
227+
state.selectedTimelineTrackId = null
228+
}
229+
214230
for (const sketchId of Object.keys(state.openedControlGroups)) {
215231
if (!validSketchIds.has(sketchId)) {
216232
delete state.openedControlGroups[sketchId]

packages/timeline/src/components/Timeline/Timeline.tsx

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useCallback, useEffect, useRef, useState } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22
import { usePlayheadScrub } from './usePlayheadScrub'
33
import c from './Timeline.module.css'
44
import { TimelineTrack } from './TimelineTrack'
55
import { TimelineManagerTrack } from '@/types'
6+
import { findTrackById } from '@/utils/findTrackById'
67

78
export interface TimelineProps {
89
/** Timeline data */
@@ -12,6 +13,8 @@ export interface TimelineProps {
1213
}
1314
/** Current playhead position in milliseconds */
1415
playheadPositionMs?: number
16+
selectedTrackId?: string | null
17+
setSelectedTrackId?: (trackId: string | null) => void
1518
/** Called when the user clicks on the track area to set the playhead */
1619
onPlayheadChange?: (time: number) => void
1720
/** Called when a keyframe should be deleted */
@@ -23,34 +26,25 @@ export interface TimelineProps {
2326
export function Timeline({
2427
timeline,
2528
playheadPositionMs = 0,
29+
selectedTrackId: controlledSelectedTrackId,
30+
setSelectedTrackId: controlledSetSelectedTrackId,
2631
onPlayheadChange,
2732
onKeyframeDelete,
2833
onKeyframeInsert,
2934
}: TimelineProps) {
3035
const { durationMs, tracks } = timeline
3136
const rulerAreaRef = useRef<HTMLDivElement>(null)
32-
const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null)
33-
const [selectedKeyframes, setSelectedKeyframes] = useState<string[] | null>(null)
34-
35-
const findTrackById = useCallback(
36-
(trackList: TimelineManagerTrack[], trackId: string): TimelineManagerTrack | null => {
37-
for (const track of trackList) {
38-
if (track.id === trackId) {
39-
return track
40-
}
41-
42-
if (track.trackType === 'vector') {
43-
const childTrack = findTrackById(track.childTracks, trackId)
44-
if (childTrack) {
45-
return childTrack
46-
}
47-
}
48-
}
49-
50-
return null
51-
},
52-
[],
37+
const [uncontrolledSelectedTrackId, setUncontrolledSelectedTrackId] = useState<string | null>(
38+
null,
5339
)
40+
const isControlled = controlledSetSelectedTrackId !== undefined
41+
const selectedTrackId = isControlled
42+
? (controlledSelectedTrackId ?? null)
43+
: uncontrolledSelectedTrackId
44+
const setSelectedTrackId = isControlled
45+
? controlledSetSelectedTrackId
46+
: setUncontrolledSelectedTrackId
47+
const [selectedKeyframes, setSelectedKeyframes] = useState<string[] | null>(null)
5448

5549
useEffect(() => {
5650
const getChildKeyframeTrackIds = (track: TimelineManagerTrack): string[] => {

packages/timeline/src/components/TimelineGlobalPanel/TimelineGlobalPanel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
NodeContainer,
44
useNodeOptionNodes,
55
useParamValue,
6+
useAppStore,
67
ControlGrid,
78
} from '@hedron-gl/ui-core'
89
import { useTimelineData } from './useTimelineData'
@@ -25,6 +26,9 @@ export const TimelineGlobalPanel = ({ engine }: TimelineGlobalPanelProps) => {
2526
manager,
2627
})
2728

29+
const selectedTrackId = useAppStore((state) => state.selectedTimelineTrackId)
30+
const setSelectedTrackId = useAppStore((state) => state.setSelectedTimelineTrackId)
31+
2832
const optionNodes = useNodeOptionNodes<TimelineOptionNodes>(DEFAULT_TIMELINE_ID)
2933
const isPlayingNode = optionNodes['isPlaying']!
3034
const playHeadPositionNode = optionNodes['playheadPositionMs']!
@@ -44,6 +48,8 @@ export const TimelineGlobalPanel = ({ engine }: TimelineGlobalPanelProps) => {
4448
<Timeline
4549
timeline={timeline}
4650
playheadPositionMs={playheadPositionMs}
51+
selectedTrackId={selectedTrackId}
52+
setSelectedTrackId={setSelectedTrackId}
4753
onPlayheadChange={handlePlayheadChange}
4854
onKeyframeDelete={handleKeyframeDelete}
4955
onKeyframeInsert={handleKeyframeInsert}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useEffect } from 'react'
2+
import { HedronEngine, InputNode } from '@hedron-gl/engine'
3+
import { useNodeOptionNodes, useParamValue, useAppStore } from '@hedron-gl/ui-core'
4+
import { useTimelineData } from '@/components/TimelineGlobalPanel/useTimelineData'
5+
import { useTimelineHandlers } from '@/components/TimelineGlobalPanel/useTimelineHandlers'
6+
import { useTimelineManager } from '@/components/TimelineGlobalPanel/useTimelineManager'
7+
import { DEFAULT_TIMELINE_ID } from '@/constants'
8+
import { Timeline } from '@/components/Timeline/Timeline'
9+
import { TimelineOptionNodes } from '@/TimelineInput'
10+
import { findTrackById } from '@/utils/findTrackById'
11+
12+
interface TimelineInputPanelProps {
13+
input: InputNode
14+
engine: HedronEngine
15+
}
16+
17+
/**
18+
* Per-node view for a timeline-track input: the same editor as the global timeline panel
19+
* (header, playhead, keyframes), scoped down to just this input's own track.
20+
*/
21+
export const TimelineInputPanel = ({ input, engine }: TimelineInputPanelProps) => {
22+
const timeline = useTimelineData()
23+
const manager = useTimelineManager(DEFAULT_TIMELINE_ID)
24+
25+
const { handlePlayheadChange, handleKeyframeDelete, handleKeyframeInsert } = useTimelineHandlers({
26+
engine,
27+
manager,
28+
})
29+
30+
const selectedTrackId = useAppStore((state) => state.selectedTimelineTrackId)
31+
const setSelectedTrackId = useAppStore((state) => state.setSelectedTimelineTrackId)
32+
33+
// Viewing this input's track makes it the selected one, so keyboard shortcuts (i/x) act on it
34+
// and it stays in sync with the global timeline panel's selection.
35+
useEffect(() => {
36+
setSelectedTrackId(input.id)
37+
}, [input.id, setSelectedTrackId])
38+
39+
const optionNodes = useNodeOptionNodes<TimelineOptionNodes>(DEFAULT_TIMELINE_ID)
40+
const playHeadPositionNode = optionNodes['playheadPositionMs']!
41+
const playheadPositionMs = useParamValue<number>(playHeadPositionNode.id)
42+
43+
const track = findTrackById(timeline.tracks, input.id)
44+
45+
return (
46+
<Timeline
47+
timeline={{ durationMs: timeline.durationMs, tracks: track ? [track] : [] }}
48+
playheadPositionMs={playheadPositionMs}
49+
selectedTrackId={selectedTrackId}
50+
setSelectedTrackId={setSelectedTrackId}
51+
onPlayheadChange={handlePlayheadChange}
52+
onKeyframeDelete={handleKeyframeDelete}
53+
onKeyframeInsert={handleKeyframeInsert}
54+
/>
55+
)
56+
}

packages/timeline/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { TimelineManager } from './TimelineManager'
99
export type { TrackValues, OnUpdateCallback } from './TimelineManager'
1010
export { TimelineInput } from './TimelineInput'
1111
export { TimelineGlobalPanel } from './components/TimelineGlobalPanel/TimelineGlobalPanel'
12+
export { TimelineInputPanel } from './components/TimelineInputPanel/TimelineInputPanel'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { TimelineManagerTrack } from '@/types'
2+
3+
export const findTrackById = (
4+
trackList: TimelineManagerTrack[],
5+
trackId: string,
6+
): TimelineManagerTrack | null => {
7+
for (const track of trackList) {
8+
if (track.id === trackId) {
9+
return track
10+
}
11+
12+
if (track.trackType === 'vector') {
13+
const childTrack = findTrackById(track.childTracks, trackId)
14+
if (childTrack) {
15+
return childTrack
16+
}
17+
}
18+
}
19+
20+
return null
21+
}

0 commit comments

Comments
 (0)