Skip to content

Commit 357dcf4

Browse files
committed
Merge remote-tracking branch 'upstream/alpha' into timeline-track-selection
2 parents 4342cca + 7334c2e commit 357dcf4

10 files changed

Lines changed: 165 additions & 69 deletions

File tree

apps/desktop/src/main/mainWindow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const createWindow = (): void => {
2626
show: false,
2727
autoHideMenuBar: !process.platform.startsWith('win'),
2828
...(process.platform === 'linux' ? { icon } : {}),
29+
title: 'Hedron',
2930
webPreferences: {
3031
preload: join(__dirname, '../preload/index.js'),
3132
sandbox: false,

apps/desktop/src/renderer/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html>
33
<head>
44
<meta charset="UTF-8" />
5-
<title>Electron</title>
5+
<title>Hedron</title>
66
<meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' blob:;" />
77
</head>
88

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "root",
3+
"productName": "Hedron",
34
"private": true,
45
"scripts": {
56
"dev": "lerna run dev --scope desktop",

packages/engine/src/store/types/ParamNode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export type ParamValueType = ParamNode['valueType'] | null
151151

152152
export type ParamVector = ParamVector2 | ParamVector3 | ParamRGB
153153
export type ParamVectorValueType = ParamVector['valueType']
154+
export type ParamNonVectorValueType = Exclude<ParamValueType, ParamVectorValueType>
154155

155156
export type EnsureRequiredValueType<T> = T extends { valueType?: infer V }
156157
? Omit<T, 'valueType'> & { valueType: V }

packages/timeline/src/TimelineInput.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
OptionNodesFromConfigs,
88
isParamVector,
99
ParamNode,
10+
ParamValue,
1011
ShotNode,
1112
isParamVectorComponent,
1213
} from '@hedron-gl/engine'
@@ -121,14 +122,28 @@ export class TimelineInput implements IPlugin {
121122
}
122123

123124
const tracks = getKeyframeTracks(allTracks)
124-
const changedTargetNodeIds = changedTrackIds.map(
125-
(trackId) => tracks.find((track) => track.id === trackId)?.targetNodeId ?? trackId,
126-
)
127-
128-
engine.setMultipleParamValues(
129-
changedTargetNodeIds,
130-
changedTrackIds.map((trackId) => changed[trackId]),
131-
)
125+
126+
const paramTargetNodeIds: string[] = []
127+
const paramValues: ParamValue[] = []
128+
129+
for (const trackId of changedTrackIds) {
130+
const targetNodeId =
131+
tracks.find((track) => track.id === trackId)?.targetNodeId ?? trackId
132+
const targetNode = engine.getNode(targetNodeId)
133+
const value = changed[trackId]
134+
135+
if (targetNode?.nodeType === 'shot') {
136+
engine.fireShot(targetNodeId)
137+
continue
138+
}
139+
140+
paramTargetNodeIds.push(targetNodeId)
141+
paramValues.push(value)
142+
}
143+
144+
if (paramTargetNodeIds.length > 0) {
145+
engine.setMultipleParamValues(paramTargetNodeIds, paramValues)
146+
}
132147
}
133148
})
134149
})

packages/timeline/src/TimelineManager.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
TimelineManagerTrack,
66
Keyframe,
77
TimelineManagerAudioTrack,
8+
TimelineManagerKeyframeTrack,
9+
KeyframeParam,
810
} from '@/types'
911

