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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_algorithm"
4
+ require_relative "../node_index"
4
5
 
5
6
  module Elkrb
6
7
  module Layout
@@ -17,147 +18,205 @@ module Elkrb
17
18
  # - Mind maps
18
19
  # - General undirected graphs
19
20
  class Force < BaseAlgorithm
20
- DEFAULT_ITERATIONS = 300
21
- DEFAULT_REPULSION = 5.0
22
- DEFAULT_TEMPERATURE = 0.001
21
+ DEFAULT_SPACING = 80.0
22
+ MIN_DISTANCE = 0.01
23
+ PERCENTAGE_BASE = 100.0
24
+
25
+ # The registry owns these; the constants stay for existing callers.
26
+ DEFAULT_ITERATIONS = Options::Registry.default("elk.force.iterations")
27
+ DEFAULT_REPULSION = Options::Registry.default("elk.force.repulsion")
28
+ DEFAULT_TEMPERATURE = Options::Registry.default("elk.force.temperature")
23
29
 
24
30
  def layout_flat(graph, _options = {})
25
31
  return graph if graph.children.nil? || graph.children.empty?
26
32
 
27
- # Get configuration
28
- iterations = option("iterations", DEFAULT_ITERATIONS).to_i
29
- repulsion = option("repulsion", DEFAULT_REPULSION).to_f
30
- temperature = option("temperature", DEFAULT_TEMPERATURE).to_f
33
+ iterations = option("elk.force.iterations", default: 300)
34
+ spacing = option("elk.spacing.nodeNode", default: DEFAULT_SPACING)
35
+ area = layout_area(graph.children, spacing)
36
+ side = Math.sqrt(area)
37
+ ideal_length = [
38
+ Math.sqrt(area / graph.children.length),
39
+ MIN_DISTANCE,
40
+ ].max
31
41
 
32
- # Initialize positions randomly if not set
33
- initialize_positions(graph)
42
+ initialize_positions(graph, side)
43
+ resolved_edges = resolve_edge_positions(graph)
44
+ repulsion = repulsion_scale
45
+ initial_temperature = side / 10.0 * temperature_scale
34
46
 
35
- # Run force simulation
36
47
  iterations.times do |i|
37
- apply_forces(graph, repulsion, temperature, i, iterations)
48
+ temperature = initial_temperature * (1.0 - (i.to_f / iterations))
49
+ apply_forces(graph.children, resolved_edges, ideal_length,
50
+ repulsion, temperature)
38
51
  end
39
52
 
40
- # Apply padding and set graph dimensions
41
53
  apply_padding(graph)
42
-
43
54
  graph
44
55
  end
45
56
 
46
57
  private
47
58
 
48
- def initialize_positions(graph)
49
- # Calculate approximate area needed
50
- total_area = graph.children.sum do |n|
51
- (n.width + 20) * (n.height + 20)
59
+ def layout_area(nodes, spacing)
60
+ nodes.sum do |node|
61
+ ((node.width || 0.0) + spacing) *
62
+ ((node.height || 0.0) + spacing)
52
63
  end
53
- side = Math.sqrt(total_area)
64
+ end
54
65
 
66
+ def initialize_positions(graph, side)
55
67
  graph.children.each do |node|
56
- # Set random position if not already set
57
68
  unless node.x && node.y
58
- node.x = rand * side
59
- node.y = rand * side
69
+ node.x = rng.rand * side
70
+ node.y = rng.rand * side
60
71
  end
61
72
  end
62
73
  end
63
74
 
64
- def apply_forces(graph, repulsion, temperature, iteration,
65
- max_iterations)
66
- # Calculate temperature decay
67
- temp = temperature * (1.0 - (iteration.to_f / max_iterations))
75
+ # Resolves each edge's source/target ids (which may be port ids)
76
+ # to their owning node's position in graph.children, once, so
77
+ # the iteration loop below never re-scans ids per edge.
78
+ def resolve_edge_positions(graph)
79
+ index = NodeIndex.build(graph)
80
+ positions = graph.children.each_with_index.to_h { |n, i| [n.id, i] }
68
81
 
69
- # Calculate forces for each node
70
- forces = calculate_forces(graph, repulsion)
82
+ index.edges.filter_map do |edge|
83
+ source_id = edge.sources&.first
84
+ target_id = edge.targets&.first
85
+ next unless source_id && target_id
71
86
 
