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,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "port_order"
|
|
4
|
+
require_relative "port_spread"
|
|
5
|
+
require_relative "orthogonal/gap_solver"
|
|
6
|
+
require_relative "orthogonal/junction_points"
|
|
7
|
+
|
|
8
|
+
module Elkrb
|
|
9
|
+
module Layout
|
|
10
|
+
module Algorithms
|
|
11
|
+
module Layered
|
|
12
|
+
# Orthogonal routes for the edges between adjacent layers. Each gap
|
|
13
|
+
# between two layers holds the perpendicular runs of its edges, one
|
|
14
|
+
# per routing slot; an edge leaves the side of its node that faces
|
|
15
|
+
# the next layer, runs to its slot, along it, and on to its target.
|
|
16
|
+
#
|
|
17
|
+
# All geometry is read through callables, so the same router sizes
|
|
18
|
+
# the gaps before nodes have a layer position and routes after.
|
|
19
|
+
#
|
|
20
|
+
# Not ported: the segment splitting described on
|
|
21
|
+
# Orthogonal::GapSolver.
|
|
22
|
+
class OrthogonalRouter
|
|
23
|
+
# Cross-axis geometry, each a callable.
|
|
24
|
+
#
|
|
25
|
+
# `start` is where an item starts, `extent` its cross size and
|
|
26
|
+
# `port` the cross size of a port.
|
|
27
|
+
Measure = Struct.new(:start, :extent, :port, keyword_init: true)
|
|
28
|
+
|
|
29
|
+
# An edge's route: its points, source first, and the junction
|
|
30
|
+
# points where it meets other edges of its hyperedge segment.
|
|
31
|
+
Route = Struct.new(:points, :junctions)
|
|
32
|
+
# Bend points and junction points per edge, per gap.
|
|
33
|
+
class Book
|
|
34
|
+
attr_reader :bends, :junctions
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@bends = {}.compare_by_identity
|
|
38
|
+
@junctions = {}.compare_by_identity
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def record(edge, gap, bends, junctions)
|
|
42
|
+
(@bends[edge] ||= {})[gap] = bends
|
|
43
|
+
(@junctions[edge] ||= {})[gap] = junctions
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
private_constant :Book
|
|
47
|
+
|
|
48
|
+
# Distance between neighbouring routing slots.
|
|
49
|
+
EDGE_SPACING = 10.0
|
|
50
|
+
# Distance between a layer and its first routing slot.
|
|
51
|
+
EDGE_NODE_SPACING = 10.0
|
|
52
|
+
TOLERANCE = Orthogonal::HyperEdgeSegment::TOLERANCE
|
|
53
|
+
# Node side that faces the next layer, and the side that faces the
|
|
54
|
+
# previous one, per direction.
|
|
55
|
+
FACING_SIDES = {
|
|
56
|
+
"RIGHT" => %w[EAST WEST], "LEFT" => %w[WEST EAST],
|
|
57
|
+
"DOWN" => %w[SOUTH NORTH], "UP" => %w[NORTH SOUTH]
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# @param layers [Array<Array>] nodes and DummySlots per layer
|
|
61
|
+
# @param port_order [PortOrder]
|
|
62
|
+
# @param index [NodeIndex] resolves edge endpoints to nodes
|
|
63
|
+
# @param direction [String] RIGHT, LEFT, DOWN or UP
|
|
64
|
+
# @param measure [Measure] cross-axis geometry
|
|
65
|
+
def initialize(layers, port_order, index, direction:, measure:)
|
|
66
|
+
@layers = layers
|
|
67
|
+
@port_order = port_order
|
|
68
|
+
@index = index
|
|
69
|
+
@facing = FACING_SIDES.fetch(direction)
|
|
70
|
+
@vertical = %w[DOWN UP].include?(direction)
|
|
71
|
+
@sign = %w[LEFT UP].include?(direction) ? -1 : 1
|
|
72
|
+
@measure = measure
|
|
73
|
+
@items = layers.flatten.to_h { |item| [item.id, item] }
|
|
74
|
+
@offsets = spread_offsets
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Number of routing slots in each gap between two layers.
|
|
78
|
+
def slot_counts
|
|
79
|
+
gaps.map { |segments| Orthogonal::GapSolver.slot_count(segments) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param along_range [#call] [low, high] of an item on the layer
|
|
83
|
+
# axis, in final coordinates
|
|
84
|
+
# @return [Hash{Edge=>Route}] the route of each edge that crosses
|
|
85
|
+
# a layer gap, source first; an edge whose declared port is not
|
|
86
|
+
# on the side facing its neighbour layer is left out
|
|
87
|
+
def routes(along_range)
|
|
88
|
+
@along_range = along_range
|
|
89
|
+
book = gap_book
|
|
90
|
+
grouped_segments.select { |_edge, links| facing?(links) }
|
|
91
|
+
.to_h { |edge, links| [edge, route(edge, links, book)] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def spread_offsets
|
|
97
|
+
sizes = @items.transform_values do |item|
|
|
98
|
+
@measure.extent.call(item)
|
|
99
|
+
end
|
|
100
|
+
PortSpread.offsets(@layers, @port_order, sizes, @measure.port)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def gaps
|
|
104
|
+
@layers.each_cons(2).map do |layer, _next_layer|
|
|
105
|
+
ports = layer.flat_map do |item|
|
|
106
|
+
@port_order.visual(item.id, :east)
|
|
107
|
+
end
|
|
108
|
+
Orthogonal::GapSolver.new(EDGE_SPACING)
|
|
109
|
+
.solve(ports, method(:cross_of))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cross_of(port)
|
|
114
|
+
item = @items.fetch(port.item_id)
|
|
115
|
+
offset = item.respond_to?(:dummy?) ? 0.0 : @offsets.fetch(port)
|
|
116
|
+
@measure.start.call(item) + offset
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Bend points and junction points per edge, per gap: lists of
|
|
120
|
+
# [along, cross].
|
|
121
|
+
def gap_book
|
|
122
|
+
book = Book.new
|
|
123
|
+
pairs = @layers.each_cons(2).zip(gaps)
|
|
124
|
+
pairs.each_with_index do |(pair, segments), gap|
|
|
125
|
+
start = layer_high(pair.first) + EDGE_NODE_SPACING
|
|
126
|
+
joints = Orthogonal::JunctionPoints.new
|
|
127
|
+
segments.each { |s| place(s, [start, gap], book, joints) }
|
|
128
|
+
end
|
|
129
|
+
book
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def place(segment, (start, gap), book, joints)
|
|
133
|
+
return if segment.straight?
|
|
134
|
+
|
|
135
|
+
along = start + (segment.slot * EDGE_SPACING)
|
|
136
|
+
source_links(segment).each do |link|
|
|
137
|
+
points = bend_points(link, along) or next
|
|
138
|
+
book.record(link.edge, gap, points, joints.among(segment, points))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def source_links(segment)
|
|
143
|
+
segment.ports.select { |port| port.side == :east }
|
|
144
|
+
.flat_map(&:segments)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def bend_points(link, along)
|
|
148
|
+
from = cross_of(link.from_port)
|
|
149
|
+
to = cross_of(link.to_port)
|
|
150
|
+
return if (from - to).abs <= TOLERANCE
|
|
151
|
+
|
|
152
|
+
[[along, from], [along, to]]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def grouped_segments
|
|
156
|
+
groups = {}.compare_by_identity
|
|
157
|
+
@port_order.segments.each do |link|
|
|
158
|
+
(groups[link.edge] ||= []) << link
|
|
159
|
+
end
|
|
160
|
+
groups.transform_values do |links|
|
|
161
|
+
links.sort_by { |link| layer_index(link.from_port.item_id) }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def facing?(links)
|
|
166
|
+
links.all? do |link|
|
|
167
|
+
port_faces?(link.from_port, @facing.first) &&
|
|
168
|
+
port_faces?(link.to_port, @facing.last)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def port_faces?(port, side)
|
|
173
|
+
port.declared.nil? || port.declared.side == side
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def layer_index(item_id)
|
|
177
|
+
@layer_index ||= index_layers
|
|
178
|
+
@layer_index.fetch(item_id)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def index_layers
|
|
182
|
+
map = {}
|
|
183
|
+
@layers.each_with_index do |layer, i|
|
|
184
|
+
layer.each { |item| map[item.id] = i }
|
|
185
|
+
end
|
|
186
|
+
map
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def route(edge, links, book)
|
|
190
|
+
first = links.first.from_port
|
|
191
|
+
inner = gap_points(edge, links, book.bends)
|
|
192
|
+
points = [anchor(first, :high), *inner,
|
|
193
|
+
anchor(links.last.to_port, :low)]
|
|
194
|
+
joined = gap_points(edge, links, book.junctions)
|
|
195
|
+
forward = forward?(edge, first)
|
|
196
|
+
Route.new(oriented(points, forward), oriented(joined, forward))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def oriented(points, forward)
|
|
200
|
+
real = points.map { |along, cross| real_point(along, cross) }
|
|
201
|
+
forward ? real : real.reverse
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def gap_points(edge, links, per_gap)
|
|
205
|
+
links.flat_map do |link|
|
|
206
|
+
per_gap.dig(edge, layer_index(link.from_port.item_id)) || []
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def forward?(edge, first_port)
|
|
211
|
+
@index.node(edge.sources.first)&.id == first_port.item_id
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The point where an edge meets a port: on the item's border, or
|
|
215
|
+
# beyond it for a declared port.
|
|
216
|
+
def anchor(port, side)
|
|
217
|
+
item = @items.fetch(port.item_id)
|
|
218
|
+
low, high = abstract_range(item)
|
|
219
|
+
reach = port.declared ? declared_extent(port.declared) : 0.0
|
|
220
|
+
along = side == :high ? high + reach : low - reach
|
|
221
|
+
[along, cross_of(port)]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def declared_extent(declared)
|
|
225
|
+
(declared.public_send(@vertical ? :height : :width) || 0).to_f
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def abstract_range(item)
|
|
229
|
+
low, high = @along_range.call(item)
|
|
230
|
+
@sign.positive? ? [low, high] : [-high, -low]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The far edge of a layer, port overhang included.
|
|
234
|
+
def layer_high(layer)
|
|
235
|
+
return 0.0 if layer.empty?
|
|
236
|
+
|
|
237
|
+
layer.map do |item|
|
|
238
|
+
declared = @port_order.visual(item.id, :east)
|
|
239
|
+
.filter_map(&:declared)
|
|
240
|
+
reach = declared.map { |d| declared_extent(d) }.max || 0.0
|
|
241
|
+
abstract_range(item).last + reach
|
|
242
|
+
end.max
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def real_point(along, cross)
|
|
246
|
+
real = @sign * along
|
|
247
|
+
@vertical ? [cross, real] : [real, cross]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
module Algorithms
|
|
6
|
+
module Layered
|
|
7
|
+
# The port slots every layer item exposes toward its neighbouring
|
|
8
|
+
# layers, and the order they sit in.
|
|
9
|
+
#
|
|
10
|
+
# An edge crossing one layer gap is a Segment from an EAST slot of the
|
|
11
|
+
# item in the earlier layer to a WEST slot of the item in the later
|
|
12
|
+
# one. A slot is an explicit port, or the one slot a port-less
|
|
13
|
+
# endpoint gets per edge.
|
|
14
|
+
#
|
|
15
|
+
# A list is kept in the order ELK keeps it: EAST lists run top to
|
|
16
|
+
# bottom, WEST lists bottom to top. #visual always answers top to
|
|
17
|
+
# bottom. Sorting a WEST list ascending therefore places the lowest
|
|
18
|
+
# value at the bottom; the sweep in CrossingMinimizer depends on
|
|
19
|
+
# that.
|
|
20
|
+
class PortOrder
|
|
21
|
+
# One slot on the EAST or WEST side of a layer item.
|
|
22
|
+
class Port
|
|
23
|
+
attr_reader :item_id, :side, :declared, :sequence, :segments
|
|
24
|
+
|
|
25
|
+
def initialize(item_id:, side:, declared:, sequence:)
|
|
26
|
+
@item_id = item_id
|
|
27
|
+
@side = side
|
|
28
|
+
@declared = declared
|
|
29
|
+
@sequence = sequence
|
|
30
|
+
@segments = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def others
|
|
34
|
+
segments.map { |segment| segment.other_port(self) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One edge crossing one layer gap, `from_port` in the earlier layer.
|
|
39
|
+
Segment = Struct.new(:edge, :from_port, :to_port) do
|
|
40
|
+
def other_port(port)
|
|
41
|
+
port.equal?(from_port) ? to_port : from_port
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
SIDES = %i[east west].freeze
|
|
46
|
+
|
|
47
|
+
attr_reader :segments
|
|
48
|
+
|
|
49
|
+
# @param layers [Array<Array>] nodes and DummySlots per layer
|
|
50
|
+
# @param index [NodeIndex, nil] resolves edge endpoints to nodes
|
|
51
|
+
def initialize(layers, index)
|
|
52
|
+
@layer_of = layer_map(layers)
|
|
53
|
+
@slots = dummy_slots_by_edge(layers)
|
|
54
|
+
@ports = {}
|
|
55
|
+
@segments = []
|
|
56
|
+
@index = index
|
|
57
|
+
(index ? index.edges : []).each_with_index do |edge, order|
|
|
58
|
+
add_edge(edge, order)
|
|
59
|
+
end
|
|
60
|
+
@lists = build_lists
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ports of one side, top to bottom.
|
|
64
|
+
def visual(item_id, side)
|
|
65
|
+
list = list(item_id, side)
|
|
66
|
+
side == :west ? list.reverse : list
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reorders a list ascending by the block's value. The sort is
|
|
70
|
+
# stable, so equal values keep their order.
|
|
71
|
+
def sort!(item_id, side)
|
|
72
|
+
list = list(item_id, side)
|
|
73
|
+
@lists[[item_id, side]] =
|
|
74
|
+
list.each_with_index.sort_by do |port, position|
|
|
75
|
+
[yield(port), position]
|
|
76
|
+
end.map(&:first)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The port lists as they are now, for #restore.
|
|
80
|
+
def snapshot
|
|
81
|
+
@lists.transform_values(&:dup)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def restore(snapshot)
|
|
85
|
+
@lists = snapshot.transform_values(&:dup)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def list(item_id, side)
|
|
91
|
+
@lists.fetch([item_id, side], [])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def layer_map(layers)
|
|
95
|
+
layers.each_with_index.with_object({}) do |(layer, number), map|
|
|
96
|
+
layer.each { |item| map[item.id] = number }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def dummy_slots_by_edge(layers)
|
|
101
|
+
slots = {}.compare_by_identity
|
|
102
|
+
layers.flatten.select { |item| item.respond_to?(:layer_index) }
|
|
103
|
+
.sort_by(&:layer_index).each do |slot|
|
|
104
|
+
(slots[slot.edge] ||= []) << slot
|
|
105
|
+
end
|
|
106
|
+
slots
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_edge(edge, order)
|
|
110
|
+
source = owner(edge.sources)
|
|
111
|
+
target = owner(edge.targets)
|
|
112
|
+
return unless placed?(source) && placed?(target)
|
|
113
|
+
|
|
114
|
+
add_chain(edge, order, *earlier_first(edge, source, target))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The two nodes, earlier layer first, with the endpoint ids the
|
|
118
|
+
# edge names on each.
|
|
119
|
+
def earlier_first(edge, source, target)
|
|
120
|
+
named = [edge.sources.first, edge.targets.first]
|
|
121
|
+
if @layer_of[source.id] < @layer_of[target.id]
|
|
122
|
+
[[source, target], named]
|
|
123
|
+
else
|
|
124
|
+
[[target, source], named.reverse]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def placed?(node)
|
|
129
|
+
node && @layer_of.key?(node.id)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# `named` holds the endpoint ids the edge gives on the earlier and
|
|
133
|
+
# the later layer.
|
|
134
|
+
def add_chain(edge, order, ends, named)
|
|
135
|
+
chain = [ends.first, *Array(@slots[edge]), ends.last]
|
|
136
|
+
chain.each_cons(2).with_index do |pair, position|
|
|
137
|
+
next unless adjacent?(*pair)
|
|
138
|
+
|
|
139
|
+
add_segment(edge, order, pair,
|
|
140
|
+
[position.zero? && named.first,
|
|
141
|
+
position == chain.length - 2 && named.last])
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def adjacent?(from, to)
|
|
146
|
+
@layer_of[to.id] - @layer_of[from.id] == 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def add_segment(edge, order, items, endpoints)
|
|
150
|
+
segment = Segment.new(
|
|
151
|
+
edge,
|
|
152
|
+
port(items.first, :east, order, endpoints.first),
|
|
153
|
+
port(items.last, :west, order, endpoints.last),
|
|
154
|
+
)
|
|
155
|
+
segment.from_port.segments << segment
|
|
156
|
+
segment.to_port.segments << segment
|
|
157
|
+
@segments << segment
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def port(item, side, order, endpoint)
|
|
161
|
+
declared = declared_port(item, endpoint)
|
|
162
|
+
key = declared ? declared.id : [:edge, order]
|
|
163
|
+
@ports[[item.id, side, key]] ||= Port.new(
|
|
164
|
+
item_id: item.id, side: side, declared: declared,
|
|
165
|
+
sequence: declared ? [0, item.ports.index(declared)] : [1, order]
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def declared_port(item, endpoint)
|
|
170
|
+
return unless endpoint && item.respond_to?(:ports)
|
|
171
|
+
|
|
172
|
+
item.ports&.find { |candidate| candidate.id == endpoint }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def owner(endpoints)
|
|
176
|
+
id = (endpoints || []).first
|
|
177
|
+
@index.owner(id) if id
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_lists
|
|
181
|
+
@ports.values.group_by { |port| [port.item_id, port.side] }
|
|
182
|
+
.transform_values { |ports| ports.sort_by(&:sequence) }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
module Algorithms
|
|
6
|
+
module Layered
|
|
7
|
+
# Where the ports of one side sit along an item's cross axis: spread
|
|
8
|
+
# evenly, as ELK spaces free ports.
|
|
9
|
+
module PortSpread
|
|
10
|
+
# Cross offset of each port's centre from the start of its item.
|
|
11
|
+
#
|
|
12
|
+
# @param layers [Array<Array>] items per layer
|
|
13
|
+
# @param port_order [PortOrder]
|
|
14
|
+
# @param sizes [Hash{String=>Numeric}] cross size per item id
|
|
15
|
+
# @param port_size_of [#call] cross size of a port
|
|
16
|
+
# @return [Hash] offset per port, keyed by identity
|
|
17
|
+
def self.offsets(layers, port_order, sizes, port_size_of)
|
|
18
|
+
offsets = {}.compare_by_identity
|
|
19
|
+
layers.flatten.product(PortOrder::SIDES) do |item, side|
|
|
20
|
+
ports = port_order.visual(item.id, side)
|
|
21
|
+
spread(ports, sizes.fetch(item.id), port_size_of)
|
|
22
|
+
.each { |port, mid| offsets[port] = mid }
|
|
23
|
+
end
|
|
24
|
+
offsets
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.spread(ports, extent, port_size_of)
|
|
28
|
+
widths = ports.map { |port| port_size_of.call(port) }
|
|
29
|
+
ports.zip(centres(widths, extent))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.centres(widths, extent)
|
|
33
|
+
gap = (extent - widths.sum) / (widths.length + 1)
|
|
34
|
+
widths.each_with_index.map do |width, i|
|
|
35
|
+
widths.first(i).sum + (gap * (i + 1)) + (width / 2.0)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|