Add squarified treemap layout and spatial navigation
Edge-snapped integer layout that partitions its bounds exactly, level-of-detail selection, and a deterministic geometric neighbour search for arrow-key navigation. Fuzz-tested.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package treemap
|
||||
|
||||
// Direction is an arrow-key direction.
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
Left Direction = iota
|
||||
Right
|
||||
Up
|
||||
Down
|
||||
)
|
||||
|
||||
// Neighbour finds the block a user expects to reach by pressing an arrow
|
||||
// key from the block with ID sel:
|
||||
//
|
||||
// 1. Only blocks entirely beyond the selected block's edge in the
|
||||
// direction of travel are candidates.
|
||||
// 2. Blocks whose span overlaps the selected block's orthogonal span are
|
||||
// strongly preferred over all others.
|
||||
// 3. Then the smallest gap along the direction of travel wins.
|
||||
// 4. Then the largest overlap with the selected block's span.
|
||||
// 5. Then the block whose orthogonal span is closest to the selected
|
||||
// block's centre line (0 if it contains it).
|
||||
// 6. Ties break by position (top, then left) and finally ID.
|
||||
//
|
||||
// It returns false if there is no block in that direction.
|
||||
func Neighbour(blocks []Block, sel int64, d Direction) (int64, bool) {
|
||||
var s Block
|
||||
found := false
|
||||
for _, b := range blocks {
|
||||
if b.ID == sel {
|
||||
s, found = b, true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 0, false
|
||||
}
|
||||
type score struct {
|
||||
tier, gap, ovl, off, y, x int
|
||||
id int64
|
||||
}
|
||||
less := func(a, b score) bool {
|
||||
switch {
|
||||
case a.tier != b.tier:
|
||||
return a.tier < b.tier
|
||||
case a.gap != b.gap:
|
||||
return a.gap < b.gap
|
||||
case a.ovl != b.ovl:
|
||||
return a.ovl > b.ovl
|
||||
case a.off != b.off:
|
||||
return a.off < b.off
|
||||
case a.y != b.y:
|
||||
return a.y < b.y
|
||||
case a.x != b.x:
|
||||
return a.x < b.x
|
||||
}
|
||||
return a.id < b.id
|
||||
}
|
||||
var best score
|
||||
have := false
|
||||
for _, c := range blocks {
|
||||
if c.ID == sel {
|
||||
continue
|
||||
}
|
||||
var gap, lo, hi, clo, chi int
|
||||
var ok bool
|
||||
switch d {
|
||||
case Right:
|
||||
ok, gap = c.X >= s.X+s.W, c.X-(s.X+s.W)
|
||||
lo, hi, clo, chi = s.Y, s.Y+s.H, c.Y, c.Y+c.H
|
||||
case Left:
|
||||
ok, gap = c.X+c.W <= s.X, s.X-(c.X+c.W)
|
||||
lo, hi, clo, chi = s.Y, s.Y+s.H, c.Y, c.Y+c.H
|
||||
case Down:
|
||||
ok, gap = c.Y >= s.Y+s.H, c.Y-(s.Y+s.H)
|
||||
lo, hi, clo, chi = s.X, s.X+s.W, c.X, c.X+c.W
|
||||
case Up:
|
||||
ok, gap = c.Y+c.H <= s.Y, s.Y-(c.Y+c.H)
|
||||
lo, hi, clo, chi = s.X, s.X+s.W, c.X, c.X+c.W
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tier, ovl := 1, min(hi, chi)-max(lo, clo)
|
||||
if ovl > 0 {
|
||||
tier = 0
|
||||
} else {
|
||||
ovl = 0
|
||||
}
|
||||
// Distance (doubled, to stay integral) from the selected centre
|
||||
// line to the candidate's span; 0 if the span contains it.
|
||||
centre := lo + hi
|
||||
off := 0
|
||||
if centre < 2*clo {
|
||||
off = 2*clo - centre
|
||||
} else if centre > 2*chi {
|
||||
off = centre - 2*chi
|
||||
}
|
||||
sc := score{tier, gap, ovl, off, c.Y, c.X, c.ID}
|
||||
if !have || less(sc, best) {
|
||||
best, have = sc, true
|
||||
}
|
||||
}
|
||||
return best.id, have
|
||||
}
|
||||
Reference in New Issue
Block a user