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
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
|
|
100
|
+
def temperature_scale
|
|
101
|
+
option("elk.force.temperature", default: DEFAULT_TEMPERATURE).to_f /
|
|
102
|
+
DEFAULT_TEMPERATURE
|
|
103
|
+
end
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
def repulsion_scale
|
|
106
|
+
option("elk.force.repulsion", default: DEFAULT_REPULSION).to_f /
|
|
107
|
+
PERCENTAGE_BASE
|
|
108
|
+
end
|
|
93
109
|
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
127
|
+
def calculate_forces(nodes, resolved_edges, ideal_length, repulsion)
|
|
128
|
+
forces = Array.new(nodes.length) { [0.0, 0.0] }
|
|
109
129
|
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
197
|
+
def center_x(node)
|
|
198
|
+
node.x + ((node.width || 0.0) / 2.0)
|
|
199
|
+
end
|
|
145
200
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|