integrate constraint engine into determining box positioning

This commit is contained in:
Brian Picciano 2017-12-03 12:38:53 -07:00
parent 79a171323d
commit 754b75407a
5 changed files with 212 additions and 107 deletions

View File

@ -57,6 +57,7 @@ type Edge struct {
// through method calls
type Vertex struct {
VertexType
ID string // identifier of the vertex, unique within the graph
Value Identifier // Value is valid if-and-only-if VertexType is Value
In, Out []Edge
}
@ -148,13 +149,17 @@ func (v vertex) cpAndDelOpenEdge(oe OpenEdge) (vertex, bool) {
// Graph is a wrapper around a set of connected Vertices
type Graph struct {
vM map[string]vertex // only contains value vertices
view map[string]*Vertex
// generated by makeView on-demand
byVal map[string]*Vertex
all map[string]*Vertex
}
// Null is the root empty graph, and is the base off which all graphs are built
var Null = &Graph{
vM: map[string]vertex{},
view: map[string]*Vertex{},
byVal: map[string]*Vertex{},
all: map[string]*Vertex{},
}
// this does _not_ copy the view, as it's assumed the only reason to copy a
@ -361,22 +366,22 @@ func (g *Graph) Union(g2 *Graph) *Graph {
// Graph traversal
func (g *Graph) makeView() {
if g.view != nil {
if g.byVal != nil {
return
}
// view only contains value vertices, but we need to keep track of all
// vertices while constructing the view
g.view = make(map[string]*Vertex, len(g.vM))
all := map[string]*Vertex{}
g.byVal = make(map[string]*Vertex, len(g.vM))
g.all = map[string]*Vertex{}
var getV func(vertex, bool) *Vertex
getV = func(v vertex, top bool) *Vertex {
vID := identify(v)
V, ok := all[vID]
V, ok := g.all[vID]
if !ok {
V = &Vertex{VertexType: v.VertexType, Value: v.val}
all[vID] = V
V = &Vertex{VertexType: v.VertexType, ID: vID, Value: v.val}
g.all[vID] = V
}
// we can be sure all Value vertices will be called with top==true at
@ -396,7 +401,7 @@ func (g *Graph) makeView() {
}
if v.VertexType == Value {
g.view[identify(v.val)] = V
g.byVal[identify(v.val)] = V
}
return V
@ -411,14 +416,14 @@ func (g *Graph) makeView() {
// contain a vertex for the value then nil is returned
func (g *Graph) Value(val Identifier) *Vertex {
g.makeView()
return g.view[identify(val)]
return g.byVal[identify(val)]
}
// Values returns all Value Vertices in the Graph
func (g *Graph) Values() []*Vertex {
g.makeView()
vv := make([]*Vertex, 0, len(g.view))
for _, v := range g.view {
vv := make([]*Vertex, 0, len(g.byVal))
for _, v := range g.byVal {
vv = append(vv, v)
}
return vv
@ -457,11 +462,11 @@ func Equal(g1, g2 *Graph) bool {
func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) {
// TODO figure out how to make Walk deterministic
g.makeView()
if len(g.view) == 0 {
if len(g.byVal) == 0 {
return
}
seen := make(map[*Vertex]bool, len(g.view))
seen := make(map[*Vertex]bool, len(g.byVal))
var innerWalk func(*Vertex) bool
innerWalk = func(v *Vertex) bool {
if seen[v] {
@ -484,9 +489,15 @@ func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) {
}
}
for _, v := range g.view {
for _, v := range g.byVal {
if !innerWalk(v) {
return
}
}
}
// ByID returns all vertices indexed by their ID field
func (g *Graph) ByID() map[string]*Vertex {
g.makeView()
return g.all
}

View File

@ -86,9 +86,9 @@ func (b box) rect() geo.Rect {
switch b.flowDir {
case geo.Left, geo.Right:
edgesRect.Size = geo.XY{neededByEdges, 2}
case geo.Up, geo.Down:
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))
}

View File

@ -1,6 +1,8 @@
// 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.
@ -25,6 +27,34 @@ var Units = []XY{
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
}
// Len returns the length (aka magnitude) of the XY as a vector, using the
// Rounder to round to an int
func (xy XY) Len(r Rounder) 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 r.Round(lf)
}
// Add returns the result of adding the two XYs' fields individually
func (xy XY) Add(xy2 XY) XY {
xy[0] += xy2[0]
@ -65,13 +95,6 @@ func (xy XY) Sub(xy2 XY) XY {
return xy.Add(xy2.Inv())
}
func (xy XY) toF64() [2]float64 {
return [2]float64{
float64(xy[0]),
float64(xy[1]),
}
}
// Midpoint returns the midpoint between the two XYs. The rounder indicates what
// to do about non-whole values when they're come across
func (xy XY) Midpoint(xy2 XY, r Rounder) XY {

View File

@ -18,9 +18,9 @@ import (
// TODO
// - assign edges to "slots" on boxes
// - finish initial implementation of constraint, use that to sort boxes by
// primary and secondary flowDir based on their edges
// - edge values
// - be able to draw circular graphs
// - audit all steps, make sure everything is deterministic
const (
framerate = 10
@ -70,7 +70,8 @@ func main() {
v := view{
g: mkGraph(),
flowDir: geo.Down,
primFlowDir: geo.Right,
secFlowDir: geo.Down,
start: gg.Str("c"),
center: geo.Zero.Midpoint(term.WindowSize(), rounder),
}

View File

@ -1,99 +1,169 @@
package main
import (
"sort"
"github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/constraint"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
type view struct {
g *gg.Graph
flowDir geo.XY
start gg.Str
center geo.XY
// "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 {
primEng := constraint.NewEngine()
secEng := constraint.NewEngine()
strM := g.ByID()
for vID, v := range strM {
var prevIn *gg.Vertex
for _, e := range v.In {
primEng.AddConstraint(constraint.Constraint{
Elem: e.From.ID,
LT: vID,
})
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
}
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
// mutates the boxes to be centered around the given point, keeping their
// relative position to each other
func centerBoxes(boxes []*box, around geo.XY) {
var graphRect geo.Rect
for _, b := range boxes {
graphRect = graphRect.Union(b.rect())
}
graphMid := graphRect.Center(rounder)
delta := v.center.Sub(graphMid)
delta := around.Sub(graphMid)
for _, b := range boxes {
b.topLeft = b.topLeft.Add(delta)
}
}
type view struct {
g *gg.Graph
primFlowDir, secFlowDir geo.XY
start gg.Str
center geo.XY
}
func (view *view) draw(term *terminal.Terminal) {
relPos := posSolve(view.g)
// create boxes
var boxes []*box
boxesM := map[*box]*gg.Vertex{}
boxesMr := map[*gg.Vertex]*box{}
const (
primPadding = 5
secPadding = 3
)
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(rounder)
secBoxLen := bSize.Mul(view.secFlowDir).Len(rounder)
if primBoxLen > maxPrim {
maxPrim = primBoxLen
}
secPos += secBoxLen + secPadding
}
centerBoxes(primBoxes, view.primFlowDir.Scale(primPos))
primPos += maxPrim + primPadding
}
// create lines
var lines []line
for _, b := range boxes {
v := boxesM[b]
for _, e := range v.In {
bFrom := boxesMr[e.From]
lines = append(lines, line{bFrom, b})
}
}
// translate all boxes so the graph is centered around v.center
centerBoxes(boxes, view.center)
// actually draw the boxes and lines
for _, box := range boxes {
box.draw(term)
for _, b := range boxes {
b.draw(term)
}
for _, line := range lines {
line.draw(term, v.flowDir)
line.draw(term, view.primFlowDir)
}
}