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
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Package treemap is pure geometry: squarified layout on a cell grid,
|
||||||
|
// level-of-detail selection and spatial neighbour search. It has no
|
||||||
|
// knowledge of filesystems or terminals.
|
||||||
|
package treemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rect is an integer cell rectangle.
|
||||||
|
type Rect struct{ X, Y, W, H int }
|
||||||
|
|
||||||
|
// Empty reports whether r has no cells.
|
||||||
|
func (r Rect) Empty() bool { return r.W <= 0 || r.H <= 0 }
|
||||||
|
|
||||||
|
// Area returns W*H.
|
||||||
|
func (r Rect) Area() int { return max(r.W, 0) * max(r.H, 0) }
|
||||||
|
|
||||||
|
// Contains reports whether the cell (x, y) is inside r.
|
||||||
|
func (r Rect) Contains(x, y int) bool { return x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H }
|
||||||
|
|
||||||
|
// Inset shrinks r by n cells on each side.
|
||||||
|
func (r Rect) Inset(n int) Rect {
|
||||||
|
r = Rect{r.X + n, r.Y + n, r.W - 2*n, r.H - 2*n}
|
||||||
|
if r.W < 0 {
|
||||||
|
r.W = 0
|
||||||
|
}
|
||||||
|
if r.H < 0 {
|
||||||
|
r.H = 0
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item is something to lay out. Size must be > 0 to receive space.
|
||||||
|
type Item struct {
|
||||||
|
ID int64
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block is a laid-out item.
|
||||||
|
type Block struct {
|
||||||
|
ID int64
|
||||||
|
Rect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Squarify lays items out inside bounds using the squarified treemap
|
||||||
|
// algorithm (Bruls, Huizing, van Wijk). Items with non-positive size are
|
||||||
|
// ignored. The output partitions bounds exactly: rectangle edges are snapped
|
||||||
|
// to the grid by rounding shared float edges, so there are no gaps and no
|
||||||
|
// overlaps. Items that round to zero width or height are omitted. The result
|
||||||
|
// is deterministic for a given input set regardless of input order.
|
||||||
|
func Squarify(items []Item, bounds Rect) []Block {
|
||||||
|
if bounds.Empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
its := make([]Item, 0, len(items))
|
||||||
|
total := 0.0
|
||||||
|
for _, it := range items {
|
||||||
|
if it.Size > 0 && !math.IsInf(it.Size, 0) && !math.IsNaN(it.Size) {
|
||||||
|
its = append(its, it)
|
||||||
|
total += it.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(its) == 0 || total <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sort.SliceStable(its, func(i, j int) bool {
|
||||||
|
if its[i].Size != its[j].Size {
|
||||||
|
return its[i].Size > its[j].Size
|
||||||
|
}
|
||||||
|
return its[i].ID < its[j].ID
|
||||||
|
})
|
||||||
|
scale := float64(bounds.W) * float64(bounds.H) / total
|
||||||
|
areas := make([]float64, len(its))
|
||||||
|
for i, it := range its {
|
||||||
|
areas[i] = it.Size * scale
|
||||||
|
}
|
||||||
|
frs := layout(areas, float64(bounds.X), float64(bounds.Y), float64(bounds.W), float64(bounds.H))
|
||||||
|
out := make([]Block, 0, len(its))
|
||||||
|
for i, f := range frs {
|
||||||
|
x0, y0 := int(math.Round(f[0])), int(math.Round(f[1]))
|
||||||
|
x1, y1 := int(math.Round(f[0]+f[2])), int(math.Round(f[1]+f[3]))
|
||||||
|
x1 = min(x1, bounds.X+bounds.W)
|
||||||
|
y1 = min(y1, bounds.Y+bounds.H)
|
||||||
|
if x1 > x0 && y1 > y0 {
|
||||||
|
out = append(out, Block{ID: its[i].ID, Rect: Rect{x0, y0, x1 - x0, y1 - y0}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// worst returns the worst aspect ratio of a row with the given sum, largest
|
||||||
|
// and smallest areas laid along a side of length side.
|
||||||
|
func worst(sum, largest, smallest, side float64) float64 {
|
||||||
|
s2, w2 := sum*sum, side*side
|
||||||
|
return math.Max(w2*largest/s2, s2/(w2*smallest))
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout returns float rectangles [x, y, w, h] for areas sorted descending.
|
||||||
|
func layout(areas []float64, x, y, w, h float64) [][4]float64 {
|
||||||
|
out := make([][4]float64, len(areas))
|
||||||
|
n := len(areas)
|
||||||
|
for i := 0; i < n; {
|
||||||
|
short := math.Min(w, h)
|
||||||
|
j := i + 1
|
||||||
|
sum := areas[i]
|
||||||
|
cur := worst(sum, areas[i], areas[i], short)
|
||||||
|
for j < n {
|
||||||
|
s2 := sum + areas[j]
|
||||||
|
if nw := worst(s2, areas[i], areas[j], short); nw > cur {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
cur = nw
|
||||||
|
}
|
||||||
|
sum = s2
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
last := j == n
|
||||||
|
if w >= h {
|
||||||
|
thick := sum / h
|
||||||
|
if last || thick > w {
|
||||||
|
thick = w
|
||||||
|
}
|
||||||
|
yy := y
|
||||||
|
for k := i; k < j; k++ {
|
||||||
|
hh := areas[k] / sum * h
|
||||||
|
if k == j-1 {
|
||||||
|
hh = y + h - yy
|
||||||
|
}
|
||||||
|
out[k] = [4]float64{x, yy, thick, hh}
|
||||||
|
yy += hh
|
||||||
|
}
|
||||||
|
x += thick
|
||||||
|
w -= thick
|
||||||
|
} else {
|
||||||
|
thick := sum / w
|
||||||
|
if last || thick > h {
|
||||||
|
thick = h
|
||||||
|
}
|
||||||
|
xx := x
|
||||||
|
for k := i; k < j; k++ {
|
||||||
|
ww := areas[k] / sum * w
|
||||||
|
if k == j-1 {
|
||||||
|
ww = x + w - xx
|
||||||
|
}
|
||||||
|
out[k] = [4]float64{xx, y, ww, thick}
|
||||||
|
xx += ww
|
||||||
|
}
|
||||||
|
y += thick
|
||||||
|
h -= thick
|
||||||
|
}
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible decides how many leading items of a size-sorted (descending)
|
||||||
|
// list deserve their own block when area cells represent total: items are
|
||||||
|
// kept while each would get at least minArea cells and fewer than maxItems
|
||||||
|
// have been kept. The remainder should be merged into one group block.
|
||||||
|
func Visible(sizes []float64, total float64, area, minArea, maxItems int) int {
|
||||||
|
if total <= 0 || area <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
k := 0
|
||||||
|
for k < len(sizes) && k < maxItems {
|
||||||
|
if sizes[k]/total*float64(area) < float64(minArea) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
// Grouping a single leftover item gains nothing; show it directly if it
|
||||||
|
// gets at least one cell.
|
||||||
|
if k == len(sizes)-1 && k < maxItems && sizes[k]/total*float64(area) >= 1 {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the block containing the cell (x, y).
|
||||||
|
func At(blocks []Block, x, y int) (int64, bool) {
|
||||||
|
for _, b := range blocks {
|
||||||
|
if b.Contains(x, y) {
|
||||||
|
return b.ID, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package treemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkPartition(t *testing.T, blocks []Block, b Rect, exact bool) {
|
||||||
|
t.Helper()
|
||||||
|
grid := make([]int, b.W*b.H)
|
||||||
|
for _, bl := range blocks {
|
||||||
|
if bl.W <= 0 || bl.H <= 0 {
|
||||||
|
t.Fatalf("non-positive block %+v", bl)
|
||||||
|
}
|
||||||
|
if bl.X < b.X || bl.Y < b.Y || bl.X+bl.W > b.X+b.W || bl.Y+bl.H > b.Y+b.H {
|
||||||
|
t.Fatalf("block %+v outside bounds %+v", bl, b)
|
||||||
|
}
|
||||||
|
for y := bl.Y; y < bl.Y+bl.H; y++ {
|
||||||
|
for x := bl.X; x < bl.X+bl.W; x++ {
|
||||||
|
i := (y-b.Y)*b.W + (x - b.X)
|
||||||
|
if grid[i] != 0 {
|
||||||
|
t.Fatalf("overlap at %d,%d", x, y)
|
||||||
|
}
|
||||||
|
grid[i] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exact {
|
||||||
|
for i, v := range grid {
|
||||||
|
if v == 0 {
|
||||||
|
t.Fatalf("gap at %d,%d", b.X+i%b.W, b.Y+i/b.W)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSquarifyBasic(t *testing.T) {
|
||||||
|
b := Rect{2, 3, 60, 20}
|
||||||
|
items := []Item{{1, 6}, {2, 6}, {3, 4}, {4, 3}, {5, 2}, {6, 2}, {7, 1}}
|
||||||
|
blocks := Squarify(items, b)
|
||||||
|
if len(blocks) != len(items) {
|
||||||
|
t.Fatalf("got %d blocks", len(blocks))
|
||||||
|
}
|
||||||
|
checkPartition(t, blocks, b, true)
|
||||||
|
// Areas roughly proportional.
|
||||||
|
for _, bl := range blocks {
|
||||||
|
want := float64(b.Area()) * items[bl.ID-1].Size / 24
|
||||||
|
if got := float64(bl.Area()); got < want*0.6-4 || got > want*1.4+4 {
|
||||||
|
t.Errorf("block %d area %v want ~%v", bl.ID, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deterministic regardless of input order.
|
||||||
|
rev := append([]Item(nil), items...)
|
||||||
|
for i, j := 0, len(rev)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
rev[i], rev[j] = rev[j], rev[i]
|
||||||
|
}
|
||||||
|
b2 := Squarify(rev, b)
|
||||||
|
for i := range blocks {
|
||||||
|
if blocks[i] != b2[i] {
|
||||||
|
t.Fatal("layout depends on input order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSquarifyEdgeCases(t *testing.T) {
|
||||||
|
if Squarify(nil, Rect{0, 0, 10, 10}) != nil {
|
||||||
|
t.Fatal("nil items")
|
||||||
|
}
|
||||||
|
if Squarify([]Item{{1, 0}, {2, -5}}, Rect{0, 0, 10, 10}) != nil {
|
||||||
|
t.Fatal("zero sizes should produce nothing")
|
||||||
|
}
|
||||||
|
for _, b := range []Rect{{0, 0, 0, 0}, {0, 0, 1, 1}, {0, 0, 1, 50}, {0, 0, 50, 1}, {0, 0, -3, 4}} {
|
||||||
|
bl := Squarify([]Item{{1, 5}, {2, 3}, {3, 1}}, b)
|
||||||
|
if !b.Empty() {
|
||||||
|
checkPartition(t, bl, b, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// One enormous item and many tiny ones.
|
||||||
|
items := []Item{{0, 1e15}}
|
||||||
|
for i := 1; i < 500; i++ {
|
||||||
|
items = append(items, Item{int64(i), 1})
|
||||||
|
}
|
||||||
|
bl := Squarify(items, Rect{0, 0, 80, 24})
|
||||||
|
checkPartition(t, bl, Rect{0, 0, 80, 24}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResizeRecomputes(t *testing.T) {
|
||||||
|
items := []Item{{1, 50}, {2, 30}, {3, 20}}
|
||||||
|
for _, b := range []Rect{{0, 0, 80, 24}, {0, 0, 120, 40}, {0, 0, 300, 80}, {0, 0, 70, 20}, {0, 0, 180, 50}} {
|
||||||
|
bl := Squarify(items, b)
|
||||||
|
checkPartition(t, bl, b, true)
|
||||||
|
if len(bl) != 3 {
|
||||||
|
t.Fatalf("%v: %d blocks", b, len(bl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzSquarify(f *testing.F) {
|
||||||
|
f.Add(int64(1), 80, 24, 10)
|
||||||
|
f.Add(int64(2), 3, 2, 100)
|
||||||
|
f.Add(int64(3), 300, 90, 1000)
|
||||||
|
f.Fuzz(func(t *testing.T, seed int64, w, h, n int) {
|
||||||
|
w, h, n = w%400, h%150, n%3000
|
||||||
|
if w < 0 || h < 0 || n < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := rand.New(rand.NewSource(seed))
|
||||||
|
items := make([]Item, n)
|
||||||
|
for i := range items {
|
||||||
|
items[i] = Item{int64(i), float64(r.Int63n(1 << uint(r.Intn(40)+1)))}
|
||||||
|
}
|
||||||
|
b := Rect{r.Intn(10), r.Intn(10), w, h}
|
||||||
|
bl := Squarify(items, b)
|
||||||
|
if !b.Empty() && len(bl) > 0 {
|
||||||
|
checkPartition(t, bl, b, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisible(t *testing.T) {
|
||||||
|
sizes := []float64{100, 50, 10, 1, 1, 1}
|
||||||
|
if k := Visible(sizes, 163, 1630, 20, 100); k != 3 {
|
||||||
|
t.Fatalf("k=%d", k)
|
||||||
|
}
|
||||||
|
if k := Visible(sizes, 163, 1630, 20, 2); k != 2 {
|
||||||
|
t.Fatalf("maxItems k=%d", k)
|
||||||
|
}
|
||||||
|
if k := Visible([]float64{10, 1}, 11, 1100, 200, 10); k != 2 {
|
||||||
|
t.Fatalf("single leftover k=%d", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout:
|
||||||
|
//
|
||||||
|
// ┌───────┬───────┐
|
||||||
|
// │ A │ B │
|
||||||
|
// ├───┬───┴───────┤
|
||||||
|
// │ C │ D │
|
||||||
|
// └───┴───────────┘
|
||||||
|
func TestNeighbour(t *testing.T) {
|
||||||
|
A := Block{1, Rect{0, 0, 8, 4}}
|
||||||
|
B := Block{2, Rect{8, 0, 8, 4}}
|
||||||
|
C := Block{3, Rect{0, 4, 4, 4}}
|
||||||
|
D := Block{4, Rect{4, 4, 12, 4}}
|
||||||
|
bs := []Block{D, C, B, A} // order must not matter
|
||||||
|
cases := []struct {
|
||||||
|
from int64
|
||||||
|
d Direction
|
||||||
|
want int64
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{1, Right, 2, true}, {2, Left, 1, true}, {1, Down, 3, true}, {2, Down, 4, true},
|
||||||
|
{3, Right, 4, true}, {4, Left, 3, true}, {3, Up, 1, true}, {4, Up, 2, true},
|
||||||
|
{1, Left, 0, false}, {1, Up, 0, false}, {4, Right, 0, false}, {4, Down, 0, false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got, ok := Neighbour(bs, c.from, c.d)
|
||||||
|
if ok != c.ok || got != c.want {
|
||||||
|
t.Errorf("from %d dir %d: got %d,%v want %d,%v", c.from, c.d, got, ok, c.want, c.ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNeighbourPrefersAdjacentOverArrayOrder(t *testing.T) {
|
||||||
|
// S is on the left; R is directly right; F is far right and lower but
|
||||||
|
// appears first in the slice.
|
||||||
|
S := Block{1, Rect{0, 0, 10, 5}}
|
||||||
|
R := Block{2, Rect{10, 0, 10, 5}}
|
||||||
|
F := Block{3, Rect{10, 20, 30, 5}}
|
||||||
|
if got, _ := Neighbour([]Block{F, S, R}, 1, Right); got != 2 {
|
||||||
|
t.Fatalf("got %d", got)
|
||||||
|
}
|
||||||
|
// Tall block on the left with three stacked blocks to the right: pick
|
||||||
|
// the one at the centre line.
|
||||||
|
T := Block{10, Rect{0, 0, 10, 9}}
|
||||||
|
r1 := Block{11, Rect{10, 0, 10, 3}}
|
||||||
|
r2 := Block{12, Rect{10, 3, 10, 3}}
|
||||||
|
r3 := Block{13, Rect{10, 6, 10, 3}}
|
||||||
|
if got, _ := Neighbour([]Block{r3, r1, r2, T}, 10, Right); got != 12 {
|
||||||
|
t.Fatalf("centre preference: got %d", got)
|
||||||
|
}
|
||||||
|
if got, _ := Neighbour([]Block{r3, r1, r2, T}, 13, Left); got != 10 {
|
||||||
|
t.Fatalf("back left: got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNeighbourRandomLayoutsAlwaysAdjacent(t *testing.T) {
|
||||||
|
r := rand.New(rand.NewSource(7))
|
||||||
|
for iter := 0; iter < 200; iter++ {
|
||||||
|
items := make([]Item, 2+r.Intn(30))
|
||||||
|
for i := range items {
|
||||||
|
items[i] = Item{int64(i), float64(1 + r.Intn(1000))}
|
||||||
|
}
|
||||||
|
bl := Squarify(items, Rect{0, 0, 120, 40})
|
||||||
|
for _, s := range bl {
|
||||||
|
for _, d := range []Direction{Left, Right, Up, Down} {
|
||||||
|
id, ok := Neighbour(bl, s.ID, d)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var c Block
|
||||||
|
for _, b := range bl {
|
||||||
|
if b.ID == id {
|
||||||
|
c = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In a partition, if any block touches the edge in direction d
|
||||||
|
// with overlapping span, the chosen one must touch too.
|
||||||
|
var touch bool
|
||||||
|
switch d {
|
||||||
|
case Right:
|
||||||
|
touch = c.X == s.X+s.W
|
||||||
|
case Left:
|
||||||
|
touch = c.X+c.W == s.X
|
||||||
|
case Down:
|
||||||
|
touch = c.Y == s.Y+s.H
|
||||||
|
case Up:
|
||||||
|
touch = c.Y+c.H == s.Y
|
||||||
|
}
|
||||||
|
if !touch {
|
||||||
|
t.Fatalf("iter %d: from %+v dir %d chose non-adjacent %+v", iter, s, d, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSquarify(b *testing.B) {
|
||||||
|
items := make([]Item, 300)
|
||||||
|
for i := range items {
|
||||||
|
items[i] = Item{int64(i), float64(1 + i*i)}
|
||||||
|
}
|
||||||
|
for b.Loop() {
|
||||||
|
Squarify(items, Rect{0, 0, 200, 60})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNeighbour(b *testing.B) {
|
||||||
|
items := make([]Item, 300)
|
||||||
|
for i := range items {
|
||||||
|
items[i] = Item{int64(i), float64(1 + i*i)}
|
||||||
|
}
|
||||||
|
bl := Squarify(items, Rect{0, 0, 200, 60})
|
||||||
|
for b.Loop() {
|
||||||
|
Neighbour(bl, 150, Right)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user