Refactor show model/validation into show.go and simplify timeline internals
This commit is contained in:
@@ -102,18 +102,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMockShow() (*Show, error) {
|
|
||||||
buf, err := staticFS.ReadFile("static/show.json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var show Show
|
|
||||||
if err := json.Unmarshal(buf, &show); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &show, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, v any) {
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
|
|||||||
127
cmd/qrunproxy/show.go
Normal file
127
cmd/qrunproxy/show.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Show struct {
|
||||||
|
Tracks []*Track `json:"tracks"`
|
||||||
|
Blocks []*Block `json:"blocks"`
|
||||||
|
Triggers []*Trigger `json:"triggers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Track string `json:"track,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Loop bool `json:"loop,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trigger struct {
|
||||||
|
Source TriggerSource `json:"source"`
|
||||||
|
Targets []TriggerTarget `json:"targets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerSource struct {
|
||||||
|
Block string `json:"block"`
|
||||||
|
Signal string `json:"signal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerTarget struct {
|
||||||
|
Block string `json:"block"`
|
||||||
|
Hook string `json:"hook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMockShow() (*Show, error) {
|
||||||
|
buf, err := staticFS.ReadFile("static/show.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var show Show
|
||||||
|
if err := json.Unmarshal(buf, &show); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &show, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidEventForBlock(block *Block, event string) bool {
|
||||||
|
if block.Type == "cue" {
|
||||||
|
return event == "GO"
|
||||||
|
}
|
||||||
|
switch event {
|
||||||
|
case "START", "FADE_OUT", "END":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (show *Show) validate() error {
|
||||||
|
if show == nil {
|
||||||
|
return fmt.Errorf("show is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
trackIDs := map[string]bool{}
|
||||||
|
for _, track := range show.Tracks {
|
||||||
|
if trackIDs[track.ID] {
|
||||||
|
return fmt.Errorf("duplicate track id %q", track.ID)
|
||||||
|
}
|
||||||
|
trackIDs[track.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
blocksByID := map[string]*Block{}
|
||||||
|
for _, block := range show.Blocks {
|
||||||
|
if blocksByID[block.ID] != nil {
|
||||||
|
return fmt.Errorf("duplicate block id %q", block.ID)
|
||||||
|
}
|
||||||
|
blocksByID[block.ID] = block
|
||||||
|
if block.Type == "cue" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !trackIDs[block.Track] {
|
||||||
|
return fmt.Errorf("block %q uses unknown track %q", block.ID, block.Track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTargeted := map[string]bool{}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if !isValidEventForBlock(sourceBlock, trigger.Source.Signal) {
|
||||||
|
return fmt.Errorf("trigger source signal %q is invalid for block %q", trigger.Source.Signal, trigger.Source.Block)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range trigger.Targets {
|
||||||
|
targetBlock := blocksByID[target.Block]
|
||||||
|
if targetBlock == nil {
|
||||||
|
return fmt.Errorf("trigger target block %q not found", target.Block)
|
||||||
|
}
|
||||||
|
if !isValidEventForBlock(targetBlock, target.Hook) {
|
||||||
|
return fmt.Errorf("trigger target hook %q is invalid for block %q", target.Hook, target.Block)
|
||||||
|
}
|
||||||
|
if target.Hook == "START" {
|
||||||
|
startTargeted[target.Block] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range show.Blocks {
|
||||||
|
if block.Type == "cue" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !startTargeted[block.ID] {
|
||||||
|
return fmt.Errorf("block %q has no trigger for its START", block.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,43 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
const cueTrackID = "_cue"
|
const cueTrackID = "_cue"
|
||||||
|
|
||||||
type Show struct {
|
|
||||||
Tracks []*Track `json:"tracks"`
|
|
||||||
Blocks []*Block `json:"blocks"`
|
|
||||||
Triggers []*Trigger `json:"triggers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Track struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Block struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Track string `json:"track,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Loop bool `json:"loop,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Trigger struct {
|
|
||||||
Source TriggerSource `json:"source"`
|
|
||||||
Targets []TriggerTarget `json:"targets"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSource struct {
|
|
||||||
Block string `json:"block"`
|
|
||||||
Signal string `json:"signal"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerTarget struct {
|
|
||||||
Block string `json:"block"`
|
|
||||||
Hook string `json:"hook"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimelineTrack struct {
|
type TimelineTrack struct {
|
||||||
*Track
|
*Track
|
||||||
Cells []*TimelineCell `json:"cells"`
|
Cells []*TimelineCell `json:"cells"`
|
||||||
@@ -49,6 +13,7 @@ type Timeline struct {
|
|||||||
|
|
||||||
show *Show `json:"-"`
|
show *Show `json:"-"`
|
||||||
trackIdx map[string]*TimelineTrack `json:"-"`
|
trackIdx map[string]*TimelineTrack `json:"-"`
|
||||||
|
cellIdx map[cellKey]*TimelineCell `json:"-"`
|
||||||
constraints []constraint `json:"-"`
|
constraints []constraint `json:"-"`
|
||||||
exclusives []exclusiveGroup `json:"-"`
|
exclusives []exclusiveGroup `json:"-"`
|
||||||
}
|
}
|
||||||
@@ -76,30 +41,13 @@ type exclusiveGroup struct {
|
|||||||
members []*TimelineCell
|
members []*TimelineCell
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateShow(show *Show) error {
|
type cellKey struct {
|
||||||
startTargeted := map[string]bool{}
|
blockID string
|
||||||
for _, trigger := range show.Triggers {
|
event string
|
||||||
for _, target := range trigger.Targets {
|
|
||||||
if target.Hook == "START" {
|
|
||||||
startTargeted[target.Block] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, block := range show.Blocks {
|
|
||||||
if block.Type == "cue" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !startTargeted[block.ID] {
|
|
||||||
return fmt.Errorf("block %q has no trigger for its START", block.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildTimeline(show *Show) (Timeline, error) {
|
func BuildTimeline(show *Show) (Timeline, error) {
|
||||||
if err := validateShow(show); err != nil {
|
if err := show.validate(); err != nil {
|
||||||
return Timeline{}, err
|
return Timeline{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +55,7 @@ func BuildTimeline(show *Show) (Timeline, error) {
|
|||||||
show: show,
|
show: show,
|
||||||
Blocks: map[string]*Block{},
|
Blocks: map[string]*Block{},
|
||||||
trackIdx: map[string]*TimelineTrack{},
|
trackIdx: map[string]*TimelineTrack{},
|
||||||
|
cellIdx: map[cellKey]*TimelineCell{},
|
||||||
}
|
}
|
||||||
|
|
||||||
cueTrack := &TimelineTrack{Track: &Track{ID: cueTrackID, Name: "Cue"}}
|
cueTrack := &TimelineTrack{Track: &Track{ID: cueTrackID, Name: "Cue"}}
|
||||||
@@ -175,12 +124,9 @@ func getBlockCells(block *Block) []*TimelineCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) findCell(blockID, event string) *TimelineCell {
|
func (tl *Timeline) findCell(blockID, event string) *TimelineCell {
|
||||||
track := tl.trackIdx[tl.Blocks[blockID].Track]
|
if c := tl.cellIdx[cellKey{blockID: blockID, event: event}]; c != nil {
|
||||||
for _, c := range track.Cells {
|
|
||||||
if !c.IsGap && c.BlockID == blockID && c.Event == event {
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
|
||||||
panic("cell not found: " + blockID + " " + event)
|
panic("cell not found: " + blockID + " " + event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +146,12 @@ func (tl *Timeline) buildCells(endChains map[string]bool) {
|
|||||||
cells = getBlockCells(block)
|
cells = getBlockCells(block)
|
||||||
}
|
}
|
||||||
track.appendCells(cells...)
|
track.appendCells(cells...)
|
||||||
|
for _, c := range cells {
|
||||||
|
if c.Event == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tl.cellIdx[cellKey{blockID: c.BlockID, event: c.Event}] = c
|
||||||
|
}
|
||||||
if block.Type != "cue" && !endChains[block.ID] && lastOnTrack[block.Track] != block {
|
if block.Type != "cue" && !endChains[block.ID] && lastOnTrack[block.Track] != block {
|
||||||
track.appendCells(&TimelineCell{IsGap: true, IsBreak: true})
|
track.appendCells(&TimelineCell{IsGap: true, IsBreak: true})
|
||||||
}
|
}
|
||||||
@@ -291,7 +243,24 @@ func (tl *Timeline) enforceExclusives() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tl *Timeline) isAllGapRow(row int, except *TimelineTrack) bool {
|
func (tl *Timeline) shiftBreakDownOne(track *TimelineTrack, row int) {
|
||||||
|
below := track.Cells[row+1]
|
||||||
|
track.Cells[row].IsBreak = false
|
||||||
|
below.IsBreak = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) canShiftBreakDownOne(track *TimelineTrack, row int) bool {
|
||||||
|
if row+1 >= len(track.Cells) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
below := track.Cells[row+1]
|
||||||
|
if !below.IsGap || below.IsBreak {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) isAllRemovableGapRow(row int, except *TimelineTrack) bool {
|
||||||
for _, t := range tl.Tracks {
|
for _, t := range tl.Tracks {
|
||||||
if t == except {
|
if t == except {
|
||||||
continue
|
continue
|
||||||
@@ -299,7 +268,11 @@ func (tl *Timeline) isAllGapRow(row int, except *TimelineTrack) bool {
|
|||||||
if row >= len(t.Cells) {
|
if row >= len(t.Cells) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !t.Cells[row].IsGap {
|
c := t.Cells[row]
|
||||||
|
if !c.IsGap {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.IsBreak && !tl.canShiftBreakDownOne(t, row) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,7 +281,11 @@ func (tl *Timeline) isAllGapRow(row int, except *TimelineTrack) bool {
|
|||||||
|
|
||||||
func (tl *Timeline) removeGapAt(track *TimelineTrack, index int) {
|
func (tl *Timeline) removeGapAt(track *TimelineTrack, index int) {
|
||||||
track.Cells = append(track.Cells[:index], track.Cells[index+1:]...)
|
track.Cells = append(track.Cells[:index], track.Cells[index+1:]...)
|
||||||
for i := index; i < len(track.Cells); i++ {
|
tl.reindexRowsFrom(track, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *Timeline) reindexRowsFrom(track *TimelineTrack, start int) {
|
||||||
|
for i := start; i < len(track.Cells); i++ {
|
||||||
track.Cells[i].row = i
|
track.Cells[i].row = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +309,7 @@ func (tl *Timeline) gapInsertionPoint(track *TimelineTrack, index int) int {
|
|||||||
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
||||||
beforeIndex = tl.gapInsertionPoint(track, beforeIndex)
|
beforeIndex = tl.gapInsertionPoint(track, beforeIndex)
|
||||||
|
|
||||||
if tl.isAllGapRow(beforeIndex, track) {
|
if tl.isAllRemovableGapRow(beforeIndex, track) {
|
||||||
for _, t := range tl.Tracks {
|
for _, t := range tl.Tracks {
|
||||||
if t == track {
|
if t == track {
|
||||||
continue
|
continue
|
||||||
@@ -340,7 +317,12 @@ func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
|||||||
if beforeIndex >= len(t.Cells) {
|
if beforeIndex >= len(t.Cells) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if t.Cells[beforeIndex].IsGap {
|
c := t.Cells[beforeIndex]
|
||||||
|
if c.IsBreak {
|
||||||
|
tl.shiftBreakDownOne(t, beforeIndex)
|
||||||
|
c = t.Cells[beforeIndex]
|
||||||
|
}
|
||||||
|
if c.IsGap && !c.IsBreak {
|
||||||
tl.removeGapAt(t, beforeIndex)
|
tl.removeGapAt(t, beforeIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,12 +341,6 @@ func (tl *Timeline) insertGap(track *TimelineTrack, beforeIndex int) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
newCells := make([]*TimelineCell, 0, len(track.Cells)+1)
|
track.Cells = append(track.Cells[:beforeIndex], append([]*TimelineCell{gap}, track.Cells[beforeIndex:]...)...)
|
||||||
newCells = append(newCells, track.Cells[:beforeIndex]...)
|
tl.reindexRowsFrom(track, beforeIndex+1)
|
||||||
newCells = append(newCells, gap)
|
|
||||||
newCells = append(newCells, track.Cells[beforeIndex:]...)
|
|
||||||
track.Cells = newCells
|
|
||||||
for i := beforeIndex + 1; i < len(track.Cells); i++ {
|
|
||||||
track.Cells[i].row = i
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user