Ditch gim... for now

It was an interesting idea, but now that an actual text-based syntax is
worked out and definitely going to be used gim is just making tests
fail for no gain. It can be resurrected from the git history in the
future, if needed.
This commit is contained in:
Brian Picciano 2021-12-29 12:51:16 -07:00
parent e7991adfaa
commit f5f0f6e436
17 changed files with 0 additions and 1887 deletions

111
gim/NOTES
View File

@ -1,111 +0,0 @@
Notes from reading https://www.graphviz.org/Documentation/TSE93.pdf, which
describes an algorithm for drawing an acyclic graph in basically the way which I
want.
This document assumes the primary flow of drawing is downward, and secondary is
right.
For all of this it might be easier to not even consider edge values yet, as
those could be done by converting them into vertices themselves after the
cyclic-edge-reversal and then converting them back later.
Drawing the graph is a four step process:
1) Rank nodes in the Y axis
- Graph must be acyclic.
- This can be accomplished by strategically reversing edges which cause
a cycle, and then reversing them back as a post-processing step.
- Edges can be found by:
- walking out from a particular node depth-first from some arbitrary
node.
- As you do so you assign a rank based on depth to each node you
encounter.
- If any edge is destined for a node which has already been seen you
look at the ranks of the source and destination, and if the source
is _greater_ than the destination you reverse the edge's
direction.
- I think that algorithm only works if there's a source/sink? might have
to be modified, or the walk must traverse both to & from.
- Assign all edges a weight, default 1, but possibly externally assigned to
be greater.
- Take a "feasible" minimum spanning tree (MST) of the graph
- Feasibility is defined as each edge being "tight", meaning, once you
rank each node by their distance from the root and define the length
of an edge as the difference of rank of its head and tail, that each
tree edge will have a length of 1.
- Perform the following on the MST:
- For each edge of the graph assign the cut value
- If you were to remove any edge of an MST it would create two
separate MSTs. The side the edge was pointing from is the tail,
the side it was pointing to is the head.
- Looking at edges _in the original graph_, sum the weights of all
edges directed from the tail to the head (including the one
removed) and subtract from that the sum of the weights of the
edges directed from the head to the tail. This is the cut value.
- "...note that the cut values can be computed using information
local to an edge if the search is ordered from the leaves of the
feasible tree inward. It is trivial to compute the cut value of a
tree edge with one of its endpoints a leaf in the tree, since
either the head or the tail component consists of a single node.
Now, assuming the cut values are known for all the edges incident
on a given node except one, the cut value of the remaining edge is
the sum of the known cut values plus a term dependent only on the
edges incident to the given node."
- Take an edge with a negative cut value and remove it. Find the graph
edge between the remaining head and tail MSTs with the smallest
"slack" (distance in rank between its ends) and add that edge to the
MST to make it connected again.
- Repeat until there are no negative cut values.
- Apparently searching "cyclically" through the negative edges, rather
than iterating from the start each time, is worthwhile.
- Normalize the MST by assigning the root node the rank of 0 (and so on), if
it changed.
- All edges in the MST are of length 1, and the rest can be inferred from
that.
- To reduce crowding, nodes with equal in/out edge weights and which could
be placed on multiple rankings are moved to the ranking with the fewest
nodes.
2) Order nodes in the X axis to reduce edge crossings
- Add ephemeral vertices along edges with lengths greater than 1, so all
"spaces" are filled.
- If any vertices have edges to vertices on their same rank, those are
ordered so that all these "flag edges" are pointed in the same direction
across that rank, and the ordering of those particular vertices is always
kept.
- Iterate over the graph some fixed number of times (the paper recommends
24)
- possibly with some heuristic which looks at percentage improvement
each time to determine if it's worth the effort.
- on one iteration move "down" the graph, on the next move "up", etc...
shaker style
- On each iteration:
- For each vertex look at the median position of all of the vertices
it has edges to in the previous rank
- If the number of previous vertices is even do this complicated
thing (P is the set of positions previous):
```
if |P| = 2 then
return (P[0] + P[1])/2;
else
left = P[m-1] - P[0];
right = P[|P| -1] - P[m];
return (P[m-1]*right + P[m]*left)/(left+right);
endif
```
- Sort the vertices by their median position
- vertices with no previous vertices remain fixed
- Then, for each vertex in the rank attempt to transpose it with its
neighbor and see if that reduces the number of edge crossings
between the rank and its previous.
- If equality is found during these two steps (same median, or same
number of crossings) the vertices in question should be flipped.
3) Compute node coordinates
- Determining the Y coordinates is considered trivial: find the maxHeight of
each rank, and ensure they are separated by that much plus whatever the
separation value is.
- For the X coordinates: do some insane shit involving the network simplex
again.
4) Determine edge splines

View File

@ -1,139 +0,0 @@
// 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.
type XY [2]int
// Zero is the zero point, or a zero vector, depending on what you're doing
var Zero = XY{0, 0}
// Unit vectors
var (
Up = XY{0, -1}
Down = XY{0, 1}
Left = XY{-1, 0}
Right = XY{1, 0}
)
// Units is the set of unit vectors
var Units = []XY{
Up,
Down,
Left,
Right,
}
func (xy XY) toF64() [2]float64 {
return [2]float64{
float64(xy[0]),
float64(xy[1]),
}
}
func abs(i int) int {
if i < 0 {
return i * -1
}
return i
}
// Abs returns the XY with all fields made positive, if they weren't already
func (xy XY) Abs() XY {
return XY{abs(xy[0]), abs(xy[1])}
}
// Unit returns the XY with each field divided by its absolute value (i.e.
// scaled down to 1 or -1). Fields which are 0 are left alone
func (xy XY) Unit() XY {
for i := range xy {
if xy[i] > 0 {
xy[i] = 1
} else if xy[i] < 0 {
xy[i] = -1
}
}
return xy
}
// Len returns the length (aka magnitude) of the XY as a vector.
func (xy XY) Len() int {
if xy[0] == 0 {
return abs(xy[1])
} else if xy[1] == 0 {
return abs(xy[0])
}
xyf := xy.toF64()
lf := math.Sqrt((xyf[0] * xyf[0]) + (xyf[1] * xyf[1]))
return Rounder.Round(lf)
}
// Add returns the result of adding the two XYs' fields individually
func (xy XY) Add(xy2 XY) XY {
xy[0] += xy2[0]
xy[1] += xy2[1]
return xy
}
// Mul returns the result of multiplying the two XYs' fields individually
func (xy XY) Mul(xy2 XY) XY {
xy[0] *= xy2[0]
xy[1] *= xy2[1]
return xy
}
// Div returns the results of dividing the two XYs' field individually.
func (xy XY) Div(xy2 XY) XY {
xyf, xy2f := xy.toF64(), xy2.toF64()
return XY{
Rounder.Round(xyf[0] / xy2f[0]),
Rounder.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})
}
// Inv inverses the XY, a shortcut for xy.Scale(-1)
func (xy XY) Inv() XY {
return xy.Scale(-1)
}
// Sub subtracts xy2 from xy and returns the result. A shortcut for
// xy.Add(xy2.Inv())
func (xy XY) Sub(xy2 XY) XY {
return xy.Add(xy2.Inv())
}
// Midpoint returns the midpoint between the two XYs.
func (xy XY) Midpoint(xy2 XY) XY {
return xy.Add(xy2.Sub(xy).Div(XY{2, 2}))
}
// 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
}