72
- # Apply forces with temperature
73
- graph.children.each_with_index do |node, i|
74
- force = forces[i]
75
- magnitude = Math.sqrt((force[:x]**2) + (force[:y]**2))
87
+ source_node = index.node(source_id)
88
+ target_node = index.node(target_id)
89
+ next unless source_node && target_node
76
90
 
77
- next if magnitude.zero?
91
+ source_position = positions[source_node.id]
92
+ target_position = positions[target_node.id]
93
+ next unless source_position && target_position
94
+ next if source_position == target_position
78
95
 
79
- # Apply displacement with temperature
80
- displacement = [magnitude, temp].min
81
- node.x += (force[:x] / magnitude) * displacement
82
- node.y += (force[:y] / magnitude) * displacement
96
+ [source_position, target_position]
83
97
  end
84
98
  end
85
99
 
86
- def calculate_forces(graph, repulsion)
87
- forces = graph.children.map { { x: 0.0, y: 0.0 } }
100
+ def temperature_scale
101
+ option("elk.force.temperature", default: DEFAULT_TEMPERATURE).to_f /
102
+ DEFAULT_TEMPERATURE
103
+ end
88
104
 
89
- # Repulsive forces between all pairs
90
- graph.children.each_with_index do |node1, i|
91
- graph.children.each_with_index do |node2, j|
92
- next if i >= j
105
+ def repulsion_scale
106
+ option("elk.force.repulsion", default: DEFAULT_REPULSION).to_f /
107
+ PERCENTAGE_BASE
108
+ end
93
109
 
