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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +364 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +695 -557
  5. data/exe/elkrb +36 -2
  6. data/lib/elkrb/atomic_destination.rb +53 -0
  7. data/lib/elkrb/best_effort_write.rb +49 -0
  8. data/lib/elkrb/cli/layout_flag_options.rb +54 -0
  9. data/lib/elkrb/cli.rb +155 -82
  10. data/lib/elkrb/command_resolver.rb +186 -0
  11. data/lib/elkrb/commands/batch_command.rb +28 -5
  12. data/lib/elkrb/commands/convert_command.rb +22 -52
  13. data/lib/elkrb/commands/diagram_command.rb +234 -102
  14. data/lib/elkrb/commands/options_command.rb +87 -0
  15. data/lib/elkrb/commands/render_command.rb +6 -1
  16. data/lib/elkrb/commands/validate_command.rb +146 -91
  17. data/lib/elkrb/errors.rb +20 -1
  18. data/lib/elkrb/format_sniffer.rb +362 -0
  19. data/lib/elkrb/geometry/bezier.rb +14 -4
  20. data/lib/elkrb/geometry/point.rb +5 -6
  21. data/lib/elkrb/geometry/rectangle.rb +2 -2
  22. data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
  23. data/lib/elkrb/graph/edge.rb +89 -3
  24. data/lib/elkrb/graph/graph.rb +43 -15
  25. data/lib/elkrb/graph/label.rb +16 -2
  26. data/lib/elkrb/graph/node.rb +13 -2
  27. data/lib/elkrb/graph/node_constraints.rb +139 -8
  28. data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
  29. data/lib/elkrb/graph/port.rb +29 -21
  30. data/lib/elkrb/graph/read_only_mapping.rb +21 -0
  31. data/lib/elkrb/graphviz_wrapper.rb +152 -44
  32. data/lib/elkrb/layout/algorithm_registry.rb +65 -8
  33. data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
  34. data/lib/elkrb/layout/algorithms/box.rb +47 -20
  35. data/lib/elkrb/layout/algorithms/disco.rb +150 -61
  36. data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
  37. data/lib/elkrb/layout/algorithms/force.rb +145 -86
  38. data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
  39. data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
  40. data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
  41. data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
  42. data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
  43. data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
  44. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
  45. data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
  46. data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
  47. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
  48. data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
  49. data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
  50. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
  51. data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
  52. data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
  53. data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
  54. data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
  55. data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
  56. data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
  57. data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
  58. data/lib/elkrb/layout/algorithms/layered.rb +548 -13
  59. data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
  60. data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
  61. data/lib/elkrb/layout/algorithms/radial.rb +188 -29
  62. data/lib/elkrb/layout/algorithms/random.rb +131 -17
  63. data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
  64. data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
  65. data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
  66. data/lib/elkrb/layout/algorithms/stress.rb +391 -84
  67. data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
  68. data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
  69. data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
  70. data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
  71. data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
  72. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
  73. data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
  74. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
  75. data/lib/elkrb/layout/edge_router.rb +533 -337
  76. data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
  77. data/lib/elkrb/layout/java_random.rb +59 -0
  78. data/lib/elkrb/layout/label_placer.rb +165 -157
  79. data/lib/elkrb/layout/layout_engine.rb +46 -23
  80. data/lib/elkrb/layout/node_index.rb +149 -0
  81. data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
  82. data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
  83. data/lib/elkrb/layout/polyomino/grid.rb +106 -0
  84. data/lib/elkrb/layout/polyomino/packer.rb +95 -0
  85. data/lib/elkrb/layout/polyomino/piece.rb +52 -0
  86. data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
  87. data/lib/elkrb/layout/spore/body.rb +65 -0
  88. data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
  89. data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
  90. data/lib/elkrb/layout/spore/gap.rb +76 -0
  91. data/lib/elkrb/layout/spore/geometry.rb +104 -0
  92. data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
  93. data/lib/elkrb/layout/spore/placement.rb +57 -0
  94. data/lib/elkrb/layout/spore/rays.rb +119 -0
  95. data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
  96. data/lib/elkrb/layout/spore/triangulation.rb +159 -0
  97. data/lib/elkrb/layout_flags.rb +69 -0
  98. data/lib/elkrb/options/decimal.rb +50 -0
  99. data/lib/elkrb/options/element_options.rb +43 -0
  100. data/lib/elkrb/options/elk_padding.rb +20 -14
  101. data/lib/elkrb/options/k_vector.rb +6 -7
  102. data/lib/elkrb/options/k_vector_chain.rb +12 -20
  103. data/lib/elkrb/options/option_map.rb +85 -0
  104. data/lib/elkrb/options/registry.rb +335 -0
  105. data/lib/elkrb/options/resolver.rb +151 -0
  106. data/lib/elkrb/options/spellings.rb +62 -0
  107. data/lib/elkrb/options/unhonoured_report.rb +194 -0
  108. data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
  109. data/lib/elkrb/parsers/elkt/parser.rb +424 -0
  110. data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
  111. data/lib/elkrb/parsers/elkt/token.rb +40 -0
  112. data/lib/elkrb/parsers/elkt_parser.rb +32 -230
  113. data/lib/elkrb/serializers/dot_serializer.rb +342 -54
  114. data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
  115. data/lib/elkrb/version.rb +1 -1
  116. data/lib/elkrb.rb +60 -61
  117. metadata +71 -49
  118. data/.rspec +0 -3
  119. data/.rubocop.yml +0 -11
  120. data/Gemfile +0 -13
  121. data/Rakefile +0 -64
  122. data/benchmarks/README.md +0 -172
  123. data/benchmarks/elkjs_benchmark.js +0 -140
  124. data/benchmarks/elkrb_benchmark.rb +0 -145
  125. data/benchmarks/fixtures/graphs.json +0 -10777
  126. data/benchmarks/generate_report.rb +0 -241
  127. data/benchmarks/generate_test_graphs.rb +0 -154
  128. data/benchmarks/results/elkrb_results.json +0 -280
  129. data/benchmarks/results/elkrb_summary.json +0 -285
  130. data/elkrb.gemspec +0 -39
  131. data/examples/dot_export_demo.rb +0 -133
  132. data/examples/hierarchical_graph.rb +0 -19
  133. data/examples/layout_constraints_demo.rb +0 -272
  134. data/examples/port_constraints_demo.rb +0 -291
  135. data/examples/self_loop_demo.rb +0 -391
  136. data/examples/simple_graph.rb +0 -50
  137. data/examples/spline_routing_demo.rb +0 -235
  138. data/lib/elkrb/graph/layout_options.rb +0 -247
  139. data/sig/elkrb/constraints.rbs +0 -114
  140. data/sig/elkrb/geometry.rbs +0 -61
  141. data/sig/elkrb/graph.rbs +0 -112
  142. data/sig/elkrb/layout.rbs +0 -107
  143. data/sig/elkrb/options.rbs +0 -81
  144. 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 = graph.layout_options&.[]("disco.componentAlgorithm") || "layered"
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 = graph.layout_options&.[]("disco.componentSpacing") || 20.0
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
- component = {
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
- component[:nodes] << current
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
- component[:edges].concat(connected_edges)
71
-
72
- connected_edges.each do |edge|
73
- nodes = []
74
- edge.sources&.each do |port|
75
- nodes << (port.respond_to?(:node) ? port.node : port)
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
- components << component
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
- return unless algorithm_class
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.layout_options&.[]("disco.componentArrangement") || "row"
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
- # Calculate component bounds
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 - min_x
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 += component_width + spacing
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
- # Calculate component bounds
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 - min_y
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 += component_height + spacing
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
- # Calculate component bounds
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 - min_x
192
- node.y += y_offset - min_y
280
+ node.x += x_offset - box.x
281
+ node.y += y_offset - box.y
193
282
  end
194
283
 
195
- x_offset += component_width + spacing
196
- max_row_height = [max_row_height, component_height].max
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. Only applies padding
11
- # and calculates graph dimensions based on existing node positions.
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
- # Ensure all nodes have positions
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
- # Simply apply padding and calculate dimensions
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