From 64e76445cff270062719318429b5b4b305de2a50 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 20 Feb 2026 08:57:41 -0700 Subject: [PATCH] Collapse empty rows in timeline, preserving intentional break gaps --- cmd/qrunproxy/timeline.go | 75 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/cmd/qrunproxy/timeline.go b/cmd/qrunproxy/timeline.go index c7c60f4..5e1cc4c 100644 --- a/cmd/qrunproxy/timeline.go +++ b/cmd/qrunproxy/timeline.go @@ -57,7 +57,8 @@ type TimelineCell struct { Event string `json:"event,omitempty"` IsTitle bool `json:"is_title,omitempty"` IsSignal bool `json:"is_signal,omitempty"` - IsGap bool `json:"-"` + IsGap bool `json:"-"` + IsBreak bool `json:"-"` } type cellID struct { @@ -147,6 +148,7 @@ func BuildTimeline(show Show) (Timeline, error) { b.buildCells() b.buildConstraints() b.assignRows() + b.collapseEmptyRows() return Timeline{ Tracks: b.tracks, @@ -215,7 +217,7 @@ func (b *timelineBuilder) buildCells() { } b.trackCells[idx] = append(b.trackCells[idx], cells...) if block.Type != "cue" && !b.endChainsSameTrack(block.ID) { - b.trackCells[idx] = append(b.trackCells[idx], TimelineCell{IsGap: true}) + b.trackCells[idx] = append(b.trackCells[idx], TimelineCell{IsGap: true, IsBreak: true}) } } } @@ -364,6 +366,75 @@ func (b *timelineBuilder) insertGap(track, beforeIndex int) { } } +func (b *timelineBuilder) collapseEmptyRows() { + maxLen := 0 + for _, cells := range b.trackCells { + if len(cells) > maxLen { + maxLen = len(cells) + } + } + + keep := make([]bool, maxLen) + for r := 0; r < maxLen; r++ { + allGaps := true + hasBreak := false + for _, cells := range b.trackCells { + if r >= len(cells) { + continue + } + c := cells[r] + if !c.IsGap { + allGaps = false + break + } + if c.IsBreak { + hasBreak = true + } + } + if !allGaps { + keep[r] = true + continue + } + if hasBreak && r+1 < maxLen { + canMove := true + for _, cells := range b.trackCells { + if r >= len(cells) { + continue + } + if !cells[r].IsBreak { + continue + } + if r+1 >= len(cells) || !cells[r+1].IsGap { + canMove = false + break + } + } + if canMove { + for trackIdx, cells := range b.trackCells { + if r >= len(cells) || !cells[r].IsBreak { + continue + } + b.trackCells[trackIdx][r+1].IsBreak = true + } + } else { + keep[r] = true + } + } else if hasBreak { + keep[r] = true + } + } + + for trackIdx := range b.trackCells { + var filtered []TimelineCell + for r, c := range b.trackCells[trackIdx] { + if keep[r] { + filtered = append(filtered, c) + } + } + b.trackCells[trackIdx] = filtered + } +} + func (b *timelineBuilder) renderRows() []TimelineRow { maxLen := 0 for _, cells := range b.trackCells {