94
- apply_repulsive_force(node1, node2, forces[i], forces[j],
110
+ def apply_forces(nodes, resolved_edges, ideal_length, repulsion,
111
+ temperature)
112
+ forces = calculate_forces(nodes, resolved_edges, ideal_length,
95
113
  repulsion)
96
- end
97
- end
98
114
 
99
- # Attractive forces for edges
100
- all_edges = collect_all_edges(graph)
101
- all_edges.each do |edge|
102
- source_id = edge.sources&.first
103
- target_id = edge.targets&.first
115
+ nodes.each_with_index do |node, i|
116
+ force_x, force_y = forces[i]
117
+ magnitude = Math.hypot(force_x, force_y)
104
118
 
105
- next unless source_id && target_id
119
+ next if magnitude.zero?
120
+
121
+ displacement = [magnitude, temperature].min
122
+ node.x += (force_x / magnitude) * displacement
123
+ node.y += (force_y / magnitude) * displacement
124
+ end
125
+ end
106
126
 
107
- source_idx = graph.children.index { |n| n.id == source_id }
108
- target_idx = graph.children.index { |n| n.id == target_id }
127
+ def calculate_forces(nodes, resolved_edges, ideal_length, repulsion)
128
+ forces = Array.new(nodes.length) { [0.0, 0.0] }
109
129
 
110
- next unless source_idx && target_idx
130
+ nodes.each_index do |i|
131
+ ((i + 1)...nodes.length).each do |j|
132
+ apply_repulsive_force(nodes[i], nodes[j], forces[i], forces[j],
133
+ ideal_length, repulsion)
134
+ end
135
+ end
111
136
 
137
+ resolved_edges.each do |source_idx, target_idx|
112
138
  apply_attractive_force(
113
- graph.children[source_idx],
114
- graph.children[target_idx],
139
+ nodes[source_idx],
140
+ nodes[target_idx],
115
141
  forces[source_idx],
116
142
  forces[target_idx],
143
+ ideal_length,
117
144
  )
118
145
  end
119
146
 
120
147
  forces
121
148
  end
122
149
 
123
- def apply_repulsive_force(node1, node2, force1, force2, repulsion)
124
- dx = node2.x - node1.x
125
- dy = node2.y - node1.y
126
- distance_sq = (dx**2) + (dy**2)
150
+ def apply_repulsive_force(node1, node2, force1, force2, ideal_length,
151
+ repulsion)
152
+ direction_x, direction_y, distance = border_vector(node1, node2)
153
+ magnitude = repulsion * (ideal_length**2) / distance
154
+ apply_force_pair(force1, force2, direction_x, direction_y,
155
+ -magnitude)
156
+ end
157
+
158
+ def apply_attractive_force(node1, node2, force1, force2, ideal_length)
159
+ direction_x, direction_y, distance = border_vector(node1, node2)
160
+ magnitude = (distance**2) / ideal_length
161
+ apply_force_pair(force1, force2, direction_x, direction_y, magnitude)
162
+ end
163
+
164
+ def border_vector(node1, node2)
165
+ dx = center_x(node2) - center_x(node1)
166
+ dy = center_y(node2) - center_y(node1)
127
167
 
128
- # Avoid division by zero
129
- return if distance_sq < 0.01
168
+ if dx.zero? && dy.zero?
169
+ angle = rng.rand * 2.0 * Math::PI
170
+ dx = Math.cos(angle) * MIN_DISTANCE
171
+ dy = Math.sin(angle) * MIN_DISTANCE
172
+ end
130
173
 
131
- # Repulsive force inversely proportional to distance
132
- force = repulsion / distance_sq
133
- force1[:x] -= (dx / Math.sqrt(distance_sq)) * force
134
- force1[:y] -= (dy / Math.sqrt(distance_sq)) * force
135
- force2[:x] += (dx / Math.sqrt(distance_sq)) * force
136
- force2[:y] += (dy / Math.sqrt(distance_sq)) * force
174
+ center_distance = Math.hypot(dx, dy)
175
+ direction_x = dx / center_distance
176
+ direction_y = dy / center_distance
177
+ horizontal_gap = [dx.abs - half_widths(node1, node2), 0.0].max
178
+ vertical_gap = [dy.abs - half_heights(node1, node2), 0.0].max
179
+ distance = [
180
+ Math.hypot(horizontal_gap, vertical_gap),
181
+ MIN_DISTANCE,
182
+ ].max
183
+
184
+ [direction_x, direction_y, distance]
137
185
  end
138
186
 
139
- def apply_attractive_force(node1, node2, force1, force2)
140
- dx = node2.x - node1.x
141
- dy = node2.y - node1.y
142
- distance = Math.sqrt((dx**2) + (dy**2))
187
+ def apply_force_pair(force1, force2, direction_x, direction_y,
188
+ magnitude)
189
+ force_x = direction_x * magnitude
190
+ force_y = direction_y * magnitude
191
+ force1[0] += force_x
192
+ force1[1] += force_y
193
+ force2[0] -= force_x
194
+ force2[1] -= force_y
195
+ end
143
196
 
144
- return if distance.zero?
197
+ def center_x(node)
198
+ node.x + ((node.width || 0.0) / 2.0)
199
+ end
145
200
 
146
- # Spring force proportional to distance
147
- force = distance / 10.0
148
- force1[:x] += (dx / distance) * force
149
- force1[:y] += (dy / distance) * force
150
- force2[:x] -= (dx / distance) * force
151
- force2[:y] -= (dy / distance) * force
201
+ def center_y(node)
202
+ node.y + ((node.height || 0.0) / 2.0)
152
203
  end
153
204
 
154
- def collect_all_edges(graph)
155
- edges = []
156
- edges.concat(graph.edges) if graph.edges
157
- graph.children&.each do |node|
158
- edges.concat(node.edges) if node.edges
159
- end
160
- edges
205
+ def half_widths(node1, node2)
206
+ ((node1.width || 0.0) + (node2.width || 0.0)) / 2.0
207
+ end
208
+
209
+ def half_heights(node1, node2)
210
+ ((node1.height || 0.0) + (node2.height || 0.0)) / 2.0
211
+ end
212
+
213
+ # ELK's force layout uses 50px padding even though the shared core
214
+ # registry default is 12px. An explicit graph/call option still wins.
215
+ def padding
216
+ option(
217
+ "elk.padding",
218
+ default: { left: 50.0, top: 50.0, right: 50.0, bottom: 50.0 },
219
+ ).to_h
161
220
  end
162
221
  end
163
222
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Layout
5
+ module Algorithms
6
+ module Layered
7
+ module Bk
8
+ # Vertical alignment of Brandes-Koepf: chains nodes into blocks
9
+ # along median edges, then offsets each block's nodes so the edges
10
+ # that joined them leave straight.
11
+ class Aligner
12
+ # @return [Hash] first node of each block, per node id
13
+ attr_reader :root
14
+ # @return [Hash] next node of the block; the last points at the
15
+ # root
16
+ attr_reader :align
17
+ # @return [Hash] distance of a node below the top of its block
18
+ attr_reader :inner
19
+
20
+ # @param marked [Hash] segments type 1 conflicts forbid aligning
21
+ def initialize(view, marked)
22
+ @view = view
23
+ @marked = marked
24
+ @root = {}
25
+ @align = {}
26
+ @via = {}
27
+ @inner = Hash.new(0.0)
28
+ view.layers.flatten.each { |id| start_block(id) }
29
+ align_layers
30
+ shift_blocks
31
+ end
32
+
33
+ def block(root_id)
34
+ chain = [root_id]
35
+ node = @align[root_id]
36
+ until node == root_id
37
+ chain << node
38
+ node = @align[node]
39
+ end
40
+ chain
41
+ end
42
+
43
+ private
44
+
45
+ def start_block(id)
46
+ @root[id] = id
47
+ @align[id] = id
48
+ end
49
+
50
+ def align_layers
51
+ @view.layers.drop(1).each do |layer|
52
+ previous = -1
53
+ layer.each do |id|
54
+ link = alignable_link(id, previous)
55
+ next unless link
56
+
57
+ join(id, link)
58
+ previous = @view.position(link.neighbor_id)
59
+ end
60
+ end
61
+ end
62
+
63
+ def alignable_link(id, previous)
64
+ links = @view.links(id)
65
+ return if links.empty?
66
+
67
+ links[((links.length - 1) / 2)..(links.length / 2)].find do |link|
68
+ !@marked[link.segment] &&
69
+ previous < @view.position(link.neighbor_id)
70
+ end
71
+ end
72
+
73
+ def join(id, link)
74
+ upper = link.neighbor_id
75
+ @align[upper] = id
76
+ @root[id] = @root[upper]
77
+ @align[id] = @root[id]
78
+ @via[id] = link
79
+ end
80
+
81
+ def shift_blocks
82
+ @root.each_key { |id| shift_block(id) if @root[id] == id }
83
+ end
84
+
85
+ def shift_block(root_id)
86
+ chain = block(root_id)
87
+ chain.each_cons(2) do |upper, lower|
88
+ @inner[lower] = @inner[upper] + drop(@via.fetch(lower))
89
+ end
90
+ top = chain.map { |member| @inner[member] }.min
91
+ chain.each { |member| @inner[member] -= top }
92
+ end
93
+
94
+ # How far below its upper neighbour a node sits so the edge
95
+ # joining them leaves straight.
96
+ def drop(link)
97
+ link.neighbor_offset - link.own_offset
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Layout
5
+ module Algorithms
6
+ module Layered
7
+ module Bk
8
+ # Horizontal compaction of Brandes-Koepf: places every block as
9
+ # close to the start of its layer as the spacing allows, then
10
+ # slides whole classes of blocks toward each other.
11
+ class Compactor
12
+ # @param spacing [#call] gap required between two neighbours,
13
+ # given their ids
14
+ def initialize(view, aligner, spacing)
15
+ @view = view
16
+ @aligner = aligner
17
+ @spacing = spacing
18
+ @coordinate = {}
19
+ @sink = Hash.new { |_, id| id }
20
+ @shift = Hash.new(Float::INFINITY)
21
+ end
22
+
23
+ # @return [Hash{String=>Float}] start of every item along the
24
+ # view's cross axis
25
+ def place
26
+ roots = @view.layers.flatten.select do |id|
27
+ @aligner.root[id] == id
28
+ end
29
+ roots.each { |root| place_block(root) }
30
+ @view.layers.flatten.to_h { |id| [id, absolute(id)] }
31
+ end
32
+
33
+ private
34
+
35
+ def absolute(id)
36
+ root = @aligner.root[id]
37
+ shift = @shift[@sink[root]]
38
+ base = @coordinate[root]
39
+ base += shift if shift.finite?
40
+ base + @aligner.inner[id]
41
+ end
42
+
43
+ def place_block(root)
44
+ return if @coordinate.key?(root)
45
+
46
+ @coordinate[root] = 0.0
47
+ @aligner.block(root).each do |member|
48
+ neighbour = @view.predecessor(member)
49
+ place_against(root, member, neighbour) if neighbour
50
+ end
51
+ end
52
+
53
+ def place_against(root, member, neighbour)
54
+ other = @aligner.root[neighbour]
55
+ place_block(other)
56
+ @sink[root] = @sink[other] if @sink[root] == root
57
+ required = separation(neighbour, member)
58
+ if @sink[root] == @sink[other]
59
+ push_apart(root, other, required)
60
+ else
61
+ record_slack(root, other, required)
62
+ end
63
+ end
64
+
65
+ # Distance between the tops of two blocks, so that `neighbour`
66
+ # and `member` end up the spacing apart.
67
+ def separation(neighbour, member)
68
+ @aligner.inner[neighbour] + @view.size(neighbour) +
69
+ @spacing.call(neighbour, member) - @aligner.inner[member]
70
+ end
71
+
72
+ def push_apart(root, other, required)
73
+ @coordinate[root] =
74
+ [@coordinate[root], @coordinate[other] + required].max
75
+ end
76
+
77
+ def record_slack(root, other, required)
78
+ sink = @sink[other]
79
+ slack = @coordinate[root] - @coordinate[other] - required
80
+ @shift[sink] = [@shift[sink], slack].min
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Layout
5
+ module Algorithms
6
+ module Layered
7
+ module Bk
8
+ # The layers of one Brandes-Koepf pass, turned so the pass always
9
+ # sweeps from the first layer to the last and packs toward the
10
+ # start of each layer. The four passes differ only in this view.
11
+ class View
12
+ # An edge toward the previous layer, with the offset of both of
13
+ # its ports measured along the view's cross axis.
14
+ Link = Struct.new(:neighbor_id, :own_offset, :neighbor_offset,
15
+ :segment)
16
+
17
+ attr_reader :layers
18
+
19
+ # @param orientation [Array<Symbol>] the sweep, then the cross
20
+ # direction. :left reads the earlier layer as the neighbour,
21
+ # :right the later one; :down packs toward the top of a layer,
22
+ # :up toward the bottom
23
+ # @param sizes [Hash{String=>Numeric}] cross size per item id
24
+ # @param offsets [Hash] cross offset of each port from the top
25
+ # of its item
26
+ def initialize(layers, port_order, sizes, offsets, orientation)
27
+ sweep, cross = orientation
28
+ @port_order = port_order
29
+ @sizes = sizes
30
+ @offsets = offsets
31
+ @facing = sweep == :left ? :west : :east
32
+ @flip = cross == :up
33
+ @layers = orient(layers, sweep)
34
+ @position = positions
35
+ @links = {}
36
+ end
37
+
38
+ def size(id)
39
+ @sizes.fetch(id)
40
+ end
41
+
42
+ def position(id)
43
+ @position.fetch(id)
44
+ end
45
+
46
+ def predecessor(id)
47
+ layer = @layers[layer_of(id)]
48
+ index = position(id)
49
+ layer[index - 1] if index.positive?
50
+ end
51
+
52
+ # Edges to the previous layer ordered by the neighbour's position.
53
+ def links(id)
54
+ @links[id] ||= begin
55
+ all = @port_order.visual(id, @facing).flat_map do |port|
56
+ port.segments.map { |segment| link(id, port, segment) }
57
+ end
58
+ all.each_with_index
59
+ .sort_by { |link, i| [position(link.neighbor_id), i] }
60
+ .map(&:first)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def orient(layers, sweep)
67
+ ordered = sweep == :right ? layers.reverse : layers
68
+ ordered.map do |layer|
69
+ ids = layer.map(&:id)
70
+ @flip ? ids.reverse : ids
71
+ end
72
+ end
73
+
74
+ def positions
75
+ @layer_by_id = {}
76
+ @layers.each_with_index.with_object({}) do |(layer, number), map|
77
+ layer.each_with_index do |id, index|
78
+ map[id] = index
79
+ @layer_by_id[id] = number
80
+ end
81
+ end
82
+ end
83
+
84
+ def layer_of(id)
85
+ @layer_by_id.fetch(id)
86
+ end
87
+
88
+ def link(id, port, segment)
89
+ other = segment.other_port(port)
90
+ Link.new(other.item_id, offset(id, port),
91
+ offset(other.item_id, other), segment)
92
+ end
93
+
94
+ def offset(id, port)
95
+ value = @offsets.fetch(port)
96
+ @flip ? size(id) - value : value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end