View File

@ -1,127 +0,0 @@
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 describes a straight edge starting at its first XY and ending at its
// second
type Edge [2]XY
// EdgeCoord 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) EdgeCoord(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))
}
}
// Edge returns an Edge instance for the edge of the Rect indicated by the given
// direction (Up, Down, Left, or Right). secDir indicates the direction the
// returned Edge should be pointing (i.e. the order of its XY's) and must be
// perpendicular to dir
func (r Rect) Edge(dir, secDir XY) Edge {
var e Edge
switch dir {
case Up:
e[0], e[1] = r.Corner(Left, Up), r.Corner(Right, Up)
case Down:
e[0], e[1] = r.Corner(Left, Down), r.Corner(Right, Down)
case Left:
e[0], e[1] = r.Corner(Left, Up), r.Corner(Left, Down)
case Right:
e[0], e[1] = r.Corner(Right, Up), r.Corner(Right, Down)
default:
panic(fmt.Sprintf("unsupported direction: %#v", dir))
}
switch secDir {
case Left, Up:
e[0], e[1] = e[1], e[0]
default:
// do nothing
}
return e
}
// Midpoint returns the point which is the midpoint of the Edge
func (e Edge) Midpoint() XY {
return e[0].Midpoint(e[1])
}
func (r Rect) halfSize() XY {
return r.Size.Div(XY{2, 2})
}
// Center returns the centerpoint of the rectangle.
func (r Rect) Center() XY {
return r.TopLeft.Add(r.halfSize())
}
// 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.
func (r Rect) Centered(on XY) Rect {
r.TopLeft = on.Sub(r.halfSize())
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}),
}
}

View File

@ -1,113 +0,0 @@
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.EdgeCoord(Up))
assert.Equal(t, 3, r.EdgeCoord(Down))
assert.Equal(t, 1, r.EdgeCoord(Left))
assert.Equal(t, 2, r.EdgeCoord(Right))
lu := XY{1, 2}
ld := XY{1, 3}
ru := XY{2, 2}
rd := XY{2, 3}
assert.Equal(t, lu, r.Corner(Left, Up))
assert.Equal(t, ld, r.Corner(Left, Down))
assert.Equal(t, ru, r.Corner(Right, Up))
assert.Equal(t, rd, r.Corner(Right, Down))
assert.Equal(t, Edge{lu, ld}, r.Edge(Left, Down))
assert.Equal(t, Edge{ru, rd}, r.Edge(Right, Down))
assert.Equal(t, Edge{lu, ru}, r.Edge(Up, Right))
assert.Equal(t, Edge{ld, rd}, r.Edge(Down, Right))
assert.Equal(t, Edge{ld, lu}, r.Edge(Left, Up))
assert.Equal(t, Edge{rd, ru}, r.Edge(Right, Up))
assert.Equal(t, Edge{ru, lu}, r.Edge(Up, Left))
assert.Equal(t, Edge{rd, ld}, r.Edge(Down, Left))
}
func TestRectCenter(t *T) {
assertCentered := func(exp, given Rect, center XY) {
got := given.Centered(center)
assert.Equal(t, exp, got)
assert.Equal(t, center, got.Center())
}
{
r := Rect{
Size: XY{4, 4},
}
assert.Equal(t, XY{2, 2}, r.Center())
assertCentered(
Rect{TopLeft: XY{1, 1}, Size: XY{4, 4}},
r, XY{3, 3},
)
}
{
r := Rect{
Size: XY{5, 5},
}
assert.Equal(t, XY{3, 3}, r.Center())
assertCentered(
Rect{TopLeft: XY{0, 0}, Size: XY{5, 5}},
r, XY{3, 3},
)
}
}
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)
}
}

View File

@ -1,33 +0,0 @@
package geo
import (
"math"
)
// RounderFunc is a function which converts a floating point number into an
// integer.
type RounderFunc func(float64) int64
// Round is helper for calling the RounderFunc and converting the result to an
// int.
func (rf RounderFunc) Round(f float64) int {
return int(rf(f))
}
// A few RounderFuncs which can be used. Set the Rounder global variable to pick
// one.
var (
Floor RounderFunc = func(f float64) int64 { return int64(math.Floor(f)) }
Ceil RounderFunc = func(f float64) int64 { return int64(math.Ceil(f)) }
Round RounderFunc = func(f float64) int64 {
if f < 0 {
f = math.Ceil(f - 0.5)
}
f = math.Floor(f + 0.5)
return int64(f)
}
)
// Rounder is the RounderFunc which will be used by all functions and methods in
// this package when needed.
var Rounder = Ceil

View File

