do a lot of work on gim to get it sort of rendering gg.Graphs
This commit is contained in:
parent
5ab1d4c7f0
commit
f68bb4d8a2
145
gim/box.go
145
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]++
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
@ -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
92
gim/geo/rect.go
Normal 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
119
gim/geo/rect_test.go
Normal 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
44
gim/geo/round.go
Normal 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))
|
||||
}
|
19
gim/line.go
19
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 {
|
||||
|
188
gim/main.go
188
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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user