From 17638ad18f888b171caa1daccea09f75aea91585 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 20 Feb 2026 08:19:08 -0700 Subject: [PATCH] Simplify timeline types: merge video/audio to media, remove trackCell/blockCells indirection --- cmd/qrunproxy/static/index.html | 9 +- cmd/qrunproxy/static/show.json | 18 ++-- cmd/qrunproxy/timeline.go | 148 ++++++++++++++++---------------- 3 files changed, 84 insertions(+), 91 deletions(-) diff --git a/cmd/qrunproxy/static/index.html b/cmd/qrunproxy/static/index.html index 39fe34c..769c282 100644 --- a/cmd/qrunproxy/static/index.html +++ b/cmd/qrunproxy/static/index.html @@ -17,10 +17,8 @@ --cue-bg: rgba(58, 24, 0, 0.7); --light-color: #c8e; --light-bg: rgba(42, 10, 42, 0.55); - --video-color: #4d4; - --video-bg: rgba(10, 42, 10, 0.55); - --audio-color: #58f; - --audio-bg: rgba(10, 10, 42, 0.55); + --media-color: #4d4; + --media-bg: rgba(10, 42, 10, 0.55); --delay-color: #999; --delay-bg: rgba(26, 26, 26, 0.55); --infinity-color: #666; @@ -106,8 +104,7 @@ header h1 { font-size: 16px; font-weight: 600; letter-spacing: 0.05em; } border-bottom: 1px solid rgba(255, 204, 0, 0.3); } .block.light { color: var(--light-color); border-color: var(--light-color); background: var(--light-bg); } -.block.video { color: var(--video-color); border-color: var(--video-color); background: var(--video-bg); } -.block.audio { color: var(--audio-color); border-color: var(--audio-color); background: var(--audio-bg); } +.block.media { color: var(--media-color); border-color: var(--media-color); background: var(--media-bg); } .block.delay { color: var(--delay-color); border-color: var(--delay-color); background: var(--delay-bg); } .hook { diff --git a/cmd/qrunproxy/static/show.json b/cmd/qrunproxy/static/show.json index e7b52e7..0b5d8f9 100644 --- a/cmd/qrunproxy/static/show.json +++ b/cmd/qrunproxy/static/show.json @@ -11,27 +11,27 @@ {"id": "block_q10_preshow", "type": "cue", "name": "Q10 Preshow"}, {"id": "block_preshow_wash", "type": "light", "track": "track_lighting_a", "name": "Preshow Wash"}, {"id": "block_warm_70", "type": "light", "track": "track_lighting_b", "name": "Warm 70%"}, - {"id": "block_preshow_loop", "type": "video", "track": "track_video", "name": "Preshow Loop", "loop": true}, - {"id": "block_preshow_music", "type": "audio", "track": "track_audio", "name": "Preshow Music", "loop": true}, + {"id": "block_preshow_loop", "type": "media", "track": "track_video", "name": "Preshow Loop", "loop": true}, + {"id": "block_preshow_music", "type": "media", "track": "track_audio", "name": "Preshow Music", "loop": true}, {"id": "block_q11_house_open", "type": "cue", "name": "Q11 House Open"}, {"id": "block_q12_top_of_show", "type": "cue", "name": "Q12 Top of Show"}, {"id": "block_cool_50", "type": "light", "track": "track_lighting_b", "name": "Cool 50%"}, {"id": "block_3s_delay", "type": "delay", "track": "track_video", "name": "3s Delay"}, {"id": "block_sc1_focus", "type": "light", "track": "track_lighting_a", "name": "SC1 Focus"}, {"id": "block_sc1_blue_80", "type": "light", "track": "track_lighting_b", "name": "SC1 Blue 80%"}, - {"id": "block_sc1_projection", "type": "video", "track": "track_video", "name": "Sc1 Projection"}, - {"id": "block_lightning_flash", "type": "video", "track": "track_video_ovl", "name": "Lightning Flash"}, - {"id": "block_storm_ambience", "type": "audio", "track": "track_audio", "name": "Storm Ambience", "loop": true}, + {"id": "block_sc1_projection", "type": "media", "track": "track_video", "name": "Sc1 Projection"}, + {"id": "block_lightning_flash", "type": "media", "track": "track_video_ovl", "name": "Lightning Flash"}, + {"id": "block_storm_ambience", "type": "media", "track": "track_audio", "name": "Storm Ambience", "loop": true}, {"id": "block_q13_sc1_dialog", "type": "cue", "name": "Q13 Sc1 Dialog"}, - {"id": "block_wave_overlay", "type": "video", "track": "track_video_ovl", "name": "Wave Overlay"}, + {"id": "block_wave_overlay", "type": "media", "track": "track_video_ovl", "name": "Wave Overlay"}, {"id": "block_dialog_spots", "type": "light", "track": "track_lighting_a", "name": "Dialog Spots"}, {"id": "block_warm_90", "type": "light", "track": "track_lighting_b", "name": "Warm 90%"}, - {"id": "block_dialog_underscore", "type": "audio", "track": "track_audio", "name": "Dialog Underscore"}, + {"id": "block_dialog_underscore", "type": "media", "track": "track_audio", "name": "Dialog Underscore"}, {"id": "block_q14_sc2_trans", "type": "cue", "name": "Q14 Sc2 Trans"}, {"id": "block_sc2_focus", "type": "light", "track": "track_lighting_a", "name": "SC2 Focus"}, {"id": "block_sc2_amber_60", "type": "light", "track": "track_lighting_b", "name": "SC2 Amber 60%"}, - {"id": "block_sc2_background", "type": "video", "track": "track_video", "name": "Sc2 Background", "loop": true}, - {"id": "block_sc2_atmos", "type": "audio", "track": "track_audio", "name": "SC2 Atmos", "loop": true} + {"id": "block_sc2_background", "type": "media", "track": "track_video", "name": "Sc2 Background", "loop": true}, + {"id": "block_sc2_atmos", "type": "media", "track": "track_audio", "name": "SC2 Atmos", "loop": true} ], "triggers": [ { diff --git a/cmd/qrunproxy/timeline.go b/cmd/qrunproxy/timeline.go index 6584df3..780c607 100644 --- a/cmd/qrunproxy/timeline.go +++ b/cmd/qrunproxy/timeline.go @@ -55,6 +55,7 @@ type TimelineCell struct { Event string `json:"event,omitempty"` IsTitle bool `json:"is_title,omitempty"` IsSignal bool `json:"is_signal,omitempty"` + IsGap bool `json:"-"` } type cellID struct { @@ -72,24 +73,21 @@ type exclusiveGroup struct { members []cellID } -type trackCell struct { - cell TimelineCell - isGap bool -} - type timelineBuilder struct { + show Show blocks map[string]Block - tracks []Track - trackIdx map[string]int + tracks []Track + trackIdx map[string]int startSigs map[string][]TriggerTarget - trackCells [][]trackCell + trackCells [][]TimelineCell constraints []constraint exclusives []exclusiveGroup } -func BuildTimeline(show Show) (Timeline, error) { +func newTimelineBuilder(show Show) *timelineBuilder { b := &timelineBuilder{ + show: show, blocks: map[string]Block{}, trackIdx: map[string]int{}, startSigs: map[string][]TriggerTarget{}, @@ -110,10 +108,16 @@ func BuildTimeline(show Show) (Timeline, error) { } } - b.trackCells = make([][]trackCell, len(b.tracks)) + b.trackCells = make([][]TimelineCell, len(b.tracks)) - b.buildCells(show) + return b +} +func BuildTimeline(show Show) (Timeline, error) { + b := newTimelineBuilder(show) + + b.buildCells() + b.buildConstraints() b.assignRows() return Timeline{ @@ -123,16 +127,6 @@ func BuildTimeline(show Show) (Timeline, error) { }, nil } -func (b *timelineBuilder) appendCell(trackID string, cell TimelineCell) cellID { - idx, ok := b.trackIdx[trackID] - if !ok { - return cellID{-1, -1} - } - i := len(b.trackCells[idx]) - b.trackCells[idx] = append(b.trackCells[idx], trackCell{cell: cell}) - return cellID{track: idx, index: i} -} - func (b *timelineBuilder) addConstraint(kind string, a, b2 cellID) { if a.track < 0 || b2.track < 0 { return @@ -151,53 +145,64 @@ func (b *timelineBuilder) getTrack(blockID string) string { return block.Track } -type blockCells struct { - start cellID - title cellID - fadeOut cellID - end cellID +func getCueCells(block Block) []TimelineCell { + return []TimelineCell{{ + BlockID: block.ID, + IsStart: true, + IsEnd: true, + Event: "GO", + }} } -func (b *timelineBuilder) buildCells(show Show) { - cells := map[string]blockCells{} +func getBlockCells(block Block) []TimelineCell { + return []TimelineCell{ + {BlockID: block.ID, IsStart: true, Event: "START"}, + {BlockID: block.ID, IsTitle: true}, + {BlockID: block.ID, Event: "FADE_OUT"}, + {BlockID: block.ID, IsEnd: true, Event: "END"}, + } +} - for _, block := range show.Blocks { +func (b *timelineBuilder) findCell(blockID, event string) cellID { + trackID := b.getTrack(blockID) + if trackID == "" { + return cellID{-1, -1} + } + track := b.trackIdx[trackID] + for i, c := range b.trackCells[track] { + if !c.IsGap && c.BlockID == blockID && c.Event == event { + return cellID{track: track, index: i} + } + } + return cellID{-1, -1} +} + +func (b *timelineBuilder) buildCells() { + for _, block := range b.show.Blocks { trackID := b.getTrack(block.ID) if trackID == "" { continue } - if block.Type == "cue" { - cueID := b.appendCell(trackID, TimelineCell{ - BlockID: block.ID, - IsStart: true, - IsEnd: true, - Event: "GO", - }) - cells[block.ID] = blockCells{start: cueID, title: cueID, fadeOut: cueID, end: cueID} - continue + idx := b.trackIdx[trackID] + var cells []TimelineCell + switch block.Type { + case "cue": + cells = getCueCells(block) + default: + cells = getBlockCells(block) } - startID := b.appendCell(trackID, TimelineCell{BlockID: block.ID, IsStart: true, Event: "START"}) - titleID := b.appendCell(trackID, TimelineCell{BlockID: block.ID, IsTitle: true}) - fadeOutID := b.appendCell(trackID, TimelineCell{BlockID: block.ID, Event: "FADE_OUT"}) - endID := b.appendCell(trackID, TimelineCell{BlockID: block.ID, IsEnd: true, Event: "END"}) - cells[block.ID] = blockCells{start: startID, title: titleID, fadeOut: fadeOutID, end: endID} + b.trackCells[idx] = append(b.trackCells[idx], cells...) } +} - for _, trigger := range show.Triggers { +func (b *timelineBuilder) buildConstraints() { + for _, trigger := range b.show.Triggers { if trigger.Source.Signal == "START" { continue } - sourceCell := cells[trigger.Source.Block] - var sourceID cellID - switch trigger.Source.Signal { - case "GO": - sourceID = sourceCell.start - case "END": - sourceID = sourceCell.end - case "FADE_OUT": - sourceID = sourceCell.fadeOut - default: + sourceID := b.findCell(trigger.Source.Block, trigger.Source.Signal) + if sourceID.track < 0 { continue } @@ -206,16 +211,8 @@ func (b *timelineBuilder) buildCells(show Show) { allTargets := b.expandTargets(trigger.Targets) for _, target := range allTargets { - tc := cells[target.Block] - var targetID cellID - switch target.Hook { - case "START": - targetID = tc.start - case "END": - targetID = tc.end - case "FADE_OUT": - targetID = tc.fadeOut - default: + targetID := b.findCell(target.Block, target.Hook) + if targetID.track < 0 { continue } if sourceID.track == targetID.track { @@ -232,7 +229,6 @@ func (b *timelineBuilder) buildCells(show Show) { } b.exclusives = append(b.exclusives, group) } - } func (b *timelineBuilder) expandTargets(targets []TriggerTarget) []TriggerTarget { @@ -261,7 +257,7 @@ func (b *timelineBuilder) setSignal(id cellID) { if id.track < 0 { return } - b.trackCells[id.track][id.index].cell.IsSignal = true + b.trackCells[id.track][id.index].IsSignal = true } func (b *timelineBuilder) assignRows() { @@ -323,8 +319,8 @@ func (b *timelineBuilder) enforceExclusives() bool { if row >= len(b.trackCells[trackIdx]) { continue } - tc := b.trackCells[trackIdx][row] - if tc.isGap || tc.cell.BlockID == "" { + c := b.trackCells[trackIdx][row] + if c.IsGap || c.BlockID == "" { continue } b.insertGap(trackIdx, row) @@ -340,9 +336,9 @@ func (b *timelineBuilder) rowOf(id cellID) int { func (b *timelineBuilder) insertGap(track, beforeIndex int) { cells := b.trackCells[track] - newCells := make([]trackCell, 0, len(cells)+1) + newCells := make([]TimelineCell, 0, len(cells)+1) newCells = append(newCells, cells[:beforeIndex]...) - newCells = append(newCells, trackCell{isGap: true}) + newCells = append(newCells, TimelineCell{IsGap: true}) newCells = append(newCells, cells[beforeIndex:]...) b.trackCells[track] = newCells @@ -382,16 +378,16 @@ func (b *timelineBuilder) renderRows() []TimelineRow { activeBlock := "" for r := 0; r < maxLen; r++ { if r < len(cells) { - tc := cells[r] - if tc.isGap { + c := cells[r] + if c.IsGap { if activeBlock != "" { rows[r].Cells[trackIdx] = TimelineCell{BlockID: activeBlock} } } else { - rows[r].Cells[trackIdx] = tc.cell - if tc.cell.BlockID != "" && !tc.cell.IsEnd { - activeBlock = tc.cell.BlockID - } else if tc.cell.IsEnd { + rows[r].Cells[trackIdx] = c + if c.BlockID != "" && !c.IsEnd { + activeBlock = c.BlockID + } else if c.IsEnd { activeBlock = "" } }