do a lot of work on gim to get it sort of rendering gg.Graphs

This commit is contained in:
Brian Picciano 2017-11-19 14:39:56 -07:00
parent 5ab1d4c7f0
commit f68bb4d8a2
8 changed files with 491 additions and 208 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
@ -27,14 +28,27 @@ var boxDefault = []string{
}
type box struct {
pos geo.XY
size geo.XY // if unset, auto-determined
body string
topLeft geo.XY
flowDir geo.XY
numIn, numOut int
body string
transparent bool
}
func (b box) lines() []string {
func boxFromVertex(v *gg.Vertex, flowDir geo.XY) box {
b := box{
flowDir: flowDir,
numIn: len(v.In),
numOut: len(v.Out),
}
if v.VertexType == gg.Value {
b.body = string(v.Value.(str))
}
return b
}
func (b box) bodyLines() []string {
lines := strings.Split(b.body, "\n")
// if the last line is empty don't include it, it means there was a trailing
// newline (or the whole string is empty)
@ -44,114 +58,91 @@ func (b box) lines() []string {
return lines
}
func (b box) innerSize() geo.XY {
if b.size != (geo.XY{}) {
return b.size
}
func (b box) bodySize() geo.XY {
var size geo.XY
for _, line := range b.lines() {
for _, line := range b.bodyLines() {
size[1]++
if l := len(line); l > size[0] {
size[0] = l
}
}
return size
}
func (b box) rectSize() geo.XY {
return b.innerSize().Add(geo.XY{2, 2})
func (b box) rect() geo.Rect {
bodyRect := geo.Rect{
Size: b.bodySize().Add(geo.XY{2, 2}),
}
var edgesRect geo.Rect
{
var neededByEdges int
if b.numIn > b.numOut {
neededByEdges = b.numIn*2 + 1
} else {
neededByEdges = b.numOut*2 + 1
}
switch b.flowDir {
case geo.Left, geo.Right:
edgesRect.Size = geo.XY{neededByEdges, 2}
case geo.Up, geo.Down:
edgesRect.Size = geo.XY{2, neededByEdges}
default:
panic(fmt.Sprintf("unknown flowDir: %#v", b.flowDir))
}
}
return bodyRect.Union(edgesRect).Translate(b.topLeft)
}
// edge returns the coordinate of the edge indicated by the given direction (Up,
// Down, Left, or Right). The coordinate will be for the axis applicable to the
// direction, so for Left/Right it will be the x coordinate and for Up/Down the
// y.
func (b box) rectEdge(dir geo.XY) int {
size := b.rectSize()
switch dir {
case geo.Up:
return b.pos[1]
case geo.Down:
return b.pos[1] + size[1]
case geo.Left:
return b.pos[0]
case geo.Right:
return b.pos[0] + size[0]
default:
panic(fmt.Sprintf("unsupported direction: %#v", dir))
}
}
func (b box) rectCorner(xDir, yDir geo.XY) geo.XY {
switch {
case xDir == geo.Left && yDir == geo.Up:
return b.pos
case xDir == geo.Right && yDir == geo.Up:
size := b.rectSize()
return b.pos.Add(size.Mul(geo.Right)).Add(geo.XY{-1, 0})
case xDir == geo.Left && yDir == geo.Down:
size := b.rectSize()
return b.pos.Add(size.Mul(geo.Down)).Add(geo.XY{0, -1})
case xDir == geo.Right && yDir == geo.Down:
size := b.rectSize()
return b.pos.Add(size).Add(geo.XY{-1, -1})
default:
panic(fmt.Sprintf("unsupported rectCorner args: %v, %v", xDir, yDir))
}
func (b box) bodyRect() geo.Rect {
center := b.rect().Center(rounder)
return geo.Rect{Size: b.bodySize()}.Centered(center, rounder)
}
func (b box) draw(term *terminal.Terminal) {
chars := boxDefault
pos := b.pos
size := b.innerSize()
w, h := size[0], size[1]
rect := b.rect()
pos := rect.TopLeft
w, h := rect.Size[0], rect.Size[1]
// draw top line
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderTL])
for i := 0; i < w; i++ {
for i := 0; i < w-2; i++ {
term.Printf(chars[boxBorderHoriz])
}
term.Printf(chars[boxBorderTR])
pos[1]++
drawLine := func(line string) {
pos[1]++
// draw vertical lines
for i := 0; i < h-2; i++ {
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderVert])
if len(line) > w {
line = line[:w]
}
term.Printf(line)
if b.transparent {
term.MoveCursor(geo.XY{w + 1, 0})
term.MoveCursorTo(pos.Add(geo.XY{w, 0}))
} else {
term.Printf(strings.Repeat(" ", w-len(line)))
term.Printf(strings.Repeat(" ", w-2))
}
term.Printf(chars[boxBorderVert])
}
// truncate lines if necessary
lines := b.lines()
if len(lines) > h {
lines = lines[:h]
}
// draw body
for _, line := range lines {
drawLine(line)
}
// draw empty lines
for i := 0; i < h-len(lines); i++ {
drawLine("")
pos[1]++
}
// draw bottom line
pos[1]++
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderBL])
for i := 0; i < w; i++ {
for i := 0; i < w-2; i++ {
term.Printf(chars[boxBorderHoriz])
}
term.Printf(chars[boxBorderBR])
// write out inner lines
pos = b.bodyRect().TopLeft
for _, line := range b.bodyLines() {
term.MoveCursorTo(pos)
term.Printf(line)
pos[1]++
}
}

View File

@ -1,28 +0,0 @@
package main
import (
. "testing"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/stretchr/testify/assert"
)
func TestBox(t *T) {
b := box{
pos: geo.XY{1, 2},
size: geo.XY{10, 11},
}
assert.Equal(t, geo.XY{10, 11}, b.innerSize())
assert.Equal(t, geo.XY{12, 13}, b.rectSize())
assert.Equal(t, 2, b.rectEdge(geo.Up))
assert.Equal(t, 15, b.rectEdge(geo.Down))
assert.Equal(t, 1, b.rectEdge(geo.Left))
assert.Equal(t, 13, b.rectEdge(geo.Right))
assert.Equal(t, geo.XY{1, 2}, b.rectCorner(geo.Left, geo.Up))
assert.Equal(t, geo.XY{1, 14}, b.rectCorner(geo.Left, geo.Down))
assert.Equal(t, geo.XY{12, 2}, b.rectCorner(geo.Right, geo.Up))
assert.Equal(t, geo.XY{12, 14}, b.rectCorner(geo.Right, geo.Down))
}

View File

@ -1,8 +1,6 @@
// Package geo implements basic geometric concepts used by gim
package geo
import "math"
// XY describes a 2-dimensional position or vector. The origin of the
// 2-dimensional space is a 0,0, with the x-axis going to the left and the
// y-axis going down.
@ -33,6 +31,16 @@ func (xy XY) Mul(xy2 XY) XY {
return xy
}
// Div returns the results of dividing the two XYs' field individually, using
// the Rounder to resolve floating results
func (xy XY) Div(xy2 XY, r Rounder) XY {
xyf, xy2f := xy.toF64(), xy2.toF64()
return XY{
r.Round(xyf[0] / xy2f[0]),
r.Round(xyf[1] / xy2f[1]),
}
}
// Scale returns the result of multiplying both of the XY's fields by the scalar
func (xy XY) Scale(scalar int) XY {
return xy.Mul(XY{scalar, scalar})
@ -49,21 +57,6 @@ func (xy XY) Sub(xy2 XY) XY {
return xy.Add(xy2.Inv())
}
func round(f float64, r int) int {
switch {
case r < 0:
f = math.Floor(f)
case r == 0:
if f < 0 {
f = math.Ceil(f - 0.5)
}
f = math.Floor(f + 0.5)
case r > 0:
f = math.Ceil(f)
}
return int(f)
}
func (xy XY) toF64() [2]float64 {
return [2]float64{
float64(xy[0]),
@ -72,16 +65,29 @@ func (xy XY) toF64() [2]float64 {
}
// Midpoint returns the midpoint between the two XYs. The rounder indicates what
// to do about non-whole values when they're come across:
// - rounder < 0 : floor
// - rounder = 0 : round
// - rounder > 0 : ceil
func (xy XY) Midpoint(xy2 XY, rounder int) XY {
xyf, xy2f := xy.toF64(), xy2.toF64()
xf := xyf[0] + ((xy2f[0] - xyf[0]) / 2)
yf := xyf[1] + ((xy2f[1] - xyf[1]) / 2)
return XY{
round(xf, rounder),
round(yf, rounder),
}
// to do about non-whole values when they're come across
func (xy XY) Midpoint(xy2 XY, r Rounder) XY {
return xy.Add(xy2.Sub(xy).Div(XY{2, 2}, r))
}
// Min returns an XY whose fields are the minimum values of the two XYs'
// fields compared individually
func (xy XY) Min(xy2 XY) XY {
for i := range xy {
if xy2[i] < xy[i] {
xy[i] = xy2[i]
}
}
return xy
}
// Max returns an XY whose fields are the Maximum values of the two XYs'
// fields compared individually
func (xy XY) Max(xy2 XY) XY {
for i := range xy {
if xy2[i] > xy[i] {
xy[i] = xy2[i]
}
}
return xy
}

92
gim/geo/rect.go Normal file
View File

@ -0,0 +1,92 @@
package geo
import (
"fmt"
)
// Rect describes a rectangle based on the position of its top-left corner and
// size
type Rect struct {
TopLeft XY
Size XY
}
// Edge returns the coordinate of the edge indicated by the given direction (Up,
// Down, Left, or Right). The coordinate will be for the axis applicable to the
// direction, so for Left/Right it will be the x coordinate and for Up/Down the
// y.
func (r Rect) Edge(dir XY) int {
switch dir {
case Up:
return r.TopLeft[1]
case Down:
return r.TopLeft[1] + r.Size[1] - 1
case Left:
return r.TopLeft[0]
case Right:
return r.TopLeft[0] + r.Size[0] - 1
default:
panic(fmt.Sprintf("unsupported direction: %#v", dir))
}
}
// Corner returns the position of the corner identified by the given directions
// (Left/Right, Up/Down)
func (r Rect) Corner(xDir, yDir XY) XY {
switch {
case r.Size[0] == 0 || r.Size[1] == 0:
panic(fmt.Sprintf("rectangle with non-multidimensional size has no corners: %v", r.Size))
case xDir == Left && yDir == Up:
return r.TopLeft
case xDir == Right && yDir == Up:
return r.TopLeft.Add(r.Size.Mul(Right)).Add(XY{-1, 0})
case xDir == Left && yDir == Down:
return r.TopLeft.Add(r.Size.Mul(Down)).Add(XY{0, -1})
case xDir == Right && yDir == Down:
return r.TopLeft.Add(r.Size).Add(XY{-1, -1})
default:
panic(fmt.Sprintf("unsupported Corner args: %v, %v", xDir, yDir))
}
}
func (r Rect) halfSize(rounder Rounder) XY {
return r.Size.Div(XY{2, 2}, rounder)
}
// Center returns the centerpoint of the rectangle, using the given Rounder to
// resolve non-integers
func (r Rect) Center(rounder Rounder) XY {
return r.TopLeft.Add(r.halfSize(rounder))
}
// Translate returns an instance of Rect which is the same as this one but
// translated by the given amount
func (r Rect) Translate(by XY) Rect {
r.TopLeft = r.TopLeft.Add(by)
return r
}
// Centered returns an instance of Rect which is this one but translated to be
// centered on the given point. It will use the given Rounder to resolve
// non-integers
func (r Rect) Centered(on XY, rounder Rounder) Rect {
r.TopLeft = on.Sub(r.halfSize(rounder))
return r
}
// Union returns the smallest Rect which encompasses the given Rect and the one
// being called upon.
func (r Rect) Union(r2 Rect) Rect {
if r.Size == Zero {
return r2
} else if r2.Size == Zero {
return r
}
tl := r.TopLeft.Min(r2.TopLeft)
br := r.Corner(Right, Down).Max(r2.Corner(Right, Down))
return Rect{
TopLeft: tl,
Size: br.Sub(tl).Add(XY{1, 1}),
}
}

119
gim/geo/rect_test.go Normal file
View File

@ -0,0 +1,119 @@
package geo
import (
. "testing"
"github.com/stretchr/testify/assert"
)
func TestRect(t *T) {
r := Rect{
TopLeft: XY{1, 2},
Size: XY{2, 2},
}
assert.Equal(t, 2, r.Edge(Up))
assert.Equal(t, 3, r.Edge(Down))
assert.Equal(t, 1, r.Edge(Left))
assert.Equal(t, 2, r.Edge(Right))
assert.Equal(t, XY{1, 2}, r.Corner(Left, Up))
assert.Equal(t, XY{1, 3}, r.Corner(Left, Down))
assert.Equal(t, XY{2, 2}, r.Corner(Right, Up))
assert.Equal(t, XY{2, 3}, r.Corner(Right, Down))
}
func TestRectCenter(t *T) {
assertCentered := func(exp, given Rect, center XY, rounder Rounder) {
got := given.Centered(center, rounder)
assert.Equal(t, exp, got)
assert.Equal(t, center, got.Center(rounder))
}
{
r := Rect{
Size: XY{4, 4},
}
assert.Equal(t, XY{2, 2}, r.Center(Round))
assert.Equal(t, XY{2, 2}, r.Center(Floor))
assert.Equal(t, XY{2, 2}, r.Center(Ceil))
assertCentered(
Rect{TopLeft: XY{1, 1}, Size: XY{4, 4}},
r, XY{3, 3}, Round,
)
assertCentered(
Rect{TopLeft: XY{1, 1}, Size: XY{4, 4}},
r, XY{3, 3}, Floor,
)
assertCentered(
Rect{TopLeft: XY{1, 1}, Size: XY{4, 4}},
r, XY{3, 3}, Ceil,
)
}
{
r := Rect{
Size: XY{5, 5},
}
assert.Equal(t, XY{3, 3}, r.Center(Round))
assert.Equal(t, XY{2, 2}, r.Center(Floor))
assert.Equal(t, XY{3, 3}, r.Center(Ceil))
assertCentered(
Rect{TopLeft: XY{0, 0}, Size: XY{5, 5}},
r, XY{3, 3}, Round,
)
assertCentered(
Rect{TopLeft: XY{1, 1}, Size: XY{5, 5}},
r, XY{3, 3}, Floor,
)
assertCentered(
Rect{TopLeft: XY{0, 0}, Size: XY{5, 5}},
r, XY{3, 3}, Ceil,
)
}
}
func TestRectUnion(t *T) {
assertUnion := func(exp, r1, r2 Rect) {
assert.Equal(t, exp, r1.Union(r2))
assert.Equal(t, exp, r2.Union(r1))
}
{ // Zero
r := Rect{TopLeft: XY{1, 1}, Size: XY{2, 2}}
assertUnion(r, r, Rect{})
}
{ // Equal
r := Rect{Size: XY{2, 2}}
assertUnion(r, r, r)
}
{ // Overlapping corner
r1 := Rect{TopLeft: XY{0, 0}, Size: XY{2, 2}}
r2 := Rect{TopLeft: XY{1, 1}, Size: XY{2, 2}}
ex := Rect{TopLeft: XY{0, 0}, Size: XY{3, 3}}
assertUnion(ex, r1, r2)
}
{ // 2 overlapping corners
r1 := Rect{TopLeft: XY{0, 0}, Size: XY{4, 4}}
r2 := Rect{TopLeft: XY{1, 1}, Size: XY{4, 2}}
ex := Rect{TopLeft: XY{0, 0}, Size: XY{5, 4}}
assertUnion(ex, r1, r2)
}
{ // Shared edge
r1 := Rect{TopLeft: XY{0, 0}, Size: XY{2, 1}}
r2 := Rect{TopLeft: XY{1, 0}, Size: XY{1, 2}}
ex := Rect{TopLeft: XY{0, 0}, Size: XY{2, 2}}
assertUnion(ex, r1, r2)
}
{ // Adjacent edge
r1 := Rect{TopLeft: XY{0, 0}, Size: XY{2, 2}}
r2 := Rect{TopLeft: XY{2, 0}, Size: XY{2, 2}}
ex := Rect{TopLeft: XY{0, 0}, Size: XY{4, 2}}
assertUnion(ex, r1, r2)
}
}

44
gim/geo/round.go Normal file
View File

@ -0,0 +1,44 @@
package geo
import (
"fmt"
"math"
)
// Rounder describes how a floating point number should be converted to an int
type Rounder int
const (
// Round will round up or down depending on the number itself
Round Rounder = iota
// Floor will use the math.Floor function
Floor
// Ceil will use the math.Ceil function
Ceil
)
// Round64 converts a float to an in64 based on the rounding function indicated
// by the Rounder's value
func (r Rounder) Round64(f float64) int64 {
switch r {
case Round:
if f < 0 {
f = math.Ceil(f - 0.5)
}
f = math.Floor(f + 0.5)
case Floor:
f = math.Floor(f)
case Ceil:
f = math.Ceil(f)
default:
panic(fmt.Sprintf("invalid Rounder: %#v", r))
}
return int64(f)
}
// Round is like Round64 but convers the int64 to an int
func (r Rounder) Round(f float64) int {
return int(r.Round64(f))
}

View File

@ -10,21 +10,22 @@ import (
// boxEdgeAdj returns the midpoint of a box's edge, using the given direction
// (single-dimension unit-vector) to know which edge to look at.
func boxEdgeAdj(box box, dir geo.XY) geo.XY {
boxRect := box.rect()
var a, b geo.XY
switch dir {
case geo.Up:
a, b = box.rectCorner(geo.Left, geo.Up), box.rectCorner(geo.Right, geo.Up)
a, b = boxRect.Corner(geo.Left, geo.Up), boxRect.Corner(geo.Right, geo.Up)
case geo.Down:
a, b = box.rectCorner(geo.Left, geo.Down), box.rectCorner(geo.Right, geo.Down)
a, b = boxRect.Corner(geo.Left, geo.Down), boxRect.Corner(geo.Right, geo.Down)
case geo.Left:
a, b = box.rectCorner(geo.Left, geo.Up), box.rectCorner(geo.Left, geo.Down)
a, b = boxRect.Corner(geo.Left, geo.Up), boxRect.Corner(geo.Left, geo.Down)
case geo.Right:
a, b = box.rectCorner(geo.Right, geo.Up), box.rectCorner(geo.Right, geo.Down)
a, b = boxRect.Corner(geo.Right, geo.Up), boxRect.Corner(geo.Right, geo.Down)
default:
panic(fmt.Sprintf("unsupported direction: %#v", dir))
}
mid := a.Midpoint(b, 0)
mid := a.Midpoint(b, rounder)
return mid
}
@ -40,9 +41,10 @@ var dirs = []geo.XY{
// and Left. The secondary direction will never be zero if primary is given,
// even if the two boxes are in-line
func boxesRelDir(from, to box) (geo.XY, geo.XY) {
fromRect, toRect := from.rect(), to.rect()
rels := make([]int, len(dirs))
for i, dir := range dirs {
rels[i] = to.rectEdge(dir.Inv()) - from.rectEdge(dir)
rels[i] = toRect.Edge(dir.Inv()) - fromRect.Edge(dir)
if dir == geo.Up || dir == geo.Left {
rels[i] *= -1
}
@ -86,9 +88,6 @@ func boxesRelDir(from, to box) (geo.XY, geo.XY) {
return primary, secondary
}
// liner will draw a line from one box to another
type liner func(*terminal.Terminal, box, box)
var lineSegments = func() map[[2]geo.XY]string {
m := map[[2]geo.XY]string{
{{-1, 0}, {1, 0}}: "─",
@ -134,7 +133,7 @@ func basicLine(term *terminal.Terminal, from, to box) {
dirInv := dir.Inv()
start := boxEdgeAdj(from, dir)
end := boxEdgeAdj(to, dirInv)
mid := start.Midpoint(end, 0)
mid := start.Midpoint(end, rounder)
along := func(xy, dir geo.XY) int {
if dir[0] != 0 {

View File

@ -2,20 +2,38 @@ package main
import (
"fmt"
"hash"
"math/rand"
"os"
"strings"
"time"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
// Leave room for:
// - Changing the "flow" direction
// - Absolute positioning of some/all vertices
// TODO
// - actually use flowDir
// - assign edges to "slots" on boxes
// - figure out how to keep boxes sorted on their levels (e.g. the "b" nodes)
const (
framerate = 10
frameperiod = time.Second / time.Duration(framerate)
rounder = geo.Ceil
)
type str string
func (s str) Identify(h hash.Hash) {
fmt.Fprintln(h, s)
}
func debugf(str string, args ...interface{}) {
if !strings.HasSuffix(str, "\n") {
str += "\n"
@ -23,88 +41,130 @@ func debugf(str string, args ...interface{}) {
fmt.Fprintf(os.Stderr, str, args...)
}
// TODO
// * Use actual gg graphs and not fake "boxes"
// - This will involve wrapping the vertices in some way, to preserve position
// * Once gg graphs are used we can use that birds-eye-view to make better
// decisions about edge placement
func mkGraph() *gg.Graph {
aE0 := gg.ValueOut(str("a"), str("aE0"))
aE1 := gg.ValueOut(str("a"), str("aE1"))
aE2 := gg.ValueOut(str("a"), str("aE2"))
aE3 := gg.ValueOut(str("a"), str("aE3"))
g := gg.Null
g = g.AddValueIn(aE0, str("b0"))
g = g.AddValueIn(aE1, str("b1"))
g = g.AddValueIn(aE2, str("b2"))
g = g.AddValueIn(aE3, str("b3"))
jE := gg.JunctionOut([]gg.OpenEdge{
gg.ValueOut(str("b0"), str("")),
gg.ValueOut(str("b1"), str("")),
gg.ValueOut(str("b2"), str("")),
gg.ValueOut(str("b3"), str("")),
}, str("jE"))
g = g.AddValueIn(jE, str("c"))
return g
}
//func mkGraph() *gg.Graph {
// g := gg.Null
// g = g.AddValueIn(gg.ValueOut(str("a"), str("e")), str("b"))
// return g
//}
func main() {
rand.Seed(time.Now().UnixNano())
term := terminal.New()
term.Reset()
termSize := term.WindowSize()
g := mkGraph()
type movingBox struct {
box
xRight bool
yDown bool
// level 0 is at the bottom of the screen, cause life is easier that way
levels := map[*gg.Vertex]int{}
getLevel := func(v *gg.Vertex) int {
// if any of the tos have a level, this will be greater than the max
toMax := -1
for _, e := range v.Out {
lvl, ok := levels[e.To]
if !ok {
continue
} else if lvl > toMax {
toMax = lvl
}
}
if toMax >= 0 {
return toMax + 1
}
// otherwise level is 0
return 0
}
randBox := func() movingBox {
tsize := term.WindowSize()
return movingBox{
box: box{
pos: geo.XY{rand.Intn(tsize[0]), rand.Intn(tsize[1])},
size: geo.XY{30, 2},
},
xRight: rand.Intn(1) == 0,
yDown: rand.Intn(1) == 0,
g.Walk(g.Value(str("c")), func(v *gg.Vertex) bool {
levels[v] = getLevel(v)
return true
})
// consolidate by level
byLevel := map[int][]*gg.Vertex{}
maxLvl := -1
for v, lvl := range levels {
byLevel[lvl] = append(byLevel[lvl], v)
if lvl > maxLvl {
maxLvl = lvl
}
}
boxes := []movingBox{
randBox(),
randBox(),
randBox(),
randBox(),
randBox(),
// create boxes
boxes := map[*gg.Vertex]box{}
for lvl := 0; lvl <= maxLvl; lvl++ {
vv := byLevel[lvl]
for i, v := range vv {
b := boxFromVertex(v, geo.Right)
bSize := b.rect().Size
b.topLeft = geo.XY{
10*(i-(len(vv)/2)) - (bSize[0] / 2),
lvl * -10,
}
boxes[v] = b
}
}
// center boxes. first find overall dimensions, use that to create delta
// vector which would move that to the center
var graphRect geo.Rect
for _, b := range boxes {
graphRect = graphRect.Union(b.rect())
}
graphMid := graphRect.Center(rounder)
screenMid := geo.Zero.Midpoint(termSize, rounder)
delta := screenMid.Sub(graphMid)
// translate all boxes by delta
for v, b := range boxes {
b.topLeft = b.topLeft.Add(delta)
boxes[v] = b
}
// create lines
var lines [][2]box
for v := range levels {
b := boxes[v]
for _, e := range v.In {
bFrom := boxes[e.From]
lines = append(lines, [2]box{bFrom, b})
}
}
for range time.Tick(frameperiod) {
// update phase
termSize := term.WindowSize()
for i := range boxes {
b := &boxes[i]
b.body = fmt.Sprintf("%d) %v", i, b.rectCorner(geo.Left, geo.Up))
b.body += fmt.Sprintf(" | %v\n", b.rectCorner(geo.Right, geo.Up))
b.body += fmt.Sprintf(" %v", b.rectCorner(geo.Left, geo.Down))
b.body += fmt.Sprintf(" | %v", b.rectCorner(geo.Right, geo.Down))
size := b.rectSize()
if b.pos[0] <= 0 {
b.xRight = true
} else if b.pos[0]+size[0] >= termSize[0] {
b.xRight = false
}
if b.pos[1] <= 0 {
b.yDown = true
} else if b.pos[1]+size[1] >= termSize[1] {
b.yDown = false
}
if b.xRight {
b.pos[0] += 3
} else {
b.pos[0] -= 3
}
if b.yDown {
b.pos[1]++
} else {
b.pos[1]--
}
}
// nufin
// draw phase
term.Reset()
for i := range boxes {
boxes[i].draw(term)
for v := range boxes {
boxes[v].draw(term)
}
term.Flush()
for i := range boxes {
if i == 0 {
continue
}
basicLine(term, boxes[i-1].box, boxes[i].box)
for _, line := range lines {
basicLine(term, line[0], line[1])
}
term.Flush()
}