@ -1,91 +0,0 @@
package main
import (
"math/rand"
"time"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
"github.com/mediocregopher/ginger/gim/view"
)
// TODO be able to draw circular graphs
// TODO audit all steps, make sure everything is deterministic
// TODO self-edges
//const (
// framerate = 10
// frameperiod = time.Second / time.Duration(framerate)
//)
//func debugf(str string, args ...interface{}) {
// if !strings.HasSuffix(str, "\n") {
// str += "\n"
// }
// fmt.Fprintf(os.Stderr, str, args...)
//}
func mkGraph() (*gg.Graph, gg.Value) {
a := gg.NewValue("a")
aE0 := gg.NewValue("aE0")
aE1 := gg.NewValue("aE1")
aE2 := gg.NewValue("aE2")
aE3 := gg.NewValue("aE3")
b0 := gg.NewValue("b0")
b1 := gg.NewValue("b1")
b2 := gg.NewValue("b2")
b3 := gg.NewValue("b3")
oaE0 := gg.ValueOut(a, aE0)
oaE1 := gg.ValueOut(a, aE1)
oaE2 := gg.ValueOut(a, aE2)
oaE3 := gg.ValueOut(a, aE3)
g := gg.ZeroGraph
g = g.AddValueIn(oaE0, b0)
g = g.AddValueIn(oaE1, b1)
g = g.AddValueIn(oaE2, b2)
g = g.AddValueIn(oaE3, b3)
c := gg.NewValue("c")
empty := gg.NewValue("")
jE := gg.TupleOut([]gg.OpenEdge{
gg.ValueOut(b0, empty),
gg.ValueOut(b1, empty),
gg.ValueOut(b2, empty),
gg.ValueOut(b3, empty),
}, gg.NewValue("jE"))
g = g.AddValueIn(jE, c)
// TODO this really fucks it up
//d := gg.NewValue("d")
//deE := gg.ValueOut(d, gg.NewValue("deE"))
//g = g.AddValueIn(deE, gg.NewValue("e"))
return g, c
}
//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()
wSize := term.WindowSize()
center := geo.Zero.Midpoint(wSize)
g, start := mkGraph()
view := view.New(g, start, geo.Right, geo.Down)
viewBuf := terminal.NewBuffer()
view.Draw(viewBuf)
buf := terminal.NewBuffer()
buf.DrawBufferCentered(center, viewBuf)
term.Clear()
term.WriteBuffer(geo.Zero, buf)
term.SetPos(wSize.Add(geo.XY{0, -1}))
term.Draw()
}

View File

@ -1,217 +0,0 @@
package terminal
import (
"fmt"
"strconv"
"unicode"
"github.com/mediocregopher/ginger/gim/geo"
)
// Reset all custom styles
const ansiReset = "\033[0m"
// Color describes the foreground or background color of text
type Color int
// Available Color values
const (
// whatever the terminal's default color scheme is
Default = iota
Black
Red
Green
Yellow
Blue
Magenta
Cyan
White
)
type bufStyle struct {
fgColor Color
bgColor Color
}
// returns foreground and background ansi codes
func (bf bufStyle) ansi() (string, string) {
var fg, bg string
if bf.fgColor != Default {
fg = "\033[0;3" + strconv.Itoa(int(bf.fgColor)-1) + "m"
}
if bf.bgColor != Default {
bg = "\033[0;4" + strconv.Itoa(int(bf.bgColor)-1) + "m"
}
return fg, bg
}
// returns the ansi sequence which would modify the style to the given one
func (bf bufStyle) diffTo(bf2 bufStyle) string {
// this implementation is naive, but whatever
if bf == bf2 {
return ""
}
fg, bg := bf2.ansi()
if (bf == bufStyle{}) {
return fg + bg
}
return ansiReset + fg + bg
}
type bufPoint struct {
r rune
bufStyle
}
// Buffer describes an infinitely sized terminal buffer to which anything may be
// drawn, and which will efficiently generate strings representing the drawn
// text.
type Buffer struct {
currStyle bufStyle
currPos geo.XY
m *mat
max geo.XY
}
// NewBuffer initializes and returns a new empty buffer. The proper way to clear
// a buffer is to toss the old one and generate a new one.
func NewBuffer() *Buffer {
return &Buffer{
m: newMat(),
max: geo.XY{-1, -1},
}
}
// Copy creates a new identical instance of this Buffer and returns it.
func (b *Buffer) Copy() *Buffer {
b2 := NewBuffer()
b.m.iter(func(x, y int, v interface{}) bool {
b2.setRune(geo.XY{x, y}, v.(bufPoint))
return true
})
b2.currStyle = b.currStyle
b2.currPos = b.currPos
return b2
}
func (b *Buffer) setRune(at geo.XY, p bufPoint) {
b.m.set(at[0], at[1], p)
b.max = b.max.Max(at)
}
// WriteRune writes the given rune to the Buffer at whatever the current
// position is, with whatever the current styling is.
func (b *Buffer) WriteRune(r rune) {
if r == '\n' {
b.currPos[0], b.currPos[1] = 0, b.currPos[1]+1
return
} else if r == '\r' {
b.currPos[0] = 0
} else if !unicode.IsPrint(r) {
panic(fmt.Sprintf("character %q is not supported by terminal.Buffer", r))
}
b.setRune(b.currPos, bufPoint{
r: r,
bufStyle: b.currStyle,
})
b.currPos[0]++
}
// WriteString writes the given string to the Buffer at whatever the current
// position is, with whatever the current styling is.
func (b *Buffer) WriteString(s string) {
for _, r := range s {
b.WriteRune(r)
}
}
// SetPos sets the cursor position in the Buffer, so Print operations will begin
// at that point. Remember that the origin is at point (0, 0).
func (b *Buffer) SetPos(xy geo.XY) {
b.currPos = xy
}
// SetFGColor sets subsequent text's foreground color.
func (b *Buffer) SetFGColor(c Color) {
b.currStyle.fgColor = c
}
// SetBGColor sets subsequent text's background color.
func (b *Buffer) SetBGColor(c Color) {
b.currStyle.bgColor = c
}
// ResetStyle unsets all text styling options which have been set.
func (b *Buffer) ResetStyle() {
b.currStyle = bufStyle{}
}
// String renders and returns a string which, when printed to a terminal, will
// print the Buffer's contents at the terminal's current cursor position.
func (b *Buffer) String() string {
s := ansiReset // always start with a reset
var style bufStyle
var pos geo.XY
move := func(to geo.XY) {
diff := to.Sub(pos)
if diff[0] > 0 {
s += "\033[" + strconv.Itoa(diff[0]) + "C"
} else if diff[0] < 0 {
s += "\033[" + strconv.Itoa(-diff[0]) + "D"
}
if diff[1] > 0 {
s += "\033[" + strconv.Itoa(diff[1]) + "B"
} else if diff[1] < 0 {
s += "\033[" + strconv.Itoa(-diff[1]) + "A"
}
pos = to
}
b.m.iter(func(x, y int, v interface{}) bool {
p := v.(bufPoint)
move(geo.XY{x, y})
s += style.diffTo(p.bufStyle)
style = p.bufStyle
s += string(p.r)
pos[0]++
return true
})
return s
}
// DrawBuffer copies the given Buffer onto this one, with the given's top-left
// corner being at the given position. The given buffer may be the same as this
// one.
//
// Calling this method does not affect this Buffer's current cursor position or
// style.
func (b *Buffer) DrawBuffer(at geo.XY, b2 *Buffer) {
if b == b2 {
b2 = b2.Copy()
}
b2.m.iter(func(x, y int, v interface{}) bool {
x += at[0]
y += at[1]
if x < 0 || y < 0 {
return true
}
b.setRune(geo.XY{x, y}, v.(bufPoint))
return true
})
}
// DrawBufferCentered is like DrawBuffer, but centered around the given point
// instead of translated by it.
func (b *Buffer) DrawBufferCentered(around geo.XY, b2 *Buffer) {
b2rect := geo.Rect{Size: b2.Size()}
b.DrawBuffer(b2rect.Centered(around).TopLeft, b2)
}
// Size returns the dimensions of the Buffer's current area which has been
// written to.
func (b *Buffer) Size() geo.XY {
return b.max.Add(geo.XY{1, 1})
}

