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
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Layout
5
+ module Algorithms
6
+ module Layered
7
+ # ELK's layer sweep: barycenter passes that alternate direction while
8
+ # the crossing count falls, started from a random order of the first
9
+ # layer and repeated, keeping the best run.
10
+ #
11
+ # Every draw comes from the seeded generator in the order ELK makes
12
+ # it, because the draws are what tell two equally good orders apart.
13
+ # The generator is shared with the greedy switch that runs after, so
14
+ # the caller passes the one it keeps.
15
+ class LayerSweep
16
+ # ELK's default for elk.layered.thoroughness.
17
+ THOROUGHNESS = 7
18
+ FLOAT_UNIT = 1.0 / (1 << 24)
19
+ JITTER_SPAN = 0.07000000029802322
20
+ JITTER_OFFSET = 0.03500000014901161
21
+
22
+ # @param layers [Array<Array>] reordered in place
23
+ # @param port_order [PortOrder] reordered in place
24
+ # @param random [JavaRandom] the shared generator
25
+ # @param crossings [#call] counts the crossings of the current order
26
+ def initialize(layers, port_order, random, crossings)
27
+ @layers = layers
28
+ @port_order = port_order
29
+ @random = random
30
+ @crossings = crossings
31
+ end
32
+
33
+ def minimize
34
+ seed = @random.next_long
35
+ @node_relative = @random.next_bits(1) != 0
36
+ @random.seed = seed
37
+ restore(best_of_runs)
38
+ end
39
+
40
+ private
41
+
42
+ # The first run wins a tie, and a run with no crossings ends the
43
+ # search.
44
+ def best_of_runs
45
+ best = nil
46
+ THOROUGHNESS.times do
47
+ crossings, order = run
48
+ next if best && crossings >= best.first
49
+
50
+ best = [crossings, order]
51
+ break if crossings.zero?
52
+ end
53
+ best.last
54
+ end
55
+
56
+ # One random start, then sweeps while the count keeps falling. The
57
+ # order kept is the one before the sweep that failed to improve.
58
+ def run
59
+ forward = @random.next_bits(1) != 0
60
+ randomize_first_layer(forward)
61
+ sweep(forward, true)
62
+ improve_while_falling(forward, @crossings.call)
63
+ end
64
+
65
+ def improve_while_falling(forward, crossings)
66
+ loop do
67
+ kept = snapshot
68
+ return [0, kept] if crossings.zero?
69
+
70
+ forward = !forward
71
+ sweep(forward, false)
72
+ fewer = @crossings.call
73
+ return [crossings, kept] unless crossings > fewer
74
+
75
+ crossings = fewer
76
+ end
77
+ end
78
+
79
+ def snapshot
80
+ [@layers.map(&:dup), @port_order.snapshot]
81
+ end
82
+
83
+ def restore(snapshot)
84
+ layers, ports = snapshot
85
+ @layers.each_with_index { |layer, i| layer.replace(layers[i]) }
86
+ @port_order.restore(ports)
87
+ end
88
+
89
+ def randomize_first_layer(forward)
90
+ layer = @layers[forward ? 0 : -1]
91
+ keys = layer.map { @random.next_double }
92
+ layer.replace(stable_order(layer, keys))
93
+ end
94
+
95
+ def stable_order(layer, keys)
96
+ layer.each_index.sort_by { |i| [keys[i], i] }
97
+ .map { |i| layer[i] }
98
+ end
99
+
100
+ def sweep(forward, first)
101
+ indexes = (0...@layers.length).to_a
102
+ indexes.reverse! unless forward
103
+ indexes.drop(1).each do |index|
104
+ fixed = forward ? index - 1 : index + 1
105
+ reorder(index, fixed, forward, first)
106
+ distribute_ports(index, fixed, forward)
107
+ end
108
+ end
109
+
110
+ def reorder(index, fixed, forward, first)
111
+ ranks = ranks_of(@layers[fixed], forward ? :east : :west)
112
+ layer = @layers[index]
113
+ side = forward ? :west : :east
114
+ values = layer.map { |item| barycenter(item, side, ranks) }
115
+ values = first ? random_fill(values) : interpolate_fill(values)
116
+ layer.replace(stable_order(layer, values))
117
+ end
118
+
119
+ # The mean rank of the fixed ports an item meets, nudged by a
120
+ # draw so that equal means are told apart.
121
+ def barycenter(item, side, ranks)
122
+ others = @port_order.visual(item.id, side).flat_map(&:others)
123
+ return if others.empty?
124
+
125
+ jitter = (@random.next_bits(24) * FLOAT_UNIT * JITTER_SPAN) -
126
+ JITTER_OFFSET
127
+ (others.sum { |other| ranks.fetch(other) } + jitter) /
128
+ others.length
129
+ end
130
+
131
+ # First sweep: an item with no edge to the fixed layer lands at a
132
+ # random place between the known means.
133
+ def random_fill(values)
134
+ span = (values.compact.max || 0) + 2
135
+ values.map do |value|
136
+ value || ((@random.next_bits(24) * FLOAT_UNIT * span) - 1)
137
+ end
138
+ end
139
+
140
+ # Later sweeps: such an item sits halfway between its neighbours.
141
+ def interpolate_fill(values)
142
+ last = -1
143
+ values.each_with_index.map do |value, i|
144
+ value ||= (last + (values[i..].compact.first || (last + 1))) / 2.0
145
+ last = value
146
+ end
147
+ end
148
+
149
+ # Orders the free layer's ports toward the fixed layer, then the
150
+ # fixed layer's ports toward the free one.
151
+ def distribute_ports(index, fixed, forward)
152
+ side = forward ? :west : :east
153
+ far = forward ? :east : :west
154
+ [[index, side, fixed, far], [fixed, far, index, side]]
155
+ .each do |layer, near, other, other_side|
156
+ ranks = ranks_of(@layers[other], other_side)
157
+ @layers[layer].each { |item| order_ports(item, near, ranks) }
158
+ end
159
+ end
160
+
161
+ # WEST lists run bottom to top, so a WEST key is negated.
162
+ def order_ports(item, side, ranks)
163
+ sign = side == :west ? -1 : 1
164
+ @port_order.sort!(item.id, side) do |port|
165
+ sign * port.others.sum { |other| ranks.fetch(other) } /
166
+ port.others.length.to_f
167
+ end
168
+ end
169
+
170
+ # ELK numbers the ports of a layer one of two ways, chosen by a
171
+ # draw: running across the whole layer, or as a fraction inside
172
+ # each node's own slot.
173
+ def ranks_of(layer, side)
174
+ ranks = {}.compare_by_identity
175
+ consumed = 0
176
+ layer.each_with_index do |item, position|
177
+ ports = @port_order.visual(item.id, side)
178
+ ports.each_with_index do |port, k|
179
+ ranks[port] = port_rank(position, consumed, k, ports.length)
180
+ end
181
+ consumed += ports.length
182
+ end
183
+ ranks
184
+ end
185
+
186
+ def port_rank(position, consumed, index, count)
187
+ return consumed + index + 1 unless @node_relative
188
+
189
+ position + ((index + 1.0) / (count + 1))
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "bk_node_placer"
4
+ require_relative "port_order"
5
+ require_relative "orthogonal_router"
6
+
3
7
  module Elkrb
