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 // through method calls
type Vertex struct { type Vertex struct {
VertexType VertexType
ID string // identifier of the vertex, unique within the graph
Value Identifier // Value is valid if-and-only-if VertexType is Value Value Identifier // Value is valid if-and-only-if VertexType is Value
In, Out []Edge 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 // Graph is a wrapper around a set of connected Vertices
type Graph struct { type Graph struct {
vM map[string]vertex // only contains value vertices 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 // Null is the root empty graph, and is the base off which all graphs are built
var Null = &Graph{ var Null = &Graph{
vM: map[string]vertex{}, 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 // 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 // Graph traversal
func (g *Graph) makeView() { func (g *Graph) makeView() {
if g.view != nil { if g.byVal != nil {
return return
} }
// view only contains value vertices, but we need to keep track of all // view only contains value vertices, but we need to keep track of all
// vertices while constructing the view // vertices while constructing the view
g.view = make(map[string]*Vertex, len(g.vM)) g.byVal = make(map[string]*Vertex, len(g.vM))
all := map[string]*Vertex{} g.all = map[string]*Vertex{}
var getV func(vertex, bool) *Vertex var getV func(vertex, bool) *Vertex
getV = func(v vertex, top bool) *Vertex { getV = func(v vertex, top bool) *Vertex {
vID := identify(v) vID := identify(v)
V, ok := all[vID] V, ok := g.all[vID]
if !ok { if !ok {
V = &Vertex{VertexType: v.VertexType, Value: v.val} V = &Vertex{VertexType: v.VertexType, ID: vID, Value: v.val}
all[vID] = V g.all[vID] = V
} }
// we can be sure all Value vertices will be called with top==true at // 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 { if v.VertexType == Value {
g.view[identify(v.val)] = V g.byVal[identify(v.val)] = V
} }
return V return V
@ -411,14 +416,14 @@ func (g *Graph) makeView() {
// contain a vertex for the value then nil is returned // contain a vertex for the value then nil is returned
func (g *Graph) Value(val Identifier) *Vertex { func (g *Graph) Value(val Identifier) *Vertex {
g.makeView() g.makeView()
return g.view[identify(val)] return g.byVal[identify(val)]
} }
// Values returns all Value Vertices in the Graph // Values returns all Value Vertices in the Graph
func (g *Graph) Values() []*Vertex { func (g *Graph) Values() []*Vertex {
g.makeView() g.makeView()
vv := make([]*Vertex, 0, len(g.view)) vv := make([]*Vertex, 0, len(g.byVal))
for _, v := range g.view { for _, v := range g.byVal {
vv = append(vv, v) vv = append(vv, v)
} }
return vv return vv
@ -457,11 +462,11 @@ func Equal(g1, g2 *Graph) bool {
func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) { func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) {
// TODO figure out how to make Walk deterministic // TODO figure out how to make Walk deterministic
g.makeView() g.makeView()
if len(g.view) == 0 { if len(g.byVal) == 0 {
return return
} }
seen := make(map[*Vertex]bool, len(g.view)) seen := make(map[*Vertex]bool, len(g.byVal))
var innerWalk func(*Vertex) bool var innerWalk func(*Vertex) bool
innerWalk = func(v *Vertex) bool { innerWalk = func(v *Vertex) bool {
if seen[v] { 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) { if !innerWalk(v) {
return 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 { switch b.flowDir {
case geo.Left, geo.Right: case geo.Left, geo.Right:
edgesRect.Size = geo.XY{neededByEdges, 2}
case geo.Up, geo.Down:
edgesRect.Size = geo.XY{2, neededByEdges} edgesRect.Size = geo.XY{2, neededByEdges}
case geo.Up, geo.Down:
edgesRect.Size = geo.XY{neededByEdges, 2}
default: default:
panic(fmt.Sprintf("unknown flowDir: %#v", b.flowDir)) 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 implements basic geometric concepts used by gim
package geo package geo
import "math"
// XY describes a 2-dimensional position or vector. The origin of the // 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 // 2-dimensional space is a 0,0, with the x-axis going to the left and the
// y-axis going down. // y-axis going down.
@ -25,6 +27,34 @@ var Units = []XY{
Right, 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 // 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]
@ -65,13 +95,6 @@ func (xy XY) Sub(xy2 XY) XY {
return xy.Add(xy2.Inv()) 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 // Midpoint returns the midpoint between the two XYs. The rounder indicates what
// to do about non-whole values when they're come across // to do about non-whole values when they're come across
func (xy XY) Midpoint(xy2 XY, r Rounder) XY { func (xy XY) Midpoint(xy2 XY, r Rounder) XY {

View File

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

View File

@ -1,99 +1,169 @@
package main package main
import ( import (
"sort"
"github.com/mediocregopher/ginger/gg" "github.com/mediocregopher/ginger/gg"
"github.com/mediocregopher/ginger/gim/constraint"
"github.com/mediocregopher/ginger/gim/geo" "github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal" "github.com/mediocregopher/ginger/gim/terminal"
) )
type view struct { // "Solves" vertex position by detemining relative positions of vertices in
g *gg.Graph // primary and secondary directions (independently), with relative positions
flowDir geo.XY // being described by "levels", where multiple vertices can occupy one level.
start gg.Str //
center geo.XY // 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()
func (v *view) draw(term *terminal.Terminal) { strM := g.ByID()
// level 0 is at the bottom of the screen, cause life is easier that way for vID, v := range strM {
levels := map[*gg.Vertex]int{} var prevIn *gg.Vertex
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 { for _, e := range v.In {
bFrom := boxes[e.From] primEng.AddConstraint(constraint.Constraint{
lines = append(lines, line{bFrom, b}) 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
} }
} }
// translate all boxes so the graph is centered around v.center. Since the outStr := make([][]string, maxPrim+1)
// lines use pointers to the boxes this will update them as well 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
}
// 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 var graphRect geo.Rect
for _, b := range boxes { for _, b := range boxes {
graphRect = graphRect.Union(b.rect()) graphRect = graphRect.Union(b.rect())
} }
graphMid := graphRect.Center(rounder) graphMid := graphRect.Center(rounder)
delta := v.center.Sub(graphMid) delta := around.Sub(graphMid)
for _, b := range boxes { for _, b := range boxes {
b.topLeft = b.topLeft.Add(delta) 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 // actually draw the boxes and lines
for _, box := range boxes { for _, b := range boxes {
box.draw(term) b.draw(term)
} }
for _, line := range lines { for _, line := range lines {
line.draw(term, v.flowDir) line.draw(term, view.primFlowDir)
} }
} }