View File

@ -1,59 +0,0 @@
package main
import (
"log"
"time"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
func main() {
b := terminal.NewBuffer()
b.WriteString("this is fun")
b.SetFGColor(terminal.Blue)
b.SetBGColor(terminal.Green)
b.SetPos(geo.XY{18, 0})
b.WriteString("blue and green")
b.ResetStyle()
b.SetFGColor(terminal.Red)
b.SetPos(geo.XY{3, 3})
b.WriteString("red!!!")
b.ResetStyle()
b.SetFGColor(terminal.Blue)
b.SetPos(geo.XY{20, 0})
b.WriteString("boo")
bcp := b.Copy()
b.DrawBuffer(geo.XY{2, 2}, bcp)
b.DrawBuffer(geo.XY{-1, 1}, bcp)
brect := terminal.NewBuffer()
brect.DrawRect(geo.Rect{Size: b.Size().Add(geo.XY{2, 2})}, terminal.SingleLine)
log.Printf("b.Size:%v", b.Size())
brect.DrawBuffer(geo.XY{1, 1}, b)
t := terminal.New()
p := geo.XY{0, 0}
dirH, dirV := geo.Right, geo.Down
wsize := t.WindowSize()
for range time.Tick(time.Second / 15) {
t.Clear()
t.WriteBuffer(p, brect)
t.Draw()
brectSize := brect.Size()
p = p.Add(dirH).Add(dirV)
if p[0] < 0 || p[0]+brectSize[0] > wsize[0] {
dirH = dirH.Scale(-1)
p = p.Add(dirH.Scale(2))
}
if p[1] < 0 || p[1]+brectSize[1] > wsize[1] {
dirV = dirV.Scale(-1)
p = p.Add(dirV.Scale(2))
}
}
}

View File

@ -1,117 +0,0 @@
package terminal
import (
"container/list"
)
type matEl struct {
x int
v interface{}
}
type matRow struct {
y int
l *list.List
}
// a 2-d sparse matrix
type mat struct {
rows *list.List
currY int
currRowEl *list.Element
currEl *list.Element
}
func newMat() *mat {
return &mat{
rows: list.New(),
}
}
func (m *mat) getRow(y int) *list.List {
m.currY = y // this will end up being true no matter what
if m.currRowEl == nil { // first call
l := list.New()
m.currRowEl = m.rows.PushFront(matRow{y: y, l: l})
return l
} else if m.currRowEl.Value.(matRow).y > y {
m.currRowEl = m.rows.Front()
}
for {
currRow := m.currRowEl.Value.(matRow)
switch {
case currRow.y == y:
return currRow.l
case currRow.y < y:
if m.currRowEl = m.currRowEl.Next(); m.currRowEl == nil {
l := list.New()
m.currRowEl = m.rows.PushBack(matRow{y: y, l: l})
return l
}
default: // currRow.y > y
l := list.New()
m.currRowEl = m.rows.InsertBefore(matRow{y: y, l: l}, m.currRowEl)
return l
}
}
}
func (m *mat) getEl(x, y int) *matEl {
var rowL *list.List
if m.currRowEl == nil || m.currY != y {
rowL = m.getRow(y)
m.currEl = rowL.Front()
} else {
rowL = m.currRowEl.Value.(matRow).l
}
if m.currEl == nil || m.currEl.Value.(*matEl).x > x {
if m.currEl = rowL.Front(); m.currEl == nil {
// row is empty
mel := &matEl{x: x}
m.currEl = rowL.PushFront(mel)
return mel
}
}
for {
currEl := m.currEl.Value.(*matEl)
switch {
case currEl.x == x:
return currEl
case currEl.x < x:
if m.currEl = m.currEl.Next(); m.currEl == nil {
mel := &matEl{x: x}
m.currEl = rowL.PushBack(mel)
return mel
}
default: // currEl.x > x
mel := &matEl{x: x}
m.currEl = rowL.InsertBefore(mel, m.currEl)
return mel
}
}
}
func (m *mat) get(x, y int) interface{} {
return m.getEl(x, y).v
}
func (m *mat) set(x, y int, v interface{}) {
m.getEl(x, y).v = v
}
func (m *mat) iter(f func(x, y int, v interface{}) bool) {
for rowEl := m.rows.Front(); rowEl != nil; rowEl = rowEl.Next() {
row := rowEl.Value.(matRow)
for el := row.l.Front(); el != nil; el = el.Next() {
mel := el.Value.(*matEl)
if !f(mel.x, row.y, mel.v) {
return
}
}
}
}

View File

@ -1,59 +0,0 @@
package terminal
import (
"fmt"
"math/rand"
"strings"
. "testing"
"time"
)
func TestMat(t *T) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
type xy struct {
x, y int
}
type action struct {
xy
set int
}
run := func(aa []action) {
aaStr := func(i int) string {
s := fmt.Sprintf("%#v", aa[:i+1])
return strings.Replace(s, "terminal.", "", -1)
}
m := newMat()
mm := map[xy]int{}
for i, a := range aa {
if a.set > 0 {
mm[a.xy] = a.set
m.set(a.xy.x, a.xy.y, a.set)
continue
}
expI, expOk := mm[a.xy]
gotI, gotOk := m.get(a.xy.x, a.xy.y).(int)
if expOk != gotOk {
t.Fatalf("get failed: expOk:%v gotOk:%v actions:%#v", expOk, gotOk, aaStr(i))
} else if expI != gotI {
t.Fatalf("get failed: expI:%v gotI:%v actions:%#v", expI, gotI, aaStr(i))
}
}
}
for i := 0; i < 10000; i++ {
var actions []action
for j := r.Intn(1000); j > 0; j-- {
a := action{xy: xy{x: r.Intn(5), y: r.Intn(5)}}
if r.Intn(3) == 0 {
a.set = r.Intn(10000) + 1
}
actions = append(actions, a)
}
run(actions)
}
}

