Compare commits
1 Commits
main
...
wip-iterat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1d8cefd22 |
33
WIP.md
Normal file
33
WIP.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# WIP: Iterative buildCells rewrite
|
||||
|
||||
## What's done
|
||||
- Block has `weight uint64`, `triggers []*Trigger`, `topRow int` fields
|
||||
- TriggerSource and TriggerTarget have `block *Block` pointer fields
|
||||
- `linkTriggers()` resolves string IDs to block pointers, populates `block.triggers`
|
||||
- `computeWeights()` sets weight = `(cue index) << 32` + 0 if cue-ended, 1 otherwise
|
||||
- Higher weight = more likely to be moved
|
||||
- Later cues = higher weight
|
||||
- `buildCells()` is rewritten to be iterative:
|
||||
1. Init: cues get sequential topRow, non-cues start at 0
|
||||
2. Loop (trigger pass): move target blocks so trigger source row >= target hook row
|
||||
3. Loop (overlap pass): sort same-track blocks by (topRow, weight), push overlaps to prev end + 1
|
||||
4. After convergence: move each cue to max row of its triggered cells
|
||||
5. `emitCells()` creates actual TimelineCell objects from computed topRows
|
||||
- Test uses `BuildTimelineDebug` with testWriter for debug output
|
||||
|
||||
## What's broken
|
||||
- Cue track comes out unordered after the cue-move step (cues placed at their highest triggered cell row, which scrambles scene order)
|
||||
- Gap padding in emitCells skips cue track (`if block.Type != "cue"`) so cue cell rows don't match topRow
|
||||
- The +1 gap between blocks may be wrong (was +2, changed to +1)
|
||||
- assignRows still can't converge after buildCells — off-by-one misalignments between cue rows and target rows
|
||||
|
||||
## Key design decisions
|
||||
- topRow coordinate space should include gap cells (emitCells just places cells at topRow)
|
||||
- blockHeight: cue=0 (own track, can overlap), other=4
|
||||
- Gap between same-track blocks = 1 row (chain or gap cell)
|
||||
- Cross-track alignment still handled by assignRows solver after buildCells
|
||||
|
||||
## Next steps
|
||||
- Fix cue track: need gap padding on cue track too so cell rows match topRow
|
||||
- May need to rethink cue-move step — moving cues to highest triggered row scrambles cue order
|
||||
- Verify topRow math matches actual emitted cell rows end-to-end
|
||||
@@ -20,8 +20,9 @@ type Block struct {
|
||||
Name string `json:"name"`
|
||||
Loop bool `json:"loop,omitempty"`
|
||||
|
||||
weight int
|
||||
weight uint64
|
||||
triggers []*Trigger
|
||||
topRow int
|
||||
}
|
||||
|
||||
type Trigger struct {
|
||||
|
||||
@@ -216,44 +216,38 @@ func (tl *Timeline) linkTriggers() {
|
||||
}
|
||||
|
||||
func (tl *Timeline) computeWeights() {
|
||||
for _, block := range tl.show.Blocks {
|
||||
block.weight = 0
|
||||
}
|
||||
|
||||
cueEndedBlocks := map[string]bool{}
|
||||
for _, trigger := range tl.show.Triggers {
|
||||
if trigger.Source.block.Type != "cue" {
|
||||
continue
|
||||
}
|
||||
for _, target := range trigger.Targets {
|
||||
if target.Hook == "END" || target.Hook == "FADE_OUT" {
|
||||
if target.block.weight < 1 {
|
||||
target.block.weight = 1
|
||||
}
|
||||
cueEndedBlocks[target.Block] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cueIdx := uint64(0)
|
||||
for _, block := range tl.show.Blocks {
|
||||
if block.Type == "cue" {
|
||||
tl.computeWeightDFS(block)
|
||||
tl.setWeightDFS(block, cueIdx<<32, cueEndedBlocks)
|
||||
cueIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tl *Timeline) computeWeightDFS(b *Block) int {
|
||||
maxChild := 0
|
||||
func (tl *Timeline) setWeightDFS(b *Block, base uint64, cueEndedBlocks map[string]bool) {
|
||||
if cueEndedBlocks[b.ID] {
|
||||
b.weight = base
|
||||
} else {
|
||||
b.weight = base + 1
|
||||
}
|
||||
for _, trigger := range b.triggers {
|
||||
for _, target := range trigger.Targets {
|
||||
w := tl.computeWeightDFS(target.block)
|
||||
if w > maxChild {
|
||||
maxChild = w
|
||||
}
|
||||
tl.setWeightDFS(target.block, base, cueEndedBlocks)
|
||||
}
|
||||
}
|
||||
if maxChild > b.weight {
|
||||
b.weight = maxChild
|
||||
}
|
||||
return b.weight
|
||||
}
|
||||
|
||||
func (tl *Timeline) findEndChains() map[string]bool {
|
||||
@@ -307,34 +301,163 @@ func (tl *Timeline) findCell(blockID, event string) *TimelineCell {
|
||||
panic("cell not found: " + blockID + " " + event)
|
||||
}
|
||||
|
||||
func blockHeight(b *Block) int {
|
||||
if b.Type == "cue" {
|
||||
return 0
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
func eventOffset(event string) int {
|
||||
switch event {
|
||||
case "GO":
|
||||
return 0
|
||||
case "START":
|
||||
return 0
|
||||
case "FADE_OUT":
|
||||
return 2
|
||||
case "END":
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (tl *Timeline) buildCells() {
|
||||
endChains := tl.findEndChains()
|
||||
lastOnTrack := map[string]*Block{}
|
||||
cueIdx := 0
|
||||
for _, block := range tl.show.Blocks {
|
||||
lastOnTrack[block.Track] = block
|
||||
if block.Type == "cue" {
|
||||
block.topRow = cueIdx
|
||||
cueIdx++
|
||||
} else {
|
||||
block.topRow = 0
|
||||
}
|
||||
}
|
||||
|
||||
for range 1000 {
|
||||
changed := false
|
||||
|
||||
for _, trigger := range tl.show.Triggers {
|
||||
srcRow := trigger.Source.block.topRow + eventOffset(trigger.Source.Signal)
|
||||
for _, target := range trigger.Targets {
|
||||
targetRow := srcRow - eventOffset(target.Hook)
|
||||
if target.block.topRow < targetRow {
|
||||
target.block.topRow = targetRow
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, track := range tl.Tracks {
|
||||
trackBlocks := tl.blocksByTrack(track.ID)
|
||||
slices.SortFunc(trackBlocks, func(a, b *Block) int {
|
||||
if a.topRow != b.topRow {
|
||||
return a.topRow - b.topRow
|
||||
}
|
||||
if a.weight != b.weight {
|
||||
if a.weight < b.weight {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
for i := 1; i < len(trackBlocks); i++ {
|
||||
prev := trackBlocks[i-1]
|
||||
cur := trackBlocks[i]
|
||||
minRow := prev.topRow + blockHeight(prev) + 1
|
||||
if cur.topRow < minRow {
|
||||
cur.topRow = minRow
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, block := range tl.show.Blocks {
|
||||
track := tl.trackIdx[block.Track]
|
||||
var cells []*TimelineCell
|
||||
switch block.Type {
|
||||
case "cue":
|
||||
cells = getCueCells(block)
|
||||
default:
|
||||
cells = getBlockCells(block)
|
||||
if block.Type != "cue" {
|
||||
continue
|
||||
}
|
||||
track.appendCells(cells...)
|
||||
for _, c := range cells {
|
||||
if c.Event == "" {
|
||||
continue
|
||||
maxRow := block.topRow
|
||||
for _, trigger := range block.triggers {
|
||||
for _, target := range trigger.Targets {
|
||||
r := target.block.topRow + eventOffset(target.Hook)
|
||||
if r > maxRow {
|
||||
maxRow = r
|
||||
}
|
||||
}
|
||||
tl.cellIdx[cellKey{blockID: c.BlockID, event: c.Event}] = c
|
||||
}
|
||||
if block.Type != "cue" && lastOnTrack[block.Track] != block {
|
||||
if endChains[block.ID] {
|
||||
track.appendCells(&TimelineCell{Type: CellChain})
|
||||
} else {
|
||||
track.appendCells(&TimelineCell{Type: CellGap})
|
||||
block.topRow = maxRow
|
||||
}
|
||||
|
||||
tl.emitCells()
|
||||
tl.debugState()
|
||||
}
|
||||
|
||||
func (tl *Timeline) blocksByTrack(trackID string) []*Block {
|
||||
var blocks []*Block
|
||||
for _, block := range tl.show.Blocks {
|
||||
if block.Track == trackID {
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func (tl *Timeline) emitCells() {
|
||||
endChains := tl.findEndChains()
|
||||
|
||||
type trackEntry struct {
|
||||
block *Block
|
||||
}
|
||||
trackBlocks := map[string][]*Block{}
|
||||
for _, block := range tl.show.Blocks {
|
||||
trackBlocks[block.Track] = append(trackBlocks[block.Track], block)
|
||||
}
|
||||
|
||||
for trackID, blocks := range trackBlocks {
|
||||
track := tl.trackIdx[trackID]
|
||||
slices.SortFunc(blocks, func(a, b *Block) int {
|
||||
return a.topRow - b.topRow
|
||||
})
|
||||
|
||||
row := 0
|
||||
for i, block := range blocks {
|
||||
for row < block.topRow {
|
||||
if block.Type != "cue" {
|
||||
track.appendCells(&TimelineCell{Type: CellGap})
|
||||
}
|
||||
row++
|
||||
}
|
||||
|
||||
var cells []*TimelineCell
|
||||
switch block.Type {
|
||||
case "cue":
|
||||
cells = getCueCells(block)
|
||||
default:
|
||||
cells = getBlockCells(block)
|
||||
}
|
||||
track.appendCells(cells...)
|
||||
for _, c := range cells {
|
||||
if c.Event == "" {
|
||||
continue
|
||||
}
|
||||
tl.cellIdx[cellKey{blockID: c.BlockID, event: c.Event}] = c
|
||||
}
|
||||
row += len(cells)
|
||||
|
||||
isLast := i == len(blocks)-1
|
||||
if !isLast && block.Type != "cue" {
|
||||
if endChains[block.ID] {
|
||||
track.appendCells(&TimelineCell{Type: CellChain})
|
||||
} else {
|
||||
track.appendCells(&TimelineCell{Type: CellGap})
|
||||
}
|
||||
row++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestBuildTimelineFromMockShow(t *testing.T) {
|
||||
t.Logf("Validate: %v", time.Since(t1))
|
||||
|
||||
t2 := time.Now()
|
||||
tl, err := BuildTimeline(show)
|
||||
tl, err := BuildTimelineDebug(show, &testWriter{t})
|
||||
t.Logf("BuildTimeline: %v", time.Since(t2))
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTimeline failed: %v", err)
|
||||
@@ -26,6 +26,13 @@ func TestBuildTimelineFromMockShow(t *testing.T) {
|
||||
t.Logf("tracks=%d blocks=%d", len(tl.Tracks), len(tl.Blocks))
|
||||
}
|
||||
|
||||
type testWriter struct{ t *testing.T }
|
||||
|
||||
func (w *testWriter) Write(p []byte) (int, error) {
|
||||
w.t.Log(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func BenchmarkBuildTimeline(b *testing.B) {
|
||||
show := GenerateMockShow(42, 5, 20, 4, 5)
|
||||
if err := show.Validate(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user