integrate constraint engine into determining box positioning
This commit is contained in:
parent
79a171323d
commit
754b75407a
45
gg/gg.go
45
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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
13
gim/main.go
13
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) {
|
||||
|
220
gim/view.go
220
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user