View File

@ -1,189 +0,0 @@
package terminal
import (
"fmt"
"strings"
"github.com/mediocregopher/ginger/gim/geo"
)
// SingleLine is a set of single-pixel-width lines.
var SingleLine = LineStyle{
Horiz: '─',
Vert: '│',
TopLeft: '┌',
TopRight: '┐',
BottomLeft: '└',
BottomRight: '┘',
PerpUp: '┴',
PerpDown: '┬',
PerpLeft: '┤',
PerpRight: '├',
ArrowUp: '^',
ArrowDown: 'v',
ArrowLeft: '<',
ArrowRight: '>',
}
// LineStyle defines a set of characters to use together when drawing lines and
// corners.
type LineStyle struct {
Horiz, Vert rune
// Corner characters, identified as corners of a rectangle
TopLeft, TopRight, BottomLeft, BottomRight rune
// Characters for a straight segment a perpendicular attached
PerpUp, PerpDown, PerpLeft, PerpRight rune
// Characters for pointing arrows
ArrowUp, ArrowDown, ArrowLeft, ArrowRight rune
}
// Segment takes two different directions (i.e. geo.Up/Down/Left/Right) and
// returns the line character which points in both of those directions.
//
// For example, SingleLine.Segment(geo.Up, geo.Left) returns '┘'.
func (ls LineStyle) Segment(a, b geo.XY) rune {
inner := func(a, b geo.XY) rune {
type c struct{ a, b geo.XY }
switch (c{a, b}) {
case c{geo.Up, geo.Down}:
return ls.Vert
case c{geo.Left, geo.Right}:
return ls.Horiz
case c{geo.Down, geo.Right}:
return ls.TopLeft
case c{geo.Down, geo.Left}:
return ls.TopRight
case c{geo.Up, geo.Right}:
return ls.BottomLeft
case c{geo.Up, geo.Left}:
return ls.BottomRight
default:
return 0
}
}
if r := inner(a, b); r != 0 {
return r
} else if r = inner(b, a); r != 0 {
return r
}
panic(fmt.Sprintf("invalid LineStyle.Segment directions: %v, %v", a, b))
}
// Perpendicular returns the line character for a perpendicular segment
// traveling in the given direction.
func (ls LineStyle) Perpendicular(dir geo.XY) rune {
switch dir {
case geo.Up:
return ls.PerpUp
case geo.Down:
return ls.PerpDown
case geo.Left:
return ls.PerpLeft
case geo.Right:
return ls.PerpRight
default:
panic(fmt.Sprintf("invalid LineStyle.Perpendicular direction: %v", dir))
}
}
// Arrow returns the arrow character for an arrow pointing in the given
// direction.
func (ls LineStyle) Arrow(dir geo.XY) rune {
switch dir {
case geo.Up:
return ls.ArrowUp
case geo.Down:
return ls.ArrowDown
case geo.Left:
return ls.ArrowLeft
case geo.Right:
return ls.ArrowRight
default:
panic(fmt.Sprintf("invalid LineStyle.Arrow direction: %v", dir))
}
}
// DrawRect draws the given Rect to the Buffer with the given LineStyle. The
// Rect's TopLeft field is used for its position.
//
// If Rect's Size is not at least 2x2 this does nothing.
func (b *Buffer) DrawRect(r geo.Rect, ls LineStyle) {
if r.Size[0] < 2 || r.Size[1] < 2 {
return
}
horiz := strings.Repeat(string(ls.Horiz), r.Size[0]-2)
b.SetPos(r.TopLeft)
b.WriteRune(ls.TopLeft)
b.WriteString(horiz)
b.WriteRune(ls.TopRight)
for i := 0; i < r.Size[1]-2; i++ {
b.SetPos(r.TopLeft.Add(geo.XY{0, i + 1}))
b.WriteRune(ls.Vert)
b.SetPos(r.TopLeft.Add(geo.XY{r.Size[0] - 1, i + 1}))
b.WriteRune(ls.Vert)
}
b.SetPos(r.TopLeft.Add(geo.XY{0, r.Size[1] - 1}))
b.WriteRune(ls.BottomLeft)
b.WriteString(horiz)
b.WriteRune(ls.BottomRight)
}
// DrawLine draws a line from the start point to the ending one, primarily
// moving in the given direction, using the given LineStyle to do so.
func (b *Buffer) DrawLine(start, end, dir geo.XY, ls LineStyle) {
// given the "primary" direction the line should be headed, pick a possible
// secondary one which may be used to detour along the path in order to
// reach the destination (in the case that the two boxes are diagonal from
// each other)
var perpDir geo.XY
perpDir[0], perpDir[1] = dir[1], dir[0]
dirSec := end.Sub(start).Mul(perpDir.Abs()).Unit()
mid := start.Midpoint(end)
along := func(xy, dir geo.XY) int {
if dir[0] != 0 {
return xy[0]
}
return xy[1]
}
// collect the points along the line into an array
var pts []geo.XY
var curr geo.XY
midPrim := along(mid, dir)
endSec := along(end, dirSec)
for curr = start; curr != end; {
pts = append(pts, curr)
if prim := along(curr, dir); prim == midPrim {
if sec := along(curr, dirSec); sec != endSec {
curr = curr.Add(dirSec)
continue
}
}
curr = curr.Add(dir)
}
pts = append(pts, curr) // appending end
// draw each point
for i, pt := range pts {
var prev, next geo.XY
switch {
case i == 0:
prev = pt.Add(dir.Inv())
next = pts[i+1]
case i == len(pts)-1:
prev = pts[i-1]
next = pt.Add(dir)
default:
prev, next = pts[i-1], pts[i+1]
}
b.SetPos(pt)
b.WriteRune(ls.Segment(prev.Sub(pt), next.Sub(pt)))
}
}

