Compare commits
1 Commits
wip-iterat
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc3bbf894f |
33
WIP.md
33
WIP.md
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -98,7 +98,7 @@ func main() {
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.Point{}, draw.Src)
|
||||
charW := font.MeasureString(stripFont, "0").Ceil()
|
||||
pad := 8
|
||||
for e := 0; e < count; e++ {
|
||||
for e := range count {
|
||||
ci := p.Start + e
|
||||
ch := channels[ci]
|
||||
cc := ch.Color
|
||||
@@ -107,10 +107,7 @@ func main() {
|
||||
valStr := fmt.Sprintf("%03d %02x", ch.Value, ch.Value)
|
||||
nameW := font.MeasureString(stripFont, ch.Name).Ceil()
|
||||
valW := font.MeasureString(stripFont, valStr).Ceil()
|
||||
textW := nameW
|
||||
if valW > textW {
|
||||
textW = valW
|
||||
}
|
||||
textW := max(valW, nameW)
|
||||
x0 := mid - textW/2 - pad
|
||||
x1 := mid + textW/2 + pad
|
||||
|
||||
@@ -155,8 +152,8 @@ func main() {
|
||||
if i == page {
|
||||
img := image.NewRGBA(image.Rect(0, 0, sz, sz))
|
||||
draw.Draw(img, img.Bounds(), txt, image.Point{}, draw.Src)
|
||||
for y := 0; y < sz; y++ {
|
||||
for x := 0; x < sz; x++ {
|
||||
for y := range sz {
|
||||
for x := range sz {
|
||||
if x < b || x >= sz-b || y < b || y >= sz-b {
|
||||
img.Set(x, y, color.RGBA{255, 255, 255, 255})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Show struct {
|
||||
@@ -20,9 +22,8 @@ type Block struct {
|
||||
Name string `json:"name"`
|
||||
Loop bool `json:"loop,omitempty"`
|
||||
|
||||
weight uint64
|
||||
weight int
|
||||
triggers []*Trigger
|
||||
topRow int
|
||||
}
|
||||
|
||||
type Trigger struct {
|
||||
@@ -31,11 +32,12 @@ type Trigger struct {
|
||||
}
|
||||
|
||||
func (t *Trigger) String() string {
|
||||
s := fmt.Sprintf("%s/%s ->", t.Source.Block, t.Source.Signal)
|
||||
var s strings.Builder
|
||||
s.WriteString(fmt.Sprintf("%s/%s ->", t.Source.Block, t.Source.Signal))
|
||||
for _, target := range t.Targets {
|
||||
s += fmt.Sprintf(" %s/%s", target.Block, target.Hook)
|
||||
s.WriteString(fmt.Sprintf(" %s/%s", target.Block, target.Hook))
|
||||
}
|
||||
return s
|
||||
return s.String()
|
||||
}
|
||||
|
||||
type TriggerSource struct {
|
||||
|
||||
@@ -49,12 +49,7 @@ func (t *TimelineTrack) cellTypeAt(index int, types ...CellType) bool {
|
||||
if index < 0 || index >= len(t.Cells) {
|
||||
return false
|
||||
}
|
||||
for _, typ := range types {
|
||||
if t.Cells[index].Type == typ {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(types, t.Cells[index].Type)
|
||||
}
|
||||
|
||||
func (c *TimelineCell) String() string {
|
||||
@@ -98,14 +93,15 @@ func (g exclusiveGroup) satisfied(tracks []*TimelineTrack) bool {
|
||||
}
|
||||
|
||||
func (g exclusiveGroup) String() string {
|
||||
s := "exclusive("
|
||||
var s strings.Builder
|
||||
s.WriteString("exclusive(")
|
||||
for i, m := range g.members {
|
||||
if i > 0 {
|
||||
s += ", "
|
||||
s.WriteString(", ")
|
||||
}
|
||||
s += m.String()
|
||||
s.WriteString(m.String())
|
||||
}
|
||||
return s + ")"
|
||||
return s.String() + ")"
|
||||
}
|
||||
|
||||
func (tl *Timeline) debugf(format string, args ...any) {
|
||||
@@ -216,38 +212,44 @@ func (tl *Timeline) linkTriggers() {
|
||||
}
|
||||
|
||||
func (tl *Timeline) computeWeights() {
|
||||
cueEndedBlocks := map[string]bool{}
|
||||
for _, block := range tl.show.Blocks {
|
||||
block.weight = 0
|
||||
}
|
||||
|
||||
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" {
|
||||
cueEndedBlocks[target.Block] = true
|
||||
if target.block.weight < 1 {
|
||||
target.block.weight = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cueIdx := uint64(0)
|
||||
for _, block := range tl.show.Blocks {
|
||||
if block.Type == "cue" {
|
||||
tl.setWeightDFS(block, cueIdx<<32, cueEndedBlocks)
|
||||
cueIdx++
|
||||
tl.computeWeightDFS(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tl *Timeline) setWeightDFS(b *Block, base uint64, cueEndedBlocks map[string]bool) {
|
||||
if cueEndedBlocks[b.ID] {
|
||||
b.weight = base
|
||||
} else {
|
||||
b.weight = base + 1
|
||||
}
|
||||
func (tl *Timeline) computeWeightDFS(b *Block) int {
|
||||
maxChild := 0
|
||||
for _, trigger := range b.triggers {
|
||||
for _, target := range trigger.Targets {
|
||||
tl.setWeightDFS(target.block, base, cueEndedBlocks)
|
||||
w := tl.computeWeightDFS(target.block)
|
||||
if w > maxChild {
|
||||
maxChild = w
|
||||
}
|
||||
}
|
||||
}
|
||||
if maxChild > b.weight {
|
||||
b.weight = maxChild
|
||||
}
|
||||
return b.weight
|
||||
}
|
||||
|
||||
func (tl *Timeline) findEndChains() map[string]bool {
|
||||
@@ -301,163 +303,34 @@ 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() {
|
||||
cueIdx := 0
|
||||
for _, block := range tl.show.Blocks {
|
||||
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 {
|
||||
if block.Type != "cue" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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{}
|
||||
lastOnTrack := map[string]*Block{}
|
||||
for _, block := range tl.show.Blocks {
|
||||
trackBlocks[block.Track] = append(trackBlocks[block.Track], block)
|
||||
lastOnTrack[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++
|
||||
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)
|
||||
}
|
||||
track.appendCells(cells...)
|
||||
for _, c := range cells {
|
||||
if c.Event == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
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++
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestBuildTimelineFromMockShow(t *testing.T) {
|
||||
t.Logf("Validate: %v", time.Since(t1))
|
||||
|
||||
t2 := time.Now()
|
||||
tl, err := BuildTimelineDebug(show, &testWriter{t})
|
||||
tl, err := BuildTimeline(show)
|
||||
t.Logf("BuildTimeline: %v", time.Since(t2))
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTimeline failed: %v", err)
|
||||
@@ -26,13 +26,6 @@ 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 {
|
||||
|
||||
@@ -69,7 +69,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for i := uint8(0); i < 8; i++ {
|
||||
for i := range uint8(8) {
|
||||
updateLCD(out, i)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ var ModelXL = Model{
|
||||
}
|
||||
|
||||
var ModelPlus = Model{
|
||||
Name: "Plus",
|
||||
Keys: 8,
|
||||
KeyRows: 2,
|
||||
KeyCols: 4,
|
||||
KeySize: 120,
|
||||
Encoders: 4,
|
||||
LCDWidth: 800,
|
||||
Name: "Plus",
|
||||
Keys: 8,
|
||||
KeyRows: 2,
|
||||
KeyCols: 4,
|
||||
KeySize: 120,
|
||||
Encoders: 4,
|
||||
LCDWidth: 800,
|
||||
LCDHeight: 100,
|
||||
}
|
||||
|
||||
@@ -101,10 +101,10 @@ func OpenModel(m *Model) (*Device, error) {
|
||||
return &Device{dev: dev, model: m}, nil
|
||||
}
|
||||
|
||||
func (d *Device) Model() *Model { return d.model }
|
||||
func (d *Device) Close() error { return d.dev.Close() }
|
||||
func (d *Device) Model() *Model { return d.model }
|
||||
func (d *Device) Close() error { return d.dev.Close() }
|
||||
func (d *Device) SerialNumber() string { return d.dev.SerialNumber() }
|
||||
func (d *Device) Product() string { return d.dev.Product() }
|
||||
func (d *Device) Product() string { return d.dev.Product() }
|
||||
|
||||
func (d *Device) FirmwareVersion() (string, error) {
|
||||
buf, err := d.dev.GetFeatureReport(5)
|
||||
@@ -150,8 +150,8 @@ func (d *Device) SetKeyImage(key int, img image.Image) error {
|
||||
var src image.Image = scaled
|
||||
if d.model.FlipKeys {
|
||||
flipped := image.NewRGBA(scaled.Bounds())
|
||||
for y := 0; y < sz; y++ {
|
||||
for x := 0; x < sz; x++ {
|
||||
for y := range sz {
|
||||
for x := range sz {
|
||||
flipped.Set(sz-1-x, sz-1-y, scaled.At(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,11 +108,11 @@ type TextSpan struct {
|
||||
func DrawOutlinedSpans(img *image.RGBA, face font.Face, defaultOutline color.Color, thickness int, lines ...[]TextSpan) {
|
||||
var full []string
|
||||
for _, line := range lines {
|
||||
s := ""
|
||||
var s strings.Builder
|
||||
for _, span := range line {
|
||||
s += span.Text
|
||||
s.WriteString(span.Text)
|
||||
}
|
||||
full = append(full, s)
|
||||
full = append(full, s.String())
|
||||
}
|
||||
|
||||
metrics := face.Metrics()
|
||||
|
||||
Reference in New Issue
Block a user