Files
qrun/cmd/qrunproxy/timeline.go

544 lines
13 KiB
Go

package main
import (
"cmp"
"fmt"
"io"
"slices"
"strings"
)
const cueTrackID = "_cue"
type TimelineTrack struct {
*Track
Cells []*TimelineCell `json:"cells"`
}
type Timeline struct {
Tracks []*TimelineTrack `json:"tracks"`
Blocks map[string]*Block `json:"blocks"`
show *Show `json:"-"`
trackIdx map[string]*TimelineTrack `json:"-"`
cellIdx map[cellKey]*TimelineCell `json:"-"`
constraints []constraint `json:"-"`
exclusives []exclusiveGroup `json:"-"`
debugW io.Writer `json:"-"`
}
type CellType string
const (
CellEvent CellType = "event"
CellTitle CellType = "title"
CellContinuation CellType = "continuation"
CellGap CellType = "gap"
CellChain CellType = "chain"
CellSignal CellType = "signal"
)
type TimelineCell struct {
Type CellType `json:"type"`
BlockID string `json:"block_id,omitempty"`
Event string `json:"event,omitempty"`
row int `json:"-"`
track *TimelineTrack `json:"-"`
}
func (t *TimelineTrack) cellTypeAt(index int, types ...CellType) bool {
if index < 0 || index >= len(t.Cells) {
return false
}
for _, typ := range types {
if t.Cells[index].Type == typ {
return true
}
}
return false
}
func (c *TimelineCell) String() string {
return fmt.Sprintf("%s/%s@%s:r%d", c.BlockID, c.Event, c.track.ID, c.row)
}
type constraintKind string
const (
constraintSameRow constraintKind = "same_row"
)
type constraint struct {
kind constraintKind
a *TimelineCell
b *TimelineCell
}
func (c constraint) satisfied() bool {
switch c.kind {
case constraintSameRow:
return c.a.row == c.b.row
default:
panic("invalid constraint kind: " + string(c.kind))
}
}
func (c constraint) String() string {
switch c.kind {
case constraintSameRow:
return fmt.Sprintf("same_row(%s, %s)", c.a, c.b)
default:
panic("invalid constraint kind: " + string(c.kind))
}
}
type exclusiveGroup struct {
members []*TimelineCell
memberTracks map[*TimelineTrack]bool
}
func (g exclusiveGroup) satisfied(tracks []*TimelineTrack) bool {
row := g.members[0].row
for _, m := range g.members {
if m.row != row {
return true
}
}
for _, t := range tracks {
if g.memberTracks[t] {
continue
}
if t.cellTypeAt(row, CellEvent, CellTitle, CellSignal) {
return false
}
}
return true
}
func (g exclusiveGroup) String() string {
s := "exclusive("
for i, m := range g.members {
if i > 0 {
s += ", "
}
s += m.String()
}
return s + ")"
}
func (tl *Timeline) debugf(format string, args ...any) {
if tl.debugW != nil {
fmt.Fprintf(tl.debugW, format+"\n", args...)
}
}
func (tl *Timeline) debugState() {
if tl.debugW == nil {
return
}
fmt.Fprintf(tl.debugW, "=== state ===\n")
for _, t := range tl.Tracks {
var parts []string
for _, c := range t.Cells {
switch c.Type {
case CellEvent, CellSignal:
parts = append(parts, fmt.Sprintf("r%d:%s/%s", c.row, c.BlockID, c.Event))
case CellTitle:
parts = append(parts, fmt.Sprintf("r%d:title(%s)", c.row, c.BlockID))
default:
parts = append(parts, fmt.Sprintf("r%d:%s", c.row, c.Type))
}
}
fmt.Fprintf(tl.debugW, " %s: [%s]\n", t.ID, strings.Join(parts, " "))
}
for _, c := range tl.constraints {
sat := "OK"
if !c.satisfied() {
sat = "UNSATISFIED"
}
fmt.Fprintf(tl.debugW, " constraint %s %s\n", c, sat)
}
for _, g := range tl.exclusives {
sat := "OK"
if !g.satisfied(tl.Tracks) {
sat = "UNSATISFIED"
}
fmt.Fprintf(tl.debugW, " exclusive %s %s\n", g, sat)
}
fmt.Fprintf(tl.debugW, "=============\n")
}
type cellKey struct {
blockID string
event string
}
func BuildTimeline(show *Show) (Timeline, error) {
return BuildTimelineDebug(show, nil)
}
func BuildTimelineDebug(show *Show, debugW io.Writer) (Timeline, error) {
tl := Timeline{
show: show,
Blocks: map[string]*Block{},
trackIdx: map[string]*TimelineTrack{},
cellIdx: map[cellKey]*TimelineCell{},
debugW: debugW,
}
cueTrack := &TimelineTrack{Track: &Track{ID: cueTrackID, Name: "Cue"}}
tl.Tracks = append(tl.Tracks, cueTrack)
tl.trackIdx[cueTrackID] = cueTrack
for _, track := range show.Tracks {
tt := &TimelineTrack{Track: track}
tl.Tracks = append(tl.Tracks, tt)
tl.trackIdx[track.ID] = tt
}
for _, block := range show.Blocks {
if block.Type == "cue" {
block.Track = cueTrackID
}
tl.Blocks[block.ID] = block
}
sortedBlocks := tl.sortBlocks()
endChains := map[string]bool{}
for _, trigger := range show.Triggers {
if trigger.Source.Signal != "END" {
continue
}
srcTrack := tl.Blocks[trigger.Source.Block].Track
for _, target := range trigger.Targets {
if target.Hook == "START" && tl.Blocks[target.Block].Track == srcTrack {
endChains[trigger.Source.Block] = true
}
}
}
tl.buildCells(endChains, sortedBlocks)
tl.buildConstraints()
if err := tl.assignRows(); err != nil {
return Timeline{}, err
}
return tl, nil
}
type trackCueKey struct {
track string
cue string
}
func (tl *Timeline) sortBlocks() []*Block {
for _, b := range tl.show.Blocks {
b.weight = 0
}
trackDeepest := map[trackCueKey]*Block{}
changed := true
for pass := 0; changed; pass++ {
if pass > 1000 {
tl.debugf("sortBlocks: did not converge after %d passes", pass)
break
}
changed = false
for i, b := range tl.show.Blocks {
if b.Type == "cue" {
changed = tl.setWeightRecursive(b, uint64(i+1)<<32, b.ID, trackDeepest, trackDeepest) || changed
}
}
}
sorted := slices.Clone(tl.show.Blocks)
slices.SortFunc(sorted, func(a, b *Block) int {
return cmp.Compare(a.weight, b.weight)
})
for _, b := range sorted {
tl.debugf("weight %s track=%s type=%s w=%d", b.ID, b.Track, b.Type, b.weight)
}
return sorted
}
func (tl *Timeline) setWeight(b *Block, weight uint64) bool {
if weight <= b.weight {
return false
}
b.weight = weight
return true
}
func (tl *Timeline) updateTrackDeepest(b *Block, cue string, trackDeepest map[trackCueKey]*Block) {
if trackDeepest == nil {
return
}
key := trackCueKey{track: b.Track, cue: cue}
if prev := trackDeepest[key]; prev == nil || b.weight > prev.weight {
tl.debugf("trackDeepest set {%s, %s} = %s (w=%d, prev=%v)", key.track, key.cue, b.ID, b.weight, prev)
trackDeepest[key] = b
}
}
func (tl *Timeline) setWeightRecursive(b *Block, weight uint64, cue string, tdRead, tdWrite map[trackCueKey]*Block) bool {
changed := tl.setWeight(b, weight)
tl.updateTrackDeepest(b, cue, tdWrite)
for _, t := range tl.show.Triggers {
// TODO: needs a lookup table
if t.Source.Block != b.ID {
continue
}
for _, target := range t.Targets {
trg := tl.Blocks[target.Block]
targetWeight := b.weight + 1
tw := tdWrite
if trg.Track != b.Track {
if b.Track != cueTrackID {
if deep := tdRead[trackCueKey{track: trg.Track, cue: cue}]; deep != nil {
if deep.weight+1 > targetWeight {
tl.debugf("trackDeepest read {%s, %s} = %s (w=%d): bumping %s from %d to %d", trg.Track, cue, deep.ID, deep.weight, trg.ID, targetWeight, deep.weight+1)
targetWeight = deep.weight + 1
}
}
tw = nil
}
}
changed = tl.setWeightRecursive(trg, targetWeight, cue, tdRead, tw) || changed
if trg.Track == b.Track {
changed = tl.setWeight(b, trg.weight-1) || changed
}
// TODO: needs to go to other targets
}
}
return changed
}
func (tl *Timeline) addConstraint(kind constraintKind, a, b *TimelineCell) {
tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b})
}
func (track *TimelineTrack) appendCells(cells ...*TimelineCell) {
for _, c := range cells {
c.row = len(track.Cells)
c.track = track
track.Cells = append(track.Cells, c)
}
}
func getCueCells(block *Block) []*TimelineCell {
return []*TimelineCell{{
Type: CellEvent,
BlockID: block.ID,
Event: "GO",
}}
}
func getBlockCells(block *Block) []*TimelineCell {
return []*TimelineCell{
{Type: CellEvent, BlockID: block.ID, Event: "START"},
{Type: CellTitle, BlockID: block.ID},
{Type: CellEvent, BlockID: block.ID, Event: "FADE_OUT"},
{Type: CellEvent, BlockID: block.ID, Event: "END"},
}
}
func (tl *Timeline) findCell(blockID, event string) *TimelineCell {
if c := tl.cellIdx[cellKey{blockID: blockID, event: event}]; c != nil {
return c
}
panic("cell not found: " + blockID + " " + event)
}
func (tl *Timeline) buildCells(endChains map[string]bool, sortedBlocks []*Block) {
lastOnTrack := map[string]*Block{}
for _, block := range sortedBlocks {
lastOnTrack[block.Track] = block
}
for _, block := range sortedBlocks {
track := tl.trackIdx[block.Track]
var cells []*TimelineCell
switch block.Type {
case "cue":
cells = getCueCells(block)
default:
cells = getBlockCells(block)
}
track.appendCells(cells...)
for _, c := range cells {
if c.Event == "" {
continue
}
tl.cellIdx[cellKey{blockID: c.BlockID, event: c.Event}] = c
}
if block.Type != "cue" && lastOnTrack[block.Track] != block {
if endChains[block.ID] {
track.appendCells(&TimelineCell{Type: CellChain})
} else {
track.appendCells(&TimelineCell{Type: CellGap})
}
}
}
}
func (tl *Timeline) buildConstraints() {
for _, trigger := range tl.show.Triggers {
source := tl.findCell(trigger.Source.Block, trigger.Source.Signal)
group := exclusiveGroup{
members: []*TimelineCell{source},
memberTracks: map[*TimelineTrack]bool{source.track: true},
}
for _, target := range trigger.Targets {
t := tl.findCell(target.Block, target.Hook)
if source.track != t.track {
tl.addConstraint(constraintSameRow, source, t)
source.Type = CellSignal
}
group.members = append(group.members, t)
group.memberTracks[t.track] = true
}
tl.exclusives = append(tl.exclusives, group)
}
}
func (tl *Timeline) assignRows() error {
tl.debugState()
for i := range 1000000 {
if tl.enforceConstraints(i) {
continue
}
if tl.enforceExclusives(i) {
continue
}
tl.debugf("converged after %d iterations", i)
return nil
}
for _, c := range tl.constraints {
if !c.satisfied() {
return fmt.Errorf("assignRows: unsatisfied %s", c)
}
}
for _, g := range tl.exclusives {
if !g.satisfied(tl.Tracks) {
return fmt.Errorf("assignRows: unsatisfied %s", g)
}
}
return fmt.Errorf("assignRows: did not converge")
}
func (tl *Timeline) enforceConstraints(iter int) bool {
for _, c := range tl.constraints {
if c.satisfied() {
continue
}
switch c.kind {
case constraintSameRow:
if c.a.row < c.b.row {
tl.debugf("iter %d: constraint %s: insert gap on %s before r%d", iter, c, c.a.track.ID, c.a.row)
tl.insertGap(c.a.track, c.a.row)
} else {
tl.debugf("iter %d: constraint %s: insert gap on %s before r%d", iter, c, c.b.track.ID, c.b.row)
tl.insertGap(c.b.track, c.b.row)
}
default:
panic("invalid constraint kind: " + string(c.kind))
}
return true
}
return false
}
func (tl *Timeline) enforceExclusives(iter int) bool {
for _, g := range tl.exclusives {
if g.satisfied(tl.Tracks) {
continue
}
row := g.members[0].row
tl.debugf("iter %d: exclusive %s: split at r%d", iter, g, row)
for _, t := range tl.Tracks {
if row >= len(t.Cells) {
continue
}
if g.memberTracks[t] {
tl.debugf(" member %s: insertGapInt before r%d", t.ID, row)
tl.insertGapInt(t, row)
} else {
tl.debugf(" non-member %s: insertGapInt before r%d", t.ID, row+1)
tl.insertGapInt(t, row+1)
}
}
return true
}
return false
}
func (tl *Timeline) isAllRemovableGapRow(row int, except *TimelineTrack) bool {
for _, t := range tl.Tracks {
if t == except {
continue
}
if row >= len(t.Cells) {
continue
}
if !t.cellTypeAt(row, CellGap, CellChain, CellContinuation) {
return false
}
hasBefore := t.cellTypeAt(row-1, CellEvent, CellTitle, CellSignal)
hasAfter := t.cellTypeAt(row+1, CellEvent, CellTitle, CellSignal)
if hasBefore && hasAfter {
return false
}
}
return true
}
func (tl *Timeline) removeGapAt(track *TimelineTrack, index int) {
track.Cells = append(track.Cells[:index], track.Cells[index+1:]...)
tl.reindexRowsFrom(track, index)
}
func (tl *Timeline) reindexRowsFrom(track *TimelineTrack, start int) {
for i := start; i < len(track.Cells); i++ {
track.Cells[i].row = i
}
}
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
if tl.isAllRemovableGapRow(beforeIndex, track) {
tl.debugf(" insertGap(%s, r%d): removable row, removing from other tracks", track.ID, beforeIndex)
for _, t := range tl.Tracks {
if t == track {
continue
}
if beforeIndex >= len(t.Cells) {
continue
}
tl.debugf(" removeGap %s r%d (%s)", t.ID, beforeIndex, t.Cells[beforeIndex].Type)
tl.removeGapAt(t, beforeIndex)
}
return
}
tl.debugf(" insertGap(%s, r%d): inserting", track.ID, beforeIndex)
tl.insertGapInt(track, beforeIndex)
}
func (tl *Timeline) insertGapInt(track *TimelineTrack, beforeIndex int) {
gap := &TimelineCell{Type: CellGap, row: beforeIndex, track: track}
if beforeIndex > 0 {
prev := track.Cells[beforeIndex-1]
if prev.Type == CellChain {
gap.Type = CellChain
} else if prev.BlockID != "" && prev.Event != "END" && prev.Event != "GO" {
gap.Type = CellContinuation
gap.BlockID = prev.BlockID
}
}
track.Cells = slices.Insert(track.Cells, beforeIndex, gap)
tl.reindexRowsFrom(track, beforeIndex+1)
}