View File

@ -1,108 +0,0 @@
// Package terminal implements functionality related to interacting with a
// terminal. Using this package takes the place of using stdout directly
package terminal
import (
"bytes"
"fmt"
"io"
"os"
"syscall"
"unsafe"
"github.com/mediocregopher/ginger/gim/geo"
)
// Terminal provides an interface to a terminal which allows for "drawing"
// rather than just writing. Note that all operations on a Terminal aren't
// actually drawn to the screen until Flush is called.
//
// The coordinate system described by Terminal looks like this:
//
// 0,0 ------------------> x
// |
// |
// |
// |
// |
// |
// |
// |
// v
// y
//
type Terminal struct {
buf *bytes.Buffer
// When initialized this will be set to os.Stdout, but can be set to
// anything
Out io.Writer
}
// New initializes and returns a usable Terminal
func New() *Terminal {
return &Terminal{
buf: new(bytes.Buffer),
Out: os.Stdout,
}
}
// WindowSize returns the size of the terminal window (width/height)
// TODO this doesn't support winblows
func (t *Terminal) WindowSize() geo.XY {
var sz struct {
rows uint16
cols uint16
xpixels uint16
ypixels uint16
}
_, _, err := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(syscall.Stdin),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&sz)),
)
if err != 0 {
panic(err.Error())
}
return geo.XY{int(sz.cols), int(sz.rows)}
}
// SetPos sets the terminal's actual cursor position to the given coordinates.
func (t *Terminal) SetPos(to geo.XY) {
// actual terminal uses 1,1 as top-left, because 1-indexing is a great idea
fmt.Fprintf(t.buf, "\033[%d;%dH", to[1]+1, to[0]+1)
}
// HideCursor causes the cursor to not actually be shown
func (t *Terminal) HideCursor() {
fmt.Fprintf(t.buf, "\033[?25l")
}
// ShowCursor causes the cursor to be shown, if it was previously hidden
func (t *Terminal) ShowCursor() {
fmt.Fprintf(t.buf, "\033[?25h")
}
// Clear completely clears all drawn characters on the screen and returns the
// cursor to the origin. This implicitly calls Draw.
func (t *Terminal) Clear() {
t.buf.Reset()
fmt.Fprintf(t.buf, "\033[2J")
t.Draw()
}
// WriteBuffer writes the contents to the Buffer to the Terminal's buffer,
// starting at the given coordinate.
func (t *Terminal) WriteBuffer(at geo.XY, b *Buffer) {
t.SetPos(at)
t.buf.WriteString(b.String())
}
// Draw writes all buffered changes to the screen
func (t *Terminal) Draw() {
if _, err := io.Copy(t.Out, t.buf); err != nil {
panic(err)
}
t.buf.Reset()
}

View File

@ -1,67 +0,0 @@
package view
import (
"fmt"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
type box struct {
topLeft geo.XY
flowDir geo.XY
numIn, numOut int
bodyBuf *terminal.Buffer
transparent bool
}
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.ValueVertex {
b.bodyBuf = terminal.NewBuffer()
b.bodyBuf.WriteString(v.Value.V.(string))
}
return b
}
func (b box) rect() geo.Rect {
var bodyRect geo.Rect
if b.bodyBuf != nil {
bodyRect.Size = b.bodyBuf.Size().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{2, neededByEdges}
case geo.Up, geo.Down:
edgesRect.Size = geo.XY{neededByEdges, 2}
default:
panic(fmt.Sprintf("unknown flowDir: %#v", b.flowDir))
}
}
return bodyRect.Union(edgesRect).Translate(b.topLeft)
}
func (b box) draw(buf *terminal.Buffer) {
rect := b.rect()
buf.DrawRect(rect, terminal.SingleLine)
if b.bodyBuf != nil {
buf.DrawBufferCentered(rect.Center(), b.bodyBuf)
}
}

View File

