diff --git a/cmd/qrunproxy/main.go b/cmd/qrunproxy/main.go index 0d5a67c..c49e181 100644 --- a/cmd/qrunproxy/main.go +++ b/cmd/qrunproxy/main.go @@ -28,7 +28,7 @@ func main() { runAndExit = strings.Fields(*runAndExitStr) } - show := GenerateMockShow(7, 100, 1000) + show := GenerateMockShow(5, 100, 1000) if err := show.Validate(); err != nil { fmt.Fprintf(os.Stderr, "Error validating show: %v\n", err) os.Exit(1) diff --git a/cmd/qrunproxy/mockshow.go b/cmd/qrunproxy/mockshow.go index 0fc371f..4c6f7ad 100644 --- a/cmd/qrunproxy/mockshow.go +++ b/cmd/qrunproxy/mockshow.go @@ -1,70 +1,144 @@ package main -import "fmt" +import ( + "fmt" + "math/rand/v2" +) + +var trackNamePool = []string{ + "Lighting", "Fill Light", "Spots", "Video", "Video OVL", + "Audio", "SFX", "Ambience", "Pyro", "Fog", "Motors", + "Follow Spot", "Haze", "Projector", "LED Wall", +} + +var lightNamePool = []string{ + "Wash", "Focus", "Spot", "Amber", "Blue", "Cool", "Warm", + "Flood", "Strobe", "Blackout", "Dim", "Bright", "Sunrise", +} + +var mediaNamePool = []string{ + "Loop", "Projection", "Background", "Overlay", "Flash", + "Ambience", "Underscore", "Sting", "Bumper", "Transition", +} + +var delayNamePool = []string{ + "1s Delay", "2s Delay", "3s Delay", "5s Delay", "Hold", +} func GenerateMockShow(numTracks, numCues, numBlocks int) *Show { - show := &Show{} + rng := rand.New(rand.NewPCG(42, 0)) + show := &Show{} + blockIdx := 0 + nextBlockID := func() string { + id := fmt.Sprintf("b%d", blockIdx) + blockIdx++ + return id + } + + names := make([]string, len(trackNamePool)) + copy(names, trackNamePool) + rng.Shuffle(len(names), func(i, j int) { + names[i], names[j] = names[j], names[i] + }) for i := range numTracks { + name := names[i%len(names)] + if i >= len(names) { + name = fmt.Sprintf("%s %d", name, i/len(names)+1) + } show.Tracks = append(show.Tracks, &Track{ ID: fmt.Sprintf("track_%d", i), - Name: fmt.Sprintf("Track %d", i), + Name: name, }) } - for i := range numCues { - show.Blocks = append(show.Blocks, &Block{ - ID: fmt.Sprintf("cue_%d", i), + randBlock := func(trackIdx int) *Block { + r := rng.Float64() + var typ, name string + var loop bool + switch { + case r < 0.30: + typ, name = "light", lightNamePool[rng.IntN(len(lightNamePool))] + case r < 0.55: + typ, name = "media", mediaNamePool[rng.IntN(len(mediaNamePool))] + case r < 0.70: + typ, name, loop = "media", mediaNamePool[rng.IntN(len(mediaNamePool))], true + case r < 0.80: + typ, name = "delay", delayNamePool[rng.IntN(len(delayNamePool))] + default: + typ, name = "light", lightNamePool[rng.IntN(len(lightNamePool))] + } + return &Block{ + ID: nextBlockID(), + Type: typ, + Track: fmt.Sprintf("track_%d", trackIdx), + Name: name, + Loop: loop, + } + } + + placed := 0 + cueIdx := 0 + + for placed < numBlocks && cueIdx < numCues { + cue := &Block{ + ID: fmt.Sprintf("q%d", cueIdx*10), Type: "cue", - Name: fmt.Sprintf("Cue %d", i), - }) - } - - blocksByTrack := make([][]*Block, numTracks) - for i := range numBlocks { - trackIdx := i % numTracks - trackID := fmt.Sprintf("track_%d", trackIdx) - block := &Block{ - ID: fmt.Sprintf("block_%d_%d", trackIdx, len(blocksByTrack[trackIdx])), - Type: "media", - Track: trackID, - Name: fmt.Sprintf("Block %d-%d", trackIdx, len(blocksByTrack[trackIdx])), + Name: fmt.Sprintf("Q%d", cueIdx*10), } - show.Blocks = append(show.Blocks, block) - blocksByTrack[trackIdx] = append(blocksByTrack[trackIdx], block) - } + show.Blocks = append(show.Blocks, cue) + cueIdx++ - for trackIdx := range numTracks { - blocks := blocksByTrack[trackIdx] - for i := 1; i < len(blocks); i++ { - show.Triggers = append(show.Triggers, &Trigger{ - Source: TriggerSource{Block: blocks[i-1].ID, Signal: "END"}, - Targets: []TriggerTarget{{Block: blocks[i].ID, Hook: "START"}}, - }) - } - } + tracksThisCue := numTracks - rng.IntN(2) + perm := rng.Perm(numTracks) - headPerTrack := make([]int, numTracks) - for i := range numCues { - cue := show.Blocks[i] - targets := []TriggerTarget{} - for trackIdx := range numTracks { - if headPerTrack[trackIdx] >= len(blocksByTrack[trackIdx]) { - continue + cueTargets := []TriggerTarget{} + for _, trackIdx := range perm[:tracksThisCue] { + if placed >= numBlocks { + break + } + block := randBlock(trackIdx) + show.Blocks = append(show.Blocks, block) + cueTargets = append(cueTargets, TriggerTarget{Block: block.ID, Hook: "START"}) + placed++ + + prev := block + chainLen := rng.IntN(3) + for range chainLen { + if placed >= numBlocks { + break + } + if !prev.hasDefinedTiming() { + break + } + next := randBlock(trackIdx) + show.Blocks = append(show.Blocks, next) + show.Triggers = append(show.Triggers, &Trigger{ + Source: TriggerSource{Block: prev.ID, Signal: "END"}, + Targets: []TriggerTarget{{Block: next.ID, Hook: "START"}}, + }) + prev = next + placed++ } - block := blocksByTrack[trackIdx][headPerTrack[trackIdx]] - targets = append(targets, TriggerTarget{Block: block.ID, Hook: "START"}) - depth := len(blocksByTrack[trackIdx]) - headPerTrack[trackIdx] - advance := max(depth/(numCues-i), 1) - headPerTrack[trackIdx] += advance } - if len(targets) > 0 { + + if len(cueTargets) > 0 { show.Triggers = append(show.Triggers, &Trigger{ Source: TriggerSource{Block: cue.ID, Signal: "GO"}, - Targets: targets, + Targets: cueTargets, }) } } + for cueIdx < numCues { + cue := &Block{ + ID: fmt.Sprintf("q%d", cueIdx*10), + Type: "cue", + Name: fmt.Sprintf("Q%d", cueIdx*10), + } + show.Blocks = append(show.Blocks, cue) + cueIdx++ + } + return show } diff --git a/cmd/qrunproxy/show.go b/cmd/qrunproxy/show.go index fff814f..9d5ddd1 100644 --- a/cmd/qrunproxy/show.go +++ b/cmd/qrunproxy/show.go @@ -36,6 +36,16 @@ type TriggerTarget struct { Hook string `json:"hook"` } +func (block *Block) hasDefinedTiming() bool { + if block.Type == "cue" || block.Type == "delay" { + return true + } + if block.Type == "media" && !block.Loop { + return true + } + return false +} + func isValidEventForBlock(block *Block, event string) bool { if block.Type == "cue" { return event == "GO" @@ -75,6 +85,11 @@ func (show *Show) Validate() error { } } + type blockEvent struct { + block string + event string + } + hookTargeted := map[blockEvent]bool{} startTargeted := map[string]bool{} for _, trigger := range show.Triggers { sourceBlock := blocksByID[trigger.Source.Block] @@ -93,6 +108,7 @@ func (show *Show) Validate() error { if !isValidEventForBlock(targetBlock, target.Hook) { return fmt.Errorf("trigger target hook %q is invalid for block %q", target.Hook, target.Block) } + hookTargeted[blockEvent{target.Block, target.Hook}] = true if target.Hook == "START" { startTargeted[target.Block] = true } @@ -108,5 +124,28 @@ func (show *Show) Validate() error { } } + for _, trigger := range show.Triggers { + sourceBlock := blocksByID[trigger.Source.Block] + for _, target := range trigger.Targets { + targetBlock := blocksByID[target.Block] + if sourceBlock.Type != "cue" && targetBlock.Type != "cue" && sourceBlock.Track == targetBlock.Track && target.Hook == "START" && trigger.Source.Signal != "END" { + return fmt.Errorf("same-track START trigger from %q to %q must use END signal, not %s", sourceBlock.ID, targetBlock.ID, trigger.Source.Signal) + } + } + if sourceBlock.hasDefinedTiming() { + continue + } + signal := trigger.Source.Signal + if signal != "FADE_OUT" && signal != "END" { + continue + } + if signal == "END" && hookTargeted[blockEvent{sourceBlock.ID, "FADE_OUT"}] { + continue + } + if !hookTargeted[blockEvent{sourceBlock.ID, signal}] { + return fmt.Errorf("block %q has no defined timing and nothing triggers its %s, so its %s signal will never fire", sourceBlock.ID, signal, signal) + } + } + return nil } diff --git a/cmd/qrunproxy/static/index.html b/cmd/qrunproxy/static/index.html index 63f134f..baaf093 100644 --- a/cmd/qrunproxy/static/index.html +++ b/cmd/qrunproxy/static/index.html @@ -186,7 +186,8 @@ function render(data) { div.className = 'cell' + rowCls; if (c.is_title) { const block = data.blocks[c.block_id] || {}; - div.innerHTML = `