1012
export type TrackValues = Record<string, ParamValue>
@@ -22,6 +24,8 @@ export class TimelineManager {
2224
private sortedKeyframesCache: Map<string, Keyframe[]> = new Map()
2325
private audioCache: Map<string, HTMLAudioElement> = new Map()
2426
private lastKeyframeIndex: Map<string, number> = new Map()
27+
// Number of shot keyframes at or before the position as of the last check, per track.
28+
private shotKeyframeCount: Map<string, number> = new Map()
2529
private clock: Clock | null = null
2630

2731
constructor(timeline: TimelineManagerData, clock?: Clock) {
@@ -55,8 +59,8 @@ export class TimelineManager {
5559
this.lastKeyframeIndex.clear()
5660
}
5761

58-
private getTrackValue(track: TimelineManagerTrack): ParamValue | undefined {
59-
const sorted = this.sortedKeyframesCache.get(track.id) ?? []
62+
private getHeldValue(track: TimelineManagerKeyframeTrack): ParamValue | undefined {
63+
const sorted = (this.sortedKeyframesCache.get(track.id) ?? []) as KeyframeParam[]
6064
const startIndex = this.lastKeyframeIndex.get(track.id) ?? 0
6165
let value = startIndex > 0 ? sorted[startIndex - 1].value : undefined
6266
let lastIndex = startIndex
@@ -66,12 +70,42 @@ export class TimelineManager {
6670
lastIndex = i + 1
6771
}
6872
this.lastKeyframeIndex.set(track.id, lastIndex)
73+
74+
// interpolation for number values
75+
const current = sorted[lastIndex - 1]
76+
const next = sorted[lastIndex]
77+
if (current?.valueType === 'number' && next?.valueType === 'number') {
78+
const t = (this.position - current.time) / (next.time - current.time)
79+
return current.value + (next.value - current.value) * t
80+
}
81+
6982
return value
7083
}
7184

85+
// Shot keyframes are momentary triggers, not held state: fire once whenever the number of
86+
// keyframes crossed (time <= position) increases since the last check.
87+
private getShouldShotFire(track: TimelineManagerTrack): true | undefined {
88+
const sorted = this.sortedKeyframesCache.get(track.id) ?? []
89+
const count = sorted.filter((kf) => kf.time < this.position).length
90+
const lastCount = this.shotKeyframeCount.get(track.id) ?? 0
91+
this.shotKeyframeCount.set(track.id, count)
92+
return count > lastCount ? true : undefined
93+
}
94+
95+
private isShotTrack(track: TimelineManagerTrack): boolean {
96+
return track.trackType === 'keyframe' && track.keyframes[0]?.nodeType === 'shot'
97+
}
98+
99+
// Returns the value for a track at the current position, or undefined if no value is held.
100+
// Also returns true for shot tracks if a shot should fire at the current position, or undefined if not.
101+
private getTrackValue(track: TimelineManagerKeyframeTrack): ParamValue | true | undefined {
102+
return this.isShotTrack(track) ? this.getShouldShotFire(track) : this.getHeldValue(track)
103+
}
104+
72105
private computeValues(): TrackValues {
73106
const values: TrackValues = {}
74107
for (const track of this.getAllTracks()) {
108+
if (track.trackType !== 'keyframe') continue
75109
const value = this.getTrackValue(track)
76110
if (value !== undefined) {
77111
values[track.id] = value
@@ -82,11 +116,19 @@ export class TimelineManager {
82116

83117
private getChangedValues(newValues: TrackValues): TrackValues {
84118
const changed: TrackValues = {}
85-
for (const key of Object.keys(newValues)) {
86-
if (this.cachedValues[key] !== newValues[key]) {
87-
changed[key] = newValues[key]
119+
for (const track of this.getAllTracks()) {
120+
const value = newValues[track.id]
121+
if (value === undefined) continue
122+
123+
// Shot fires are already edge-detected in getShotFired, so a `true` is forwarded as-is
124+
// rather than compared against the last value (there's no "held state" to diff against).
125+
if (this.isShotTrack(track)) {
126+
changed[track.id] = value
127+
} else if (this.cachedValues[track.id] !== value) {
128+
changed[track.id] = value
88129
}
89130
}
131+
90132
return changed
91133
}
92134

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export function Timeline({
6060
}
6161

6262
const handleKeyDown = (e: KeyboardEvent) => {
63+
const target = e.target as HTMLElement | null
64+
if (target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA' || target?.isContentEditable) {
65+
return
66+
}
6367
if (e.key === 'x' && selectedKeyframes) {
6468
selectedKeyframes.forEach((keyframeId) => {
6569
onKeyframeDelete?.(keyframeId)

packages/timeline/src/components/TimelineGlobalPanel/useTimelineHandlers.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useCallback } from 'react'
2-
import { HedronEngine, ParamNode } from '@hedron-gl/engine'
2+
import { HedronEngine } from '@hedron-gl/engine'
33
import { useNodeOptionNodes } from '@hedron-gl/ui-core'
44
import { DEFAULT_TIMELINE_ID } from '@/constants'
55
import { TimelineManager } from '@/TimelineManager'
6-
import type { Keyframe, TimelineTrackInput } from '@/types'
6+
import type { Keyframe, KeyframeParam, TimelineTrackInput } from '@/types'
77

88
interface UseTimelineHandlersParams {
99
engine: HedronEngine
@@ -50,25 +50,35 @@ export const useTimelineHandlers = ({ engine, manager }: UseTimelineHandlersPara
5050
return
5151
}
5252

53-
const targetParam = engine.getNode(inputNode.targetNodeId) as ParamNode | undefined
53+
const targetNode = engine.getNode(inputNode.targetNodeId)
5454

55-
if (!targetParam) {
56-
console.error(`Target param not found for input node ${inputNode.id}`)
55+
if (!targetNode) {
56+
console.error(`Target node not found for input node ${inputNode.id}`)
5757
return
5858
}
5959

60-
const targetParamValue = engine.getParamValue(inputNode.targetNodeId)
61-
62-
if (targetParamValue === undefined) {
63-
console.error(`Target param value not found for input node ${inputNode.id}`)
64-
return
65-
}
66-
67-
const keyframe = {
68-
id: crypto.randomUUID(),
69-
time,
70-
valueType: targetParam.valueType,
71-
value: targetParamValue,
60+
let keyframe: Keyframe
61+
if (targetNode.nodeType === 'shot') {
62+
keyframe = { id: crypto.randomUUID(), time, nodeType: 'shot' }
63+
} else if (targetNode.nodeType === 'param') {
64+
const targetParamValue = engine.getParamValue(inputNode.targetNodeId)
65+
66+
if (targetParamValue === undefined) {
67+
console.error(`Target param value not found for input node ${inputNode.id}`)
68+
return
69+
}
70+
71+
keyframe = {
72+
id: crypto.randomUUID(),
73+
time,
74+
valueType: targetNode.valueType,
75+
value: targetParamValue,
76+
nodeType: 'param',
77+
} as KeyframeParam
78+
} else {
79+
throw new Error(
80+
`Unsupported target node type for input node ${inputNode.id} with type ${targetNode.nodeType}`,
81+
)
7282
}
7383

7484
const nextKeyframes: Keyframe[] = [...(inputNode.customData?.keyframes ?? []), keyframe].sort(

packages/timeline/src/stories/Timeline.stories.tsx

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TimelineManager } from '@/TimelineManager'
88
import type { TrackValues } from '@/TimelineManager'
99
import { Timeline } from '@/components/Timeline/Timeline'
1010
import type {
11+
KeyframeParam,
1112
TimelineManagerData,
1213
TimelineManagerKeyframeTrack,
1314
TimelineManagerTrack,
@@ -55,10 +56,10 @@ export const WithKeyframes: Story = {
5556
label: 'Visibility',
5657
trackType: 'keyframe',
5758
keyframes: [
58-
{ id: 'kf-1', time: 1000, valueType: 'boolean', value: true },
59-
{ id: 'kf-2', time: 3000, valueType: 'boolean', value: false },
60-
{ id: 'kf-3', time: 5500, valueType: 'boolean', value: true },
61-
{ id: 'kf-4', time: 8000, valueType: 'boolean', value: false },
59+
{ id: 'kf-1', time: 1000, valueType: 'boolean', value: true, nodeType: 'param' },
60+
{ id: 'kf-2', time: 3000, valueType: 'boolean', value: false, nodeType: 'param' },
61+
{ id: 'kf-3', time: 5500, valueType: 'boolean', value: true, nodeType: 'param' },
62+
{ id: 'kf-4', time: 8000, valueType: 'boolean', value: false, nodeType: 'param' },
6263
],
6364
},
6465
],
@@ -82,19 +83,19 @@ export const WithVectorTrack: Story = {
8283
label: 'X',
8384
trackType: 'keyframe',
8485
keyframes: [
85-
{ id: 'kf-px-1', time: 1000, valueType: 'number', value: 0 },
86-
{ id: 'kf-px-2', time: 5000, valueType: 'number', value: 0.75 },
87-
{ id: 'kf-px-3', time: 9000, valueType: 'number', value: -0.2 },
86+
{ id: 'kf-px-1', time: 1000, valueType: 'number', value: 0, nodeType: 'param' },
87+
{ id: 'kf-px-2', time: 5000, valueType: 'number', value: 0.75, nodeType: 'param' },
88+
{ id: 'kf-px-3', time: 9000, valueType: 'number', value: -0.2, nodeType: 'param' },
8889
],
8990
},
9091
{
9192
id: 'track-pos-y',
9293
label: 'Y',
9394
trackType: 'keyframe',
9495
keyframes: [
95-
{ id: 'kf-py-1', time: 1500, valueType: 'number', value: -0.25 },
96-
{ id: 'kf-py-2', time: 4500, valueType: 'number', value: 0.5 },
97-
{ id: 'kf-py-3', time: 8000, valueType: 'number', value: 0.1 },
96+
{ id: 'kf-py-1', time: 1500, valueType: 'number', value: -0.25, nodeType: 'param' },
97+
{ id: 'kf-py-2', time: 4500, valueType: 'number', value: 0.5, nodeType: 'param' },
98+
{ id: 'kf-py-3', time: 8000, valueType: 'number', value: 0.1, nodeType: 'param' },
9899
],
99100
},
100101
],
@@ -128,19 +129,19 @@ export const Interactive = () => {
128129
label: 'X',
129130
trackType: 'keyframe',
130131
keyframes: [
131-
{ id: 'kf-px1', time: 500, valueType: 'number', value: -0.5 },
132-
{ id: 'kf-px2', time: 4000, valueType: 'number', value: 0.35 },
133-
{ id: 'kf-px3', time: 7000, valueType: 'number', value: 0.8 },
132+
{ id: 'kf-px1', time: 500, valueType: 'number', value: -0.5, nodeType: 'param' },
133+
{ id: 'kf-px2', time: 4000, valueType: 'number', value: 0.35, nodeType: 'param' },
134+
{ id: 'kf-px3', time: 7000, valueType: 'number', value: 0.8, nodeType: 'param' },
134135
],
135136
},
136137
{
137138
id: 'track-pos-y',
138139
label: 'Y',
139140
trackType: 'keyframe',
140141
keyframes: [
141-
{ id: 'kf-py1', time: 1000, valueType: 'number', value: 0.25 },
142-
{ id: 'kf-py2', time: 5000, valueType: 'number', value: -0.1 },
143-
{ id: 'kf-py3', time: 8500, valueType: 'number', value: 0.55 },
142+
{ id: 'kf-py1', time: 1000, valueType: 'number', value: 0.25, nodeType: 'param' },
143+
{ id: 'kf-py2', time: 5000, valueType: 'number', value: -0.1, nodeType: 'param' },
144+
{ id: 'kf-py3', time: 8500, valueType: 'number', value: 0.55, nodeType: 'param' },
144145
],
145146
},
146147
],
@@ -150,31 +151,31 @@ export const Interactive = () => {
150151
label: 'Visibility',
151152
trackType: 'keyframe',
152153
keyframes: [
153-
{ id: 'kf-v1', time: 0, valueType: 'boolean', value: true },
154-
{ id: 'kf-v2', time: 3000, valueType: 'boolean', value: false },
155-
{ id: 'kf-v3', time: 6000, valueType: 'boolean', value: true },
154+
{ id: 'kf-v1', time: 0, valueType: 'boolean', value: true, nodeType: 'param' },
155+
{ id: 'kf-v2', time: 3000, valueType: 'boolean', value: false, nodeType: 'param' },
156+
{ id: 'kf-v3', time: 6000, valueType: 'boolean', value: true, nodeType: 'param' },
156157
],
157158
},
158159
{
159160
id: 'track-2',
160161
label: 'Strobe',
161162
trackType: 'keyframe',
162163
keyframes: [
163-
{ id: 'kf-s1', time: 1000, valueType: 'boolean', value: true },
164-
{ id: 'kf-s2', time: 2000, valueType: 'boolean', value: false },
165-
{ id: 'kf-s3', time: 4000, valueType: 'boolean', value: true },
166-
{ id: 'kf-s4', time: 5000, valueType: 'boolean', value: false },
167-
{ id: 'kf-s5', time: 7000, valueType: 'boolean', value: true },
168-
{ id: 'kf-s6', time: 8000, valueType: 'boolean', value: false },
164+
{ id: 'kf-s1', time: 1000, valueType: 'boolean', value: true, nodeType: 'param' },
165+
{ id: 'kf-s2', time: 2000, valueType: 'boolean', value: false, nodeType: 'param' },
166+
{ id: 'kf-s3', time: 4000, valueType: 'boolean', value: true, nodeType: 'param' },
167+
{ id: 'kf-s4', time: 5000, valueType: 'boolean', value: false, nodeType: 'param' },
168+
{ id: 'kf-s5', time: 7000, valueType: 'boolean', value: true, nodeType: 'param' },
169+
{ id: 'kf-s6', time: 8000, valueType: 'boolean', value: false, nodeType: 'param' },
169170
],
170171
},
171172
{
172173
id: 'track-3',
173174
label: 'Invert',
174175
trackType: 'keyframe',
175176
keyframes: [
176-
{ id: 'kf-i1', time: 2500, valueType: 'boolean', value: true },
177-
{ id: 'kf-i2', time: 7500, valueType: 'boolean', value: false },
177+
{ id: 'kf-i1', time: 2500, valueType: 'boolean', value: true, nodeType: 'param' },
178+
{ id: 'kf-i2', time: 7500, valueType: 'boolean', value: false, nodeType: 'param' },
178179
],
179180
},
180181
],
@@ -255,14 +256,15 @@ export const Interactive = () => {
255256
tracks: prev.tracks.map((track) => {
256257
const insertKeyframe = (track: TimelineManagerTrack): TimelineManagerTrack => {
257258
if (track.trackType === 'keyframe' && track.id === trackId) {
258-
const lastKeyframe = track.keyframes[track.keyframes.length - 1]
259+
const lastKeyframe = track.keyframes[track.keyframes.length - 1] as KeyframeParam
259260
const valueType = lastKeyframe?.valueType ?? 'boolean'
260261
const value =
261262
lastKeyframe?.value ??
262263
(valueType === 'number' ? 0 : valueType === 'boolean' ? true : null)
263264

264265
return {
265266
...track,
267+
// @ts-ignore -- this is just a story file we don't need to care so much
266268
keyframes: [
267269
...track.keyframes,
268270
{
@@ -360,11 +362,11 @@ export const LongDuration: Story = {
360362
label: 'Active',
361363
trackType: 'keyframe',
362364
keyframes: [
363-
{ id: 'kf-1', time: 10000, valueType: 'boolean', value: true },
364-
{ id: 'kf-2', time: 30000, valueType: 'boolean', value: false },
365-
{ id: 'kf-3', time: 60000, valueType: 'boolean', value: true },
366-
{ id: 'kf-4', time: 90000, valueType: 'boolean', value: false },
367-
{ id: 'kf-5', time: 110000, valueType: 'boolean', value: true },
365+
{ id: 'kf-1', time: 10000, valueType: 'boolean', value: true, nodeType: 'param' },
366+
{ id: 'kf-2', time: 30000, valueType: 'boolean', value: false, nodeType: 'param' },
367+
{ id: 'kf-3', time: 60000, valueType: 'boolean', value: true, nodeType: 'param' },
368+
{ id: 'kf-4', time: 90000, valueType: 'boolean', value: false, nodeType: 'param' },
369+
{ id: 'kf-5', time: 110000, valueType: 'boolean', value: true, nodeType: 'param' },
368370
],
369371
},
370372
],

0 commit comments

Comments
 (0)