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.
- 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,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "port_order"
|
|
4
|
+
require_relative "port_spread"
|
|
5
|
+
require_relative "bk/view"
|
|
6
|
+
require_relative "bk/aligner"
|
|
7
|
+
require_relative "bk/compactor"
|
|
8
|
+
|
|
9
|
+
module Elkrb
|
|
10
|
+
module Layout
|
|
11
|
+
module Algorithms
|
|
12
|
+
module Layered
|
|
13
|
+
# Brandes-Koepf node placement along the cross axis. Four passes
|
|
14
|
+
# run (each sweep direction packed toward each end). By default the
|
|
15
|
+
# narrowest pass wins, the first of them on a tie, which is what
|
|
16
|
+
# ELK does when no fixed alignment is set. With `place(alignment:
|
|
17
|
+
# :balanced)` the passes are aligned on the narrowest and every node
|
|
18
|
+
# takes the mean of its two middle coordinates.
|
|
19
|
+
class BkNodePlacer
|
|
20
|
+
PASSES = [%i[left down], %i[left up],
|
|
21
|
+
%i[right down], %i[right up]].freeze
|
|
22
|
+
|
|
23
|
+
# @param layers [Array<Array>] nodes and DummySlots per layer
|
|
24
|
+
# @param port_order [PortOrder]
|
|
25
|
+
# @param spacing [#call] gap between two neighbours in a layer,
|
|
26
|
+
# given their ids
|
|
27
|
+
# @param size_of [#call] cross size of a layer item
|
|
28
|
+
# @param port_size_of [#call] cross size of a port
|
|
29
|
+
def initialize(layers, port_order, spacing:, size_of:,
|
|
30
|
+
port_size_of:)
|
|
31
|
+
@layers = layers
|
|
32
|
+
@port_order = port_order
|
|
33
|
+
@spacing = spacing
|
|
34
|
+
@sizes = layers.flatten.to_h do |item|
|
|
35
|
+
[item.id, size_of.call(item)]
|
|
36
|
+
end
|
|
37
|
+
@offsets = PortSpread.offsets(layers, port_order, @sizes,
|
|
38
|
+
port_size_of)
|
|
39
|
+
@marked = type_one_conflicts
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param alignment [Symbol] :smallest or :balanced
|
|
43
|
+
# @return [Hash{String=>Float}] cross coordinate of each item,
|
|
44
|
+
# starting at zero
|
|
45
|
+
def place(alignment: :smallest)
|
|
46
|
+
return {} if @sizes.empty?
|
|
47
|
+
|
|
48
|
+
passes = PASSES.map { |orientation| pass(orientation) }
|
|
49
|
+
tops = alignment == :balanced ? balance(passes) : narrowest(passes)
|
|
50
|
+
low = tops.values.min
|
|
51
|
+
tops.transform_values { |top| top - low }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def pass(orientation)
|
|
57
|
+
view = Bk::View.new(@layers, @port_order, @sizes, @offsets,
|
|
58
|
+
orientation)
|
|
59
|
+
aligner = Bk::Aligner.new(view, @marked)
|
|
60
|
+
starts = Bk::Compactor.new(view, aligner, @spacing).place
|
|
61
|
+
return starts unless orientation.last == :up
|
|
62
|
+
|
|
63
|
+
starts.to_h { |id, start| [id, -(start + @sizes[id])] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def narrowest(passes)
|
|
67
|
+
passes.min_by { |tops| width(span(tops)) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def balance(passes)
|
|
71
|
+
aligned = align_to_narrowest(passes)
|
|
72
|
+
@sizes.keys.to_h { |id| [id, median(aligned.map { |t| t[id] })] }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Shifts every pass so it shares the narrowest pass's start (packed
|
|
76
|
+
# downward) or end (packed upward).
|
|
77
|
+
def align_to_narrowest(passes)
|
|
78
|
+
spans = passes.map { |tops| span(tops) }
|
|
79
|
+
narrow = spans.min_by { |bounds| width(bounds) }
|
|
80
|
+
passes.zip(spans, PASSES).map do |tops, bounds, orientation|
|
|
81
|
+
delta = anchor(narrow, orientation) - anchor(bounds, orientation)
|
|
82
|
+
tops.transform_values { |top| top + delta }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def anchor(bounds, orientation)
|
|
87
|
+
orientation.last == :down ? bounds.first : bounds.last
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def width(bounds)
|
|
91
|
+
bounds.last - bounds.first
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def span(tops)
|
|
95
|
+
[tops.map { |_, top| top }.min,
|
|
96
|
+
tops.map { |id, top| top + @sizes[id] }.max]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def median(values)
|
|
100
|
+
middle = values.sort[1..2]
|
|
101
|
+
middle.sum / 2.0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Type 1 conflicts: a segment crossing an inner segment (both ends
|
|
105
|
+
# dummies) must never join a block, or the long edge would bend.
|
|
106
|
+
def type_one_conflicts
|
|
107
|
+
marked = {}.compare_by_identity
|
|
108
|
+
@layers.each_cons(2) do |left, right|
|
|
109
|
+
mark_pair(left, right, marked)
|
|
110
|
+
end
|
|
111
|
+
marked
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def mark_pair(left, right, marked)
|
|
115
|
+
positions = left.each_with_index.to_h { |item, i| [item.id, i] }
|
|
116
|
+
low = 0
|
|
117
|
+
scanned = 0
|
|
118
|
+
right.each_with_index do |item, index|
|
|
119
|
+
high = run_end(item, index == right.length - 1, left, positions)
|
|
120
|
+
next unless high
|
|
121
|
+
|
|
122
|
+
mark_run(right[scanned..index], positions, low..high, marked)
|
|
123
|
+
scanned = index + 1
|
|
124
|
+
low = high
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Where the run of nodes ending at `item` may attach in the layer
|
|
129
|
+
# before it; nil while the run continues.
|
|
130
|
+
def run_end(item, last, left, positions)
|
|
131
|
+
inner = inner_segment(item)
|
|
132
|
+
return positions.fetch(inner.from_port.item_id) if inner
|
|
133
|
+
|
|
134
|
+
left.length - 1 if last
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def mark_run(run, positions, allowed, marked)
|
|
138
|
+
run.each do |item|
|
|
139
|
+
segments_toward_west(item).each do |segment|
|
|
140
|
+
above = positions.fetch(segment.from_port.item_id)
|
|
141
|
+
marked[segment] = true unless allowed.cover?(above) ||
|
|
142
|
+
inner?(segment)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def segments_toward_west(item)
|
|
148
|
+
@port_order.visual(item.id, :west).flat_map(&:segments)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def inner_segment(item)
|
|
152
|
+
return unless dummy?(item)
|
|
153
|
+
|
|
154
|
+
segments_toward_west(item).find { |segment| inner?(segment) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def inner?(segment)
|
|
158
|
+
@dummy_ids ||= @layers.flatten.select { |i| dummy?(i) }.to_set(&:id)
|
|
159
|
+
@dummy_ids.include?(segment.from_port.item_id) &&
|
|
160
|
+
@dummy_ids.include?(segment.to_port.item_id)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def dummy?(item)
|
|
164
|
+
item.respond_to?(:dummy?) && item.dummy?
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "crossing_minimizer"
|
|
4
|
+
|
|
5
|
+
module Elkrb
|
|
6
|
+
module Layout
|
|
7
|
+
module Algorithms
|
|
8
|
+
module Layered
|
|
9
|
+
# Connected components of one hierarchy level, and the row packing
|
|
10
|
+
# that puts the laid-out components back side by side.
|
|
11
|
+
module Components
|
|
12
|
+
# One component after its own layout.
|
|
13
|
+
Part = Struct.new(:graph, :layers, :index, :port_order, :slots)
|
|
14
|
+
|
|
15
|
+
# The crossing minimizer of one component. ELK counts every node of
|
|
16
|
+
# the level, not the component's own, against the greedy-switch
|
|
17
|
+
# limit.
|
|
18
|
+
class Minimizer < CrossingMinimizer
|
|
19
|
+
def initialize(graph, layers, index, resolver, level_size)
|
|
20
|
+
super(graph, layers, index, resolver)
|
|
21
|
+
@node_count = level_size
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Splits a level into one graph per connected component: nodes in
|
|
26
|
+
# input order, components in order of their first node, each
|
|
27
|
+
# graph-owned edge with the component its endpoints are in.
|
|
28
|
+
class Splitter
|
|
29
|
+
# @param graph [Graph::Graph] the level being laid out
|
|
30
|
+
# @param index [NodeIndex] the level's index
|
|
31
|
+
def initialize(graph, index)
|
|
32
|
+
@graph = graph
|
|
33
|
+
@index = index
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Array<Graph::Graph>]
|
|
37
|
+
def graphs
|
|
38
|
+
groups.map do |nodes|
|
|
39
|
+
Graph::Graph.new(
|
|
40
|
+
id: @graph.id, children: nodes, edges: edges_of(nodes),
|
|
41
|
+
layout_options: @graph.layout_options
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def groups
|
|
49
|
+
seen = {}
|
|
50
|
+
@graph.children.filter_map do |node|
|
|
51
|
+
next if seen[node.id]
|
|
52
|
+
|
|
53
|
+
members = reach(node.id, seen)
|
|
54
|
+
@graph.children.select { |other| members.include?(other.id) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reach(start, seen)
|
|
59
|
+
found = []
|
|
60
|
+
pending = [start]
|
|
61
|
+
until pending.empty?
|
|
62
|
+
id = pending.pop
|
|
63
|
+
next if seen[id]
|
|
64
|
+
|
|
65
|
+
seen[id] = true
|
|
66
|
+
found << id
|
|
67
|
+
pending.concat(neighbours[id])
|
|
68
|
+
end
|
|
69
|
+
found
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def neighbours
|
|
73
|
+
@neighbours ||= Hash.new { |map, id| map[id] = [] }.tap do |map|
|
|
74
|
+
@index.edges.each do |edge|
|
|
75
|
+
ids = owner_ids(edge)
|
|
76
|
+
ids.each { |id| map[id].concat(ids) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def owner_ids(edge)
|
|
82
|
+
@index.endpoint_owners(edge.sources + edge.targets).map(&:id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def edges_of(nodes)
|
|
86
|
+
ids = nodes.map(&:id)
|
|
87
|
+
@graph.edges.to_a.select do |edge|
|
|
88
|
+
ids.include?(owner_ids(edge).first)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Places boxes in rows the way ELK's simple row placer does: the
|
|
94
|
+
# smallest box first, a new row once the next box would pass the
|
|
95
|
+
# larger of the widest box and sqrt(total area) * aspect ratio.
|
|
96
|
+
class RowPacker
|
|
97
|
+
# The next free spot in the current row.
|
|
98
|
+
Cursor = Struct.new(:x, :y, :tallest) do
|
|
99
|
+
# This cursor, or the start of the next row when a box of
|
|
100
|
+
# `width` would pass the limit.
|
|
101
|
+
def fit(width, limit, spacing)
|
|
102
|
+
return self unless x + width > limit
|
|
103
|
+
|
|
104
|
+
Cursor.new(0.0, y + tallest + spacing, 0.0)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def corner
|
|
108
|
+
[x, y]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def past(width, height, spacing)
|
|
112
|
+
Cursor.new(x + width + spacing, y, [tallest, height].max)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
private_constant :Cursor
|
|
116
|
+
|
|
117
|
+
# @param sizes [Array<Array(Float, Float)>] width and height of
|
|
118
|
+
# each box, without padding
|
|
119
|
+
# @param spacing [Float] gap between boxes and between rows
|
|
120
|
+
# @param aspect_ratio [Float]
|
|
121
|
+
def initialize(sizes, spacing:, aspect_ratio:)
|
|
122
|
+
@sizes = sizes
|
|
123
|
+
@spacing = spacing
|
|
124
|
+
@aspect_ratio = aspect_ratio
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [Array<Array(Float, Float)>] the top-left of each box,
|
|
128
|
+
# in the order the sizes were given
|
|
129
|
+
def offsets
|
|
130
|
+
placed = {}
|
|
131
|
+
cursor = Cursor.new(0.0, 0.0, 0.0)
|
|
132
|
+
order.each do |box|
|
|
133
|
+
width, height = @sizes[box]
|
|
134
|
+
cursor = cursor.fit(width, limit, @spacing)
|
|
135
|
+
placed[box] = cursor.corner
|
|
136
|
+
cursor = cursor.past(width, height, @spacing)
|
|
137
|
+
end
|
|
138
|
+
placed.sort.map(&:last)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param offsets [Array<Array(Float, Float)>] from #offsets
|
|
142
|
+
# @return [Array(Float, Float)] the packed width and height
|
|
143
|
+
def size(offsets)
|
|
144
|
+
boxes = @sizes.zip(offsets)
|
|
145
|
+
[[@spacing, *boxes.map { |(w, _), (x, _)| x + w }].max,
|
|
146
|
+
boxes.map { |(_, h), (_, y)| y + h }.max]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def order
|
|
152
|
+
@sizes.each_index.sort_by { |box| [@sizes[box].inject(:*), box] }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# ELK takes the square root in single precision.
|
|
156
|
+
def limit
|
|
157
|
+
@limit ||= row_limit
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def row_limit
|
|
161
|
+
area = @sizes.sum { |width, height| width * height }
|
|
162
|
+
root = [Math.sqrt(area)].pack("f").unpack1("f")
|
|
163
|
+
[@sizes.map(&:first).max, root * @aspect_ratio].max
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../java_random"
|
|
4
|
+
require_relative "discovery_order"
|
|
5
|
+
require_relative "layer_sweep"
|
|
6
|
+
require_relative "port_order"
|
|
7
|
+
|
|
8
|
+
module Elkrb
|
|
9
|
+
module Layout
|
|
10
|
+
module Algorithms
|
|
11
|
+
module Layered
|
|
12
|
+
# Reorders nodes inside assigned layers with barycenter sweeps, the
|
|
13
|
+
# way ELK's layer sweep does: a forward sweep, then backward sweeps
|
|
14
|
+
# alternating while the crossing count keeps falling.
|
|
15
|
+
#
|
|
16
|
+
# A node's barycenter is the mean rank of the ports its edges meet
|
|
17
|
+
# in the fixed layer, so two edges leaving one node pull their
|
|
18
|
+
# targets apart. Each sweep also reorders the ports of the layer it
|
|
19
|
+
# moves; #port_order carries the result to the node placer.
|
|
20
|
+
class CrossingMinimizer
|
|
21
|
+
STRATEGY = "elk.layered.crossingMinimization.strategy"
|
|
22
|
+
MODEL_ORDER = "elk.layered.considerModelOrder.strategy"
|
|
23
|
+
# ELK only runs its greedy switch on graphs smaller than this.
|
|
24
|
+
GREEDY_SWITCH_NODE_LIMIT = 40
|
|
25
|
+
# ELK's default for elk.randomSeed.
|
|
26
|
+
RANDOM_SEED = 1
|
|
27
|
+
|
|
28
|
+
# @return [PortOrder] the port lists as the last sweep left them
|
|
29
|
+
attr_reader :port_order
|
|
30
|
+
|
|
31
|
+
def initialize(graph, layers, index, resolver)
|
|
32
|
+
@layers = layers
|
|
33
|
+
@strategy = resolver.get(STRATEGY, graph)
|
|
34
|
+
@preserve_input_order =
|
|
35
|
+
resolver.get(MODEL_ORDER, graph) == "NODES_AND_EDGES"
|
|
36
|
+
@input_order = input_order(graph, layers)
|
|
37
|
+
DiscoveryOrder.new(graph, index).sort(layers)
|
|
38
|
+
@port_order = PortOrder.new(layers, index)
|
|
39
|
+
@node_count = (graph.children || []).length
|
|
40
|
+
@random = JavaRandom.new(RANDOM_SEED)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def minimize
|
|
44
|
+
unless trivial?
|
|
45
|
+
sweep_layers unless @strategy == "NONE"
|
|
46
|
+
greedy_switch
|
|
47
|
+
end
|
|
48
|
+
@layers
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# ELK has nothing to minimize in an empty graph or a lone node.
|
|
54
|
+
def trivial?
|
|
55
|
+
@layers.all?(&:empty?) ||
|
|
56
|
+
(@layers.length == 1 && @layers.first.length == 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sweep_layers
|
|
60
|
+
return sweep_until_stable if @preserve_input_order
|
|
61
|
+
|
|
62
|
+
LayerSweep.new(@layers, @port_order, @random,
|
|
63
|
+
-> { count_crossings }).minimize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# ELK runs its two-sided greedy switch after whichever minimizer
|
|
67
|
+
# ran, NONE included: adjacent nodes swap while that lowers the
|
|
68
|
+
# crossings against both neighbouring layers, in the port order
|
|
69
|
+
# the sweep left. Its first direction is a draw from the seeded
|
|
70
|
+
# generator, which the sweep may already have advanced.
|
|
71
|
+
def greedy_switch
|
|
72
|
+
return if @node_count >= GREEDY_SWITCH_NODE_LIMIT
|
|
73
|
+
|
|
74
|
+
@random.next_long
|
|
75
|
+
forward = @random.next_bits(1) != 0
|
|
76
|
+
switch_until_unchanged(forward)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def switch_until_unchanged(forward)
|
|
80
|
+
loop do
|
|
81
|
+
first, *free = (0...@layers.length).to_a
|
|
82
|
+
first, *free = [first, *free].reverse unless forward
|
|
83
|
+
improved = switch_once?(first)
|
|
84
|
+
free.each { |index| improved |= switch_until_stable(index) }
|
|
85
|
+
forward = !forward
|
|
86
|
+
break unless improved
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def switch_until_stable(index)
|
|
91
|
+
improved = false
|
|
92
|
+
improved = true while switch_once?(index)
|
|
93
|
+
improved
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def switch_once?(index)
|
|
97
|
+
layer = @layers[index]
|
|
98
|
+
sides = switch_sides(index)
|
|
99
|
+
Array.new([layer.length - 1, 0].max) do |upper|
|
|
100
|
+
swap_if_pays?(layer, upper, sides)
|
|
101
|
+
end.any?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def swap_if_pays?(layer, upper, sides)
|
|
105
|
+
return false unless switch_pays?(layer[upper], layer[upper + 1],
|
|
106
|
+
sides)
|
|
107
|
+
|
|
108
|
+
layer[upper], layer[upper + 1] = layer[upper + 1], layer[upper]
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def switch_sides(index)
|
|
113
|
+
{ west: neighbour_ranks(index - 1, :east),
|
|
114
|
+
east: neighbour_ranks(index + 1, :west) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def neighbour_ranks(index, side)
|
|
118
|
+
return {}.compare_by_identity unless index.between?(
|
|
119
|
+
0, @layers.length - 1
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
port_ranks(@layers[index], side)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def switch_pays?(upper, lower, sides)
|
|
126
|
+
crossings_below(upper, lower, sides) >
|
|
127
|
+
crossings_below(lower, upper, sides)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Crossings between the edges of `above` and `below` when `above`
|
|
131
|
+
# sits on top.
|
|
132
|
+
def crossings_below(above, below, sides)
|
|
133
|
+
sides.sum do |side, ranks|
|
|
134
|
+
far_ranks(above, side, ranks).product(
|
|
135
|
+
far_ranks(below, side, ranks),
|
|
136
|
+
).count { |high, low| high > low }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def far_ranks(item, side, ranks)
|
|
141
|
+
@port_order.visual(item.id, side).flat_map(&:others)
|
|
142
|
+
.map { |other| ranks.fetch(other) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def sweep_until_stable
|
|
146
|
+
forward = true
|
|
147
|
+
crossings = sweep_and_count(forward)
|
|
148
|
+
loop do
|
|
149
|
+
previous = crossings
|
|
150
|
+
forward = !forward
|
|
151
|
+
crossings = sweep_and_count(forward)
|
|
152
|
+
break unless previous > crossings && crossings.positive?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def sweep_and_count(forward)
|
|
157
|
+
sweep(forward)
|
|
158
|
+
count_crossings
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def input_order(graph, layers)
|
|
162
|
+
order = (graph.children || []).each_with_index
|
|
163
|
+
.to_h { |node, position| [node.id, position] }
|
|
164
|
+
layers.flatten.select { |item| item.respond_to?(:edge_order) }
|
|
165
|
+
.each { |slot| order[slot.id] = order.length + slot.edge_order }
|
|
166
|
+
order
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def sweep(forward)
|
|
170
|
+
if forward
|
|
171
|
+
(1...@layers.length).each { |i| reorder(i, i - 1) }
|
|
172
|
+
else
|
|
173
|
+
(@layers.length - 2).downto(0) { |i| reorder(i, i + 1) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def reorder(layer_index, fixed_index)
|
|
178
|
+
forward = fixed_index < layer_index
|
|
179
|
+
free_side = forward ? :west : :east
|
|
180
|
+
ranks = port_ranks(@layers[fixed_index], forward ? :east : :west)
|
|
181
|
+
layer = @layers[layer_index]
|
|
182
|
+
sort_ports(layer, free_side, ranks)
|
|
183
|
+
layer.replace(sorted(layer, free_side, ranks))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def sort_ports(layer, side, ranks)
|
|
187
|
+
layer.each do |item|
|
|
188
|
+
@port_order.sort!(item.id, side) do |port|
|
|
189
|
+
port_key(port, side, ranks)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Model order keeps the WEST ports of a node in the order of the
|
|
195
|
+
# nodes they come from; the stored WEST list runs bottom to top.
|
|
196
|
+
def port_key(port, side, ranks)
|
|
197
|
+
if @preserve_input_order && side == :west
|
|
198
|
+
-port.others.map { |other| @input_order.fetch(other.item_id) }.min
|
|
199
|
+
else
|
|
200
|
+
mean(port.others.map { |other| ranks.fetch(other) })
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Rank of every port on one side of a layer, counted top to bottom
|
|
205
|
+
# across the whole layer.
|
|
206
|
+
def port_ranks(layer, side)
|
|
207
|
+
ranks = {}.compare_by_identity
|
|
208
|
+
layer.each do |item|
|
|
209
|
+
@port_order.visual(item.id, side).each do |port|
|
|
210
|
+
ranks[port] = ranks.length
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
ranks
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def mean(values)
|
|
217
|
+
values.sum.to_f / values.length
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def sorted(layer, side, ranks)
|
|
221
|
+
keys = fill_unknown(layer.map do |item|
|
|
222
|
+
barycenter(item, side, ranks)
|
|
223
|
+
end)
|
|
224
|
+
layer.each_with_index.sort_by do |item, position|
|
|
225
|
+
known, value = keys[position]
|
|
226
|
+
[value, known ? 0 : 1, known ? tie_breaker(item) : position]
|
|
227
|
+
end.map(&:first)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def barycenter(item, side, ranks)
|
|
231
|
+
others = @port_order.visual(item.id, side)
|
|
232
|
+
.flat_map(&:others)
|
|
233
|
+
mean(others.map { |other| ranks.fetch(other) }) unless others.empty?
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# A node with no edge toward the fixed layer takes the barycenter
|
|
237
|
+
# of the node before it, so it keeps its place.
|
|
238
|
+
def fill_unknown(values)
|
|
239
|
+
lead = values.compact.first || 0.0
|
|
240
|
+
last = nil
|
|
241
|
+
values.map do |value|
|
|
242
|
+
last = value if value
|
|
243
|
+
[!value.nil?, value || last || lead]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def tie_breaker(node)
|
|
248
|
+
if @preserve_input_order
|
|
249
|
+
@input_order.fetch(node.id)
|
|
250
|
+
else
|
|
251
|
+
node.id.to_s
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def count_crossings
|
|
256
|
+
positions = layer_positions
|
|
257
|
+
visual = visual_indexes
|
|
258
|
+
gaps = @port_order.segments.group_by do |segment|
|
|
259
|
+
positions.fetch(segment.from_port.item_id).first
|
|
260
|
+
end
|
|
261
|
+
gaps.values.sum do |segments|
|
|
262
|
+
crossings_in(slot_pairs(segments, positions, visual))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def slot_pairs(segments, positions, visual)
|
|
267
|
+
segments.map do |segment|
|
|
268
|
+
[slot(segment.from_port, positions, visual),
|
|
269
|
+
slot(segment.to_port, positions, visual)]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def crossings_in(pairs)
|
|
274
|
+
pairs.combination(2).count do |(a1, b1), (a2, b2)|
|
|
275
|
+
(a1 <=> a2) * (b1 <=> b2) == -1
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def slot(port, positions, visual)
|
|
280
|
+
[positions.fetch(port.item_id).last, visual.fetch(port)]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def layer_positions
|
|
284
|
+
@layers.each_with_index.with_object({}) do |(layer, number), map|
|
|
285
|
+
layer.each_with_index do |item, position|
|
|
286
|
+
map[item.id] = [number, position]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def visual_indexes
|
|
292
|
+
indexes = {}.compare_by_identity
|
|
293
|
+
@layers.flatten.each do |item|
|
|
294
|
+
PortOrder::SIDES.each do |side|
|
|
295
|
+
@port_order.visual(item.id, side).each_with_index do |port, i|
|
|
296
|
+
indexes[port] = i
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
indexes
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|