allowedTracks gating, constraint/exclusive helpers, duplicate trigger source validation, re-enable untimed block validation

This commit is contained in:
Ian Gulliver
2026-02-20 22:31:04 -07:00
parent 99f79e82f3
commit 7d3a23dfc1
4 changed files with 135 additions and 49 deletions

View File

@@ -28,7 +28,7 @@ func main() {
runAndExit = strings.Fields(*runAndExitStr) runAndExit = strings.Fields(*runAndExitStr)
} }
show := GenerateMockShow(5, 100, 1000) show := GenerateMockShow(5, 10, 30)
if err := show.Validate(); err != nil { if err := show.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "Error validating show: %v\n", err) fmt.Fprintf(os.Stderr, "Error validating show: %v\n", err)
os.Exit(1) os.Exit(1)

View File

@@ -30,8 +30,9 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
show := &Show{} show := &Show{}
blockIdx := 0 blockIdx := 0
nextBlockID := func() string { curCueName := ""
id := fmt.Sprintf("b%d", blockIdx) nextBlockID := func(trackIdx int) string {
id := fmt.Sprintf("%s-t%d-b%d", curCueName, trackIdx, blockIdx)
blockIdx++ blockIdx++
return id return id
} }
@@ -69,7 +70,7 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
typ, name = "light", lightNamePool[rng.IntN(len(lightNamePool))] typ, name = "light", lightNamePool[rng.IntN(len(lightNamePool))]
} }
return &Block{ return &Block{
ID: nextBlockID(), ID: nextBlockID(trackIdx),
Type: typ, Type: typ,
Track: fmt.Sprintf("track_%d", trackIdx), Track: fmt.Sprintf("track_%d", trackIdx),
Name: name, Name: name,
@@ -80,7 +81,12 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
placed := 0 placed := 0
cueIdx := 0 cueIdx := 0
scene := 0 scene := 0
lastOnTrack := make(map[int]*Block) chainFromByTrack := make(map[int]*Block)
needsEnd := make(map[string]*Block)
allowedTracks := make(map[int]bool, numTracks)
for i := range numTracks {
allowedTracks[i] = true
}
for placed < numBlocks && cueIdx < numCues { for placed < numBlocks && cueIdx < numCues {
scene++ scene++
@@ -90,38 +96,57 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
if placed >= numBlocks || cueIdx >= numCues { if placed >= numBlocks || cueIdx >= numCues {
break break
} }
clear(lastOnTrack) for trackIdx, blk := range chainFromByTrack {
if needsEnd[blk.ID] == nil {
delete(chainFromByTrack, trackIdx)
}
}
curCueName = fmt.Sprintf("S%d Q%d", scene, intra)
cue := &Block{ cue := &Block{
ID: fmt.Sprintf("q%d", cueIdx), ID: curCueName,
Type: "cue", Type: "cue",
Name: fmt.Sprintf("S%d Q%d", scene, intra), Name: curCueName,
} }
show.Blocks = append(show.Blocks, cue) show.Blocks = append(show.Blocks, cue)
cueIdx++ cueIdx++
blocksThisCue := 1 + rng.IntN(numTracks*2) blocksThisCue := 1 + rng.IntN(numTracks*2)
cueTargets := []TriggerTarget{} cueTargets := []TriggerTarget{}
for id, blk := range needsEnd {
cueTargets = append(cueTargets, TriggerTarget{Block: blk.ID, Hook: "END"})
delete(needsEnd, id)
for ti := range numTracks {
if chainFromByTrack[ti] == blk {
allowedTracks[ti] = true
}
}
}
for range blocksThisCue { for range blocksThisCue {
if placed >= numBlocks { if placed >= numBlocks {
break break
} }
trackIdx := rng.IntN(numTracks) trackIdx := rng.IntN(numTracks)
if prev := lastOnTrack[trackIdx]; prev != nil && !prev.hasDefinedTiming() { if !allowedTracks[trackIdx] {
continue continue
} }
block := randBlock(trackIdx) block := randBlock(trackIdx)
show.Blocks = append(show.Blocks, block) show.Blocks = append(show.Blocks, block)
placed++ placed++
if prev := lastOnTrack[trackIdx]; prev != nil { if prev := chainFromByTrack[trackIdx]; prev != nil {
show.Triggers = append(show.Triggers, &Trigger{ show.Triggers = append(show.Triggers, &Trigger{
Source: TriggerSource{Block: prev.ID, Signal: "END"}, Source: TriggerSource{Block: prev.ID, Signal: "END"},
Targets: []TriggerTarget{{Block: block.ID, Hook: "START"}}, Targets: []TriggerTarget{{Block: block.ID, Hook: "START"}},
}) })
delete(needsEnd, prev.ID)
} else { } else {
cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"}) cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"})
} }
lastOnTrack[trackIdx] = block if !block.hasDefinedTiming() {
needsEnd[block.ID] = block
allowedTracks[trackIdx] = false
}
chainFromByTrack[trackIdx] = block
} }
if len(cueTargets) > 0 { if len(cueTargets) > 0 {
@@ -133,17 +158,21 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
} }
endTargets := []TriggerTarget{} endTargets := []TriggerTarget{}
for _, blk := range lastOnTrack { for id, blk := range needsEnd {
if blk.hasDefinedTiming() {
continue
}
endTargets = append(endTargets, TriggerTarget{Block: blk.ID, Hook: "END"}) endTargets = append(endTargets, TriggerTarget{Block: blk.ID, Hook: "END"})
delete(needsEnd, id)
for ti := range numTracks {
if chainFromByTrack[ti] == blk {
allowedTracks[ti] = true
}
}
} }
if len(endTargets) > 0 && cueIdx < numCues { if len(endTargets) > 0 {
endCueName := fmt.Sprintf("S%d End", scene)
endCue := &Block{ endCue := &Block{
ID: fmt.Sprintf("q%d", cueIdx), ID: endCueName,
Type: "cue", Type: "cue",
Name: fmt.Sprintf("S%d End", scene), Name: endCueName,
} }
show.Blocks = append(show.Blocks, endCue) show.Blocks = append(show.Blocks, endCue)
cueIdx++ cueIdx++
@@ -156,10 +185,11 @@ func GenerateMockShow(numTracks, numCues, numBlocks int) *Show {
for cueIdx < numCues { for cueIdx < numCues {
scene++ scene++
emptyCueName := fmt.Sprintf("S%d Q1", scene)
cue := &Block{ cue := &Block{
ID: fmt.Sprintf("q%d", cueIdx), ID: emptyCueName,
Type: "cue", Type: "cue",
Name: fmt.Sprintf("S%d Q1", scene), Name: emptyCueName,
} }
show.Blocks = append(show.Blocks, cue) show.Blocks = append(show.Blocks, cue)
cueIdx++ cueIdx++

View File

@@ -91,6 +91,7 @@ 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{}
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 {
@@ -99,6 +100,11 @@ func (show *Show) Validate() error {
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)
} }
src := blockEvent{trigger.Source.Block, trigger.Source.Signal}
if sourceUsed[src] {
return fmt.Errorf("duplicate trigger source: block %q signal %q", trigger.Source.Block, trigger.Source.Signal)
}
sourceUsed[src] = true
for _, target := range trigger.Targets { for _, target := range trigger.Targets {
targetBlock := blocksByID[target.Block] targetBlock := blocksByID[target.Block]
@@ -122,12 +128,9 @@ func (show *Show) Validate() error {
if !startTargeted[block.ID] { if !startTargeted[block.ID] {
return fmt.Errorf("block %q has no trigger for its START", block.ID) return fmt.Errorf("block %q has no trigger for its START", block.ID)
} }
/* if !block.hasDefinedTiming() && !hookTargeted[blockEvent{block.ID, "FADE_OUT"}] && !hookTargeted[blockEvent{block.ID, "END"}] {
TODO: Put this back when mock is fixed return fmt.Errorf("block %q has no defined timing and nothing triggers its FADE_OUT or END", block.ID)
if !block.hasDefinedTiming() && !hookTargeted[blockEvent{block.ID, "FADE_OUT"}] && !hookTargeted[blockEvent{block.ID, "END"}] { }
return fmt.Errorf("block %q has no defined timing and nothing triggers its FADE_OUT or END", block.ID)
}
*/
} }
for _, trigger := range show.Triggers { for _, trigger := range show.Triggers {

View File

@@ -33,16 +33,76 @@ type TimelineCell struct {
track *TimelineTrack `json:"-"` track *TimelineTrack `json:"-"`
} }
func (c *TimelineCell) String() string {
return fmt.Sprintf("%s/%s@%s:r%d", c.BlockID, c.Event, c.track.ID, c.row)
}
type constraint struct { type constraint struct {
kind string kind string
a *TimelineCell a *TimelineCell
b *TimelineCell b *TimelineCell
} }
func (c constraint) satisfied() bool {
switch c.kind {
case "same_row":
return c.a.row == c.b.row
case "next_row":
return c.b.row > c.a.row
}
return true
}
func (c constraint) String() string {
switch c.kind {
case "same_row":
return fmt.Sprintf("same_row(%s, %s)", c.a, c.b)
case "next_row":
return fmt.Sprintf("next_row(%s -> %s)", c.a, c.b)
}
return fmt.Sprintf("%s(%s, %s)", c.kind, c.a, c.b)
}
type exclusiveGroup struct { type exclusiveGroup struct {
members []*TimelineCell members []*TimelineCell
} }
func (g exclusiveGroup) satisfied(tracks []*TimelineTrack) bool {
row := g.members[0].row
memberTracks := map[*TimelineTrack]bool{}
for _, m := range g.members {
memberTracks[m.track] = true
if m.row != row {
return true
}
}
for _, t := range tracks {
if memberTracks[t] {
continue
}
if row >= len(t.Cells) {
continue
}
c := t.Cells[row]
if c.IsGap || c.BlockID == "" {
continue
}
return false
}
return true
}
func (g exclusiveGroup) String() string {
s := "exclusive("
for i, m := range g.members {
if i > 0 {
s += ", "
}
s += m.String()
}
return s + ")"
}
type cellKey struct { type cellKey struct {
blockID string blockID string
event string event string
@@ -189,15 +249,13 @@ func (tl *Timeline) assignRows() error {
return nil return nil
} }
for _, c := range tl.constraints { for _, c := range tl.constraints {
switch c.kind { if !c.satisfied() {
case "same_row": return fmt.Errorf("assignRows: unsatisfied %s", c)
if c.a.row != c.b.row { }
return fmt.Errorf("assignRows: unsatisfied %s constraint: %s/%s (row %d) vs %s/%s (row %d)", c.kind, c.a.BlockID, c.a.Event, c.a.row, c.b.BlockID, c.b.Event, c.b.row) }
} for _, g := range tl.exclusives {
case "next_row": if !g.satisfied(tl.Tracks) {
if c.b.row <= c.a.row { return fmt.Errorf("assignRows: unsatisfied %s", g)
return fmt.Errorf("assignRows: unsatisfied %s constraint: %s/%s (row %d) must follow %s/%s (row %d)", c.kind, c.b.BlockID, c.b.Event, c.b.row, c.a.BlockID, c.a.Event, c.a.row)
}
} }
} }
return fmt.Errorf("assignRows: did not converge") return fmt.Errorf("assignRows: did not converge")
@@ -205,38 +263,33 @@ func (tl *Timeline) assignRows() error {
func (tl *Timeline) enforceConstraints() bool { func (tl *Timeline) enforceConstraints() bool {
for _, c := range tl.constraints { for _, c := range tl.constraints {
if c.satisfied() {
continue
}
switch c.kind { switch c.kind {
case "same_row": case "same_row":
if c.a.row < c.b.row { if c.a.row < c.b.row {
tl.insertGap(c.a.track, c.a.row) tl.insertGap(c.a.track, c.a.row)
return true } else {
} else if c.b.row < c.a.row {
tl.insertGap(c.b.track, c.b.row) tl.insertGap(c.b.track, c.b.row)
return true
} }
case "next_row": case "next_row":
if c.b.row <= c.a.row { tl.insertGap(c.b.track, c.b.row)
tl.insertGap(c.b.track, c.b.row)
return true
}
} }
return true
} }
return false return false
} }
func (tl *Timeline) enforceExclusives() bool { func (tl *Timeline) enforceExclusives() bool {
for _, g := range tl.exclusives { for _, g := range tl.exclusives {
if g.satisfied(tl.Tracks) {
continue
}
row := g.members[0].row row := g.members[0].row
allAligned := true
memberTracks := map[*TimelineTrack]bool{} memberTracks := map[*TimelineTrack]bool{}
for _, m := range g.members { for _, m := range g.members {
memberTracks[m.track] = true memberTracks[m.track] = true
if m.row != row {
allAligned = false
}
}
if !allAligned {
continue
} }
for _, t := range tl.Tracks { for _, t := range tl.Tracks {
if memberTracks[t] { if memberTracks[t] {