allowedTracks gating, constraint/exclusive helpers, duplicate trigger source validation, re-enable untimed block validation
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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++
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
|||||||
Reference in New Issue
Block a user