Implement weight-based topological sort for timeline blocks

This commit is contained in:
Ian Gulliver
2026-02-23 14:59:45 -08:00
parent 8dcd695f84
commit 6c3eb67fb3
3 changed files with 79 additions and 9 deletions

View File

@@ -19,6 +19,8 @@ type Block struct {
Track string `json:"track,omitempty"` Track string `json:"track,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Loop bool `json:"loop,omitempty"` Loop bool `json:"loop,omitempty"`
weight uint64
} }
type Trigger struct { type Trigger struct {

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"cmp"
"fmt" "fmt"
"slices" "slices"
) )
@@ -35,11 +36,11 @@ const (
) )
type TimelineCell struct { type TimelineCell struct {
Type CellType `json:"type"` Type CellType `json:"type"`
BlockID string `json:"block_id,omitempty"` BlockID string `json:"block_id,omitempty"`
Event string `json:"event,omitempty"` Event string `json:"event,omitempty"`
row int `json:"-"` row int `json:"-"`
track *TimelineTrack `json:"-"` track *TimelineTrack `json:"-"`
} }
func (t *TimelineTrack) cellTypeAt(index int, types ...CellType) bool { func (t *TimelineTrack) cellTypeAt(index int, types ...CellType) bool {
@@ -150,6 +151,8 @@ func BuildTimeline(show *Show) (Timeline, error) {
tl.Blocks[block.ID] = block tl.Blocks[block.ID] = block
} }
sortedBlocks := tl.sortBlocks()
endChains := map[string]bool{} endChains := map[string]bool{}
for _, trigger := range show.Triggers { for _, trigger := range show.Triggers {
if trigger.Source.Signal != "END" { if trigger.Source.Signal != "END" {
@@ -163,7 +166,7 @@ func BuildTimeline(show *Show) (Timeline, error) {
} }
} }
tl.buildCells(endChains) tl.buildCells(endChains, sortedBlocks)
tl.buildConstraints() tl.buildConstraints()
if err := tl.assignRows(); err != nil { if err := tl.assignRows(); err != nil {
return Timeline{}, err return Timeline{}, err
@@ -172,6 +175,33 @@ func BuildTimeline(show *Show) (Timeline, error) {
return tl, nil return tl, nil
} }
func (tl *Timeline) sortBlocks() []*Block {
for i, b := range tl.show.Blocks {
b.weight = uint64(i) << 32
}
changed := true
for changed {
changed = false
for _, t := range tl.show.Triggers {
src := tl.Blocks[t.Source.Block]
for _, target := range t.Targets {
dst := tl.Blocks[target.Block]
if dst.weight <= src.weight {
dst.weight = src.weight + 1
changed = true
}
}
}
}
sorted := slices.Clone(tl.show.Blocks)
slices.SortFunc(sorted, func(a, b *Block) int {
return cmp.Compare(a.weight, b.weight)
})
return sorted
}
func (tl *Timeline) addConstraint(kind constraintKind, a, b *TimelineCell) { func (tl *Timeline) addConstraint(kind constraintKind, a, b *TimelineCell) {
tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b}) tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b})
} }
@@ -208,13 +238,13 @@ func (tl *Timeline) findCell(blockID, event string) *TimelineCell {
panic("cell not found: " + blockID + " " + event) panic("cell not found: " + blockID + " " + event)
} }
func (tl *Timeline) buildCells(endChains map[string]bool) { func (tl *Timeline) buildCells(endChains map[string]bool, sortedBlocks []*Block) {
lastOnTrack := map[string]*Block{} lastOnTrack := map[string]*Block{}
for _, block := range tl.show.Blocks { for _, block := range sortedBlocks {
lastOnTrack[block.Track] = block lastOnTrack[block.Track] = block
} }
for _, block := range tl.show.Blocks { for _, block := range sortedBlocks {
track := tl.trackIdx[block.Track] track := tl.trackIdx[block.Track]
var cells []*TimelineCell var cells []*TimelineCell
switch block.Type { switch block.Type {

View File

@@ -35,3 +35,41 @@ func BenchmarkBuildTimeline(b *testing.B) {
BuildTimeline(show) BuildTimeline(show)
} }
} }
func TestTimelineOrderDependency(t *testing.T) {
show := &Show{
Tracks: []*Track{
{ID: "T1", Name: "Track 1"},
{ID: "T2", Name: "Track 2"},
},
Blocks: []*Block{
{ID: "B", Type: "media", Track: "T1", Name: "Block B"},
{ID: "A", Type: "media", Track: "T1", Name: "Block A"},
{ID: "C", Type: "media", Track: "T2", Name: "Block C"},
{ID: "C1", Type: "cue", Name: "Cue 1"},
},
Triggers: []*Trigger{
{
Source: TriggerSource{Block: "C1", Signal: "GO"},
Targets: []TriggerTarget{{Block: "A", Hook: "START"}},
},
{
Source: TriggerSource{Block: "A", Signal: "END"},
Targets: []TriggerTarget{{Block: "C", Hook: "START"}},
},
{
Source: TriggerSource{Block: "C", Signal: "END"},
Targets: []TriggerTarget{{Block: "B", Hook: "START"}},
},
},
}
if err := show.Validate(); err != nil {
t.Fatalf("Validate failed: %v", err)
}
_, err := BuildTimeline(show)
if err != nil {
t.Fatalf("BuildTimeline failed: %v", err)
}
}