ginger/gim/line.go

178 lines
4.0 KiB
Go

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}}: "┘",
}
// the inverse segments use the same characters
for seg, str := range m {
seg[0], seg[1] = seg[1], seg[0]
m[seg] = str
}
return m
}()
var edgeSegments = map[geo.XY]string{
geo.Up: "┴",
geo.Down: "┬",
geo.Left: "┤",
geo.Right: "├",
}
// actual unicode arrows were fucking up my terminal, and they didn't even
// connect properly with the line segments anyway
var arrows = map[geo.XY]string{
geo.Up: "^",
geo.Down: "v",
geo.Left: "<",
geo.Right: ">",
}
func basicLine(term *terminal.Terminal, from, to box) {
dir, dirSec := boxesRelDir(from, to)
// if the boxes overlap then don't draw anything
if dir == geo.Zero {
return
}
dirInv := dir.Inv()
start := boxEdgeAdj(from, dir)
end := boxEdgeAdj(to, dirInv)
mid := start.Midpoint(end, rounder)
along := func(xy, dir geo.XY) int {
if dir[0] != 0 {
return xy[0]
}
return xy[1]
}
var pts []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)
}
for i, pt := range pts {
var str string
switch {
case i == 0:
str = edgeSegments[dir]
case i == len(pts)-1:
str = arrows[dir]
default:
prev, next := pts[i-1], pts[i+1]
seg := [2]geo.XY{
prev.Sub(pt),
next.Sub(pt),
}
str = lineSegments[seg]
}
term.MoveCursorTo(pt)
term.Printf(str)
}
}