do a lot of work on gim to get it sort of rendering gg.Graphs
This commit is contained in:
parent
5ab1d4c7f0
commit
f68bb4d8a2
143
gim/box.go
143
gim/box.go
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/ginger/gg"
|
||||||
"github.com/mediocregopher/ginger/gim/geo"
|
"github.com/mediocregopher/ginger/gim/geo"
|
||||||
"github.com/mediocregopher/ginger/gim/terminal"
|
"github.com/mediocregopher/ginger/gim/terminal"
|
||||||
)
|
)
|
||||||
@ -27,14 +28,27 @@ var boxDefault = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type box struct {
|
type box struct {
|
||||||
pos geo.XY
|
topLeft geo.XY
|
||||||
size geo.XY // if unset, auto-determined
|
flowDir geo.XY
|
||||||
|
numIn, numOut int
|
||||||
body string
|
body string
|
||||||
|
|
||||||
transparent bool
|
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")
|
lines := strings.Split(b.body, "\n")
|
||||||
// if the last line is empty don't include it, it means there was a trailing
|
// if the last line is empty don't include it, it means there was a trailing
|
||||||
// newline (or the whole string is empty)
|
// newline (or the whole string is empty)
|
||||||
@ -44,114 +58,91 @@ func (b box) lines() []string {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b box) innerSize() geo.XY {
|
func (b box) bodySize() geo.XY {
|
||||||
if b.size != (geo.XY{}) {
|
|
||||||
return b.size
|
|
||||||
}
|
|
||||||
var size geo.XY
|
var size geo.XY
|
||||||
for _, line := range b.lines() {
|
for _, line := range b.bodyLines() {
|
||||||
size[1]++
|
size[1]++
|
||||||
if l := len(line); l > size[0] {
|
if l := len(line); l > size[0] {
|
||||||
size[0] = l
|
size[0] = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b box) rectSize() geo.XY {
|
func (b box) rect() geo.Rect {
|
||||||
return b.innerSize().Add(geo.XY{2, 2})
|
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,
|
func (b box) bodyRect() geo.Rect {
|
||||||
// Down, Left, or Right). The coordinate will be for the axis applicable to the
|
center := b.rect().Center(rounder)
|
||||||
// direction, so for Left/Right it will be the x coordinate and for Up/Down the
|
return geo.Rect{Size: b.bodySize()}.Centered(center, rounder)
|
||||||
// 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) draw(term *terminal.Terminal) {
|
func (b box) draw(term *terminal.Terminal) {
|
||||||
chars := boxDefault
|
chars := boxDefault
|
||||||
pos := b.pos
|
rect := b.rect()
|
||||||
size := b.innerSize()
|
pos := rect.TopLeft
|
||||||
w, h := size[0], size[1]
|
w, h := rect.Size[0], rect.Size[1]
|
||||||
|
|
||||||
// draw top line
|
// draw top line
|
||||||
term.MoveCursorTo(pos)
|
term.MoveCursorTo(pos)
|
||||||
term.Printf(chars[boxBorderTL])
|
term.Printf(chars[boxBorderTL])
|
||||||
for i := 0; i < w; i++ {
|
for i := 0; i < w-2; i++ {
|
||||||
term.Printf(chars[boxBorderHoriz])
|
term.Printf(chars[boxBorderHoriz])
|
||||||
}
|
}
|
||||||
term.Printf(chars[boxBorderTR])
|
term.Printf(chars[boxBorderTR])
|
||||||
|
|
||||||
drawLine := func(line string) {
|
|
||||||
pos[1]++
|
pos[1]++
|
||||||
|
|
||||||
|
// draw vertical lines
|
||||||
|
for i := 0; i < h-2; i++ {
|
||||||
term.MoveCursorTo(pos)
|
term.MoveCursorTo(pos)
|
||||||
term.Printf(chars[boxBorderVert])
|
term.Printf(chars[boxBorderVert])
|
||||||
if len(line) > w {
|
|
||||||
line = line[:w]
|
|
||||||
}
|
|
||||||
term.Printf(line)
|
|
||||||
if b.transparent {
|
if b.transparent {
|
||||||
term.MoveCursor(geo.XY{w + 1, 0})
|
term.MoveCursorTo(pos.Add(geo.XY{w, 0}))
|
||||||
} else {
|
} else {
|
||||||
term.Printf(strings.Repeat(" ", w-len(line)))
|
term.Printf(strings.Repeat(" ", w-2))
|
||||||
}
|
}
|
||||||
term.Printf(chars[boxBorderVert])
|
term.Printf(chars[boxBorderVert])
|
||||||
}
|
pos[1]++
|
||||||
|
|
||||||
// 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("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw bottom line
|
// draw bottom line
|
||||||
pos[1]++
|
|
||||||
term.MoveCursorTo(pos)
|
term.MoveCursorTo(pos)
|
||||||
term.Printf(chars[boxBorderBL])
|
term.Printf(chars[boxBorderBL])
|
||||||
for i := 0; i < w; i++ {
|
for i := 0; i < w-2; i++ {
|
||||||
term.Printf(chars[boxBorderHoriz])
|
term.Printf(chars[boxBorderHoriz])
|
||||||
}
|
}
|
||||||
term.Printf(chars[boxBorderBR])
|
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 implements basic geometric concepts used by gim
|
||||||
package geo
|
package geo
|
||||||
|
|
||||||
import "math"
|
|
||||||
|
|
||||||
// XY describes a 2-dimensional position or vector. The origin of the
|
// 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
|
// 2-dimensional space is a 0,0, with the x-axis going to the left and the
|
||||||
// y-axis going down.
|
// y-axis going down.
|
||||||
@ -33,6 +31,16 @@ func (xy XY) Mul(xy2 XY) XY {
|
|||||||
return 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
|
// Scale returns the result of multiplying both of the XY's fields by the scalar
|
||||||
func (xy XY) Scale(scalar int) XY {
|
func (xy XY) Scale(scalar int) XY {
|
||||||
return xy.Mul(XY{scalar, scalar})
|
return xy.Mul(XY{scalar, scalar})
|
||||||
@ -49,21 +57,6 @@ func (xy XY) Sub(xy2 XY) XY {
|
|||||||
return xy.Add(xy2.Inv())
|
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 {
|
func (xy XY) toF64() [2]float64 {
|
||||||
return [2]float64{
|
return [2]float64{
|
||||||
float64(xy[0]),
|
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
|
// Midpoint returns the midpoint between the two XYs. The rounder indicates what
|
||||||
// to do about non-whole values when they're come across:
|
// to do about non-whole values when they're come across
|
||||||
// - rounder < 0 : floor
|
func (xy XY) Midpoint(xy2 XY, r Rounder) XY {
|
||||||
// - rounder = 0 : round
|
return xy.Add(xy2.Sub(xy).Div(XY{2, 2}, r))
|
||||||
// - rounder > 0 : ceil
|
}
|
||||||
func (xy XY) Midpoint(xy2 XY, rounder int) XY {
|
|
||||||
xyf, xy2f := xy.toF64(), xy2.toF64()
|
// Min returns an XY whose fields are the minimum values of the two XYs'
|
||||||
xf := xyf[0] + ((xy2f[0] - xyf[0]) / 2)
|
// fields compared individually
|
||||||
yf := xyf[1] + ((xy2f[1] - xyf[1]) / 2)
|
func (xy XY) Min(xy2 XY) XY {
|
||||||
return XY{
|
for i := range xy {
|
||||||
round(xf, rounder),
|
if xy2[i] < xy[i] {
|
||||||
round(yf, rounder),
|
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
|
// boxEdgeAdj returns the midpoint of a box's edge, using the given direction
|
||||||
// (single-dimension unit-vector) to know which edge to look at.
|
// (single-dimension unit-vector) to know which edge to look at.
|
||||||
func boxEdgeAdj(box box, dir geo.XY) geo.XY {
|
func boxEdgeAdj(box box, dir geo.XY) geo.XY {
|
||||||
|
boxRect := box.rect()
|
||||||
var a, b geo.XY
|
var a, b geo.XY
|
||||||
switch dir {
|
switch dir {
|
||||||
case geo.Up:
|
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:
|
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:
|
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:
|
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:
|
default:
|
||||||
panic(fmt.Sprintf("unsupported direction: %#v", dir))
|
panic(fmt.Sprintf("unsupported direction: %#v", dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
mid := a.Midpoint(b, 0)
|
mid := a.Midpoint(b, rounder)
|
||||||
return mid
|
return mid
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +41,10 @@ var dirs = []geo.XY{
|
|||||||
// and Left. The secondary direction will never be zero if primary is given,
|
// and Left. The secondary direction will never be zero if primary is given,
|
||||||
// even if the two boxes are in-line
|
// even if the two boxes are in-line
|
||||||
func boxesRelDir(from, to box) (geo.XY, geo.XY) {
|
func boxesRelDir(from, to box) (geo.XY, geo.XY) {
|
||||||
|
fromRect, toRect := from.rect(), to.rect()
|
||||||
rels := make([]int, len(dirs))
|
rels := make([]int, len(dirs))
|
||||||
for i, dir := range 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 {
|
if dir == geo.Up || dir == geo.Left {
|
||||||
rels[i] *= -1
|
rels[i] *= -1
|
||||||
}
|
}
|
||||||
@ -86,9 +88,6 @@ func boxesRelDir(from, to box) (geo.XY, geo.XY) {
|
|||||||
return primary, secondary
|
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 {
|
var lineSegments = func() map[[2]geo.XY]string {
|
||||||
m := map[[2]geo.XY]string{
|
m := map[[2]geo.XY]string{
|
||||||
{{-1, 0}, {1, 0}}: "─",
|
{{-1, 0}, {1, 0}}: "─",
|
||||||
@ -134,7 +133,7 @@ func basicLine(term *terminal.Terminal, from, to box) {
|
|||||||
dirInv := dir.Inv()
|
dirInv := dir.Inv()
|
||||||
start := boxEdgeAdj(from, dir)
|
start := boxEdgeAdj(from, dir)
|
||||||
end := boxEdgeAdj(to, dirInv)
|
end := boxEdgeAdj(to, dirInv)
|
||||||
mid := start.Midpoint(end, 0)
|
mid := start.Midpoint(end, rounder)
|
||||||
|
|
||||||
along := func(xy, dir geo.XY) int {
|
along := func(xy, dir geo.XY) int {
|
||||||
if dir[0] != 0 {
|
if dir[0] != 0 {
|
||||||
|
192
gim/main.go
192
gim/main.go
@ -2,20 +2,38 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/ginger/gg"
|
||||||
"github.com/mediocregopher/ginger/gim/geo"
|
"github.com/mediocregopher/ginger/gim/geo"
|
||||||
"github.com/mediocregopher/ginger/gim/terminal"
|
"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 (
|
const (
|
||||||
framerate = 10
|
framerate = 10
|
||||||
frameperiod = time.Second / time.Duration(framerate)
|
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{}) {
|
func debugf(str string, args ...interface{}) {
|
||||||
if !strings.HasSuffix(str, "\n") {
|
if !strings.HasSuffix(str, "\n") {
|
||||||
str += "\n"
|
str += "\n"
|
||||||
@ -23,88 +41,130 @@ func debugf(str string, args ...interface{}) {
|
|||||||
fmt.Fprintf(os.Stderr, str, args...)
|
fmt.Fprintf(os.Stderr, str, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
func mkGraph() *gg.Graph {
|
||||||
// * Use actual gg graphs and not fake "boxes"
|
aE0 := gg.ValueOut(str("a"), str("aE0"))
|
||||||
// - This will involve wrapping the vertices in some way, to preserve position
|
aE1 := gg.ValueOut(str("a"), str("aE1"))
|
||||||
// * Once gg graphs are used we can use that birds-eye-view to make better
|
aE2 := gg.ValueOut(str("a"), str("aE2"))
|
||||||
// decisions about edge placement
|
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() {
|
func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
term := terminal.New()
|
term := terminal.New()
|
||||||
|
term.Reset()
|
||||||
|
termSize := term.WindowSize()
|
||||||
|
g := mkGraph()
|
||||||
|
|
||||||
type movingBox struct {
|
// level 0 is at the bottom of the screen, cause life is easier that way
|
||||||
box
|
levels := map[*gg.Vertex]int{}
|
||||||
xRight bool
|
getLevel := func(v *gg.Vertex) int {
|
||||||
yDown bool
|
// if any of the tos have a level, this will be greater than the max
|
||||||
}
|
toMax := -1
|
||||||
|
for _, e := range v.Out {
|
||||||
randBox := func() movingBox {
|
lvl, ok := levels[e.To]
|
||||||
tsize := term.WindowSize()
|
if !ok {
|
||||||
return movingBox{
|
continue
|
||||||
box: box{
|
} else if lvl > toMax {
|
||||||
pos: geo.XY{rand.Intn(tsize[0]), rand.Intn(tsize[1])},
|
toMax = lvl
|
||||||
size: geo.XY{30, 2},
|
|
||||||
},
|
|
||||||
xRight: rand.Intn(1) == 0,
|
|
||||||
yDown: rand.Intn(1) == 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boxes := []movingBox{
|
if toMax >= 0 {
|
||||||
randBox(),
|
return toMax + 1
|
||||||
randBox(),
|
}
|
||||||
randBox(),
|
|
||||||
randBox(),
|
// otherwise level is 0
|
||||||
randBox(),
|
return 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
for range time.Tick(frameperiod) {
|
||||||
|
|
||||||
// update phase
|
// update phase
|
||||||
termSize := term.WindowSize()
|
// nufin
|
||||||
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]--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw phase
|
// draw phase
|
||||||
term.Reset()
|
term.Reset()
|
||||||
for i := range boxes {
|
for v := range boxes {
|
||||||
boxes[i].draw(term)
|
boxes[v].draw(term)
|
||||||
}
|
}
|
||||||
term.Flush()
|
for _, line := range lines {
|
||||||
for i := range boxes {
|
basicLine(term, line[0], line[1])
|
||||||
if i == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
basicLine(term, boxes[i-1].box, boxes[i].box)
|
|
||||||
}
|
}
|
||||||
term.Flush()
|
term.Flush()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user