Rewrite timeline builder to constraint-based layout with exclusive trigger rows

This commit is contained in:
Ian Gulliver
2026-02-20 07:57:41 -07:00
parent 7044b87b79
commit 0893e5b8cb
4 changed files with 294 additions and 791 deletions

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"embed" "embed"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io/fs" "io/fs"
"net" "net"
@@ -17,15 +18,13 @@ import (
var staticFS embed.FS var staticFS embed.FS
func main() { func main() {
addr := ":8080" addr := flag.String("addr", ":8080", "listen address")
var runAndExit []string runAndExitStr := flag.String("run-and-exit", "", "command to run after server starts, then exit")
flag.Parse()
for _, arg := range os.Args[1:] { var runAndExit []string
if v, ok := strings.CutPrefix(arg, "--run-and-exit="); ok { if *runAndExitStr != "" {
runAndExit = strings.Fields(v) runAndExit = strings.Fields(*runAndExitStr)
} else {
addr = arg
}
} }
show, err := loadMockShow() show, err := loadMockShow()
@@ -56,14 +55,18 @@ func main() {
}) })
if len(runAndExit) > 0 { if len(runAndExit) > 0 {
ln, err := net.Listen("tcp", addr) ln, err := net.Listen("tcp", *addr)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
port := fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
srv := &http.Server{Handler: mux} srv := &http.Server{Handler: mux}
go srv.Serve(ln) go srv.Serve(ln)
for i, arg := range runAndExit {
runAndExit[i] = strings.ReplaceAll(arg, "{port}", port)
}
cmd := exec.Command(runAndExit[0], runAndExit[1:]...) cmd := exec.Command(runAndExit[0], runAndExit[1:]...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -76,8 +79,13 @@ func main() {
return return
} }
fmt.Printf("Listening on %s\n", addr) ln, err := net.Listen("tcp", *addr)
if err := http.ListenAndServe(addr, mux); err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Listening on %s\n", ln.Addr())
if err := http.Serve(ln, mux); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,96 +0,0 @@
package main
import (
"encoding/json"
"os"
"testing"
)
func TestBuildTimelineFromFixture(t *testing.T) {
raw, err := os.ReadFile("static/show.json")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
var show Show
if err := json.Unmarshal(raw, &show); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
timeline, err := BuildTimeline(show)
if err != nil {
t.Fatalf("build timeline: %v", err)
}
if len(timeline.Tracks) != len(show.Tracks)+1 {
t.Fatalf("track count mismatch: got %d want %d", len(timeline.Tracks), len(show.Tracks)+1)
}
if len(timeline.Rows) == 0 {
t.Fatalf("expected timeline rows")
}
foundCueSignal := false
for _, row := range timeline.Rows {
for _, cell := range row.Cells {
if cell.Event != "GO" || cell.BlockID == "" {
continue
}
block, ok := timeline.Blocks[cell.BlockID]
if ok && block.Type == "cue" {
foundCueSignal = true
break
}
}
if foundCueSignal {
break
}
}
if !foundCueSignal {
t.Fatalf("expected at least one cue cell represented as block_id + GO event")
}
}
func TestBuildTimelineMergesSameBlockEndCell(t *testing.T) {
show := Show{
Tracks: []Track{
{ID: "track1", Name: "Track 1"},
},
Blocks: []Block{
{ID: "cue1", Type: "cue", Name: "Q1"},
{ID: "a", Type: "light", Track: "track1", Name: "A"},
},
Triggers: []Trigger{
{
Source: TriggerSource{Block: "cue1", Signal: "GO"},
Targets: []TriggerTarget{
{Block: "a", Hook: "START"},
},
},
{
Source: TriggerSource{Block: "a", Signal: "END"},
Targets: []TriggerTarget{
{Block: "a", Hook: "END"},
},
},
},
}
timeline, err := BuildTimeline(show)
if err != nil {
t.Fatalf("build timeline: %v", err)
}
found := false
for _, row := range timeline.Rows {
for _, cell := range row.Cells {
if cell.BlockID == "a" && cell.Event == "END" {
if !cell.IsSignal || !cell.IsEnd {
t.Fatalf("expected END cell to include signal+end markers, got signal=%v is_end=%v", cell.IsSignal, cell.IsEnd)
}
found = true
}
}
}
if !found {
t.Fatalf("did not find END cell for block a")
}
}

View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
exec go run ./cmd/qrunproxy/ "--run-and-exit=shot-scraper http://localhost:8080/ -o ${2:-/tmp/timeline.png} --width 1000 --height ${1:-1200}" exec go run ./cmd/qrunproxy/ -addr :0 --run-and-exit="shot-scraper http://localhost:{port}/ -o ${2:-/tmp/timeline.png} --width 1000 --height ${1:-1200}"