@ -1,119 +0,0 @@
// Package constraint implements an extremely simple constraint engine.
// Elements, and constraints on those elements, are given to the engine, which
// uses those constraints to generate an output. Elements are defined as a
// string
package constraint
import (
"github.com/mediocregopher/ginger/gg"
)
// Constraint describes a constraint on an element. The Elem field must be
// filled in, as well as exactly one other field
type Constraint struct {
Elem string
// LT says that Elem is less than this element
LT string
}
var ltEdge = gg.NewValue("lt")
// Engine processes sets of constraints to generate an output
type Engine struct {
g *gg.Graph
vals map[string]gg.Value
}
// NewEngine initializes and returns an empty Engine
func NewEngine() *Engine {
return &Engine{g: gg.ZeroGraph, vals: map[string]gg.Value{}}
}
func (e *Engine) getVal(elem string) gg.Value {
if val, ok := e.vals[elem]; ok {
return val
}
val := gg.NewValue(elem)
e.vals[elem] = val
return val
}
// AddConstraint adds the given constraint to the engine's set, returns false if
// the constraint couldn't be added due to a conflict with a previous constraint
func (e *Engine) AddConstraint(c Constraint) bool {
elem := e.getVal(c.Elem)
g := e.g.AddValueIn(gg.ValueOut(elem, ltEdge), e.getVal(c.LT))
// Check for loops in g starting at c.Elem, bail if there are any
{
seen := map[*gg.Vertex]bool{}
start := g.ValueVertex(elem)
var hasLoop func(v *gg.Vertex) bool
hasLoop = func(v *gg.Vertex) bool {
if seen[v] {
return v == start
}
seen[v] = true
for _, out := range v.Out {
if hasLoop(out.To) {
return true
}
}
return false
}
if hasLoop(start) {
return false
}
}
e.g = g
return true
}
// Solve uses the constraints which have been added to the engine to give a
// possible solution. The given element is one which has been added to the
// engine and whose value is known to be zero.
func (e *Engine) Solve() map[string]int {
m := map[string]int{}
if len(e.g.ValueVertices()) == 0 {
return m
}
vElem := func(v *gg.Vertex) string {
return v.Value.V.(string)
}
// first the roots are determined to be the elements with no In edges, which
// _must_ exist since the graph presumably has no loops
var roots []*gg.Vertex
e.g.Iter(func(v *gg.Vertex) bool {
if len(v.In) == 0 {
roots = append(roots, v)
m[vElem(v)] = 0
}
return true
})
// sanity check
if len(roots) == 0 {
panic("no roots found in graph somehow")
}
// a vertex's value is then the length of the longest path from it to one of
// the roots
var walk func(*gg.Vertex, int)
walk = func(v *gg.Vertex, val int) {
if elem := vElem(v); val > m[elem] {
m[elem] = val
}
for _, out := range v.Out {
walk(out.To, val+1)
}
}
for _, root := range roots {
walk(root, 0)
}
return m
}

View File

@ -1,94 +0,0 @@
package constraint
import (
. "testing"
"github.com/stretchr/testify/assert"
)
func TestEngineAddConstraint(t *T) {
{
e := NewEngine()
assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "1"}))
assert.True(t, e.AddConstraint(Constraint{Elem: "1", LT: "2"}))
assert.True(t, e.AddConstraint(Constraint{Elem: "-1", LT: "0"}))
assert.False(t, e.AddConstraint(Constraint{Elem: "1", LT: "0"}))
assert.False(t, e.AddConstraint(Constraint{Elem: "2", LT: "0"}))
assert.False(t, e.AddConstraint(Constraint{Elem: "2", LT: "-1"}))
}
{
e := NewEngine()
assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "1"}))
assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "2"}))
assert.True(t, e.AddConstraint(Constraint{Elem: "1", LT: "2"}))
assert.True(t, e.AddConstraint(Constraint{Elem: "2", LT: "3"}))
}
}
func TestEngineSolve(t *T) {
assertSolve := func(exp map[string]int, cc ...Constraint) {
e := NewEngine()
for _, c := range cc {
assert.True(t, e.AddConstraint(c), "c:%#v", c)
}
assert.Equal(t, exp, e.Solve())
}
// basic
assertSolve(
map[string]int{"a": 0, "b": 1, "c": 2},
Constraint{Elem: "a", LT: "b"},
Constraint{Elem: "b", LT: "c"},
)
// "triangle" graph
assertSolve(
map[string]int{"a": 0, "b": 1, "c": 2},
Constraint{Elem: "a", LT: "b"},
Constraint{Elem: "a", LT: "c"},
Constraint{Elem: "b", LT: "c"},
)
// "hexagon" graph
assertSolve(
map[string]int{"a": 0, "b": 1, "c": 1, "d": 2, "e": 2, "f": 3},
Constraint{Elem: "a", LT: "b"},
Constraint{Elem: "a", LT: "c"},
Constraint{Elem: "b", LT: "d"},
Constraint{Elem: "c", LT: "e"},
Constraint{Elem: "d", LT: "f"},
Constraint{Elem: "e", LT: "f"},
)
// "hexagon" with centerpoint graph
assertSolve(
map[string]int{"a": 0, "b": 1, "c": 1, "center": 2, "d": 3, "e": 3, "f": 4},
Constraint{Elem: "a", LT: "b"},
Constraint{Elem: "a", LT: "c"},
Constraint{Elem: "b", LT: "d"},
Constraint{Elem: "c", LT: "e"},
Constraint{Elem: "d", LT: "f"},
Constraint{Elem: "e", LT: "f"},
Constraint{Elem: "c", LT: "center"},
Constraint{Elem: "b", LT: "center"},
Constraint{Elem: "center", LT: "e"},
Constraint{Elem: "center", LT: "d"},
)
// multi-root, using two triangles which end up connecting
assertSolve(
map[string]int{"a": 0, "b": 1, "c": 2, "d": 0, "e": 1, "f": 2, "g": 3},
Constraint{Elem: "a", LT: "b"},
Constraint{Elem: "a", LT: "c"},
Constraint{Elem: "b", LT: "c"},
Constraint{Elem: "d", LT: "e"},
Constraint{Elem: "d", LT: "f"},
Constraint{Elem: "e", LT: "f"},
Constraint{Elem: "f", LT: "g"},
)
}

View File

