Merge Timeline and timelineBuilder into single struct, remove cells()/setCells()

This commit is contained in:
Ian Gulliver
2026-02-20 16:27:23 -07:00
parent 4607e77ee7
commit 47ed09a848

View File

@@ -48,6 +48,11 @@ type TimelineTrack struct {
type Timeline struct { type Timeline struct {
Tracks []TimelineTrack `json:"tracks"` Tracks []TimelineTrack `json:"tracks"`
Blocks map[string]Block `json:"blocks"` Blocks map[string]Block `json:"blocks"`
show Show `json:"-"`
trackIdx map[string]int `json:"-"`
constraints []constraint `json:"-"`
exclusives []exclusiveGroup `json:"-"`
} }
type TimelineCell struct { type TimelineCell struct {
@@ -76,47 +81,7 @@ type exclusiveGroup struct {
members []cellID members []cellID
} }
type timelineBuilder struct {
show Show
blocks map[string]Block
tracks []Track
trackIdx map[string]int
trackCells [][]TimelineCell
constraints []constraint
exclusives []exclusiveGroup
}
func newTimelineBuilder(show Show) *timelineBuilder {
b := &timelineBuilder{
show: show,
blocks: map[string]Block{},
trackIdx: map[string]int{},
}
b.tracks = append(b.tracks, Track{ID: cueTrackID, Name: "Cue"})
b.trackIdx[cueTrackID] = 0
for i, track := range show.Tracks {
b.tracks = append(b.tracks, track)
b.trackIdx[track.ID] = i + 1
}
for _, block := range show.Blocks {
if block.Type == "cue" {
block.Track = cueTrackID
}
b.blocks[block.ID] = block
}
b.trackCells = make([][]TimelineCell, len(b.tracks))
return b
}
func validateShow(show Show) error { func validateShow(show Show) error {
blockSet := map[string]bool{}
for _, block := range show.Blocks {
blockSet[block.ID] = true
}
startTargeted := map[string]bool{} startTargeted := map[string]bool{}
for _, trigger := range show.Triggers { for _, trigger := range show.Triggers {
for _, target := range trigger.Targets { for _, target := range trigger.Targets {
@@ -143,25 +108,34 @@ func BuildTimeline(show Show) (Timeline, error) {
return Timeline{}, err return Timeline{}, err
} }
b := newTimelineBuilder(show) tl := Timeline{
show: show,
b.buildCells() Blocks: map[string]Block{},
b.buildConstraints() trackIdx: map[string]int{},
b.assignRows()
tracks := make([]TimelineTrack, len(b.tracks))
for i, t := range b.tracks {
tracks[i] = TimelineTrack{Track: t, Cells: b.trackCells[i]}
} }
return Timeline{ tl.Tracks = append(tl.Tracks, TimelineTrack{Track: Track{ID: cueTrackID, Name: "Cue"}})
Tracks: tracks, tl.trackIdx[cueTrackID] = 0
Blocks: b.blocks, for i, track := range show.Tracks {
}, nil tl.Tracks = append(tl.Tracks, TimelineTrack{Track: track})
tl.trackIdx[track.ID] = i + 1
}
for _, block := range show.Blocks {
if block.Type == "cue" {
block.Track = cueTrackID
}
tl.Blocks[block.ID] = block
}
tl.buildCells()
tl.buildConstraints()
tl.assignRows()
return tl, nil
} }
func (b *timelineBuilder) addConstraint(kind string, a, b2 cellID) { func (tl *Timeline) addConstraint(kind string, a, b cellID) {
b.constraints = append(b.constraints, constraint{kind: kind, a: a, b: b2}) tl.constraints = append(tl.constraints, constraint{kind: kind, a: a, b: b})
} }
func getCueCells(block Block) []TimelineCell { func getCueCells(block Block) []TimelineCell {
@@ -182,9 +156,9 @@ func getBlockCells(block Block) []TimelineCell {
} }
} }
func (b *timelineBuilder) findCell(blockID, event string) cellID { func (tl *Timeline) findCell(blockID, event string) cellID {
track := b.trackIdx[b.blocks[blockID].Track] track := tl.trackIdx[tl.Blocks[blockID].Track]
for i, c := range b.trackCells[track] { for i, c := range tl.Tracks[track].Cells {
if !c.IsGap && c.BlockID == blockID && c.Event == event { if !c.IsGap && c.BlockID == blockID && c.Event == event {
return cellID{track: track, index: i} return cellID{track: track, index: i}
} }
@@ -192,14 +166,14 @@ func (b *timelineBuilder) findCell(blockID, event string) cellID {
panic("cell not found: " + blockID + " " + event) panic("cell not found: " + blockID + " " + event)
} }
func (b *timelineBuilder) endChainsSameTrack(blockID string) bool { func (tl *Timeline) endChainsSameTrack(blockID string) bool {
trackID := b.blocks[blockID].Track trackID := tl.Blocks[blockID].Track
for _, trigger := range b.show.Triggers { for _, trigger := range tl.show.Triggers {
if trigger.Source.Block != blockID || trigger.Source.Signal != "END" { if trigger.Source.Block != blockID || trigger.Source.Signal != "END" {
continue continue
} }
for _, target := range trigger.Targets { for _, target := range trigger.Targets {
if target.Hook == "START" && b.blocks[target.Block].Track == trackID { if target.Hook == "START" && tl.Blocks[target.Block].Track == trackID {
return true return true
} }
} }
@@ -207,10 +181,10 @@ func (b *timelineBuilder) endChainsSameTrack(blockID string) bool {
return false return false
} }
func (b *timelineBuilder) buildCells() { func (tl *Timeline) buildCells() {
for _, sb := range b.show.Blocks { for _, sb := range tl.show.Blocks {
block := b.blocks[sb.ID] block := tl.Blocks[sb.ID]
idx := b.trackIdx[block.Track] idx := tl.trackIdx[block.Track]
var cells []TimelineCell var cells []TimelineCell
switch block.Type { switch block.Type {
case "cue": case "cue":
@@ -218,58 +192,57 @@ func (b *timelineBuilder) buildCells() {
default: default:
cells = getBlockCells(block) cells = getBlockCells(block)
} }
b.trackCells[idx] = append(b.trackCells[idx], cells...) tl.Tracks[idx].Cells = append(tl.Tracks[idx].Cells, cells...)
if block.Type != "cue" && !b.endChainsSameTrack(block.ID) { if block.Type != "cue" && !tl.endChainsSameTrack(block.ID) {
b.trackCells[idx] = append(b.trackCells[idx], TimelineCell{IsGap: true, IsBreak: true}) tl.Tracks[idx].Cells = append(tl.Tracks[idx].Cells, TimelineCell{IsGap: true, IsBreak: true})
} }
} }
} }
func (b *timelineBuilder) buildConstraints() { func (tl *Timeline) buildConstraints() {
for _, trigger := range b.show.Triggers { for _, trigger := range tl.show.Triggers {
sourceID := b.findCell(trigger.Source.Block, trigger.Source.Signal) sourceID := tl.findCell(trigger.Source.Block, trigger.Source.Signal)
group := exclusiveGroup{members: []cellID{sourceID}} group := exclusiveGroup{members: []cellID{sourceID}}
hasCrossTrack := false hasCrossTrack := false
for _, target := range trigger.Targets { for _, target := range trigger.Targets {
targetID := b.findCell(target.Block, target.Hook) targetID := tl.findCell(target.Block, target.Hook)
if sourceID.track == targetID.track { if sourceID.track == targetID.track {
b.addConstraint("next_row", sourceID, targetID) tl.addConstraint("next_row", sourceID, targetID)
} else { } else {
b.addConstraint("same_row", sourceID, targetID) tl.addConstraint("same_row", sourceID, targetID)
hasCrossTrack = true hasCrossTrack = true
} }
group.members = append(group.members, targetID) group.members = append(group.members, targetID)
} }
if hasCrossTrack { if hasCrossTrack {
b.trackCells[sourceID.track][sourceID.index].IsSignal = true tl.Tracks[sourceID.track].Cells[sourceID.index].IsSignal = true
} }
b.exclusives = append(b.exclusives, group) tl.exclusives = append(tl.exclusives, group)
} }
} }
func (tl *Timeline) assignRows() {
func (b *timelineBuilder) assignRows() {
for { for {
found := false found := false
for _, c := range b.constraints { for _, c := range tl.constraints {
aRow := b.rowOf(c.a) aRow := tl.rowOf(c.a)
bRow := b.rowOf(c.b) bRow := tl.rowOf(c.b)
switch c.kind { switch c.kind {
case "same_row": case "same_row":
if aRow < bRow { if aRow < bRow {
b.insertGap(c.a.track, c.a.index) tl.insertGap(c.a.track, c.a.index)
found = true found = true
} else if bRow < aRow { } else if bRow < aRow {
b.insertGap(c.b.track, c.b.index) tl.insertGap(c.b.track, c.b.index)
found = true found = true
} }
case "next_row": case "next_row":
if bRow <= aRow { if bRow <= aRow {
b.insertGap(c.b.track, c.b.index) tl.insertGap(c.b.track, c.b.index)
found = true found = true
} }
} }
@@ -278,7 +251,7 @@ func (b *timelineBuilder) assignRows() {
} }
} }
if !found { if !found {
found = b.enforceExclusives() found = tl.enforceExclusives()
} }
if !found { if !found {
break break
@@ -286,50 +259,51 @@ func (b *timelineBuilder) assignRows() {
} }
} }
func (b *timelineBuilder) enforceExclusives() bool { func (tl *Timeline) enforceExclusives() bool {
for _, g := range b.exclusives { for _, g := range tl.exclusives {
if len(g.members) == 0 { if len(g.members) == 0 {
continue continue
} }
row := b.rowOf(g.members[0]) row := tl.rowOf(g.members[0])
allAligned := true allAligned := true
memberTracks := map[int]bool{} memberTracks := map[int]bool{}
for _, m := range g.members { for _, m := range g.members {
memberTracks[m.track] = true memberTracks[m.track] = true
if b.rowOf(m) != row { if tl.rowOf(m) != row {
allAligned = false allAligned = false
} }
} }
if !allAligned { if !allAligned {
continue continue
} }
for trackIdx := range b.trackCells { for trackIdx := range tl.Tracks {
if memberTracks[trackIdx] { if memberTracks[trackIdx] {
continue continue
} }
if row >= len(b.trackCells[trackIdx]) { if row >= len(tl.Tracks[trackIdx].Cells) {
continue continue
} }
c := b.trackCells[trackIdx][row] c := tl.Tracks[trackIdx].Cells[row]
if c.IsGap || c.BlockID == "" { if c.IsGap || c.BlockID == "" {
continue continue
} }
b.insertGap(trackIdx, row) tl.insertGap(trackIdx, row)
return true return true
} }
} }
return false return false
} }
func (b *timelineBuilder) rowOf(id cellID) int { func (tl *Timeline) rowOf(id cellID) int {
return id.index return id.index
} }
func (b *timelineBuilder) isAllGapRow(row, exceptTrack int) bool { func (tl *Timeline) isAllGapRow(row, exceptTrack int) bool {
for trackIdx, cells := range b.trackCells { for trackIdx := range tl.Tracks {
if trackIdx == exceptTrack { if trackIdx == exceptTrack {
continue continue
} }
cells := tl.Tracks[trackIdx].Cells
if row >= len(cells) { if row >= len(cells) {
continue continue
} }
@@ -340,11 +314,12 @@ func (b *timelineBuilder) isAllGapRow(row, exceptTrack int) bool {
return true return true
} }
func (b *timelineBuilder) removeGapAt(track, index int) { func (tl *Timeline) removeGapAt(track, index int) {
b.trackCells[track] = append(b.trackCells[track][:index], b.trackCells[track][index+1:]...) cells := tl.Tracks[track].Cells
tl.Tracks[track].Cells = append(cells[:index], cells[index+1:]...)
for i := range b.constraints { for i := range tl.constraints {
c := &b.constraints[i] c := &tl.constraints[i]
if c.a.track == track && c.a.index > index { if c.a.track == track && c.a.index > index {
c.a.index-- c.a.index--
} }
@@ -352,9 +327,9 @@ func (b *timelineBuilder) removeGapAt(track, index int) {
c.b.index-- c.b.index--
} }
} }
for i := range b.exclusives { for i := range tl.exclusives {
for j := range b.exclusives[i].members { for j := range tl.exclusives[i].members {
m := &b.exclusives[i].members[j] m := &tl.exclusives[i].members[j]
if m.track == track && m.index > index { if m.track == track && m.index > index {
m.index-- m.index--
} }
@@ -362,10 +337,10 @@ func (b *timelineBuilder) removeGapAt(track, index int) {
} }
} }
func (b *timelineBuilder) insertGap(track, beforeIndex int) { func (tl *Timeline) insertGap(track, beforeIndex int) {
for { for {
blocked := false blocked := false
for _, c := range b.constraints { for _, c := range tl.constraints {
if c.kind == "next_row" && c.a.track == track && c.b.track == track && c.a.index == beforeIndex-1 && c.b.index == beforeIndex { if c.kind == "next_row" && c.a.track == track && c.b.track == track && c.a.index == beforeIndex-1 && c.b.index == beforeIndex {
beforeIndex = c.a.index beforeIndex = c.a.index
blocked = true blocked = true
@@ -377,16 +352,16 @@ func (b *timelineBuilder) insertGap(track, beforeIndex int) {
} }
} }
if b.isAllGapRow(beforeIndex, track) { if tl.isAllGapRow(beforeIndex, track) {
for trackIdx := range b.trackCells { for trackIdx := range tl.Tracks {
if trackIdx == track { if trackIdx == track {
continue continue
} }
if beforeIndex >= len(b.trackCells[trackIdx]) { if beforeIndex >= len(tl.Tracks[trackIdx].Cells) {
continue continue
} }
if b.trackCells[trackIdx][beforeIndex].IsGap { if tl.Tracks[trackIdx].Cells[beforeIndex].IsGap {
b.removeGapAt(trackIdx, beforeIndex) tl.removeGapAt(trackIdx, beforeIndex)
} }
} }
return return
@@ -394,7 +369,7 @@ func (b *timelineBuilder) insertGap(track, beforeIndex int) {
gap := TimelineCell{IsGap: true} gap := TimelineCell{IsGap: true}
for i := beforeIndex - 1; i >= 0; i-- { for i := beforeIndex - 1; i >= 0; i-- {
c := b.trackCells[track][i] c := tl.Tracks[track].Cells[i]
if c.IsGap { if c.IsGap {
continue continue
} }
@@ -404,15 +379,15 @@ func (b *timelineBuilder) insertGap(track, beforeIndex int) {
break break
} }
cells := b.trackCells[track] cells := tl.Tracks[track].Cells
newCells := make([]TimelineCell, 0, len(cells)+1) newCells := make([]TimelineCell, 0, len(cells)+1)
newCells = append(newCells, cells[:beforeIndex]...) newCells = append(newCells, cells[:beforeIndex]...)
newCells = append(newCells, gap) newCells = append(newCells, gap)
newCells = append(newCells, cells[beforeIndex:]...) newCells = append(newCells, cells[beforeIndex:]...)
b.trackCells[track] = newCells tl.Tracks[track].Cells = newCells
for i := range b.constraints { for i := range tl.constraints {
c := &b.constraints[i] c := &tl.constraints[i]
if c.a.track == track && c.a.index >= beforeIndex { if c.a.track == track && c.a.index >= beforeIndex {
c.a.index++ c.a.index++
} }
@@ -420,14 +395,12 @@ func (b *timelineBuilder) insertGap(track, beforeIndex int) {
c.b.index++ c.b.index++
} }
} }
for i := range b.exclusives { for i := range tl.exclusives {
for j := range b.exclusives[i].members { for j := range tl.exclusives[i].members {
m := &b.exclusives[i].members[j] m := &tl.exclusives[i].members[j]
if m.track == track && m.index >= beforeIndex { if m.track == track && m.index >= beforeIndex {
m.index++ m.index++
} }
} }
} }
} }