elkrb 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +364 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +695 -557
  5. data/exe/elkrb +36 -2
  6. data/lib/elkrb/atomic_destination.rb +53 -0
  7. data/lib/elkrb/best_effort_write.rb +49 -0
  8. data/lib/elkrb/cli/layout_flag_options.rb +54 -0
  9. data/lib/elkrb/cli.rb +155 -82
  10. data/lib/elkrb/command_resolver.rb +186 -0
  11. data/lib/elkrb/commands/batch_command.rb +28 -5
  12. data/lib/elkrb/commands/convert_command.rb +22 -52
  13. data/lib/elkrb/commands/diagram_command.rb +234 -102
  14. data/lib/elkrb/commands/options_command.rb +87 -0
  15. data/lib/elkrb/commands/render_command.rb +6 -1
  16. data/lib/elkrb/commands/validate_command.rb +146 -91
  17. data/lib/elkrb/errors.rb +20 -1
  18. data/lib/elkrb/format_sniffer.rb +362 -0
  19. data/lib/elkrb/geometry/bezier.rb +14 -4
  20. data/lib/elkrb/geometry/point.rb +5 -6
  21. data/lib/elkrb/geometry/rectangle.rb +2 -2
  22. data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
  23. data/lib/elkrb/graph/edge.rb +89 -3
  24. data/lib/elkrb/graph/graph.rb +43 -15
  25. data/lib/elkrb/graph/label.rb +16 -2
  26. data/lib/elkrb/graph/node.rb +13 -2
  27. data/lib/elkrb/graph/node_constraints.rb +139 -8
  28. data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
  29. data/lib/elkrb/graph/port.rb +29 -21
  30. data/lib/elkrb/graph/read_only_mapping.rb +21 -0
  31. data/lib/elkrb/graphviz_wrapper.rb +152 -44
  32. data/lib/elkrb/layout/algorithm_registry.rb +65 -8
  33. data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
  34. data/lib/elkrb/layout/algorithms/box.rb +47 -20
  35. data/lib/elkrb/layout/algorithms/disco.rb +150 -61
  36. data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
  37. data/lib/elkrb/layout/algorithms/force.rb +145 -86
  38. data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
  39. data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
  40. data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
  41. data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
  42. data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
  43. data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
  44. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
  45. data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
  46. data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
  47. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
  48. data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
  49. data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
  50. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
  51. data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
  52. data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
  53. data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
  54. data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
  55. data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
  56. data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
  57. data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
  58. data/lib/elkrb/layout/algorithms/layered.rb +548 -13
  59. data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
  60. data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
  61. data/lib/elkrb/layout/algorithms/radial.rb +188 -29
  62. data/lib/elkrb/layout/algorithms/random.rb +131 -17
  63. data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
  64. data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
  65. data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
  66. data/lib/elkrb/layout/algorithms/stress.rb +391 -84
  67. data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
  68. data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
  69. data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
  70. data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
  71. data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
  72. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
  73. data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
  74. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
  75. data/lib/elkrb/layout/edge_router.rb +533 -337
  76. data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
  77. data/lib/elkrb/layout/java_random.rb +59 -0
  78. data/lib/elkrb/layout/label_placer.rb +165 -157
  79. data/lib/elkrb/layout/layout_engine.rb +46 -23
  80. data/lib/elkrb/layout/node_index.rb +149 -0
  81. data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
  82. data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
  83. data/lib/elkrb/layout/polyomino/grid.rb +106 -0
  84. data/lib/elkrb/layout/polyomino/packer.rb +95 -0
  85. data/lib/elkrb/layout/polyomino/piece.rb +52 -0
  86. data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
  87. data/lib/elkrb/layout/spore/body.rb +65 -0
  88. data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
  89. data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
  90. data/lib/elkrb/layout/spore/gap.rb +76 -0
  91. data/lib/elkrb/layout/spore/geometry.rb +104 -0
  92. data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
  93. data/lib/elkrb/layout/spore/placement.rb +57 -0
  94. data/lib/elkrb/layout/spore/rays.rb +119 -0
  95. data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
  96. data/lib/elkrb/layout/spore/triangulation.rb +159 -0
  97. data/lib/elkrb/layout_flags.rb +69 -0
  98. data/lib/elkrb/options/decimal.rb +50 -0
  99. data/lib/elkrb/options/element_options.rb +43 -0
  100. data/lib/elkrb/options/elk_padding.rb +20 -14
  101. data/lib/elkrb/options/k_vector.rb +6 -7
  102. data/lib/elkrb/options/k_vector_chain.rb +12 -20
  103. data/lib/elkrb/options/option_map.rb +85 -0
  104. data/lib/elkrb/options/registry.rb +335 -0
  105. data/lib/elkrb/options/resolver.rb +151 -0
  106. data/lib/elkrb/options/spellings.rb +62 -0
  107. data/lib/elkrb/options/unhonoured_report.rb +194 -0
  108. data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
  109. data/lib/elkrb/parsers/elkt/parser.rb +424 -0
  110. data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
  111. data/lib/elkrb/parsers/elkt/token.rb +40 -0
  112. data/lib/elkrb/parsers/elkt_parser.rb +32 -230
  113. data/lib/elkrb/serializers/dot_serializer.rb +342 -54
  114. data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
  115. data/lib/elkrb/version.rb +1 -1
  116. data/lib/elkrb.rb +60 -61
  117. metadata +71 -49
  118. data/.rspec +0 -3
  119. data/.rubocop.yml +0 -11
  120. data/Gemfile +0 -13
  121. data/Rakefile +0 -64
  122. data/benchmarks/README.md +0 -172
  123. data/benchmarks/elkjs_benchmark.js +0 -140
  124. data/benchmarks/elkrb_benchmark.rb +0 -145
  125. data/benchmarks/fixtures/graphs.json +0 -10777
  126. data/benchmarks/generate_report.rb +0 -241
  127. data/benchmarks/generate_test_graphs.rb +0 -154
  128. data/benchmarks/results/elkrb_results.json +0 -280
  129. data/benchmarks/results/elkrb_summary.json +0 -285
  130. data/elkrb.gemspec +0 -39
  131. data/examples/dot_export_demo.rb +0 -133
  132. data/examples/hierarchical_graph.rb +0 -19
  133. data/examples/layout_constraints_demo.rb +0 -272
  134. data/examples/port_constraints_demo.rb +0 -291
  135. data/examples/self_loop_demo.rb +0 -391
  136. data/examples/simple_graph.rb +0 -50
  137. data/examples/spline_routing_demo.rb +0 -235
  138. data/lib/elkrb/graph/layout_options.rb +0 -247
  139. data/sig/elkrb/constraints.rbs +0 -114
  140. data/sig/elkrb/geometry.rbs +0 -61
  141. data/sig/elkrb/graph.rbs +0 -112
  142. data/sig/elkrb/layout.rbs +0 -107
  143. data/sig/elkrb/options.rbs +0 -81
  144. data/sig/elkrb.rbs +0 -32
@@ -0,0 +1,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