integrate constraint engine into determining box positioning
This commit is contained in:
parent
79a171323d
commit
754b75407a
41
gg/gg.go
41
gg/gg.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
220
gim/view.go
220
gim/view.go
@ -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()
|
||||||
|
|
||||||
|
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) {
|
// mutates the boxes to be centered around the given point, keeping their
|
||||||
// level 0 is at the bottom of the screen, cause life is easier that way
|
// relative position to each other
|
||||||
levels := map[*gg.Vertex]int{}
|
func centerBoxes(boxes []*box, around geo.XY) {
|
||||||
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
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user