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,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hyper_edge_segment"
|
|
4
|
+
require_relative "segment_order"
|
|
5
|
+
|
|
6
|
+
module Elkrb
|
|
7
|
+
module Layout
|
|
8
|
+
module Algorithms
|
|
9
|
+
module Layered
|
|
10
|
+
module Orthogonal
|
|
11
|
+
# Assigns routing slots to the edge runs between two adjacent
|
|
12
|
+
# layers, ordering them so edges cross and run beside each other as
|
|
13
|
+
# little as possible. Works on cross coordinates only.
|
|
14
|
+
#
|
|
15
|
+
# Not ported: splitting a segment that sits in a cycle of critical
|
|
16
|
+
# dependencies. Such a cycle is broken like a regular one, so two
|
|
17
|
+
# edges may overlap where Java ELK would add a detour.
|
|
18
|
+
class GapSolver
|
|
19
|
+
CONFLICT_THRESHOLD_FACTOR = 0.5
|
|
20
|
+
CRITICAL_CONFLICT_THRESHOLD_FACTOR = 0.2
|
|
21
|
+
CONFLICT_PENALTY = 1
|
|
22
|
+
CROSSING_PENALTY = 16
|
|
23
|
+
CRITICAL = :critical
|
|
24
|
+
|
|
25
|
+
# @param edge_spacing [Numeric] distance between neighbouring slots
|
|
26
|
+
def initialize(edge_spacing)
|
|
27
|
+
@conflict_threshold = CONFLICT_THRESHOLD_FACTOR * edge_spacing
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param source_ports [Array<PortOrder::Port>] ports of the earlier
|
|
31
|
+
# layer in layer order, top to bottom within a node
|
|
32
|
+
# @param coordinate [#call] cross coordinate of a port
|
|
33
|
+
# @return [Array<HyperEdgeSegment>] with `slot` set
|
|
34
|
+
def solve(source_ports, coordinate)
|
|
35
|
+
segments = build_segments(source_ports, coordinate)
|
|
36
|
+
@critical_threshold =
|
|
37
|
+
CRITICAL_CONFLICT_THRESHOLD_FACTOR * minimum_distance(segments)
|
|
38
|
+
link(segments)
|
|
39
|
+
break_cycles(segments)
|
|
40
|
+
number(segments)
|
|
41
|
+
segments
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Number of slots the non-straight segments occupy.
|
|
45
|
+
def self.slot_count(segments)
|
|
46
|
+
ranks = segments.reject(&:straight?).map(&:slot)
|
|
47
|
+
ranks.empty? ? 0 : ranks.max + 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_segments(source_ports, coordinate)
|
|
53
|
+
owner = {}.compare_by_identity
|
|
54
|
+
source_ports.each_with_object([]) do |port, segments|
|
|
55
|
+
next if owner.key?(port)
|
|
56
|
+
|
|
57
|
+
segment = HyperEdgeSegment.new
|
|
58
|
+
segments << segment
|
|
59
|
+
collect(port, segment, owner, coordinate)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def collect(port, segment, owner, coordinate)
|
|
64
|
+
return if owner.key?(port)
|
|
65
|
+
|
|
66
|
+
owner[port] = segment
|
|
67
|
+
if port.side == :east
|
|
68
|
+
segment.add_source(port, coordinate.call(port))
|
|
69
|
+
else
|
|
70
|
+
segment.add_target(port, coordinate.call(port))
|
|
71
|
+
end
|
|
72
|
+
port.others.each do |other|
|
|
73
|
+
collect(other, segment, owner, coordinate)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def minimum_distance(segments)
|
|
78
|
+
[segments.flat_map(&:incoming), segments.flat_map(&:outgoing)]
|
|
79
|
+
.map { |values| minimum_difference(values) }.min
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def minimum_difference(values)
|
|
83
|
+
distinct = values.uniq.sort
|
|
84
|
+
return Float::MAX if distinct.length < 2
|
|
85
|
+
|
|
86
|
+
distinct.each_cons(2).map { |low, high| high - low }.min
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Dependencies between every pair of segments that need ordering.
|
|
90
|
+
def link(segments)
|
|
91
|
+
segments.combination(2) do |first, second|
|
|
92
|
+
create_dependency(first, second)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def create_dependency(first, second)
|
|
97
|
+
return if first.straight? || second.straight?
|
|
98
|
+
|
|
99
|
+
first_first = conflicts(first.outgoing, second.incoming)
|
|
100
|
+
second_first = conflicts(second.outgoing, first.incoming)
|
|
101
|
+
if [first_first, second_first].include?(CRITICAL)
|
|
102
|
+
critical_dependencies(first, second, first_first, second_first)
|
|
103
|
+
else
|
|
104
|
+
regular_dependency(first, second, first_first, second_first)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def critical_dependencies(first, second, first_first, second_first)
|
|
109
|
+
Dependency.new(second, first, 1, critical: true) if
|
|
110
|
+
first_first == CRITICAL
|
|
111
|
+
Dependency.new(first, second, 1, critical: true) if
|
|
112
|
+
second_first == CRITICAL
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def regular_dependency(first, second, first_first, second_first)
|
|
116
|
+
left = penalty(first, second, first_first)
|
|
117
|
+
right = penalty(second, first, second_first)
|
|
118
|
+
if left < right
|
|
119
|
+
Dependency.new(first, second, right - left)
|
|
120
|
+
elsif left > right
|
|
121
|
+
Dependency.new(second, first, left - right)
|
|
122
|
+
elsif left.positive?
|
|
123
|
+
Dependency.new(first, second, 0)
|
|
124
|
+
Dependency.new(second, first, 0)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Cost of putting `before` in a lower slot than `after`.
|
|
129
|
+
def penalty(before, after, conflicts)
|
|
130
|
+
crossings =
|
|
131
|
+
crossings(before.outgoing, after.start_coordinate,
|
|
132
|
+
after.end_coordinate) +
|
|
133
|
+
crossings(after.incoming, before.start_coordinate,
|
|
134
|
+
before.end_coordinate)
|
|
135
|
+
(CONFLICT_PENALTY * conflicts) + (CROSSING_PENALTY * crossings)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Conflicts between two sorted coordinate lists, or CRITICAL as
|
|
139
|
+
# soon as two coordinates are almost equal.
|
|
140
|
+
def conflicts(first, second)
|
|
141
|
+
return 0 if first.empty? || second.empty?
|
|
142
|
+
|
|
143
|
+
count = 0
|
|
144
|
+
walk_pairs(first, second) do |one, two|
|
|
145
|
+
return CRITICAL if near?(one, two, @critical_threshold)
|
|
146
|
+
|
|
147
|
+
count += 1 if near?(one, two, @conflict_threshold)
|
|
148
|
+
end
|
|
149
|
+
count
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Yields the pairs met when stepping through two sorted lists
|
|
153
|
+
# in step with each other.
|
|
154
|
+
def walk_pairs(first, second)
|
|
155
|
+
at = [0, 0]
|
|
156
|
+
loop do
|
|
157
|
+
yield first[at[0]], second[at[1]]
|
|
158
|
+
at = next_pair(first, second, at) || break
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def next_pair(first, second, at)
|
|
163
|
+
left, right = at
|
|
164
|
+
if first[left] <= second[right] && left + 1 < first.length
|
|
165
|
+
[left + 1, right]
|
|
166
|
+
elsif second[right] <= first[left] && right + 1 < second.length
|
|
167
|
+
[left, right + 1]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def near?(first, second, threshold)
|
|
172
|
+
first > second - threshold && first < second + threshold
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def crossings(positions, from, to)
|
|
176
|
+
positions.count { |position| position.between?(from, to) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def break_cycles(segments)
|
|
180
|
+
detect_cycles(segments).each do |dependency|
|
|
181
|
+
if dependency.weight.zero?
|
|
182
|
+
dependency.remove
|
|
183
|
+
else
|
|
184
|
+
dependency.reverse
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def detect_cycles(segments)
|
|
190
|
+
SegmentOrder.new(segments).assign_marks
|
|
191
|
+
segments.flat_map do |segment|
|
|
192
|
+
segment.out_dependencies.select do |dependency|
|
|
193
|
+
segment.mark > dependency.target.mark
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def number(segments)
|
|
199
|
+
queue = segments.select { |s| s.in_dependencies.empty? }
|
|
200
|
+
remaining = segments.to_h { |s| [s, s.in_dependencies.length] }
|
|
201
|
+
queue.concat(relax(queue.shift, remaining)) until queue.empty?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Puts the targets one slot beyond `segment`; returns those that
|
|
205
|
+
# now have all their predecessors numbered.
|
|
206
|
+
def relax(segment, remaining)
|
|
207
|
+
segment.out_dependencies.filter_map do |dependency|
|
|
208
|
+
target = dependency.target
|
|
209
|
+
target.raise_slot(segment.slot)
|
|
210
|
+
remaining[target] -= 1
|
|
211
|
+
target if remaining[target].zero?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
module Algorithms
|
|
6
|
+
module Layered
|
|
7
|
+
module Orthogonal
|
|
8
|
+
# One run of edge between two layers that shares a single
|
|
9
|
+
# perpendicular stretch: the ports joined by edges, with the cross
|
|
10
|
+
# coordinates where edges leave the earlier layer (`incoming`) and
|
|
11
|
+
# enter the later one (`outgoing`), both sorted and distinct.
|
|
12
|
+
class HyperEdgeSegment
|
|
13
|
+
TOLERANCE = 1e-3
|
|
14
|
+
|
|
15
|
+
attr_reader :ports, :incoming, :outgoing,
|
|
16
|
+
:out_dependencies, :in_dependencies
|
|
17
|
+
attr_accessor :slot, :mark, :in_weight, :out_weight,
|
|
18
|
+
:critical_in_weight, :critical_out_weight
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@ports = []
|
|
22
|
+
@incoming = []
|
|
23
|
+
@outgoing = []
|
|
24
|
+
@out_dependencies = []
|
|
25
|
+
@in_dependencies = []
|
|
26
|
+
@slot = 0
|
|
27
|
+
@mark = 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_source(port, coordinate)
|
|
31
|
+
ports << port
|
|
32
|
+
incoming << coordinate unless incoming.include?(coordinate)
|
|
33
|
+
incoming.sort!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_target(port, coordinate)
|
|
37
|
+
ports << port
|
|
38
|
+
outgoing << coordinate unless outgoing.include?(coordinate)
|
|
39
|
+
outgoing.sort!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start_coordinate
|
|
43
|
+
[incoming.first, outgoing.first].compact.min
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def end_coordinate
|
|
47
|
+
[incoming.last, outgoing.last].compact.max
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A straight run takes no slot and needs no bend points.
|
|
51
|
+
def straight?
|
|
52
|
+
(start_coordinate - end_coordinate).abs < TOLERANCE
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Gives the segment the negative mark of an unplaced one, and
|
|
56
|
+
# weights taken from its dependencies.
|
|
57
|
+
def reset_weights(position)
|
|
58
|
+
@mark = -(position + 1)
|
|
59
|
+
@in_weight = in_dependencies.sum(&:weight)
|
|
60
|
+
@out_weight = out_dependencies.sum(&:weight)
|
|
61
|
+
@critical_in_weight = critical_weight(in_dependencies)
|
|
62
|
+
@critical_out_weight = critical_weight(out_dependencies)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def balance
|
|
66
|
+
out_weight - in_weight
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def unplaced?
|
|
70
|
+
mark.negative?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def drained_source?
|
|
74
|
+
in_weight <= 0 && out_weight.positive?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def drained_sink?
|
|
78
|
+
out_weight <= 0 && in_weight.positive?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A segment that must precede this one has been placed.
|
|
82
|
+
def release_in(dependency)
|
|
83
|
+
@in_weight -= dependency.weight
|
|
84
|
+
@critical_in_weight -= dependency.weight if dependency.critical?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A segment that must follow this one has been placed.
|
|
88
|
+
def release_out(dependency)
|
|
89
|
+
@out_weight -= dependency.weight
|
|
90
|
+
@critical_out_weight -= dependency.weight if
|
|
91
|
+
dependency.critical?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def raise_slot(above)
|
|
95
|
+
@slot = [slot, above + 1].max
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def critical_weight(dependencies)
|
|
101
|
+
dependencies.select(&:critical?).sum(&:weight)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# "`source` wants to sit before `target`": a lower routing slot,
|
|
106
|
+
# nearer the earlier layer. Critical ones would overlap edges if
|
|
107
|
+
# reversed.
|
|
108
|
+
class Dependency
|
|
109
|
+
attr_reader :source, :target, :weight
|
|
110
|
+
|
|
111
|
+
def initialize(source, target, weight, critical: false)
|
|
112
|
+
@weight = weight
|
|
113
|
+
@critical = critical
|
|
114
|
+
attach(source, target)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def critical?
|
|
118
|
+
@critical
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def remove
|
|
122
|
+
detach
|
|
123
|
+
@source = nil
|
|
124
|
+
@target = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def reverse
|
|
128
|
+
old_source = source
|
|
129
|
+
old_target = target
|
|
130
|
+
detach
|
|
131
|
+
attach(old_target, old_source)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def attach(new_source, new_target)
|
|
137
|
+
@source = new_source
|
|
138
|
+
@target = new_target
|
|
139
|
+
source.out_dependencies << self
|
|
140
|
+
target.in_dependencies << self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def detach
|
|
144
|
+
source.out_dependencies.delete(self)
|
|
145
|
+
target.in_dependencies.delete(self)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hyper_edge_segment"
|
|
4
|
+
|
|
5
|
+
module Elkrb
|
|
6
|
+
module Layout
|
|
7
|
+
module Algorithms
|
|
8
|
+
module Layered
|
|
9
|
+
module Orthogonal
|
|
10
|
+
# Decides which bend points of one gap are junction points: those
|
|
11
|
+
# where an edge meets others of its hyperedge segment mid-run. A
|
|
12
|
+
# point is reported once, for the first edge that bends there.
|
|
13
|
+
class JunctionPoints
|
|
14
|
+
TOLERANCE = HyperEdgeSegment::TOLERANCE
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@taken = Set.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param segment [HyperEdgeSegment]
|
|
21
|
+
# @param points [Array<Array(Float, Float)>] [along, cross] bends
|
|
22
|
+
# @return [Array<Array(Float, Float)>] those that are junctions
|
|
23
|
+
def among(segment, points)
|
|
24
|
+
points.select do |point|
|
|
25
|
+
next false if @taken.include?(point)
|
|
26
|
+
next false unless junction?(segment, point.last)
|
|
27
|
+
|
|
28
|
+
@taken << point
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Inside the segment's run, or at an end of it where another
|
|
36
|
+
# edge joins at the same cross coordinate.
|
|
37
|
+
def junction?(segment, cross)
|
|
38
|
+
inside = cross > segment.start_coordinate &&
|
|
39
|
+
cross < segment.end_coordinate
|
|
40
|
+
inside || joined_at_end?(segment, cross)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def joined_at_end?(segment, cross)
|
|
44
|
+
return false if segment.incoming.empty? ||
|
|
45
|
+
segment.outgoing.empty?
|
|
46
|
+
|
|
47
|
+
%i[first last].any? do |end_of|
|
|
48
|
+
[segment.incoming, segment.outgoing].all? do |list|
|
|
49
|
+
(cross - list.public_send(end_of)).abs < TOLERANCE
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
module Algorithms
|
|
6
|
+
module Layered
|
|
7
|
+
module Orthogonal
|
|
8
|
+
# Greedy feedback-arc-set ordering (Eades, Lin, Smyth) of the
|
|
9
|
+
# segments of one gap. A tie between segments is broken by taking
|
|
10
|
+
# the first, where Java ELK draws from its seeded generator.
|
|
11
|
+
class SegmentOrder
|
|
12
|
+
# @param segments [Array<HyperEdgeSegment>] with dependencies
|
|
13
|
+
# linked; each gets a `mark` giving its place in the order
|
|
14
|
+
def initialize(segments)
|
|
15
|
+
@segments = segments
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def assign_marks
|
|
19
|
+
@segments.each_with_index do |segment, index|
|
|
20
|
+
segment.reset_weights(index)
|
|
21
|
+
end
|
|
22
|
+
seed_queues
|
|
23
|
+
settle_all
|
|
24
|
+
shift_sink_marks
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def seed_queues
|
|
30
|
+
@sinks = @segments.select { |s| s.out_weight.zero? }
|
|
31
|
+
@sources = initial_sources - @sinks
|
|
32
|
+
@unprocessed = @segments.dup
|
|
33
|
+
@next_sink = @segments.length - 1
|
|
34
|
+
@next_source = @segments.length + 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initial_sources
|
|
38
|
+
@segments.select do |s|
|
|
39
|
+
s.out_weight.positive? && s.in_weight.zero?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def settle_all
|
|
44
|
+
until @unprocessed.empty?
|
|
45
|
+
drain_sinks
|
|
46
|
+
drain_sources
|
|
47
|
+
promote_best
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def drain_sinks
|
|
52
|
+
until @sinks.empty?
|
|
53
|
+
settle(@sinks.shift, @next_sink)
|
|
54
|
+
@next_sink -= 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def drain_sources
|
|
59
|
+
until @sources.empty?
|
|
60
|
+
settle(@sources.shift, @next_source)
|
|
61
|
+
@next_source += 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def promote_best
|
|
66
|
+
return if @unprocessed.empty?
|
|
67
|
+
|
|
68
|
+
settle(best_candidate, @next_source)
|
|
69
|
+
@next_source += 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def settle(segment, mark)
|
|
73
|
+
@unprocessed.delete(segment)
|
|
74
|
+
segment.mark = mark
|
|
75
|
+
release_targets(segment)
|
|
76
|
+
release_origins(segment)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Unprocessed segments are visited by ascending mark, i.e. the
|
|
80
|
+
# last one created first.
|
|
81
|
+
def best_candidate
|
|
82
|
+
ordered = @unprocessed.sort_by(&:mark)
|
|
83
|
+
forced_source(ordered) || most_outgoing(ordered)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def forced_source(ordered)
|
|
87
|
+
ordered.find do |s|
|
|
88
|
+
s.critical_out_weight.positive? && s.critical_in_weight <= 0
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def most_outgoing(ordered)
|
|
93
|
+
top = ordered.map(&:balance).max
|
|
94
|
+
ordered.find { |s| s.balance == top }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def release_targets(segment)
|
|
98
|
+
open_dependencies(segment.out_dependencies, :target)
|
|
99
|
+
.each do |dependency|
|
|
100
|
+
target = dependency.target
|
|
101
|
+
target.release_in(dependency)
|
|
102
|
+
@sources << target if target.drained_source?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def release_origins(segment)
|
|
107
|
+
open_dependencies(segment.in_dependencies, :source)
|
|
108
|
+
.each do |dependency|
|
|
109
|
+
origin = dependency.source
|
|
110
|
+
origin.release_out(dependency)
|
|
111
|
+
@sinks << origin if origin.drained_sink?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Dependencies that still weigh on a segment not yet placed.
|
|
116
|
+
def open_dependencies(dependencies, far_end)
|
|
117
|
+
dependencies.select do |dependency|
|
|
118
|
+
dependency.weight.positive? &&
|
|
119
|
+
dependency.public_send(far_end).unplaced?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def shift_sink_marks
|
|
124
|
+
base = @segments.length
|
|
125
|
+
@segments.each do |segment|
|
|
126
|
+
segment.mark += base + 1 if segment.mark < base
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|