-
Notifications
You must be signed in to change notification settings - Fork 0
/
autolayout.go
140 lines (117 loc) · 3.27 KB
/
autolayout.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package autog
import (
"slices"
"github.com/nulab/autog/graph"
ig "github.com/nulab/autog/internal/graph"
imonitor "github.com/nulab/autog/internal/monitor"
"github.com/nulab/autog/internal/processor"
"github.com/nulab/autog/internal/processor/postprocessor"
"github.com/nulab/autog/internal/processor/preprocessor"
)
// Layout executes the layout algorithm on the graph G obtained from source. It panics if G contains no nodes.
func Layout(source graph.Source, opts ...Option) graph.Layout {
layoutOpts := defaultOptions
for _, opt := range opts {
opt(&layoutOpts)
}
imonitor.Set(layoutOpts.monitor)
defer imonitor.Reset()
pipeline := []processor.P{
layoutOpts.p1, // cycle breaking
layoutOpts.p2, // layering
layoutOpts.p3, // ordering
layoutOpts.p4, // positioning
layoutOpts.p5, // edge routing
}
// populate the graph struct from the graph source
G := from(source)
if len(G.Nodes) == 0 {
panic("autog: node set is empty")
}
if layoutOpts.params.NodeFixedSizeFunc != nil {
for _, n := range G.Nodes {
layoutOpts.params.NodeFixedSizeFunc(n)
}
}
if layoutOpts.params.NodeSizeFunc != nil {
for _, n := range G.Nodes {
layoutOpts.params.NodeSizeFunc(n)
}
}
// return only relevant data to the caller
out := graph.Layout{}
// shift disconnected sub-graphs to the right
shift := 0.0
// process each connected components and collect results into the same layout output
for _, g := range G.ConnectedComponents() {
if len(g.Nodes) == 0 {
panic("autog: connected sub-graph node set is empty: this might be a bug")
}
out.Nodes = slices.Grow(out.Nodes, len(g.Nodes))
out.Edges = slices.Grow(out.Edges, len(g.Edges))
// pre-processing
restoreSelfLoops := preprocessor.IgnoreSelfLoops(g)
// run subgraph through the pipeline
for _, phase := range pipeline {
phase.Process(g, layoutOpts.params)
}
// post-processing
restoreSelfLoops(g)
postprocessor.UnreverseEdges(g)
// collect nodes
for _, n := range g.Nodes {
if n.IsVirtual && !layoutOpts.output.includeVirtual {
continue
}
m := graph.Node{
ID: n.ID,
Size: n.Size,
}
// apply subgraph's left shift
m.X += shift
out.Nodes = append(out.Nodes, m)
// todo: clients can't reliably tell virtual nodes from concrete nodes
}
// collect edges
for _, e := range g.Edges {
f := graph.Edge{
FromID: e.From.ID,
ToID: e.To.ID,
Points: slices.Clone(e.Points),
ArrowHeadStart: e.ArrowHeadStart,
}
// apply subgraph's left shift
for i := range f.Points {
f.Points[i][0] += shift
}
out.Edges = append(out.Edges, f)
}
// compute shift for subsequent subgraphs
rightmostX := 0.0
for _, l := range g.Layers {
if len(l.Nodes) == 0 {
// empty layers don't affect the shift
continue
}
n := l.Nodes[len(l.Nodes)-1]
rightmostX = max(rightmostX, n.X+n.W)
}
shift += rightmostX + layoutOpts.params.NodeSpacing
}
if !layoutOpts.output.includeVirtual {
out.Nodes = slices.Clip(out.Nodes)
}
return out
}
func from(source graph.Source) *ig.DGraph {
switch t := source.(type) {
case *ig.DGraph:
// special case for when the graph source is already a DGraph
// this happens only during unit testing
return t
default:
g := &ig.DGraph{}
source.Populate(g)
return g
}
}