4
8
  module Layout
5
9
  module Algorithms
@@ -9,67 +13,241 @@ module Elkrb
9
13
  # This phase calculates the x and y coordinates for each node
10
14
  # based on their layer assignment and spacing requirements.
11
15
  class NodePlacer
12
- def initialize(graph, layers, options = {})
13
- @graph = graph
16
+ # Cross size of a long-edge dummy while nodes are being placed.
17
+ EDGE_THICKNESS = 1.0
18
+ # Gap between a long edge and a node, or another long edge, beside it.
19
+ EDGE_SPACING = 10.0
20
+
21
+ attr_writer :index, :port_order, :orthogonal
22
+
23
+ # @param layer_spacing [Numeric] gap between consecutive layers
24
+ # @param node_spacing [Numeric] gap between nodes in one layer
25
+ def initialize(_graph, layers, direction: "RIGHT",
26
+ layer_spacing: 20.0, node_spacing: 20.0)
14
27
  @layers = layers
15
- @options = options
16
- @layer_spacing = options[:layer_spacing] || 60.0
17
- @node_spacing = options[:spacing_node_node] || 20.0
28
+ @direction = direction
29
+ @layer_spacing = layer_spacing
30
+ @node_spacing = node_spacing
18
31
  end
19
32
 
20
33
  def place_nodes
21
34
  return unless @layers && !@layers.empty?
22
35
 
23
- # Calculate layer widths
24
- layer_widths = calculate_layer_widths
36
+ mark_dummy_slots
37
+ cross = cross_coordinates
38
+ layer_extents = calculate_layer_extents
39
+ layer_positions = calculate_layer_positions(
40
+ layer_extents, gap_widths(cross)
41
+ )
42
+
43
+ place_all_layers(layer_positions, cross)
44
+ mirror_layers(layer_positions, layer_extents)
45
+ end
25
46
 
