Investigate timeline solver cycle and add trigger conflict validation
- Add trigger conflict validation in show.go to detect overlapping or mutually exclusive row constraints. - Implement Trigger.String() for improved error reporting. - Add detailed logging to timeline.go to trace iterative solver non-convergence and empty row optimization cycles. - Extend mock show generator with experimental cross-track triggers.
This commit is contained in:
@@ -80,11 +80,22 @@ func GenerateMockShow(numTracks, numScenes, avgCuesPerScene, avgBlocksPerCue int
|
||||
|
||||
chainFromByTrack := make(map[int]*Block)
|
||||
needsEnd := make(map[string]*Block)
|
||||
triggerIdx := make(map[TriggerSource]*Trigger)
|
||||
allowedTracks := make(map[int]bool, numTracks)
|
||||
for i := range numTracks {
|
||||
allowedTracks[i] = true
|
||||
}
|
||||
|
||||
addTrigger := func(source TriggerSource, target TriggerTarget) {
|
||||
if t := triggerIdx[source]; t != nil {
|
||||
t.Targets = append(t.Targets, target)
|
||||
return
|
||||
}
|
||||
t := &Trigger{Source: source, Targets: []TriggerTarget{target}}
|
||||
show.Triggers = append(show.Triggers, t)
|
||||
triggerIdx[source] = t
|
||||
}
|
||||
|
||||
for scene := 1; scene <= numScenes; scene++ {
|
||||
cuesInScene := 1 + rng.IntN(avgCuesPerScene*2)
|
||||
|
||||
@@ -126,11 +137,27 @@ func GenerateMockShow(numTracks, numScenes, avgCuesPerScene, avgBlocksPerCue int
|
||||
block := randBlock(trackIdx)
|
||||
show.Blocks = append(show.Blocks, block)
|
||||
if prev := chainFromByTrack[trackIdx]; prev != nil {
|
||||
show.Triggers = append(show.Triggers, &Trigger{
|
||||
Source: TriggerSource{Block: prev.ID, Signal: "END"},
|
||||
Targets: []TriggerTarget{{Block: block.ID, Hook: "START"}},
|
||||
})
|
||||
addTrigger(
|
||||
TriggerSource{Block: prev.ID, Signal: "END"},
|
||||
TriggerTarget{Block: block.ID, Hook: "START"},
|
||||
)
|
||||
delete(needsEnd, prev.ID)
|
||||
} else if rng.Float64() < 0.3 {
|
||||
var crossSrc *Block
|
||||
for ti, blk := range chainFromByTrack {
|
||||
if ti != trackIdx && blk != nil && needsEnd[blk.ID] == nil {
|
||||
crossSrc = blk
|
||||
break
|
||||
}
|
||||
}
|
||||
if crossSrc != nil {
|
||||
addTrigger(
|
||||
TriggerSource{Block: crossSrc.ID, Signal: "END"},
|
||||
TriggerTarget{Block: block.ID, Hook: "START"},
|
||||
)
|
||||
} else {
|
||||
cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"})
|
||||
}
|
||||
} else {
|
||||
cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"})
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ type Trigger struct {
|
||||
Targets []TriggerTarget `json:"targets"`
|
||||
}
|
||||
|
||||
func (t *Trigger) String() string {
|
||||
s := fmt.Sprintf("%s/%s ->", t.Source.Block, t.Source.Signal)
|
||||
for _, target := range t.Targets {
|
||||
s += fmt.Sprintf(" %s/%s", target.Block, target.Hook)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type TriggerSource struct {
|
||||
Block string `json:"block"`
|
||||
Signal string `json:"signal"`
|
||||
@@ -92,11 +100,33 @@ func (show *Show) Validate() error {
|
||||
hookTargeted := map[blockEvent]bool{}
|
||||
startTargeted := map[string]bool{}
|
||||
sourceUsed := map[blockEvent]bool{}
|
||||
signalTargetedBy := map[blockEvent]*Trigger{}
|
||||
|
||||
for _, trigger := range show.Triggers {
|
||||
for _, target := range trigger.Targets {
|
||||
signalTargetedBy[blockEvent{target.Block, target.Hook}] = trigger
|
||||
}
|
||||
}
|
||||
|
||||
for _, trigger := range show.Triggers {
|
||||
sourceBlock := blocksByID[trigger.Source.Block]
|
||||
if sourceBlock == nil {
|
||||
return fmt.Errorf("trigger source block %q not found", trigger.Source.Block)
|
||||
}
|
||||
|
||||
targetedTracks := map[string]string{}
|
||||
for _, target := range trigger.Targets {
|
||||
targetBlock := blocksByID[target.Block]
|
||||
if prev, ok := targetedTracks[targetBlock.Track]; ok {
|
||||
return fmt.Errorf("trigger conflict: %s targets multiple blocks on track %q (%q and %q)",
|
||||
trigger, targetBlock.Track, prev, target.Block)
|
||||
}
|
||||
targetedTracks[targetBlock.Track] = target.Block
|
||||
}
|
||||
|
||||
if t, ok := signalTargetedBy[blockEvent{trigger.Source.Block, trigger.Source.Signal}]; ok {
|
||||
return fmt.Errorf("trigger conflict: %s vs %s", t, trigger)
|
||||
}
|
||||
if !isValidEventForBlock(sourceBlock, trigger.Source.Signal) {
|
||||
return fmt.Errorf("trigger source signal %q is invalid for block %q", trigger.Source.Signal, trigger.Source.Block)
|
||||
}
|
||||
|
||||
@@ -257,13 +257,16 @@ func (tl *Timeline) buildConstraints() {
|
||||
}
|
||||
|
||||
func (tl *Timeline) assignRows() error {
|
||||
for range 1000000 {
|
||||
for i := range 1000000 {
|
||||
if tl.enforceConstraints() {
|
||||
continue
|
||||
}
|
||||
if tl.enforceExclusives() {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
fmt.Printf("assignRows: converged in %d iterations\n", i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, c := range tl.constraints {
|
||||
@@ -284,6 +287,7 @@ func (tl *Timeline) enforceConstraints() bool {
|
||||
if c.satisfied() {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("enforceConstraints: unsatisfied %s\n", c)
|
||||
switch c.kind {
|
||||
case constraintSameRow:
|
||||
if c.a.row < c.b.row {
|
||||
@@ -304,6 +308,7 @@ func (tl *Timeline) enforceExclusives() bool {
|
||||
if g.satisfied(tl.Tracks) {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("enforceExclusives: unsatisfied %s\n", g)
|
||||
row := g.members[0].row
|
||||
memberTracks := map[*TimelineTrack]bool{}
|
||||
for _, m := range g.members {
|
||||
@@ -316,6 +321,7 @@ func (tl *Timeline) enforceExclusives() bool {
|
||||
if !t.cellTypeAt(row, CellEvent, CellTitle, CellSignal) {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("enforceExclusives: inserting gap in track %s at row %d\n", t.ID, row)
|
||||
tl.insertGap(t, row)
|
||||
return true
|
||||
}
|
||||
@@ -344,6 +350,7 @@ func (tl *Timeline) isAllRemovableGapRow(row int, except *TimelineTrack) bool {
|
||||
}
|
||||
|
||||
func (tl *Timeline) removeGapAt(track *TimelineTrack, index int) {
|
||||
fmt.Printf("removeGapAt: track %s row %d\n", track.ID, index)
|
||||
track.Cells = append(track.Cells[:index], track.Cells[index+1:]...)
|
||||
tl.reindexRowsFrom(track, index)
|
||||
}
|
||||
@@ -355,8 +362,9 @@ func (tl *Timeline) reindexRowsFrom(track *TimelineTrack, start int) {
|
||||
}
|
||||
|
||||
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
||||
|
||||
fmt.Printf("insertGap: track %s before %d\n", track.ID, beforeIndex)
|
||||
if tl.isAllRemovableGapRow(beforeIndex, track) {
|
||||
fmt.Printf("insertGap: found removable gap row at %d\n", beforeIndex)
|
||||
for _, t := range tl.Tracks {
|
||||
if t == track {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user