diff --git a/draw.go b/draw.go index c401d69..c148360 100644 --- a/draw.go +++ b/draw.go @@ -271,23 +271,54 @@ type drawWalk struct { } // drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and -// the table row it entered, if any. +// the table rows it entered. type drawAt struct { group string route drawRoute - alt rowAlt + alt rowSet } -// rowAlt is one row of a table as an alternative: only one row renders, so reads in two rows of -// one table never meet, while reads in one row, across its columns and whatever they reach, do. -type rowAlt struct { - t *table - row int +// rowSet is the rows a walk entered, one per table: only one row of a table renders, so reads in +// two rows of one table never meet, while reads in one row, across its columns and whatever they +// reach, do. +type rowSet []tablePin + +func (s rowSet) rowOf(t *table) (int, bool) { + for _, p := range s { + if p.t == t { + return p.row, true + } + } + return 0, false +} + +// enter is s with row r of t, where t is not in it yet; the set is copied, since walks branch. +func (s rowSet) enter(t *table, r int) rowSet { + if _, in := s.rowOf(t); in { + return s + } + out := append(append(make(rowSet, 0, len(s)+1), s...), tablePin{t, r}) + sort.Slice(out, func(i, j int) bool { return out[i].t.category < out[j].t.category }) + return out +} + +// key spells the set for a map, by the tables' identities. +func (s rowSet) key() string { + var b strings.Builder + for _, p := range s { + fmt.Fprintf(&b, "%p[%d]", p.t, p.row) + } + return b.String() } // alternatives reports whether two reads sit in different rows of one table. func alternatives(a, b drawAt) bool { - return a.alt.t != nil && a.alt.t == b.alt.t && a.alt.row != b.alt.row + for _, p := range a.alt { + if r, in := b.alt.rowOf(p.t); in && r != p.row { + return true + } + } + return false } // drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label. @@ -303,13 +334,13 @@ type pathRead struct { type drawVisit struct { n node group string - alt rowAlt + alt string } type drawKey struct { group string path string - alt rowAlt + alt string } func newDrawWalk() *drawWalk { @@ -319,7 +350,7 @@ func newDrawWalk() *drawWalk { // walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops // there. func (w *drawWalk) walk(n node, at drawAt) { - v := drawVisit{n, at.group, at.alt} + v := drawVisit{n, at.group, at.alt.key()} if w.seen[v] { return } @@ -330,22 +361,35 @@ func (w *drawWalk) walk(n node, at drawAt) { if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { at.group = t.drawGroupKey } - _, isColumn := n.(*column) for _, e := range renderEdges(n) { - // The outermost row is kept: a cell reached through another row's cell renders with it. - edgeAt := at - if cell, _ := e.to.(*template); isColumn && at.alt.t == nil { - edgeAt.alt = rowAlt{cell.cellOf, cell.cellRow} + if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil { + // A row already entered renders only its own cell of this column. + if r, in := at.alt.rowOf(cell.cellOf); in && r != cell.cellRow { + continue + } + w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow)}) + continue } - w.edge(n, e, edgeAt) + w.edge(n, e, at) } } func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { + var tr *tableRead if a, reads := refRead(from, e.label); reads { - if k := (drawKey{at.group, a.path, at.alt}); !w.read[k] { + tr = tableReadOf(from.(*template).fields[a.key], a, e.to) + if k := (drawKey{at.group, a.path, at.alt.key()}); !w.read[k] { w.read[k] = true - w.reads = append(w.reads, pathRead{at, a, tableReadOf(from.(*template).fields[a.key], a, e.to)}) + w.reads = append(w.reads, pathRead{at, a, tr}) + } + } + // A read that pins the column's table renders that row's cell alone. + if c, isColumn := e.to.(*column); isColumn && tr != nil { + if r, pinned := tr.pins.pinned(c.t); pinned { + if cell := c.t.cellNode(r, c.i); cell != nil { + w.walk(cell, drawAt{at.group, at.route, at.alt.enter(c.t, r)}) + } + return } } w.walk(e.to, at) diff --git a/family.go b/family.go index 5af98f4..74d47c1 100644 --- a/family.go +++ b/family.go @@ -240,57 +240,35 @@ func checkFamilies(reads []pathRead) error { } } } - return replayAlternatives(reads) + return replayPairs(reads) } -// replayAlternatives replays the reads of each draw group into one draws per row -// alternative — the reads outside any row, then the row's own — so two rows of one -// table are never replayed together. -func replayAlternatives(reads []pathRead) error { - type alt struct { - group string - row rowAlt - } - var alts []alt - outside, inside := map[string][]pathRead{}, map[alt][]pathRead{} - for _, r := range reads { +// replayPairs replays every two reads of one draw group that can render together into +// one draws, the earlier read first. A pin conflicts with one earlier pin, never with a +// combination, so pairs find every conflict a full replay would. +func replayPairs(reads []pathRead) error { + for i, r := range reads { if r.tr == nil { continue } - if r.at.alt.t == nil { - outside[r.at.group] = append(outside[r.at.group], r) - continue - } - a := alt{r.at.group, r.at.alt} - if inside[a] == nil { - alts = append(alts, a) - } - inside[a] = append(inside[a], r) - } - for group := range outside { - alts = append(alts, alt{group, rowAlt{}}) - } - sort.Slice(alts, func(i, j int) bool { - return altOrder(alts[i].group, alts[i].row) < altOrder(alts[j].group, alts[j].row) - }) - for _, a := range alts { - var d draws - for _, r := range append(append([]pathRead(nil), outside[a.group]...), inside[a]...) { + for _, o := range reads[i+1:] { + if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) { + continue + } + var d draws if err := r.tr.replay(&d); err != nil { - return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err) + return conflict(r, err) + } + if err := o.tr.replay(&d); err != nil { + return conflict(o, err) } } } return nil } -// altOrder sorts alternatives by group, the reads outside any row first, then by table and row, -// so which conflict is reported does not vary. -func altOrder(group string, r rowAlt) string { - if r.t == nil { - return group + "\x00" - } - return fmt.Sprintf("%s\x01%s\x00%08d", group, r.t.category, r.row) +func conflict(r pathRead, err error) error { + return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err) } // replay pins the read's rows into d, where they agree with the rows pinned before.