From 754b75407a8a62307578bad5c20942a5c7384e60 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 3 Dec 2017 12:38:53 -0700 Subject: [PATCH] integrate constraint engine into determining box positioning --- gg/gg.go | 45 ++++++---- gim/box.go | 4 +- gim/geo/geo.go | 37 +++++++-- gim/main.go | 13 +-- gim/view.go | 220 ++++++++++++++++++++++++++++++++----------------- 5 files changed, 212 insertions(+), 107 deletions(-) diff --git a/gg/gg.go b/gg/gg.go index a2eb3f0..92eec62 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -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 } @@ -147,14 +148,18 @@ 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 + vM map[string]vertex // only contains value vertices + + // 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{}, + vM: 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 +} diff --git a/gim/box.go b/gim/box.go index 6593f1b..6036d6a 100644 --- a/gim/box.go +++ b/gim/box.go @@ -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)) } diff --git a/gim/geo/geo.go b/gim/geo/geo.go index 09c0020..1935e59 100644 --- a/gim/geo/geo.go +++ b/gim/geo/geo.go @@ -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 { diff --git a/gim/main.go b/gim/main.go index 59deaee..639ec70 100644 --- a/gim/main.go +++ b/gim/main.go @@ -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 @@ -69,10 +69,11 @@ func main() { term.HideCursor() v := view{ - g: mkGraph(), - flowDir: geo.Down, - start: gg.Str("c"), - center: geo.Zero.Midpoint(term.WindowSize(), rounder), + g: mkGraph(), + primFlowDir: geo.Right, + secFlowDir: geo.Down, + start: gg.Str("c"), + center: geo.Zero.Midpoint(term.WindowSize(), rounder), } for range time.Tick(frameperiod) { diff --git a/gim/view.go b/gim/view.go index 8479614..0f9607a 100644 --- a/gim/view.go +++ b/gim/view.go @@ -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) } }