diff --git a/gim/geo/geo.go b/gim/geo/geo.go index 78f073a..09c0020 100644 --- a/gim/geo/geo.go +++ b/gim/geo/geo.go @@ -17,6 +17,14 @@ var ( Right = XY{1, 0} ) +// Units is the set of unit vectors +var Units = []XY{ + Up, + Down, + Left, + Right, +} + // Add returns the result of adding the two XYs' fields individually func (xy XY) Add(xy2 XY) XY { xy[0] += xy2[0] diff --git a/gim/geo/rect.go b/gim/geo/rect.go index 0f5f827..5c45fc3 100644 --- a/gim/geo/rect.go +++ b/gim/geo/rect.go @@ -49,6 +49,27 @@ func (r Rect) Corner(xDir, yDir XY) XY { } } +// EdgeMidpoint returns the point which is the midpoint of the edge dientified by the +// direction (Up/Down/Left/Right) +func (r Rect) EdgeMidpoint(dir XY, rounder Rounder) XY { + var a, b XY + switch dir { + case Up: + a, b = r.Corner(Left, Up), r.Corner(Right, Up) + case Down: + a, b = r.Corner(Left, Down), r.Corner(Right, Down) + case Left: + a, b = r.Corner(Left, Up), r.Corner(Left, Down) + case Right: + a, b = r.Corner(Right, Up), r.Corner(Right, Down) + default: + panic(fmt.Sprintf("unsupported direction: %#v", dir)) + } + + mid := a.Midpoint(b, rounder) + return mid +} + func (r Rect) halfSize(rounder Rounder) XY { return r.Size.Div(XY{2, 2}, rounder) } diff --git a/gim/line.go b/gim/line.go index 799b50e..7937302 100644 --- a/gim/line.go +++ b/gim/line.go @@ -1,101 +1,18 @@ package main import ( - "fmt" - "github.com/mediocregopher/ginger/gim/geo" "github.com/mediocregopher/ginger/gim/terminal" ) -// boxEdgeAdj returns the midpoint of a box's edge, using the given direction -// (single-dimension unit-vector) to know which edge to look at. -func boxEdgeAdj(box box, dir geo.XY) geo.XY { - boxRect := box.rect() - var a, b geo.XY - switch dir { - case geo.Up: - a, b = boxRect.Corner(geo.Left, geo.Up), boxRect.Corner(geo.Right, geo.Up) - case geo.Down: - a, b = boxRect.Corner(geo.Left, geo.Down), boxRect.Corner(geo.Right, geo.Down) - case geo.Left: - a, b = boxRect.Corner(geo.Left, geo.Up), boxRect.Corner(geo.Left, geo.Down) - case geo.Right: - a, b = boxRect.Corner(geo.Right, geo.Up), boxRect.Corner(geo.Right, geo.Down) - default: - panic(fmt.Sprintf("unsupported direction: %#v", dir)) - } - - mid := a.Midpoint(b, rounder) - return mid -} - -var dirs = []geo.XY{ - geo.Up, - geo.Down, - geo.Left, - geo.Right, -} - -// boxesRelDir returns the "best" direction between from and to. Returns -// geo.Zero if they overlap. It also returns the secondary direction. E.g. Down -// and Left. The secondary direction will never be zero if primary is given, -// even if the two boxes are in-line -func boxesRelDir(from, to box) (geo.XY, geo.XY) { - fromRect, toRect := from.rect(), to.rect() - rels := make([]int, len(dirs)) - for i, dir := range dirs { - rels[i] = toRect.Edge(dir.Inv()) - fromRect.Edge(dir) - if dir == geo.Up || dir == geo.Left { - rels[i] *= -1 - } - } - - // find primary - var primary geo.XY - var primaryMax int - for i, rel := range rels { - if rel < 0 { - continue - } else if rel > primaryMax || i == 0 { - primary = dirs[i] - primaryMax = rel - } - } - - // if all rels were negative the boxes are overlapping, return zeros - if primary == geo.Zero { - return geo.Zero, geo.Zero - } - - // now find secondary, which must be perpendicular to primary - var secondary geo.XY - var secondaryMax int - var secondarySet bool - for i, rel := range rels { - if dirs[i] == primary { - continue - } else if dirs[i][0] == 0 && primary[0] == 0 { - continue - } else if dirs[i][1] == 0 && primary[1] == 0 { - continue - } else if !secondarySet || rel > secondaryMax { - secondary = dirs[i] - secondaryMax = rel - secondarySet = true - } - } - - return primary, secondary -} - var lineSegments = func() map[[2]geo.XY]string { m := map[[2]geo.XY]string{ - {{-1, 0}, {1, 0}}: "─", - {{0, 1}, {0, -1}}: "│", - {{1, 0}, {0, 1}}: "┌", - {{-1, 0}, {0, 1}}: "┐", - {{1, 0}, {0, -1}}: "└", - {{-1, 0}, {0, -1}}: "┘", + {geo.Left, geo.Right}: "─", + {geo.Down, geo.Up}: "│", + {geo.Right, geo.Down}: "┌", + {geo.Left, geo.Down}: "┐", + {geo.Right, geo.Up}: "└", + {geo.Left, geo.Up}: "┘", } // the inverse segments use the same characters @@ -122,17 +39,48 @@ var arrows = map[geo.XY]string{ geo.Right: ">", } -func basicLine(term *terminal.Terminal, from, to box) { - dir, dirSec := boxesRelDir(from, to) +type line [2]*box - // if the boxes overlap then don't draw anything - if dir == geo.Zero { - return +// given the "primary" direction the line should be headed, picks 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) +func (l line) secondaryDir(primary geo.XY) geo.XY { + fromRect, toRect := l[0].rect(), l[1].rect() + rels := make([]int, len(geo.Units)) + for i, dir := range geo.Units { + rels[i] = toRect.Edge(dir.Inv()) - fromRect.Edge(dir) + if dir == geo.Up || dir == geo.Left { + rels[i] *= -1 + } } + var secondary geo.XY + var secondaryMax int + var secondarySet bool + for i, rel := range rels { + if geo.Units[i] == primary { + continue + } else if geo.Units[i][0] == 0 && primary[0] == 0 { + continue + } else if geo.Units[i][1] == 0 && primary[1] == 0 { + continue + } else if !secondarySet || rel > secondaryMax { + secondary = geo.Units[i] + secondaryMax = rel + secondarySet = true + } + } + + return secondary +} + +func (l line) draw(term *terminal.Terminal, dir geo.XY) { + from, to := *l[0], *l[1] + dirSec := l.secondaryDir(dir) + dirInv := dir.Inv() - start := boxEdgeAdj(from, dir) - end := boxEdgeAdj(to, dirInv) + start := from.rect().EdgeMidpoint(dir, rounder) + end := to.rect().EdgeMidpoint(dirInv, rounder) mid := start.Midpoint(end, rounder) along := func(xy, dir geo.XY) int { diff --git a/gim/main.go b/gim/main.go index a2b6f34..527b5fe 100644 --- a/gim/main.go +++ b/gim/main.go @@ -18,9 +18,9 @@ import ( // - Absolute positioning of some/all vertices // TODO -// - actually use flowDir // - assign edges to "slots" on boxes // - figure out how to keep boxes sorted on their levels (e.g. the "b" nodes) +// - be able to draw circular graphs const ( framerate = 10 @@ -72,100 +72,18 @@ func main() { rand.Seed(time.Now().UnixNano()) term := terminal.New() term.Reset() - termSize := term.WindowSize() - g := mkGraph() + term.HideCursor() - // level 0 is at the bottom of the screen, cause life is easier that way - levels := map[*gg.Vertex]int{} - getLevel := func(v *gg.Vertex) int { - // if any of the tos have a level, this will be greater than the max - toMax := -1 - for _, e := range v.Out { - lvl, ok := levels[e.To] - if !ok { - continue - } else if lvl > toMax { - toMax = lvl - } - } - - if toMax >= 0 { - return toMax + 1 - } - - // otherwise level is 0 - return 0 - } - - g.Walk(g.Value(str("c")), func(v *gg.Vertex) bool { - levels[v] = getLevel(v) - return true - }) - - // consolidate by level - byLevel := map[int][]*gg.Vertex{} - maxLvl := -1 - for v, lvl := range levels { - byLevel[lvl] = append(byLevel[lvl], v) - if lvl > maxLvl { - maxLvl = lvl - } - } - - // create boxes - boxes := map[*gg.Vertex]box{} - for lvl := 0; lvl <= maxLvl; lvl++ { - vv := byLevel[lvl] - for i, v := range vv { - b := boxFromVertex(v, geo.Right) - bSize := b.rect().Size - b.topLeft = geo.XY{ - 10*(i-(len(vv)/2)) - (bSize[0] / 2), - lvl * -10, - } - boxes[v] = b - } - } - - // center boxes. first find overall dimensions, use that to create delta - // vector which would move that to the center - var graphRect geo.Rect - for _, b := range boxes { - graphRect = graphRect.Union(b.rect()) - } - - graphMid := graphRect.Center(rounder) - screenMid := geo.Zero.Midpoint(termSize, rounder) - delta := screenMid.Sub(graphMid) - - // translate all boxes by delta - for v, b := range boxes { - b.topLeft = b.topLeft.Add(delta) - boxes[v] = b - } - - // create lines - var lines [][2]box - for v := range levels { - b := boxes[v] - for _, e := range v.In { - bFrom := boxes[e.From] - lines = append(lines, [2]box{bFrom, b}) - } + v := view{ + g: mkGraph(), + flowDir: geo.Down, + start: str("c"), + center: geo.Zero.Midpoint(term.WindowSize(), rounder), } for range time.Tick(frameperiod) { - // update phase - // nufin - - // draw phase term.Reset() - for v := range boxes { - boxes[v].draw(term) - } - for _, line := range lines { - basicLine(term, line[0], line[1]) - } + v.draw(term) term.Flush() } } diff --git a/gim/terminal/terminal.go b/gim/terminal/terminal.go index 081dea7..dfa81d5 100644 --- a/gim/terminal/terminal.go +++ b/gim/terminal/terminal.go @@ -83,6 +83,16 @@ func (t *Terminal) MoveCursor(by geo.XY) { t.MoveCursorTo(t.pos.Add(by)) } +// 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") +} + // Reset completely clears all drawn characters on the screen and returns the // cursor to the origin func (t *Terminal) Reset() { diff --git a/gim/view.go b/gim/view.go new file mode 100644 index 0000000..94cb53a --- /dev/null +++ b/gim/view.go @@ -0,0 +1,99 @@ +package main + +import ( + "github.com/mediocregopher/ginger/gg" + "github.com/mediocregopher/ginger/gim/geo" + "github.com/mediocregopher/ginger/gim/terminal" +) + +type view struct { + g *gg.Graph + flowDir geo.XY + start str + center geo.XY +} + +func (v *view) draw(term *terminal.Terminal) { + // level 0 is at the bottom of the screen, cause life is easier that way + levels := map[*gg.Vertex]int{} + getLevel := func(v *gg.Vertex) int { + // if any of the tos have a level, this will be greater than the max + toMax := -1 + for _, e := range v.Out { + lvl, ok := levels[e.To] + if !ok { + continue + } else if lvl > toMax { + toMax = lvl + } + } + + if toMax >= 0 { + return toMax + 1 + } + + // otherwise level is 0 + return 0 + } + + v.g.Walk(v.g.Value(v.start), func(v *gg.Vertex) bool { + levels[v] = getLevel(v) + return true + }) + + // consolidate by level + byLevel := map[int][]*gg.Vertex{} + maxLvl := -1 + for v, lvl := range levels { + byLevel[lvl] = append(byLevel[lvl], v) + if lvl > maxLvl { + maxLvl = lvl + } + } + + // create boxes + boxes := map[*gg.Vertex]*box{} + for lvl := 0; lvl <= maxLvl; lvl++ { + vv := byLevel[lvl] + for i, v := range vv { + b := boxFromVertex(v, geo.Right) + bSize := b.rect().Size + // TODO make this dependent on flowDir + b.topLeft = geo.XY{ + 10*(i-(len(vv)/2)) - (bSize[0] / 2), + lvl * -10, + } + boxes[v] = &b + } + } + + // create lines + var lines []line + for v := range levels { + b := boxes[v] + for _, e := range v.In { + bFrom := boxes[e.From] + lines = append(lines, line{bFrom, b}) + } + } + + // translate all boxes so the graph is centered around v.center. Since the + // lines use pointers to the boxes this will update them as well + var graphRect geo.Rect + for _, b := range boxes { + graphRect = graphRect.Union(b.rect()) + } + graphMid := graphRect.Center(rounder) + delta := v.center.Sub(graphMid) + for _, b := range boxes { + b.topLeft = b.topLeft.Add(delta) + } + + // actually draw the boxes and lines + for _, box := range boxes { + box.draw(term) + } + for _, line := range lines { + line.draw(term, v.flowDir) + } +}