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:
parent
e7991adfaa
commit
f5f0f6e436
111
gim/NOTES
111
gim/NOTES
@ -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
|
139
gim/geo/geo.go
139
gim/geo/geo.go
@ -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
|
||||
}
|
127
gim/geo/rect.go
127
gim/geo/rect.go
@ -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}),
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
91
gim/main.go
91
gim/main.go
@ -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()
|
||||
}
|
@ -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})
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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"},
|
||||
)
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
213
gim/view/view.go
213
gim/view/view.go
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user