elkrb 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +364 -0
- data/LICENSE +25 -0
- data/README.adoc +695 -557
- data/exe/elkrb +36 -2
- data/lib/elkrb/atomic_destination.rb +53 -0
- data/lib/elkrb/best_effort_write.rb +49 -0
- data/lib/elkrb/cli/layout_flag_options.rb +54 -0
- data/lib/elkrb/cli.rb +155 -82
- data/lib/elkrb/command_resolver.rb +186 -0
- data/lib/elkrb/commands/batch_command.rb +28 -5
- data/lib/elkrb/commands/convert_command.rb +22 -52
- data/lib/elkrb/commands/diagram_command.rb +234 -102
- data/lib/elkrb/commands/options_command.rb +87 -0
- data/lib/elkrb/commands/render_command.rb +6 -1
- data/lib/elkrb/commands/validate_command.rb +146 -91
- data/lib/elkrb/errors.rb +20 -1
- data/lib/elkrb/format_sniffer.rb +362 -0
- data/lib/elkrb/geometry/bezier.rb +14 -4
- data/lib/elkrb/geometry/point.rb +5 -6
- data/lib/elkrb/geometry/rectangle.rb +2 -2
- data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
- data/lib/elkrb/graph/edge.rb +89 -3
- data/lib/elkrb/graph/graph.rb +43 -15
- data/lib/elkrb/graph/label.rb +16 -2
- data/lib/elkrb/graph/node.rb +13 -2
- data/lib/elkrb/graph/node_constraints.rb +139 -8
- data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
- data/lib/elkrb/graph/port.rb +29 -21
- data/lib/elkrb/graph/read_only_mapping.rb +21 -0
- data/lib/elkrb/graphviz_wrapper.rb +152 -44
- data/lib/elkrb/layout/algorithm_registry.rb +65 -8
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
- data/lib/elkrb/layout/algorithms/box.rb +47 -20
- data/lib/elkrb/layout/algorithms/disco.rb +150 -61
- data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
- data/lib/elkrb/layout/algorithms/force.rb +145 -86
- data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
- data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
- data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
- data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
- data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
- data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
- data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
- data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
- data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
- data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
- data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
- data/lib/elkrb/layout/algorithms/layered.rb +548 -13
- data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
- data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
- data/lib/elkrb/layout/algorithms/radial.rb +188 -29
- data/lib/elkrb/layout/algorithms/random.rb +131 -17
- data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
- data/lib/elkrb/layout/algorithms/stress.rb +391 -84
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
- data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
- data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
- data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
- data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
- data/lib/elkrb/layout/edge_router.rb +533 -337
- data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
- data/lib/elkrb/layout/java_random.rb +59 -0
- data/lib/elkrb/layout/label_placer.rb +165 -157
- data/lib/elkrb/layout/layout_engine.rb +46 -23
- data/lib/elkrb/layout/node_index.rb +149 -0
- data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
- data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
- data/lib/elkrb/layout/polyomino/grid.rb +106 -0
- data/lib/elkrb/layout/polyomino/packer.rb +95 -0
- data/lib/elkrb/layout/polyomino/piece.rb +52 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
- data/lib/elkrb/layout/spore/body.rb +65 -0
- data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
- data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
- data/lib/elkrb/layout/spore/gap.rb +76 -0
- data/lib/elkrb/layout/spore/geometry.rb +104 -0
- data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
- data/lib/elkrb/layout/spore/placement.rb +57 -0
- data/lib/elkrb/layout/spore/rays.rb +119 -0
- data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
- data/lib/elkrb/layout/spore/triangulation.rb +159 -0
- data/lib/elkrb/layout_flags.rb +69 -0
- data/lib/elkrb/options/decimal.rb +50 -0
- data/lib/elkrb/options/element_options.rb +43 -0
- data/lib/elkrb/options/elk_padding.rb +20 -14
- data/lib/elkrb/options/k_vector.rb +6 -7
- data/lib/elkrb/options/k_vector_chain.rb +12 -20
- data/lib/elkrb/options/option_map.rb +85 -0
- data/lib/elkrb/options/registry.rb +335 -0
- data/lib/elkrb/options/resolver.rb +151 -0
- data/lib/elkrb/options/spellings.rb +62 -0
- data/lib/elkrb/options/unhonoured_report.rb +194 -0
- data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
- data/lib/elkrb/parsers/elkt/parser.rb +424 -0
- data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
- data/lib/elkrb/parsers/elkt/token.rb +40 -0
- data/lib/elkrb/parsers/elkt_parser.rb +32 -230
- data/lib/elkrb/serializers/dot_serializer.rb +342 -54
- data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
- data/lib/elkrb/version.rb +1 -1
- data/lib/elkrb.rb +60 -61
- metadata +71 -49
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Rakefile +0 -64
- data/benchmarks/README.md +0 -172
- data/benchmarks/elkjs_benchmark.js +0 -140
- data/benchmarks/elkrb_benchmark.rb +0 -145
- data/benchmarks/fixtures/graphs.json +0 -10777
- data/benchmarks/generate_report.rb +0 -241
- data/benchmarks/generate_test_graphs.rb +0 -154
- data/benchmarks/results/elkrb_results.json +0 -280
- data/benchmarks/results/elkrb_summary.json +0 -285
- data/elkrb.gemspec +0 -39
- data/examples/dot_export_demo.rb +0 -133
- data/examples/hierarchical_graph.rb +0 -19
- data/examples/layout_constraints_demo.rb +0 -272
- data/examples/port_constraints_demo.rb +0 -291
- data/examples/self_loop_demo.rb +0 -391
- data/examples/simple_graph.rb +0 -50
- data/examples/spline_routing_demo.rb +0 -235
- data/lib/elkrb/graph/layout_options.rb +0 -247
- data/sig/elkrb/constraints.rbs +0 -114
- data/sig/elkrb/geometry.rbs +0 -61
- data/sig/elkrb/graph.rbs +0 -112
- data/sig/elkrb/layout.rbs +0 -107
- data/sig/elkrb/options.rbs +0 -81
- data/sig/elkrb.rbs +0 -32
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../node_index"
|
|
4
|
+
require_relative "../polyomino/component_compactor"
|
|
5
|
+
|
|
3
6
|
module Elkrb
|
|
4
7
|
module Layout
|
|
5
8
|
module Algorithms
|
|
@@ -10,6 +13,13 @@ module Elkrb
|
|
|
10
13
|
# 2. Laying out each component independently
|
|
11
14
|
# 3. Arranging components in a grid or row
|
|
12
15
|
class Disco < BaseAlgorithm
|
|
16
|
+
# ELK's default edge thickness, which widens an edge's footprint.
|
|
17
|
+
EDGE_THICKNESS = 1.0
|
|
18
|
+
# DisCo packs for a square unless the graph names an aspect ratio;
|
|
19
|
+
# the registry default of elk.aspectRatio belongs to other algorithms.
|
|
20
|
+
DEFAULT_ASPECT_RATIO = 1.0
|
|
21
|
+
private_constant :EDGE_THICKNESS, :DEFAULT_ASPECT_RATIO
|
|
22
|
+
|
|
13
23
|
def layout_flat(graph, _options = {})
|
|
14
24
|
return graph if graph.children.empty?
|
|
15
25
|
|
|
@@ -17,13 +27,13 @@ module Elkrb
|
|
|
17
27
|
components = find_connected_components(graph)
|
|
18
28
|
|
|
19
29
|
# Layout each component independently
|
|
20
|
-
component_algo =
|
|
30
|
+
component_algo = resolver.get("disco.componentAlgorithm", graph)
|
|
21
31
|
components.each do |component|
|
|
22
32
|
layout_component(component, component_algo)
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
# Arrange components
|
|
26
|
-
spacing =
|
|
36
|
+
spacing = resolver.get("disco.componentSpacing", graph)
|
|
27
37
|
arrange_components(components, graph, spacing)
|
|
28
38
|
|
|
29
39
|
# Apply padding
|
|
@@ -35,53 +45,45 @@ module Elkrb
|
|
|
35
45
|
private
|
|
36
46
|
|
|
37
47
|
def find_connected_components(graph)
|
|
48
|
+
index = NodeIndex.build(graph)
|
|
38
49
|
visited = Set.new
|
|
39
50
|
components = []
|
|
40
51
|
|
|
41
52
|
graph.children.each do |node|
|
|
42
|
-
next if visited.include?(node)
|
|
53
|
+
next if visited.include?(node.id)
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
nodes: [],
|
|
46
|
-
edges: [],
|
|
47
|
-
}
|
|
55
|
+
nodes = []
|
|
48
56
|
|
|
49
57
|
# BFS to find all connected nodes
|
|
50
58
|
queue = [node]
|
|
51
59
|
while queue.any?
|
|
52
60
|
current = queue.shift
|
|
53
|
-
next if visited.include?(current)
|
|
54
|
-
|
|
55
|
-
visited.add(current)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Find connected nodes through edges
|
|
59
|
-
connected_edges = graph.edges.select do |edge|
|
|
60
|
-
edge_nodes = []
|
|
61
|
-
edge.sources&.each do |port|
|
|
62
|
-
edge_nodes << (port.respond_to?(:node) ? port.node : port)
|
|
63
|
-
end
|
|
64
|
-
edge.targets&.each do |port|
|
|
65
|
-
edge_nodes << (port.respond_to?(:node) ? port.node : port)
|
|
66
|
-
end
|
|
67
|
-
edge_nodes.include?(current)
|
|
68
|
-
end
|
|
61
|
+
next if visited.include?(current.id)
|
|
62
|
+
|
|
63
|
+
visited.add(current.id)
|
|
64
|
+
nodes << current
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
-
edge.targets&.each do |port|
|
|
78
|
-
nodes << (port.respond_to?(:node) ? port.node : port)
|
|
79
|
-
end
|
|
80
|
-
nodes.each { |n| queue << n unless visited.include?(n) }
|
|
66
|
+
(graph.edges || []).each do |edge|
|
|
67
|
+
endpoints = index.endpoint_nodes(edge.sources) +
|
|
68
|
+
index.endpoint_nodes(edge.targets)
|
|
69
|
+
next unless endpoints.include?(current)
|
|
70
|
+
|
|
71
|
+
endpoints.each { |n| queue << n unless visited.include?(n.id) }
|
|
81
72
|
end
|
|
82
73
|
end
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
# Compute this component's edges once the whole node set is
|
|
76
|
+
# known, rather than per visited node — accumulating them
|
|
77
|
+
# incrementally during the walk records each edge twice
|
|
78
|
+
# (once from its source side, once from its target side).
|
|
79
|
+
node_ids = Set.new(nodes.map(&:id))
|
|
80
|
+
edges = (graph.edges || []).select do |edge|
|
|
81
|
+
endpoints = index.endpoint_nodes(edge.sources) +
|
|
82
|
+
index.endpoint_nodes(edge.targets)
|
|
83
|
+
endpoints.any? { |n| node_ids.include?(n.id) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
components << { nodes: nodes, edges: edges }
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
components
|
|
@@ -97,19 +99,42 @@ module Elkrb
|
|
|
97
99
|
|
|
98
100
|
# Get algorithm from registry
|
|
99
101
|
algorithm_class = Layout::AlgorithmRegistry.get(algorithm_name)
|
|
100
|
-
|
|
102
|
+
raise Elkrb::AlgorithmNotFoundError, algorithm_name unless algorithm_class
|
|
101
103
|
|
|
102
104
|
# Apply layout algorithm to component
|
|
103
105
|
algorithm = algorithm_class.new
|
|
104
106
|
algorithm.layout(temp_graph)
|
|
107
|
+
|
|
108
|
+
# The component was routed in its own coordinate space and is about
|
|
109
|
+
# to be shifted into place, so those sections are stale the moment
|
|
110
|
+
# it moves. The outer routing pass is the authoritative one — hand
|
|
111
|
+
# it a clean slate instead of letting it append to pre-offset bends.
|
|
112
|
+
component[:edges].each { |edge| edge.sections = nil }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# disco.componentCompaction.strategy is ELK's key; its one value,
|
|
116
|
+
# POLYOMINO, is also the default. disco.componentArrangement is the
|
|
117
|
+
# older elkrb key and takes ROW, COLUMN or GRID, and so does the
|
|
118
|
+
# strategy key. The graph names either key before the call does: the
|
|
119
|
+
# first reader sees the graph alone. A graph that names
|
|
120
|
+
# disco.componentArrangement and not the strategy gets that
|
|
121
|
+
# arrangement.
|
|
122
|
+
def component_arrangement(graph)
|
|
123
|
+
keys = %w[disco.componentCompaction.strategy disco.componentArrangement]
|
|
124
|
+
raw = [Options::Resolver.new, resolver].product(keys)
|
|
125
|
+
.filter_map { |reader, key| reader.get(key, graph, default: nil) }
|
|
126
|
+
.first || resolver.get(keys.first, graph)
|
|
127
|
+
raw.to_s.downcase
|
|
105
128
|
end
|
|
106
129
|
|
|
107
130
|
def arrange_components(components, graph, spacing)
|
|
108
131
|
return if components.empty?
|
|
109
132
|
|
|
110
|
-
arrangement = graph
|
|
133
|
+
arrangement = component_arrangement(graph)
|
|
111
134
|
|
|
112
135
|
case arrangement
|
|
136
|
+
when "polyomino"
|
|
137
|
+
arrange_by_polyomino(components, graph, spacing)
|
|
113
138
|
when "grid"
|
|
114
139
|
arrange_in_grid(components, spacing)
|
|
115
140
|
when "column"
|
|
@@ -119,22 +144,96 @@ module Elkrb
|
|
|
119
144
|
end
|
|
120
145
|
end
|
|
121
146
|
|
|
147
|
+
# Packs the components the way ELK's DisCo does. A component's edges
|
|
148
|
+
# have no route yet (the outer pass routes them), so each counts as the
|
|
149
|
+
# straight line between its end nodes, as wide as the spacing.
|
|
150
|
+
def arrange_by_polyomino(components, graph, spacing)
|
|
151
|
+
aspect_ratio = resolver.get("elk.aspectRatio", graph, default: DEFAULT_ASPECT_RATIO)
|
|
152
|
+
unless aspect_ratio.positive?
|
|
153
|
+
raise ValidationError, "elk.aspectRatio must be above zero, got #{aspect_ratio}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
index = NodeIndex.build(graph)
|
|
157
|
+
shapes = components.map { |component| component_shapes(component, index, spacing) }
|
|
158
|
+
offsets = Polyomino::ComponentCompactor.new(shapes, aspect_ratio: aspect_ratio).offsets
|
|
159
|
+
|
|
160
|
+
components.zip(offsets).each do |component, (dx, dy)|
|
|
161
|
+
component[:nodes].each do |node|
|
|
162
|
+
node.x = (node.x || 0.0) + dx
|
|
163
|
+
node.y = (node.y || 0.0) + dy
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def component_shapes(component, index, spacing)
|
|
169
|
+
nodes = component[:nodes]
|
|
170
|
+
shapes = nodes.flat_map { |node| node_shapes(node, spacing) }
|
|
171
|
+
component[:edges].each do |edge|
|
|
172
|
+
ends = (index.endpoint_nodes(edge.sources) + index.endpoint_nodes(edge.targets)).uniq
|
|
173
|
+
next unless ends.size == 2 && ends.all? { |n| nodes.include?(n) }
|
|
174
|
+
|
|
175
|
+
shapes << edge_shape(ends[0], ends[1], EDGE_THICKNESS + spacing)
|
|
176
|
+
end
|
|
177
|
+
shapes
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The node, and each label and port that has a position, grown by half
|
|
181
|
+
# the spacing on every side.
|
|
182
|
+
def node_shapes(node, spacing)
|
|
183
|
+
x = node.x || 0.0
|
|
184
|
+
y = node.y || 0.0
|
|
185
|
+
boxes = [[x, y, node.width, node.height]]
|
|
186
|
+
boxes += positioned(node.labels, x, y)
|
|
187
|
+
(node.ports || []).each do |port|
|
|
188
|
+
next unless port.x && port.y
|
|
189
|
+
|
|
190
|
+
px = x + port.x
|
|
191
|
+
py = y + port.y
|
|
192
|
+
boxes << [px, py, port.width, port.height]
|
|
193
|
+
boxes += positioned(port.labels, px, py)
|
|
194
|
+
end
|
|
195
|
+
boxes.map { |bx, by, w, h| grown_rectangle(bx, by, w || 0.0, h || 0.0, spacing / 2.0) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def positioned(labels, left, top)
|
|
199
|
+
(labels || []).select { |label| label.x && label.y }
|
|
200
|
+
.map { |label| [left + label.x, top + label.y, label.width, label.height] }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def grown_rectangle(left_edge, top_edge, width, height, margin)
|
|
204
|
+
left = left_edge - margin
|
|
205
|
+
top = top_edge - margin
|
|
206
|
+
right = left_edge + width + margin
|
|
207
|
+
bottom = top_edge + height + margin
|
|
208
|
+
[[left, top], [left, bottom], [right, bottom], [right, top]]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def edge_shape(from, to, thickness)
|
|
212
|
+
ax = (from.x || 0.0) + ((from.width || 0.0) / 2.0)
|
|
213
|
+
ay = (from.y || 0.0) + ((from.height || 0.0) / 2.0)
|
|
214
|
+
bx = (to.x || 0.0) + ((to.width || 0.0) / 2.0)
|
|
215
|
+
by = (to.y || 0.0) + ((to.height || 0.0) / 2.0)
|
|
216
|
+
length = Math.hypot(bx - ax, by - ay)
|
|
217
|
+
return grown_rectangle(ax, ay, 0.0, 0.0, thickness / 2.0) if length.zero?
|
|
218
|
+
|
|
219
|
+
nx = (by - ay) / length * (thickness / 2.0)
|
|
220
|
+
ny = (ax - bx) / length * (thickness / 2.0)
|
|
221
|
+
[[ax + nx, ay + ny], [bx + nx, by + ny], [bx - nx, by - ny], [ax - nx, ay - ny]]
|
|
222
|
+
end
|
|
223
|
+
|
|
122
224
|
def arrange_in_row(components, spacing)
|
|
123
225
|
x_offset = 0.0
|
|
124
226
|
|
|
125
227
|
components.each do |component|
|
|
126
|
-
|
|
127
|
-
min_x = component[:nodes].map(&:x).min || 0.0
|
|
128
|
-
max_x = component[:nodes].map { |n| n.x + n.width }.max || 0.0
|
|
129
|
-
component_width = max_x - min_x
|
|
228
|
+
box = calculate_bounding_box(component[:nodes])
|
|
130
229
|
|
|
131
230
|
# Offset all nodes in component
|
|
132
|
-
offset_x = x_offset -
|
|
231
|
+
offset_x = x_offset - box.x
|
|
133
232
|
component[:nodes].each do |node|
|
|
134
233
|
node.x += offset_x
|
|
135
234
|
end
|
|
136
235
|
|
|
137
|
-
x_offset +=
|
|
236
|
+
x_offset += box.width + spacing
|
|
138
237
|
end
|
|
139
238
|
end
|
|
140
239
|
|
|
@@ -142,18 +241,15 @@ module Elkrb
|
|
|
142
241
|
y_offset = 0.0
|
|
143
242
|
|
|
144
243
|
components.each do |component|
|
|
145
|
-
|
|
146
|
-
min_y = component[:nodes].map(&:y).min || 0.0
|
|
147
|
-
max_y = component[:nodes].map { |n| n.y + n.height }.max || 0.0
|
|
148
|
-
component_height = max_y - min_y
|
|
244
|
+
box = calculate_bounding_box(component[:nodes])
|
|
149
245
|
|
|
150
246
|
# Offset all nodes in component
|
|
151
|
-
offset_y = y_offset -
|
|
247
|
+
offset_y = y_offset - box.y
|
|
152
248
|
component[:nodes].each do |node|
|
|
153
249
|
node.y += offset_y
|
|
154
250
|
end
|
|
155
251
|
|
|
156
|
-
y_offset +=
|
|
252
|
+
y_offset += box.height + spacing
|
|
157
253
|
end
|
|
158
254
|
end
|
|
159
255
|
|
|
@@ -177,23 +273,16 @@ module Elkrb
|
|
|
177
273
|
|
|
178
274
|
component = components[index]
|
|
179
275
|
|
|
180
|
-
|
|
181
|
-
min_x = component[:nodes].map(&:x).min || 0.0
|
|
182
|
-
min_y = component[:nodes].map(&:y).min || 0.0
|
|
183
|
-
max_x = component[:nodes].map { |n| n.x + n.width }.max || 0.0
|
|
184
|
-
max_y = component[:nodes].map { |n| n.y + n.height }.max || 0.0
|
|
185
|
-
|
|
186
|
-
component_width = max_x - min_x
|
|
187
|
-
component_height = max_y - min_y
|
|
276
|
+
box = calculate_bounding_box(component[:nodes])
|
|
188
277
|
|
|
189
278
|
# Offset nodes
|
|
190
279
|
component[:nodes].each do |node|
|
|
191
|
-
node.x += x_offset -
|
|
192
|
-
node.y += y_offset -
|
|
280
|
+
node.x += x_offset - box.x
|
|
281
|
+
node.y += y_offset - box.y
|
|
193
282
|
end
|
|
194
283
|
|
|
195
|
-
x_offset +=
|
|
196
|
-
max_row_height = [max_row_height,
|
|
284
|
+
x_offset += box.width + spacing
|
|
285
|
+
max_row_height = [max_row_height, box.height].max
|
|
197
286
|
end
|
|
198
287
|
|
|
199
288
|
row_heights << max_row_height
|
|
@@ -7,25 +7,85 @@ module Elkrb
|
|
|
7
7
|
module Algorithms
|
|
8
8
|
# Fixed layout algorithm
|
|
9
9
|
#
|
|
10
|
-
# Keeps nodes at their current positions
|
|
11
|
-
#
|
|
10
|
+
# Keeps nodes at their current positions and calculates graph dimensions
|
|
11
|
+
# without translating them.
|
|
12
12
|
# Useful when node positions are pre-determined or manually set.
|
|
13
13
|
class Fixed < BaseAlgorithm
|
|
14
14
|
def layout_flat(graph, _options = {})
|
|
15
15
|
return graph if graph.children.nil? || graph.children.empty?
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
graph.children.each do |node|
|
|
19
|
-
node.x ||= 0.0
|
|
20
|
-
node.y ||= 0.0
|
|
21
|
-
end
|
|
17
|
+
graph.children.each { |node| apply_position(node) }
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
# Nodes keep their existing x, y positions
|
|
25
|
-
apply_padding(graph)
|
|
19
|
+
size_graph(graph)
|
|
26
20
|
|
|
27
21
|
graph
|
|
28
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def apply_position(node)
|
|
27
|
+
position = resolver.get("elk.position", node, default: nil)
|
|
28
|
+
node.x = position ? position.x : (node.x || 0.0)
|
|
29
|
+
node.y = position ? position.y : (node.y || 0.0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def size_graph(graph)
|
|
33
|
+
pad = padding
|
|
34
|
+
graph.width = max_right(graph) + horizontal_padding(pad)
|
|
35
|
+
graph.height = max_bottom(graph) + vertical_padding(pad)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def max_right(graph)
|
|
39
|
+
graph.children.map { |node| right_edge(node) }.max
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def max_bottom(graph)
|
|
43
|
+
graph.children.map { |node| bottom_edge(node) }.max
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def right_edge(node)
|
|
47
|
+
node.x + (node.width || 0.0)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def bottom_edge(node)
|
|
51
|
+
node.y + (node.height || 0.0)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def horizontal_padding(pad)
|
|
55
|
+
pad[:left] + pad[:right]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def vertical_padding(pad)
|
|
59
|
+
pad[:top] + pad[:bottom]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def padding
|
|
63
|
+
option(
|
|
64
|
+
"elk.padding",
|
|
65
|
+
default: { left: 15.0, top: 15.0, right: 15.0, bottom: 15.0 },
|
|
66
|
+
).to_h
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Fixed layout preserves routes. It only applies explicitly declared
|
|
70
|
+
# bend points and records the containing graph.
|
|
71
|
+
def apply_edge_routing(graph)
|
|
72
|
+
(graph.edges || []).each do |edge|
|
|
73
|
+
bends = resolver.get("elk.bendPoints", edge, default: nil)
|
|
74
|
+
apply_bends(edge, bends) if bends
|
|
75
|
+
edge.container = graph.id
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_bends(edge, bends)
|
|
80
|
+
edge.sections ||= []
|
|
81
|
+
section = edge.sections.first || Graph::EdgeSection.new(
|
|
82
|
+
id: "#{edge.id}_s0",
|
|
83
|
+
)
|
|
84
|
+
section.bend_points = bends.vectors.map do |point|
|
|
85
|
+
Geometry::Point.new(x: point.x, y: point.y)
|
|
86
|
+
end
|
|
87
|
+
edge.sections = [section]
|
|
88
|
+
end
|
|
29
89
|
end
|
|
30
90
|
end
|
|
31
91
|
end
|