move a bunch of code into view, which holds onto a bunch of the drawing logic and settings now

This commit is contained in:
Brian Picciano 2017-11-23 12:19:32 -07:00
parent f68bb4d8a2
commit 7c7502b42f
6 changed files with 190 additions and 186 deletions

View File

@ -17,6 +17,14 @@ var (
Right = XY{1, 0} 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 // Add returns the result of adding the two XYs' fields individually
func (xy XY) Add(xy2 XY) XY { func (xy XY) Add(xy2 XY) XY {
xy[0] += xy2[0] xy[0] += xy2[0]

View File

@ -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 { func (r Rect) halfSize(rounder Rounder) XY {
return r.Size.Div(XY{2, 2}, rounder) return r.Size.Div(XY{2, 2}, rounder)
} }

View File

@ -1,101 +1,18 @@
package main package main
import ( import (
"fmt"
"github.com/mediocregopher/ginger/gim/geo" "github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal" "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 { var lineSegments = func() map[[2]geo.XY]string {
m := map[[2]geo.XY]string{ m := map[[2]geo.XY]string{
{{-1, 0}, {1, 0}}: "─", {geo.Left, geo.Right}: "─",
{{0, 1}, {0, -1}}: "│", {geo.Down, geo.Up}: "│",
{{1, 0}, {0, 1}}: "┌", {geo.Right, geo.Down}: "┌",
{{-1, 0}, {0, 1}}: "┐", {geo.Left, geo.Down}: "┐",
{{1, 0}, {0, -1}}: "└", {geo.Right, geo.Up}: "└",
{{-1, 0}, {0, -1}}: "┘", {geo.Left, geo.Up}: "┘",
} }
// the inverse segments use the same characters // the inverse segments use the same characters
@ -122,17 +39,48 @@ var arrows = map[geo.XY]string{
geo.Right: ">", geo.Right: ">",
} }
func basicLine(term *terminal.Terminal, from, to box) { type line [2]*box
dir, dirSec := boxesRelDir(from, to)
// if the boxes overlap then don't draw anything // given the "primary" direction the line should be headed, picks a possible
if dir == geo.Zero { // secondary one which may be used to detour along the path in order to reach
return // 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() dirInv := dir.Inv()
start := boxEdgeAdj(from, dir) start := from.rect().EdgeMidpoint(dir, rounder)
end := boxEdgeAdj(to, dirInv) end := to.rect().EdgeMidpoint(dirInv, rounder)
mid := start.Midpoint(end, rounder) mid := start.Midpoint(end, rounder)
along := func(xy, dir geo.XY) int { along := func(xy, dir geo.XY) int {

View File

@ -18,9 +18,9 @@ import (
// - Absolute positioning of some/all vertices // - Absolute positioning of some/all vertices
// TODO // TODO
// - actually use flowDir
// - assign edges to "slots" on boxes // - assign edges to "slots" on boxes
// - figure out how to keep boxes sorted on their levels (e.g. the "b" nodes) // - figure out how to keep boxes sorted on their levels (e.g. the "b" nodes)
// - be able to draw circular graphs
const ( const (
framerate = 10 framerate = 10
@ -72,100 +72,18 @@ func main() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
term := terminal.New() term := terminal.New()
term.Reset() term.Reset()
termSize := term.WindowSize() term.HideCursor()
g := mkGraph()
// level 0 is at the bottom of the screen, cause life is easier that way v := view{
levels := map[*gg.Vertex]int{} g: mkGraph(),
getLevel := func(v *gg.Vertex) int { flowDir: geo.Down,
// if any of the tos have a level, this will be greater than the max start: str("c"),
toMax := -1 center: geo.Zero.Midpoint(term.WindowSize(), rounder),
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})
}
} }
for range time.Tick(frameperiod) { for range time.Tick(frameperiod) {
// update phase
// nufin
// draw phase
term.Reset() term.Reset()
for v := range boxes { v.draw(term)
boxes[v].draw(term)
}
for _, line := range lines {
basicLine(term, line[0], line[1])
}
term.Flush() term.Flush()
} }
} }

View File

@ -83,6 +83,16 @@ func (t *Terminal) MoveCursor(by geo.XY) {
t.MoveCursorTo(t.pos.Add(by)) 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 // Reset completely clears all drawn characters on the screen and returns the
// cursor to the origin // cursor to the origin
func (t *Terminal) Reset() { func (t *Terminal) Reset() {

99
gim/view.go Normal file
View File

@ -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)
}
}