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.
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
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
|
|
}
|