From f68bb4d8a229249a93336225dd8582df25c57f2b Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 19 Nov 2017 14:39:56 -0700 Subject: [PATCH] do a lot of work on gim to get it sort of rendering gg.Graphs --- gim/box.go | 145 ++++++++++++++++----------------- gim/box_test.go | 28 ------- gim/geo/geo.go | 64 ++++++++------- gim/geo/rect.go | 92 +++++++++++++++++++++ gim/geo/rect_test.go | 119 +++++++++++++++++++++++++++ gim/geo/round.go | 44 ++++++++++ gim/line.go | 19 +++-- gim/main.go | 188 ++++++++++++++++++++++++++++--------------- 8 files changed, 491 insertions(+), 208 deletions(-) delete mode 100644 gim/box_test.go create mode 100644 gim/geo/rect.go create mode 100644 gim/geo/rect_test.go create mode 100644 gim/geo/round.go diff --git a/gim/box.go b/gim/box.go index 342c944..d536f4a 100644 --- a/gim/box.go +++ b/gim/box.go @@ -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]++ + } } diff --git a/gim/box_test.go b/gim/box_test.go deleted file mode 100644 index 5533bf6..0000000 --- a/gim/box_test.go +++ /dev/null @@ -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)) -} diff --git a/gim/geo/geo.go b/gim/geo/geo.go index a42fd7b..78f073a 100644 --- a/gim/geo/geo.go +++ b/gim/geo/geo.go @@ -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 } diff --git a/gim/geo/rect.go b/gim/geo/rect.go new file mode 100644 index 0000000..0f5f827 --- /dev/null +++ b/gim/geo/rect.go @@ -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}), + } +} diff --git a/gim/geo/rect_test.go b/gim/geo/rect_test.go new file mode 100644 index 0000000..208cc37 --- /dev/null +++ b/gim/geo/rect_test.go @@ -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) + } +} diff --git a/gim/geo/round.go b/gim/geo/round.go new file mode 100644 index 0000000..822a4e2 --- /dev/null +++ b/gim/geo/round.go @@ -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)) +} diff --git a/gim/line.go b/gim/line.go index f8490ea..799b50e 100644 --- a/gim/line.go +++ b/gim/line.go @@ -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 { diff --git a/gim/main.go b/gim/main.go index a309c32..a2b6f34 100644 --- a/gim/main.go +++ b/gim/main.go @@ -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() }