26
- # Calculate y positions for each layer
27
- y_positions = calculate_layer_y_positions
47
+ private
28
48
 
29
- # Place nodes in each layer
30
- @layers.each_with_index do |layer_nodes, layer_index|
31
- place_layer(layer_nodes, layer_index, layer_widths[layer_index],
32
- y_positions[layer_index])
49
+ def size_dummy_slots
50
+ layer_dimension = horizontal? ? :width : :height
51
+ cross_dimension = horizontal? ? :height : :width
52
+ @layers.each do |items|
53
+ size_dummy_layer(items, layer_dimension, cross_dimension)
33
54
  end
34
55
  end
35
56
 
36
- private
57
+ def size_dummy_layer(items, layer_dimension, cross_dimension)
58
+ nodes = items.reject { |item| dummy?(item) }
59
+ layer_extent = maximum_size(nodes, layer_dimension)
60
+ cross_extent = maximum_size(nodes, cross_dimension)
61
+ items.select { |item| dummy?(item) }.each do |slot|
62
+ slot.public_send("#{layer_dimension}=", layer_extent)
63
+ slot.public_send("#{cross_dimension}=", cross_extent)
64
+ end
65
+ end
66
+
67
+ def maximum_size(nodes, dimension)
68
+ nodes.map { |node| size(node, dimension) }.max || 0
69
+ end
37
70
 
38
- def calculate_layer_widths
39
- @layers.map do |layer_nodes|
40
- return 0 if layer_nodes.empty?
71
+ def dummy?(item)
72
+ item.respond_to?(:dummy?) && item.dummy?
73
+ end
41
74
 
42
- total_width = layer_nodes.sum { |n| n.width || 0 }
43
- total_spacing = (layer_nodes.length - 1) * @node_spacing
44
- total_width + total_spacing
75
+ def place_all_layers(layer_positions, cross)
76
+ @layers.each_with_index do |nodes, layer_index|
77
+ lead = overhang(nodes, :west)
78
+ extent = layer_extent(nodes)
79
+ nodes.each do |node|
80
+ offset = dummy?(node) ? lead : layer_offset(node, nodes, extent)
81
+ place_node(node, layer_positions[layer_index] + offset,
82
+ cross.fetch(node.id) - dummy_lead(node))
83
+ end
45
84
  end
46
85
  end
47
86
 
48
- def calculate_layer_y_positions
49
- y = 0
50
- positions = []
87
+ # Where a node sits across its layer's width: nodes with more
88
+ # outgoing than incoming ports lean toward the far side, so
89
+ # unequal nodes meet their edges on the shared side of the gap.
90
+ # Declared ports stay inside the layer, so the node leans within
91
+ # the room they leave.
92
+ def layer_offset(node, nodes, extent)
93
+ room = extent - overhang(nodes, :west) - overhang(nodes, :east)
94
+ overhang(nodes, :west) +
95
+ ((room - size(node, layer_dimension)) * alignment_ratio(node))
96
+ end
97
+
98
+ def alignment_ratio(node)
99
+ outgoing = port_order.visual(node.id, :east).size
100
+ incoming = port_order.visual(node.id, :west).size
101
+ total = outgoing + incoming
102
+ total.zero? ? 0.5 : outgoing.fdiv(total)
103
+ end
104
+
105
+ def layer_dimension
106
+ horizontal? ? :width : :height
107
+ end
108
+
109
+ def cross_coordinates
110
+ BkNodePlacer.new(
111
+ @layers, @port_order || PortOrder.new(@layers, @index),
112
+ spacing: method(:spacing_between),
113
+ size_of: method(:placed_cross_size),
114
+ port_size_of: ->(port) { port_cross_size(port) }
115
+ ).place
116
+ end
117
+
118
+ # A long edge occupies a hairline in its layer; the slot keeps the
119
+ # full layer size so the edge's bend points sit on its centre.
120
+ def placed_cross_size(item)
121
+ dummy?(item) ? EDGE_THICKNESS : cross_size(item)
122
+ end
123
+
124
+ def dummy_lead(item)
125
+ return 0.0 unless dummy?(item)
51
126
 
52
- @layers.each do |layer_nodes|
53
- positions << y
54
- max_height = layer_nodes.map { |n| n.height || 0 }.max || 0
55
- y += max_height + @layer_spacing
127
+ (cross_size(item) - EDGE_THICKNESS) / 2.0
128
+ end
129
+
130
+ def spacing_between(first_id, second_id)
131
+ dummies = [first_id, second_id].count do |id|
132
+ @dummy_ids.include?(id)
56
133
  end
