diff --git a/go.mod b/go.mod index d525b79..61be7fa 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,11 @@ module github.com/nulab/autog -go 1.22.4 - -toolchain go1.22.7 +go 1.23.1 require github.com/stretchr/testify v1.8.4 -replace github.com/vibridi/graphify => ../../vibridi/graphify - require ( - github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 050bcab..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,10 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb h1:vJaelmTCdGCWuUUmERFznS6843ewGv4MGKv8nOETyt4= -github.com/vibridi/graphify v0.0.0-20240926092405-5791dfe773cb/go.mod h1:ClQsJC5L+MO0eABQoEJBx8EXSxi++0VrUuIYXcYeViY= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= diff --git a/internal/geom/spline.go b/internal/geom/spline.go index 6674e9b..af24320 100644 --- a/internal/geom/spline.go +++ b/internal/geom/spline.go @@ -70,9 +70,6 @@ func FitSpline(path []P, tanv1, tanv2 P, barriers []Segment) []ctrlp { upperps := FitSpline(path[:k+1], tanv1, tanw, barriers) lowerps := FitSpline(path[k:], tanw, tanv2, barriers) - // if upperps != nil && lowerps != nil { - // return append(upperps, lowerps...) - // } return append(upperps, lowerps...) } diff --git a/internal/graph/dgraph.go b/internal/graph/dgraph.go index 3e6e199..0ed9a92 100644 --- a/internal/graph/dgraph.go +++ b/internal/graph/dgraph.go @@ -1,13 +1,14 @@ package graph import ( + "iter" "strings" ) type DGraph struct { Nodes []*Node Edges EdgeList - Layers Layers + Layers []*Layer } func (g *DGraph) Populate(*DGraph) { @@ -25,28 +26,30 @@ func (g *DGraph) GetEdges() []*Edge { return nil } -// todo: sources and sinks don't yet account for isolated nodes with a self-loop - -// Sources returns a list of nodes with no incoming edges -func (g *DGraph) Sources() []*Node { - var sources []*Node - for _, n := range g.Nodes { - if len(n.In) == 0 { - sources = append(sources, n) +// Sources returns a sequence of nodes with no incoming edges +func (g *DGraph) Sources() iter.Seq[*Node] { + return func(yield func(*Node) bool) { + for _, n := range g.Nodes { + if n.Indeg() == 0 { + if !yield(n) { + return + } + } } } - return sources } // Sinks returns a list of nodes with no outgoing edges -func (g *DGraph) Sinks() []*Node { - var sinks []*Node - for _, n := range g.Nodes { - if len(n.Out) == 0 { - sinks = append(sinks, n) +func (g *DGraph) Sinks() iter.Seq[*Node] { + return func(yield func(*Node) bool) { + for _, n := range g.Nodes { + if n.Outdeg() == 0 { + if !yield(n) { + return + } + } } } - return sinks } func (g *DGraph) String() string { diff --git a/internal/graph/edge.go b/internal/graph/edge.go index 29ad83a..d4d9076 100644 --- a/internal/graph/edge.go +++ b/internal/graph/edge.go @@ -74,22 +74,30 @@ func (e *Edge) Crosses(f *Edge) bool { (etop.LayerPos > ftop.LayerPos && ebtm.LayerPos < fbtm.LayerPos) } -// Type returns the edge type as follows: -// - 0: both of e's adjacent nodes are concrete nodes -// - 1: exactly one of e's adjacent nodes is virtual -// - 2: both of e's adjacent nodes are virtual -func (e *Edge) Type() int { - // todo: might return an enum instead - if !e.From.IsVirtual && !e.To.IsVirtual { - return 0 - } - if e.From.IsVirtual != e.To.IsVirtual { - return 1 - } - if e.From.IsVirtual && e.To.IsVirtual { - return 2 +// EdgeType encodes information about the nodes adjacent to an edge, +type EdgeType uint8 + +const ( + // EdgeTypeConcrete indicates a type 0 edge whose adjacent nodes are both non-virtual. + EdgeTypeConcrete EdgeType = iota + // EdgeTypeHybrid indicates a type 1 edge whose adjacent nodes are one virtual and one non-virtual. + EdgeTypeHybrid + // EdgeTypeVirtual indicates a type 2 edge whose adjacent nodes are both virtual. + EdgeTypeVirtual +) + +// Type returns the edge's EdgeType +func (e *Edge) Type() EdgeType { + switch { + case !e.From.IsVirtual && !e.To.IsVirtual: + return EdgeTypeConcrete + case e.From.IsVirtual != e.To.IsVirtual: + return EdgeTypeHybrid + case e.From.IsVirtual && e.To.IsVirtual: + return EdgeTypeVirtual + default: + panic("edge type cases aren't exhaustive") } - panic("edge type cases aren't exhaustive") } func (e *Edge) String() string { diff --git a/internal/graph/maps.go b/internal/graph/maps.go index dabe778..84ee9ad 100644 --- a/internal/graph/maps.go +++ b/internal/graph/maps.go @@ -16,8 +16,6 @@ type NodeSet = hashmap[*Node, bool] type EdgeSet = hashmap[*Edge, bool] -type Layers = hashmap[int, *Layer] - func (m hashmap[K, V]) Clone() hashmap[K, V] { return maps.Clone(m) } diff --git a/internal/graph/node_iter.go b/internal/graph/node_iter.go index 6a0ca83..a6f03f1 100644 --- a/internal/graph/node_iter.go +++ b/internal/graph/node_iter.go @@ -1,26 +1,24 @@ package graph +import "iter" + func (n *Node) VisitEdges(visit func(*Edge)) { - fn, next := n.allEdges() - for next { - next = fn(visit) + for e := range n.allEdges() { + visit(e) } } -func (n *Node) allEdges() (visitor func(func(*Edge)) bool, next bool) { - i := 0 - visitor = func(yield func(*Edge)) bool { - if i >= len(n.In)+len(n.Out) { - return false - } - if i < len(n.In) { - yield(n.In[i]) - } else { - yield(n.Out[i-len(n.In)]) +func (n *Node) allEdges() iter.Seq[*Edge] { + return func(yield func(*Edge) bool) { + for i, b := 0, true; i < n.Deg(); i++ { + if i < n.Indeg() { + b = yield(n.In[i]) + } else { + b = yield(n.Out[i-n.Indeg()]) + } + if !b { + return + } } - i++ - return true } - next = true - return } diff --git a/internal/graph/node_iter_test.go b/internal/graph/node_iter_test.go index 870641e..27f10c0 100644 --- a/internal/graph/node_iter_test.go +++ b/internal/graph/node_iter_test.go @@ -8,22 +8,27 @@ import ( ) func TestVisitEdges(t *testing.T) { - t.Skip() - ids := strings.Split("abcde", "") - es := []*Edge{} - for i := range ids { - es = append(es, &Edge{edge: edge{Delta: i}}) + n := &Node{ID: "N"} + n.In = []*Edge{ + {edge: edge{From: &Node{ID: "A1"}, To: n}}, + {edge: edge{From: &Node{ID: "A2"}, To: n}}, + {edge: edge{From: &Node{ID: "A3"}, To: n}}, } - - n := &Node{ - In: es[:3], - Out: es[3:], + n.Out = []*Edge{ + {edge: edge{From: n, To: &Node{ID: "B1"}}}, + {edge: edge{From: n, To: &Node{ID: "B2"}}}, + {edge: edge{From: n, To: &Node{ID: "B3"}}}, + {edge: edge{From: n, To: &Node{ID: "B4"}}}, } i := 0 n.VisitEdges(func(e *Edge) { - assert.Equal(t, ids[i], i) + if i < 3 { + assert.True(t, strings.HasPrefix(e.From.ID, "A")) + } else { + assert.True(t, strings.HasPrefix(e.To.ID, "B")) + } i++ }) - assert.Equal(t, 5, i) + assert.Equal(t, 7, i) } diff --git a/internal/phase1/dfs.go b/internal/phase1/dfs.go index c82c688..7ed3c04 100644 --- a/internal/phase1/dfs.go +++ b/internal/phase1/dfs.go @@ -21,16 +21,12 @@ func execDepthFirst(g *graph.DGraph) { active: make(graph.NodeSet), } - // get list of source nodes (nodes with no incoming edge) - sources := g.Sources() - - for _, node := range sources { + // process nodes with no incoming edge first + for node := range g.Sources() { p.visit(node) } - nodeCount := len(g.Nodes) - for i := 0; i < nodeCount; i++ { - node := g.Nodes[i] + for _, node := range g.Nodes { if !p.visited[node] { p.visit(node) } @@ -38,7 +34,6 @@ func execDepthFirst(g *graph.DGraph) { for _, e := range p.reversable { e.Reverse() - // g.IsCyclic = true } } diff --git a/internal/phase1/greedy.go b/internal/phase1/greedy.go index e079de3..40e2a53 100644 --- a/internal/phase1/greedy.go +++ b/internal/phase1/greedy.go @@ -3,6 +3,7 @@ package phase1 import ( "math" "math/rand" + "slices" "time" "github.com/nulab/autog/internal/graph" @@ -37,8 +38,8 @@ func execGreedy(g *graph.DGraph, params graph.Params) { nodeCount := len(g.Nodes) - sources := g.Sources() - sinks := g.Sinks() + sources := slices.Collect(g.Sources()) + sinks := slices.Collect(g.Sinks()) // here ELK accounts for edge priority: particular edges that the user doesn't want to reverse // can be assigned a non-zero priority; this will artificially increase the node's in-/out-degrees. diff --git a/internal/phase2/alg_process.go b/internal/phase2/alg_process.go index ecb2203..b5ae589 100644 --- a/internal/phase2/alg_process.go +++ b/internal/phase2/alg_process.go @@ -24,27 +24,28 @@ func (alg Alg) Process(g *graph.DGraph, params graph.Params) { } initLayers: - m := map[int]*graph.Layer{} + size := 0 for _, n := range g.Nodes { - layer := m[n.Layer] - if layer == nil { - layer = &graph.Layer{Index: n.Layer} - } - layer.Nodes = append(layer.Nodes, n) - m[n.Layer] = layer + size = max(size, n.Layer) } - g.Layers = m - fillLayers(g) -} + size += 1 // the highest layer index must fit in the slice too + + ls := make([]*graph.Layer, size) -func fillLayers(g *graph.DGraph) { - highest := 0 - for i := range g.Layers { - highest = max(highest, i) + for _, n := range g.Nodes { + l := ls[n.Layer] + if l == nil { + l = &graph.Layer{Index: n.Layer} + } + l.Nodes = append(l.Nodes, n) + ls[n.Layer] = l } - for i := 0; i < highest; i++ { - _, ok := g.Layers[i] - if !ok { + g.Layers = ls + + // fill layers + for i := range size { + l := g.Layers[i] + if l == nil { g.Layers[i] = &graph.Layer{Index: i} } } diff --git a/internal/phase2/network_simplex.go b/internal/phase2/network_simplex.go index 80fa739..5893951 100644 --- a/internal/phase2/network_simplex.go +++ b/internal/phase2/network_simplex.go @@ -2,6 +2,7 @@ package phase2 import ( "math" + "slices" "github.com/nulab/autog/internal/graph" ) @@ -11,8 +12,6 @@ type networkSimplexProcessor struct { low graph.NodeIntMap // Gansner et al.: lowest postorder traversal number among nodes reachable from the input node } -// todo: exec alg on single connected components? - // this implements a graph node layering algorithm, based on: // - "Emden R. Gansner, Eleftherios Koutsofios, Stephen C. North, Kiem-Phong Vo, A technique for // drawing directed graphs. Software Engineering 19(3), pp. 214-230, 1993." @@ -129,7 +128,7 @@ func (p *networkSimplexProcessor) initLayers(g *graph.DGraph) { } // sources have layer 0 - sources := g.Sources() + sources := slices.Collect(g.Sources()) for len(sources) > 0 { n := sources[0] diff --git a/internal/phase3/wmedian.go b/internal/phase3/wmedian.go index f70f34d..5f27ebc 100644 --- a/internal/phase3/wmedian.go +++ b/internal/phase3/wmedian.go @@ -191,7 +191,7 @@ func (p *wmedianProcessor) initPositionsFlatEdges(n *graph.Node, visited graph.N // The weighted median routine assigns an order to each vertex in layer L(i) based on the current order // of adjacent nodes in the next rank. Next is L(i)-1 in top-bottom sweep, or L(i)+1 in bottom-top sweep. // Nodes with no adjacent nodes in the next layer are kept in place. -func (p *wmedianProcessor) wmedianTopBottom(layers map[int]*graph.Layer) { +func (p *wmedianProcessor) wmedianTopBottom(layers []*graph.Layer) { medians := graph.NodeFloatMap{} for r := 1; r < len(layers); r++ { for _, v := range layers[r].Nodes { @@ -201,7 +201,7 @@ func (p *wmedianProcessor) wmedianTopBottom(layers map[int]*graph.Layer) { } } -func (p *wmedianProcessor) wmedianBottomTop(layers map[int]*graph.Layer) { +func (p *wmedianProcessor) wmedianBottomTop(layers []*graph.Layer) { medians := graph.NodeFloatMap{} for r := len(layers) - 1; r >= 0; r-- { for _, v := range layers[r].Nodes { @@ -308,14 +308,14 @@ func (p *wmedianProcessor) sortLayer(nodes []*graph.Node, medians graph.NodeFloa // transpose sweeps through layers in order and swaps pairs of adjacent nodes in the same layer; // it counts the number of crossings between L, L-1 and L+1, if there's an improvement it keeps looping // until no improvement is found. -func (p *wmedianProcessor) transpose(layers map[int]*graph.Layer) { +func (p *wmedianProcessor) transpose(layers []*graph.Layer) { improved := true for improved { improved = false - for L := 0; L < len(layers); L++ { - for i := 0; i < len(layers[L].Nodes)-2; i++ { - v := layers[L].Nodes[i] - w := layers[L].Nodes[i+1] + for _, layer := range layers { + for i := 0; i < len(layer.Nodes)-2; i++ { + v := layer.Nodes[i] + w := layer.Nodes[i+1] if p.fixedPositions.mustBefore[v] == w { continue @@ -334,16 +334,16 @@ func (p *wmedianProcessor) transpose(layers map[int]*graph.Layer) { continue } - curX := crossingsAround(L, layers) + curX := crossingsAround(layer.Index, layers) p.swap(v, w) - newX := crossingsAround(L, layers) + newX := crossingsAround(layer.Index, layers) switch { case newX < curX: // improved and keep new order improved = true - layers[L].Nodes[i] = w - layers[L].Nodes[i+1] = v + layer.Nodes[i] = w + layer.Nodes[i+1] = v default: // no improvement, restore order @@ -354,7 +354,7 @@ func (p *wmedianProcessor) transpose(layers map[int]*graph.Layer) { } } -func crossings(layers map[int]*graph.Layer) int { +func crossings(layers []*graph.Layer) int { crossings := 0 for l := 1; l < len(layers); l++ { crossings += countCrossings(layers[l-1], layers[l]) @@ -362,7 +362,7 @@ func crossings(layers map[int]*graph.Layer) int { return crossings } -func crossingsAround(l int, layers map[int]*graph.Layer) int { +func crossingsAround(l int, layers []*graph.Layer) int { if l == 0 { return countCrossings(layers[l], layers[l+1]) } diff --git a/internal/phase3/wmedian_fixedpos_test.go b/internal/phase3/wmedian_fixedpos_test.go index 008ce5b..3f51d7c 100644 --- a/internal/phase3/wmedian_fixedpos_test.go +++ b/internal/phase3/wmedian_fixedpos_test.go @@ -1,7 +1,6 @@ package phase3 import ( - "fmt" "testing" "github.com/nulab/autog/internal/graph" @@ -73,9 +72,6 @@ func TestInitFlatEdges(t *testing.T) { assert.Equal(t, nodes[4], fpos.mustBefore[nodes[3]]) assert.Equal(t, nodes[5], fpos.mustBefore[nodes[4]]) - fmt.Println(fpos.mustAfter) - fmt.Println(fpos.mustBefore) - n, i := fpos.head(nodes[5]) assert.Equal(t, nodes[1], n) assert.Equal(t, 3, i) @@ -89,8 +85,6 @@ func TestInitFlatEdges(t *testing.T) { graph.NewEdge(nodes[4], nodes[5], 0), } fpos := initFixedPositions(edges) - fmt.Println(fpos.mustAfter) - fmt.Println(fpos.mustBefore) assert.Len(t, fpos.mustAfter, 3) assert.Len(t, fpos.mustBefore, 3) @@ -112,8 +106,6 @@ func TestInitFlatEdges(t *testing.T) { graph.NewEdge(nodes[1], nodes[4], 0), // should be no op } fpos := initFixedPositions(edges) - fmt.Println(fpos.mustAfter) - fmt.Println(fpos.mustBefore) assert.Len(t, fpos.mustAfter, 4) assert.Len(t, fpos.mustBefore, 4) diff --git a/internal/phase4/alg_process.go b/internal/phase4/alg_process.go index 9592252..adf6adf 100644 --- a/internal/phase4/alg_process.go +++ b/internal/phase4/alg_process.go @@ -37,10 +37,10 @@ func (alg Alg) Process(g *graph.DGraph, params graph.Params) { func assignYCoords(g *graph.DGraph, layerSpacing float64) { y := 0.0 - for i := 0; i < len(g.Layers); i++ { - for _, n := range g.Layers[i].Nodes { + for _, l := range g.Layers { + for _, n := range l.Nodes { n.Y = y } - y += g.Layers[i].H + layerSpacing + y += l.H + layerSpacing } } diff --git a/internal/phase4/brandes_koepf.go b/internal/phase4/brandes_koepf.go index 6f464c9..a803a97 100644 --- a/internal/phase4/brandes_koepf.go +++ b/internal/phase4/brandes_koepf.go @@ -1,6 +1,7 @@ package phase4 import ( + "iter" "math" "slices" "sort" @@ -145,10 +146,10 @@ func execBrandesKoepf(g *graph.DGraph, params graph.Params) { // B&K could produce a positioning with overlaps after averaging // this is a final adjustment step to mitigate the issue - for i := 0; i < len(g.Layers); i++ { - for j := 1; j < g.Layers[i].Len(); j++ { - v := g.Layers[i].Nodes[j-1] - w := g.Layers[i].Nodes[j] + for _, l := range g.Layers { + for j := 1; j < l.Len(); j++ { + v := l.Nodes[j-1] + w := l.Nodes[j] overlaps := w.X > v.X && w.X < v.X+v.W if overlaps { @@ -229,14 +230,12 @@ func initNeighbors(g *graph.DGraph) neighbors { } func (p *brandesKoepfPositioner) verticalAlign(g *graph.DGraph, layout layout) { - iter := layersIterator(g, layout.v) - for layer := iter(); layer != nil; layer = iter() { + for layer := range iterLayers(g.Layers, layout.v) { // r is the index of the nearest neighbor to which vk can be aligned // by updating r with the most recently aligned neighbor (at the end of the loop) // it's guaranteed that only one alignment is possible r := outermostPos(layout.h) - iter := nodesIterator(layer.Nodes, layout.h) - for vk := iter(); vk != nil; vk = iter() { + for vk := range iterNodes(layer.Nodes, layout.h) { vkneighbors := p.neighbors[vk][layout.v] if d := len(vkneighbors); d > 0 { for _, m := range medianNeighborIndices(d, layout.h) { @@ -277,18 +276,15 @@ func (p *brandesKoepfPositioner) horizontalCompaction(g *graph.DGraph, layout la c.xshift[n] = outermostX(layout.h) } - iter := layersIterator(g, layout.v) - for layer := iter(); layer != nil; layer = iter() { - iter := nodesIterator(layer.Nodes, layout.h) - for n := iter(); n != nil; n = iter() { + for layer := range iterLayers(g.Layers, layout.v) { + for n := range iterNodes(layer.Nodes, layout.h) { if layout.blockroot[n] == n { p.placeBlock(n, c, layout) } } } - iter = layersIterator(g, layout.v) - for layer := iter(); layer != nil; layer = iter() { + for layer := range iterLayers(g.Layers, layout.v) { n := firstNodeInLayer(layer, layout.h) if c.sinks[n] != n { continue @@ -297,31 +293,34 @@ func (p *brandesKoepfPositioner) horizontalCompaction(g *graph.DGraph, layout la c.xshift[c.sinks[n]] = 0 } - iter2 := layersIterator(g, layout.v) - for lj := iter2(); lj != nil; { - iter := nodesIterator(lj.Nodes, layout.h) - for vk := iter(); vk != nil; vk = iter() { - v := vk - if c.sinks[v] != c.sinks[vk] { - break - } - for layout.alignment[v] != layout.blockroot[v] { - v = layout.alignment[v] - lj = iter2() - lv := g.Layers[v.Layer] - if v != firstNodeInLayer(lv, layout.h) { - u := prevNodeInLayer(v, lv.Nodes, layout.h) - switch layout.h { - case left: - s := c.xshift[c.sinks[v]] + c.xcoord[v] + (c.xcoord[u] + u.W + p.nodeSpacing) - c.xshift[c.sinks[u]] = max(c.xshift[c.sinks[u]], s) - case right: - s := c.xshift[c.sinks[v]] + c.xcoord[v] - (c.xcoord[u] + p.nodeSpacing) - c.xshift[c.sinks[u]] = min(c.xshift[c.sinks[u]], s) - } + k := 0 + j := layer.Index + for j < len(g.Layers) && k < g.Layers[j].Len() { + + vjk := g.Layers[j].Nodes[k] + v := vjk + + if c.sinks[v] != c.sinks[vjk] { + break + } + + for layout.alignment[v] != layout.blockroot[v] { + v = layout.alignment[v] + lv := g.Layers[v.Layer] + if v != firstNodeInLayer(lv, layout.h) { + u := prevNodeInLayer(v, lv.Nodes, layout.h) + switch layout.h { + case left: + s := c.xshift[c.sinks[v]] + c.xcoord[v] + (c.xcoord[u] + u.W + p.nodeSpacing) + c.xshift[c.sinks[u]] = max(c.xshift[c.sinks[u]], s) + case right: + s := c.xshift[c.sinks[v]] + c.xcoord[v] - (c.xcoord[u] + p.nodeSpacing) + c.xshift[c.sinks[u]] = min(c.xshift[c.sinks[u]], s) } } + j++ } + k = v.LayerPos + 1 } } @@ -394,49 +393,37 @@ func incidentToInner(n *graph.Node) int { return -1 } -func layersIterator(g *graph.DGraph, dir direction) func() *graph.Layer { - ks := g.Layers.Keys() +func iterLayers(layers []*graph.Layer, dir direction) iter.Seq[*graph.Layer] { switch dir { case bottom: - sort.Ints(ks) + return slices.Values(layers) case top: - sort.Sort(sort.Reverse(sort.IntSlice(ks))) - default: - panic("BK positioner: invalid layer iteration direction") - } - i := 0 - return func() *graph.Layer { - if i >= len(ks) { - return nil + return func(yield func(*graph.Layer) bool) { + for _, n := range slices.Backward(layers) { + if !yield(n) { + return + } + } } - layer := g.Layers[ks[i]] - i++ - return layer + default: + panic("autog: B&K: invalid layer iteration direction") } } -func nodesIterator(nodes []*graph.Node, dir direction) func() *graph.Node { - var i int +func iterNodes(nodes []*graph.Node, dir direction) iter.Seq[*graph.Node] { switch dir { case right: - i = 0 + return slices.Values(nodes) case left: - i = len(nodes) - 1 - default: - panic("BK positioner: invalid node iteration direction") - } - return func() *graph.Node { - if (dir == right && i >= len(nodes)) || (dir == left && i < 0) { - return nil - } - node := nodes[i] - switch dir { - case right: - i++ - case left: - i-- + return func(yield func(*graph.Node) bool) { + for _, n := range slices.Backward(nodes) { + if !yield(n) { + return + } + } } - return node + default: + panic("autog: B&K: invalid node iteration direction") } } @@ -586,7 +573,7 @@ func balanceLayouts(layoutXCoords [4]xcoordinates, nodes []*graph.Node) xcoordin // it could be worth it to allow for some slack here by considering valid layouts where the nodes // don't overlap with a fraction of the spacing between them, instead of mandating full spacing // however on failure ELK returns the first layout, we still return the average. -func verifyLayout(layout xcoordinates, layers map[int]*graph.Layer, nodeSpacing float64) bool { +func verifyLayout(layout xcoordinates, layers []*graph.Layer, nodeSpacing float64) bool { for _, layer := range layers { pos := math.Inf(-1) for _, n := range layer.Nodes { diff --git a/internal/phase4/network_simplex.go b/internal/phase4/network_simplex.go index 5bd34ca..9b75af0 100644 --- a/internal/phase4/network_simplex.go +++ b/internal/phase4/network_simplex.go @@ -100,14 +100,13 @@ func (p *networkSimplexProcessor) distCenterPoints(a, b *graph.Node) float64 { return (a.W / 2) + (b.W / 2) + p.nodeSpacing } -// todo this could be merged with Edge.Type func omega(e *graph.Edge) int { switch e.Type() { - case 0: + case graph.EdgeTypeConcrete: return 1 - case 1: + case graph.EdgeTypeHybrid: return 2 - case 2: + case graph.EdgeTypeVirtual: return 8 default: panic("unexpected edge type") diff --git a/internal/phase4/pack_right.go b/internal/phase4/pack_right.go index 7f8b42f..58a179e 100644 --- a/internal/phase4/pack_right.go +++ b/internal/phase4/pack_right.go @@ -1,13 +1,16 @@ package phase4 -import "github.com/nulab/autog/internal/graph" +import ( + "slices" + + "github.com/nulab/autog/internal/graph" +) func execPackRight(g *graph.DGraph, params graph.Params) { leftBound := 0.0 for _, l := range g.Layers { x := 0.0 - iter := nodesIterator(l.Nodes, left) - for n := iter(); n != nil; n = iter() { + for _, n := range slices.Backward(l.Nodes) { x -= n.W + params.NodeSpacing n.X = x } diff --git a/internal/phase4/sink_coloring.go b/internal/phase4/sink_coloring.go index 1f3dc88..bb09cea 100644 --- a/internal/phase4/sink_coloring.go +++ b/internal/phase4/sink_coloring.go @@ -1,6 +1,8 @@ package phase4 import ( + "slices" + "github.com/nulab/autog/internal/graph" ) @@ -25,8 +27,7 @@ func execSinkColoring(g *graph.DGraph, params graph.Params) { // paint nodes, and remember the maximum same-color block width, O(n) blockwidth := graph.NodeFloatMap{} - iter := layersIterator(g, top) - for layer := iter(); layer != nil; layer = iter() { + for _, layer := range slices.Backward(g.Layers) { for _, n := range layer.Nodes { _, w := setColor(n, colors, roots, edgePriority) blockwidth[roots[n]] = max(blockwidth[roots[n]], w) @@ -35,8 +36,7 @@ func execSinkColoring(g *graph.DGraph, params graph.Params) { // init coordinates by packing nodes to the left, O(n) xcoord := graph.NodeFloatMap{} - iter = layersIterator(g, bottom) - for layer := iter(); layer != nil; layer = iter() { + for _, layer := range g.Layers { x := 0.0 for _, n := range layer.Nodes { xcoord[n] = x diff --git a/internal/phase5/route_merge.go b/internal/phase5/route_merge.go index a9d197c..2d3a2d0 100644 --- a/internal/phase5/route_merge.go +++ b/internal/phase5/route_merge.go @@ -6,31 +6,25 @@ import ( "github.com/nulab/autog/internal/graph" ) -const ( - edgeTypeNoneVirtual = iota - edgeTypeOneVirtual - edgeTypeBothVirtual -) - // merge long edges and at the same time collect route information; // merged edges are removed from the graph edge list func mergeLongEdges(g *graph.DGraph) []routableEdge { routes := make([]routableEdge, 0, len(g.Edges)) for _, e := range g.Edges { switch e.Type() { - case edgeTypeNoneVirtual: + case graph.EdgeTypeConcrete: u, v := orderedNodes(e) e.ArrowHeadStart = e.IsReversed routes = append(routes, routableEdge{e, route{[]*graph.Node{u, v}}}) - case edgeTypeOneVirtual: + case graph.EdgeTypeHybrid: // process each chain of virtual nodes only in the direction of the edge if e.From.IsVirtual { continue } routes = append(routes, routableEdge{e, route{reduceForward(g, e)}}) - case edgeTypeBothVirtual: + case graph.EdgeTypeVirtual: // skip, eventually it will be processed when encountering a type 1 edge } } diff --git a/internal/phase5/route_merge_test.go b/internal/phase5/route_merge_test.go index 2a390e9..8a64c47 100644 --- a/internal/phase5/route_merge_test.go +++ b/internal/phase5/route_merge_test.go @@ -33,7 +33,7 @@ func TestMergeLongEdges(t *testing.T) { e.Reverse() } } - G.Layers = make(graph.Layers, 5) + G.Layers = make([]*graph.Layer, 5) for _, n := range G.Nodes { setLayer(G, n) } diff --git a/internal/phase5/splines.go b/internal/phase5/splines.go index 6bc2aff..449876e 100644 --- a/internal/phase5/splines.go +++ b/internal/phase5/splines.go @@ -31,10 +31,9 @@ func execSplines(g *graph.DGraph, routes []routableEdge) { poly := geom.MergeRects(rects) ctrls := geom.FitSpline(path, geom.P{}, geom.P{}, poly.Sides()) - slices.Reverse(ctrls) e.Points = make([][2]float64, 0, len(ctrls)*4) - for _, c := range ctrls { + for _, c := range slices.Backward(ctrls) { s := c.Float64Slice() e.Points = append(e.Points, [][2]float64{s[3], s[2], s[1], s[0]}...) } diff --git a/internal/testfiles/bugfix_test.go b/internal/testfiles/bugfix_test.go index 73757e3..898b7b6 100644 --- a/internal/testfiles/bugfix_test.go +++ b/internal/testfiles/bugfix_test.go @@ -100,10 +100,10 @@ func TestCrashers(t *testing.T) { func assertNoOverlaps(t *testing.T, g *ig.DGraph, tolerance int) { overlaps := 0 - for i := 0; i < len(g.Layers); i++ { - for j := 1; j < g.Layers[i].Len(); j++ { - cur := g.Layers[i].Nodes[j] - prv := g.Layers[i].Nodes[j-1] + for _, l := range g.Layers { + for j := 1; j < l.Len(); j++ { + cur := l.Nodes[j] + prv := l.Nodes[j-1] if prv.X+prv.W > cur.X { if overlaps >= tolerance {