Rewrite timeline builder to constraint-based layout with exclusive trigger rows
This commit is contained in:
@@ -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
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user