Implement weight-based topological sort for timeline blocks
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user