134
+ dummies.zero? ? @node_spacing : EDGE_SPACING
135
+ end
57
136
 
58
- positions
137
+ def port_order
138
+ @port_order ||= PortOrder.new(@layers, @index)
59
139
  end
60
140
 
61
- def place_layer(nodes, _layer_index, _layer_width, y_pos)
62
- return if nodes.empty?
141
+ # How far the declared ports of a layer's nodes stick out of the
142
+ # nodes on one side; the layer keeps clear of them.
143
+ def overhang(nodes, side)
144
+ nodes.flat_map { |node| port_order.visual(node.id, side) }
145
+ .filter_map(&:declared)
146
+ .map { |declared| size(declared, horizontal? ? :width : :height) }
147
+ .max || 0
148
+ end
63
149
 
64
- # Center the layer horizontally
65
- x = 0
150
+ def mark_dummy_slots
151
+ size_dummy_slots
152
+ dummies = @layers.flatten.select { |item| dummy?(item) }
153
+ @dummy_ids = dummies.to_set(&:id)
154
+ end
66
155
 
67
- nodes.each do |node|
68
- node.x = x
69
- node.y = y_pos
70
- x += (node.width || 0) + @node_spacing
156
+ # Width of each gap between two layers. Orthogonal routes need room
157
+ # for one routing slot per parallel run, with a clear margin at both
158
+ # ends; a gap never drops below the layer spacing.
159
+ def gap_widths(cross)
160
+ return [] unless @orthogonal
161
+
162
+ router_for(cross).slot_counts.map do |slots|
163
+ next @layer_spacing if slots.zero?
164
+
165
+ runs = (slots - 1) * OrthogonalRouter::EDGE_SPACING
166
+ margins = 2 * OrthogonalRouter::EDGE_NODE_SPACING
167
+ width = runs + margins
168
+ [width, @layer_spacing].max
71
169
  end
72
170
  end
171
+
172
+ def router_for(cross)
173
+ OrthogonalRouter.new(
174
+ @layers, port_order, @index,
175
+ direction: @direction,
176
+ measure: OrthogonalRouter::Measure.new(
177
+ start: ->(item) { cross.fetch(item.id) },
178
+ extent: method(:placed_cross_size),
179
+ port: method(:port_cross_size),
180
+ )
181
+ )
182
+ end
183
+
184
+ def port_cross_size(port)
185
+ return 0 unless port.declared
186
+
187
+ size(port.declared, horizontal? ? :height : :width)
188
+ end
189
+
190
+ def calculate_layer_extents
191
+ @layers.map { |nodes| layer_extent(nodes) }
192
+ end
193
+
194
+ def layer_extent(nodes)
195
+ widest = nodes.map { |node| size(node, layer_dimension) }.max || 0
196
+ overhang(nodes, :west) + widest + overhang(nodes, :east)
197
+ end
198
+
199
+ def calculate_layer_positions(extents, gap_widths = [])
200
+ position = 0
201
+ extents.each_with_index.map do |extent, i|
202
+ position.tap do
203
+ position += extent + gap_widths.fetch(i, @layer_spacing)
204
+ end
205
+ end
206
+ end
207
+
208
+ def place_node(node, layer_position, cross_position)
209
+ if horizontal?
210
+ node.x = layer_position
211
+ node.y = cross_position
212
+ else
213
+ node.x = cross_position
214
+ node.y = layer_position
215
+ end
216
+ end
217
+
218
+ def mirror_layers(layer_positions, layer_extents)
219
+ return unless %w[LEFT UP].include?(@direction)
220
+
221
+ bound = layer_positions.zip(layer_extents)
222
+ .map { |position, extent| position + extent }.max
223
+ coordinate, dimension = mirror_axis
224
+
225
+ @layers.flatten.each do |node|
226
+ mirror_node(node, coordinate, dimension, bound)
227
+ end
228
+ end
229
+
230
+ def mirror_axis
231
+ @direction == "LEFT" ? %i[x width] : %i[y height]
232
+ end
233
+
234
+ def mirror_node(node, coordinate, dimension, bound)
235
+ mirrored = bound - node.public_send(coordinate) -
236
+ size(node, dimension)
237
+ node.public_send("#{coordinate}=", mirrored)
238
+ end
239
+
240
+ def horizontal?
241
+ %w[RIGHT LEFT].include?(@direction)
242
+ end
243
+
244
+ def cross_size(node)
245
+ size(node, horizontal? ? :height : :width)
246
+ end
247
+
248
+ def size(node, dimension)
249
+ node.public_send(dimension) || 0
250
+ end
73
251
  end
74
252
  end
75
253
  end