elkrb 1.0.2 → 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
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
-
@
|
|
16
|
-
@layer_spacing =
|
|
17
|
-
@node_spacing =
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
y_positions = calculate_layer_y_positions
|
|
47
|
+
private
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
def dummy?(item)
|
|
72
|
+
item.respond_to?(:dummy?) && item.dummy?
|
|
73
|
+
end
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
137
|
+
def port_order
|
|
138
|
+
@port_order ||= PortOrder.new(@layers, @index)
|
|
59
139
|
end
|
|
60
140
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|