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)
|
chainFromByTrack := make(map[int]*Block)
|
||||||
needsEnd := make(map[string]*Block)
|
needsEnd := make(map[string]*Block)
|
||||||
|
triggerIdx := make(map[TriggerSource]*Trigger)
|
||||||
allowedTracks := make(map[int]bool, numTracks)
|
allowedTracks := make(map[int]bool, numTracks)
|
||||||
for i := range numTracks {
|
for i := range numTracks {
|
||||||
allowedTracks[i] = true
|
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++ {
|
for scene := 1; scene <= numScenes; scene++ {
|
||||||
cuesInScene := 1 + rng.IntN(avgCuesPerScene*2)
|
cuesInScene := 1 + rng.IntN(avgCuesPerScene*2)
|
||||||
|
|
||||||
@@ -126,11 +137,27 @@ func GenerateMockShow(numTracks, numScenes, avgCuesPerScene, avgBlocksPerCue int
|
|||||||
block := randBlock(trackIdx)
|
block := randBlock(trackIdx)
|
||||||
show.Blocks = append(show.Blocks, block)
|
show.Blocks = append(show.Blocks, block)
|
||||||
if prev := chainFromByTrack[trackIdx]; prev != nil {
|
if prev := chainFromByTrack[trackIdx]; prev != nil {
|
||||||
show.Triggers = append(show.Triggers, &Trigger{
|
addTrigger(
|
||||||
Source: TriggerSource{Block: prev.ID, Signal: "END"},
|
TriggerSource{Block: prev.ID, Signal: "END"},
|
||||||
Targets: []TriggerTarget{{Block: block.ID, Hook: "START"}},
|
TriggerTarget{Block: block.ID, Hook: "START"},
|
||||||
})
|
)
|
||||||
delete(needsEnd, prev.ID)
|
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 {
|
} else {
|
||||||
cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"})
|
cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ type Trigger struct {
|
|||||||
Targets []TriggerTarget `json:"targets"`
|
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 {
|
type TriggerSource struct {
|
||||||
Block string `json:"block"`
|
Block string `json:"block"`
|
||||||
Signal string `json:"signal"`
|
Signal string `json:"signal"`
|
||||||
@@ -92,11 +100,33 @@ func (show *Show) Validate() error {
|
|||||||
hookTargeted := map[blockEvent]bool{}
|
hookTargeted := map[blockEvent]bool{}
|
||||||
startTargeted := map[string]bool{}
|
startTargeted := map[string]bool{}
|
||||||
sourceUsed := map[blockEvent]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 {
|
for _, trigger := range show.Triggers {
|
||||||
sourceBlock := blocksByID[trigger.Source.Block]
|
sourceBlock := blocksByID[trigger.Source.Block]
|
||||||
if sourceBlock == nil {
|
if sourceBlock == nil {
|
||||||
return fmt.Errorf("trigger source block %q not found", trigger.Source.Block)
|
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) {
|
if !isValidEventForBlock(sourceBlock, trigger.Source.Signal) {
|
||||||
return fmt.Errorf("trigger source signal %q is invalid for block %q", trigger.Source.Signal, trigger.Source.Block)
|
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 {
|
func (tl *Timeline) assignRows() error {
|
||||||
for range 1000000 {
|
for i := range 1000000 {
|
||||||
if tl.enforceConstraints() {
|
if tl.enforceConstraints() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tl.enforceExclusives() {
|
if tl.enforceExclusives() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf("assignRows: converged in %d iterations\n", i)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, c := range tl.constraints {
|
for _, c := range tl.constraints {
|
||||||
@@ -284,6 +287,7 @@ func (tl *Timeline) enforceConstraints() bool {
|
|||||||
if c.satisfied() {
|
if c.satisfied() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fmt.Printf("enforceConstraints: unsatisfied %s\n", c)
|
||||||
switch c.kind {
|
switch c.kind {
|
||||||
case constraintSameRow:
|
case constraintSameRow:
|
||||||
if c.a.row < c.b.row {
|
if c.a.row < c.b.row {
|
||||||
@@ -304,6 +308,7 @@ func (tl *Timeline) enforceExclusives() bool {
|
|||||||
if g.satisfied(tl.Tracks) {
|
if g.satisfied(tl.Tracks) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fmt.Printf("enforceExclusives: unsatisfied %s\n", g)
|
||||||
row := g.members[0].row
|
row := g.members[0].row
|
||||||
memberTracks := map[*TimelineTrack]bool{}
|
memberTracks := map[*TimelineTrack]bool{}
|
||||||
for _, m := range g.members {
|
for _, m := range g.members {
|
||||||
@@ -316,6 +321,7 @@ func (tl *Timeline) enforceExclusives() bool {
|
|||||||
if !t.cellTypeAt(row, CellEvent, CellTitle, CellSignal) {
|
if !t.cellTypeAt(row, CellEvent, CellTitle, CellSignal) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fmt.Printf("enforceExclusives: inserting gap in track %s at row %d\n", t.ID, row)
|
||||||
tl.insertGap(t, row)
|
tl.insertGap(t, row)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -344,6 +350,7 @@ func (tl *Timeline) isAllRemovableGapRow(row int, except *TimelineTrack) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) removeGapAt(track *TimelineTrack, index int) {
|
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:]...)
|
track.Cells = append(track.Cells[:index], track.Cells[index+1:]...)
|
||||||
tl.reindexRowsFrom(track, index)
|
tl.reindexRowsFrom(track, index)
|
||||||
}
|
}
|
||||||
@@ -355,8 +362,9 @@ func (tl *Timeline) reindexRowsFrom(track *TimelineTrack, start int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex 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) {
|
if tl.isAllRemovableGapRow(beforeIndex, track) {
|
||||||
|
fmt.Printf("insertGap: found removable gap row at %d\n", beforeIndex)
|
||||||
for _, t := range tl.Tracks {
|
for _, t := range tl.Tracks {
|
||||||
if t == track {
|
if t == track {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user