diff --git a/gim/NOTES b/gim/NOTES deleted file mode 100644 index 81267ca..0000000 --- a/gim/NOTES +++ /dev/null @@ -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 diff --git a/gim/geo/geo.go b/gim/geo/geo.go deleted file mode 100644 index 44900d4..0000000 --- a/gim/geo/geo.go +++ /dev/null @@ -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 -} diff --git a/gim/geo/rect.go b/gim/geo/rect.go deleted file mode 100644 index a0d3391..0000000 --- a/gim/geo/rect.go +++ /dev/null @@ -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}), - } -} diff --git a/gim/geo/rect_test.go b/gim/geo/rect_test.go deleted file mode 100644 index 80b2274..0000000 --- a/gim/geo/rect_test.go +++ /dev/null @@ -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) - } -} diff --git a/gim/geo/round.go b/gim/geo/round.go deleted file mode 100644 index b869831..0000000 --- a/gim/geo/round.go +++ /dev/null @@ -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 diff --git a/gim/main.go b/gim/main.go deleted file mode 100644 index 5c48f0d..0000000 --- a/gim/main.go +++ /dev/null @@ -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() -} diff --git a/gim/terminal/buffer.go b/gim/terminal/buffer.go deleted file mode 100644 index d630833..0000000 --- a/gim/terminal/buffer.go +++ /dev/null @@ -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}) -} diff --git a/gim/terminal/example/main.go b/gim/terminal/example/main.go deleted file mode 100644 index 1dcd905..0000000 --- a/gim/terminal/example/main.go +++ /dev/null @@ -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)) - } - } -} diff --git a/gim/terminal/mat.go b/gim/terminal/mat.go deleted file mode 100644 index 087c5ab..0000000 --- a/gim/terminal/mat.go +++ /dev/null @@ -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 - } - } - } -} diff --git a/gim/terminal/mat_test.go b/gim/terminal/mat_test.go deleted file mode 100644 index a856c0e..0000000 --- a/gim/terminal/mat_test.go +++ /dev/null @@ -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) - } -} diff --git a/gim/terminal/shape.go b/gim/terminal/shape.go deleted file mode 100644 index 3bbfb88..0000000 --- a/gim/terminal/shape.go +++ /dev/null @@ -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))) - } -} diff --git a/gim/terminal/terminal.go b/gim/terminal/terminal.go deleted file mode 100644 index 982f004..0000000 --- a/gim/terminal/terminal.go +++ /dev/null @@ -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() -} diff --git a/gim/view/box.go b/gim/view/box.go deleted file mode 100644 index 654e8d2..0000000 --- a/gim/view/box.go +++ /dev/null @@ -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) - } -} diff --git a/gim/view/constraint/constraint.go b/gim/view/constraint/constraint.go deleted file mode 100644 index 82f1cc5..0000000 --- a/gim/view/constraint/constraint.go +++ /dev/null @@ -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 -} diff --git a/gim/view/constraint/constraint_test.go b/gim/view/constraint/constraint_test.go deleted file mode 100644 index f1e5a94..0000000 --- a/gim/view/constraint/constraint_test.go +++ /dev/null @@ -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"}, - ) - -} diff --git a/gim/view/line.go b/gim/view/line.go deleted file mode 100644 index 36dd7e3..0000000 --- a/gim/view/line.go +++ /dev/null @@ -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) - } -} diff --git a/gim/view/view.go b/gim/view/view.go deleted file mode 100644 index 0530bcc..0000000 --- a/gim/view/view.go +++ /dev/null @@ -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 -}