@ -1,31 +0,0 @@
package view
import (
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
type line struct {
from, to *box
fromI, toI int
bodyBuf *terminal.Buffer
}
func (l line) draw(buf *terminal.Buffer, flowDir, secFlowDir geo.XY) {
from, to := *(l.from), *(l.to)
start := from.rect().Edge(flowDir, secFlowDir)[0].Add(secFlowDir.Scale(l.fromI*2 + 1))
end := to.rect().Edge(flowDir.Inv(), secFlowDir)[0]
end = end.Add(flowDir.Inv())
end = end.Add(secFlowDir.Scale(l.toI*2 + 1))
buf.SetPos(start)
buf.WriteRune(terminal.SingleLine.Perpendicular(flowDir))
buf.DrawLine(start.Add(flowDir), end.Add(flowDir.Inv()), flowDir, terminal.SingleLine)
buf.SetPos(end)
buf.WriteRune(terminal.SingleLine.Arrow(flowDir))
// draw the body
if l.bodyBuf != nil {
buf.DrawBufferCentered(start.Midpoint(end), l.bodyBuf)
}
}

View File

@ -1,213 +0,0 @@
// Package view implements rendering a graph to a terminal.
package view
import (
"sort"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
"github.com/mediocregopher/ginger/gim/view/constraint"
)
// View wraps a single Graph instance and a set of display options for it, and
// generates renderable terminal output for it.
type View struct {
g *gg.Graph
start gg.Value
primFlowDir, secFlowDir geo.XY
}
// New instantiates and returns a view around the given Graph instance, with
// start indicating the value vertex to consider the "root" of the graph.
//
// Drawing is done by aligning the vertices into rows and columns in such a way
// as to reduce edge crossings. primaryDir indicates the direction edges will
// primarily be pointed in. For example, if it is geo.Down then adjacent
// vertices will be arranged into columns.
//
// secondaryDir indicates the direction vertices should be arranged when they
// end up in the same "rank" (e.g. when primaryDir is geo.Down, all vertices on
// the same row will be the same "rank").
//
// A primaryDir/secondaryDir of either geo.Down/geo.Right or geo.Right/geo.Down
// are recommended, but any combination of perpendicular directions is allowed.
func New(g *gg.Graph, start gg.Value, primaryDir, secondaryDir geo.XY) *View {
return &View{
g: g,
start: start,
primFlowDir: primaryDir,
secFlowDir: secondaryDir,
}
}
// Draw renders and draws the View's Graph to the Buffer.
func (view *View) Draw(buf *terminal.Buffer) {
relPos, _, secSol := posSolve(view.g)
// create boxes
var boxes []*box
boxesM := map[*box]*gg.Vertex{}
boxesMr := map[*gg.Vertex]*box{}
const (
primPadding = 5
secPadding = 1
)
var primPos int
for _, vv := range relPos {
var primBoxes []*box // boxes on just this level
var maxPrim int
var secPos int
for _, v := range vv {
primVec := view.primFlowDir.Scale(primPos)
secVec := view.secFlowDir.Scale(secPos)
b := boxFromVertex(v, view.primFlowDir)
b.topLeft = primVec.Add(secVec)
boxes = append(boxes, &b)
primBoxes = append(primBoxes, &b)
boxesM[&b] = v
boxesMr[v] = &b
bSize := b.rect().Size
primBoxLen := bSize.Mul(view.primFlowDir).Len()
secBoxLen := bSize.Mul(view.secFlowDir).Len()
if primBoxLen > maxPrim {
maxPrim = primBoxLen
}
secPos += secBoxLen + secPadding
}
for _, b := range primBoxes {
b.topLeft = b.topLeft.Add(view.primFlowDir.Scale(primPos))
}
primPos += maxPrim + primPadding
}
// maps a vertex to all of its to edges, sorted by secSol
findFromIM := map[*gg.Vertex][]gg.Edge{}
// returns the index of this edge in from's Out
findFromI := func(from *gg.Vertex, e gg.Edge) int {
edges, ok := findFromIM[from]
if !ok {
edges = make([]gg.Edge, len(from.Out))
copy(edges, from.Out)
sort.Slice(edges, func(i, j int) bool {
// TODO if two edges go to the same vertex, how are they sorted?
return secSol[edges[i].To.ID] < secSol[edges[j].To.ID]
})
findFromIM[from] = edges
}
for i, fe := range edges {
if fe == e {
return i
}
}
panic("edge not found in from.Out")
}
// create lines
var lines []line
for _, b := range boxes {
v := boxesM[b]
for i, e := range v.In {
bFrom := boxesMr[e.From]
fromI := findFromI(e.From, e)
buf := terminal.NewBuffer()
buf.WriteString(e.Value.V.(string))
lines = append(lines, line{
from: bFrom,
fromI: fromI,
to: b,
toI: i,
bodyBuf: buf,
})
}
}
// actually draw the boxes and lines
for _, b := range boxes {
b.draw(buf)
}
for _, line := range lines {
line.draw(buf, view.primFlowDir, view.secFlowDir)
}
}
// "Solves" vertex position by detemining relative positions of vertices in
// primary and secondary directions (independently), with relative positions
// being described by "levels", where multiple vertices can occupy one level.
//
// Primary determines relative position in the primary direction by trying
// to place vertices before their outs and after their ins.
//
// Secondary determines relative position in the secondary direction by
// trying to place vertices relative to vertices they share an edge with in
// the order that the edges appear on the shared node.
func posSolve(g *gg.Graph) ([][]*gg.Vertex, map[string]int, map[string]int) {
primEng := constraint.NewEngine()
secEng := constraint.NewEngine()
strM := g.ByID()
for _, v := range strM {
var prevIn *gg.Vertex
for _, e := range v.In {
primEng.AddConstraint(constraint.Constraint{
Elem: e.From.ID,
LT: v.ID,
})
if prevIn != nil {
secEng.AddConstraint(constraint.Constraint{
Elem: prevIn.ID,
LT: e.From.ID,
})
}
prevIn = e.From
}
var prevOut *gg.Vertex
for _, e := range v.Out {
if prevOut == nil {
continue
}
secEng.AddConstraint(constraint.Constraint{
Elem: prevOut.ID,
LT: e.To.ID,
})
prevOut = e.To
}
}
prim := primEng.Solve()
sec := secEng.Solve()
// determine maximum primary level
var maxPrim int
for _, lvl := range prim {
if lvl > maxPrim {
maxPrim = lvl
}
}
outStr := make([][]string, maxPrim+1)
for v, lvl := range prim {
outStr[lvl] = append(outStr[lvl], v)
}
// sort each primary level
for _, vv := range outStr {
sort.Slice(vv, func(i, j int) bool {
return sec[vv[i]] < sec[vv[j]]
})
}
// convert to vertices
out := make([][]*gg.Vertex, len(outStr))
for i, vv := range outStr {
out[i] = make([]*gg.Vertex, len(outStr[i]))
for j, v := range vv {
out[i][j] = strM[v]
}
}
return out, prim, sec
}