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
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_algorithm"
|
|
4
|
+
require_relative "../node_index"
|
|
4
5
|
require_relative "layered/cycle_breaker"
|
|
6
|
+
require_relative "layered/components"
|
|
7
|
+
require_relative "layered/crossing_minimizer"
|
|
5
8
|
require_relative "layered/layer_assigner"
|
|
6
9
|
require_relative "layered/node_placer"
|
|
10
|
+
require_relative "layered/orthogonal_router"
|
|
7
11
|
|
|
8
12
|
module Elkrb
|
|
9
13
|
module Layout
|
|
@@ -12,9 +16,13 @@ module Elkrb
|
|
|
12
16
|
#
|
|
13
17
|
# The flagship algorithm for hierarchical graph layout.
|
|
14
18
|
# Implements the Sugiyama framework in phases:
|
|
15
|
-
# 1. Cycle breaking -
|
|
16
|
-
#
|
|
17
|
-
#
|
|
19
|
+
# 1. Cycle breaking - find the back edges, WITHOUT making the graph
|
|
20
|
+
# acyclic. The caller's edges are handed back exactly as written;
|
|
21
|
+
# the reversal is a private orientation the next phase borrows.
|
|
22
|
+
# 2. Layer assignment - assign nodes to horizontal layers, reading
|
|
23
|
+
# each back edge in its reversed direction
|
|
24
|
+
# 3. Crossing minimization - reorder nodes within assigned layers
|
|
25
|
+
# 4. Node placement - position nodes within layers
|
|
18
26
|
#
|
|
19
27
|
# Ideal for:
|
|
20
28
|
# - UML class diagrams
|
|
@@ -22,26 +30,553 @@ module Elkrb
|
|
|
22
30
|
# - Data flow diagrams
|
|
23
31
|
# - Organization charts
|
|
24
32
|
# - Any directed acyclic graph
|
|
33
|
+
#
|
|
34
|
+
# The three bang validators below (raise_hyperedge!,
|
|
35
|
+
# raise_missing_endpoint!, validate_simple_edge!) are argument
|
|
36
|
+
# validators, not the dangerous/safe pair MissingSafeMethod expects --
|
|
37
|
+
# same reasoning as GraphvizWrapper's own exemption, see that file's
|
|
38
|
+
# comment. No nested class here to inherit this and need its own reset.
|
|
39
|
+
# rubocop:disable Layout/LineLength
|
|
40
|
+
# :reek:MissingSafeMethod { exclude: [ raise_hyperedge!, raise_missing_endpoint!, validate_simple_edge! ] }
|
|
41
|
+
# rubocop:enable Layout/LineLength
|
|
25
42
|
class LayeredAlgorithm < BaseAlgorithm
|
|
43
|
+
# BaseAlgorithm intentionally skips layout_flat when a deserialized
|
|
44
|
+
# graph omits `children`. Validate that public entry point here so an
|
|
45
|
+
# unsupported edge cannot silently pass through untouched.
|
|
46
|
+
def layout(graph)
|
|
47
|
+
validate_edges(NodeIndex.build(graph)) unless graph.children
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
26
51
|
def layout_flat(graph, _options = {})
|
|
52
|
+
@routed_layers = nil
|
|
53
|
+
@parts = []
|
|
54
|
+
@level_size = graph.children.to_a.length
|
|
55
|
+
index = NodeIndex.build(graph)
|
|
56
|
+
validate_edges(index)
|
|
27
57
|
return graph if graph.children.nil? || graph.children.empty?
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
@parts = lay_out_components(graph, index)
|
|
60
|
+
@dummy_slots = @parts.flat_map(&:slots)
|
|
61
|
+
@routed_layers = [@parts.flat_map(&:layers), index]
|
|
62
|
+
graph
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# A graph with several connected components is laid out one component
|
|
68
|
+
# at a time and packed afterwards, as ELK does.
|
|
69
|
+
def lay_out_components(graph, index)
|
|
70
|
+
parts = []
|
|
71
|
+
if option("elk.separateConnectedComponents")
|
|
72
|
+
parts = Layered::Components::Splitter.new(graph, index).graphs
|
|
73
|
+
end
|
|
74
|
+
return [lay_out_part(graph, index)] if parts.length < 2
|
|
75
|
+
|
|
76
|
+
laid = parts.map { |part| lay_out_part(part, NodeIndex.build(part)) }
|
|
77
|
+
pack_components(graph, laid)
|
|
78
|
+
laid
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def lay_out_part(graph, index)
|
|
82
|
+
layers = assign_layers(graph, index)
|
|
32
83
|
|
|
33
|
-
# Phase
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
# Phase 3: Minimize crossings within the assigned layers
|
|
85
|
+
layers = minimize_crossings(graph, layers, index)
|
|
86
|
+
@dummy_slots = dummy_slots(layers)
|
|
36
87
|
|
|
37
|
-
# Phase
|
|
38
|
-
|
|
39
|
-
node_placer.place_nodes
|
|
88
|
+
# Phase 4: Place nodes
|
|
89
|
+
place_nodes(graph, layers, index)
|
|
40
90
|
|
|
41
91
|
# Apply padding and set graph dimensions
|
|
92
|
+
apply_padding_with_dummies(graph)
|
|
93
|
+
Layered::Components::Part.new(
|
|
94
|
+
graph, layers, index, @port_order, @dummy_slots
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def pack_components(graph, parts)
|
|
99
|
+
packer = row_packer(parts)
|
|
100
|
+
offsets = packer.offsets
|
|
101
|
+
parts.zip(offsets) { |part, (left, top)| move_part(part, left, top) }
|
|
102
|
+
size_packed(graph, *packer.size(offsets))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def size_packed(graph, width, height)
|
|
106
|
+
graph.width = width + padding[:left] + padding[:right]
|
|
107
|
+
graph.height = height + padding[:top] + padding[:bottom]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def row_packer(parts)
|
|
111
|
+
Layered::Components::RowPacker.new(
|
|
112
|
+
parts.map { |part| content_size(part.graph) },
|
|
113
|
+
spacing: option("elk.spacing.componentComponent").to_f,
|
|
114
|
+
aspect_ratio: option("elk.aspectRatio").to_f,
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def content_size(part_graph)
|
|
119
|
+
[part_graph.width - padding[:left] - padding[:right],
|
|
120
|
+
part_graph.height - padding[:top] - padding[:bottom]]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def move_part(part, shift_x, shift_y)
|
|
124
|
+
[*part.graph.children, *part.slots].each do |item|
|
|
125
|
+
item.x += shift_x
|
|
126
|
+
item.y += shift_y
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def assign_layers(graph, index)
|
|
131
|
+
# Phase 1: Find the back edges
|
|
132
|
+
reversed_edges = Layered::CycleBreaker.new(graph, index).break_cycles
|
|
133
|
+
|
|
134
|
+
# Phase 2: Assign layers and insert long-edge dummy slots
|
|
135
|
+
Layered::LayerAssigner.new(
|
|
136
|
+
graph, index, reversed_edges, resolver: @resolver
|
|
137
|
+
).assign_layers
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def apply_edge_routing(graph)
|
|
141
|
+
super
|
|
142
|
+
routed = route_orthogonal_edges(graph)
|
|
143
|
+
add_long_edge_bends(graph, routed)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Replaces the generic route of every edge that crosses a layer gap
|
|
147
|
+
# with the layer-gap route, when its style is ORTHOGONAL.
|
|
148
|
+
#
|
|
149
|
+
# @return [Array<Graph::Edge>] the edges it routed
|
|
150
|
+
def route_orthogonal_edges(graph)
|
|
151
|
+
return [] unless @routed_layers
|
|
152
|
+
|
|
153
|
+
routes = orthogonal_routes
|
|
154
|
+
graph.edges.to_a.select do |edge|
|
|
155
|
+
route = routes[edge]
|
|
156
|
+
next false unless route && level_edge?(edge)
|
|
157
|
+
next false unless orthogonal_edge?(graph, edge)
|
|
158
|
+
|
|
159
|
+
apply_route(edge, route, graph)
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def orthogonal_routes
|
|
165
|
+
@parts.map { |part| orthogonal_router(part) }
|
|
166
|
+
.map { |router| router.routes(method(:along_range)) }
|
|
167
|
+
.reduce({}, :merge)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A cross-hierarchy edge names a nested node; its route is not this
|
|
171
|
+
# level's to draw.
|
|
172
|
+
def level_edge?(edge)
|
|
173
|
+
index = @routed_layers.last
|
|
174
|
+
[edge.sources.first, edge.targets.first].all? { |id| index.node(id) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def orthogonal_edge?(graph, edge)
|
|
178
|
+
get_edge_routing_style(graph, edge) == "ORTHOGONAL"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def apply_route(edge, route, graph)
|
|
182
|
+
place_junctions(edge, route.junctions)
|
|
183
|
+
section = reset_section(edge, graph)
|
|
184
|
+
start, *bends, finish = route.points
|
|
185
|
+
section.start_point = point_at(start)
|
|
186
|
+
section.end_point = point_at(finish)
|
|
187
|
+
section.bend_points = []
|
|
188
|
+
bends.each { |x, y| section.add_bend_point(x, y) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def place_junctions(edge, junctions)
|
|
192
|
+
edge.junction_points = junctions.map { |point| point_at(point) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def point_at(coordinates)
|
|
196
|
+
Geometry::Point.new(x: coordinates[0], y: coordinates[1])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def orthogonal_router(part)
|
|
200
|
+
Layered::OrthogonalRouter.new(
|
|
201
|
+
part.layers, part.port_order, part.index,
|
|
202
|
+
direction: layer_direction, measure: router_measure
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def router_measure
|
|
207
|
+
Layered::OrthogonalRouter::Measure.new(
|
|
208
|
+
start: method(:cross_start), extent: method(:cross_extent),
|
|
209
|
+
port: method(:port_cross_extent)
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def layer_direction
|
|
214
|
+
direction = option("elk.direction")
|
|
215
|
+
direction == "UNDEFINED" ? "RIGHT" : direction
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def vertical_layers?
|
|
219
|
+
%w[DOWN UP].include?(layer_direction)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def along_range(item)
|
|
223
|
+
start, size = along_of(item)
|
|
224
|
+
start ||= 0.0
|
|
225
|
+
[start, start + (size || 0.0)]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def along_of(item)
|
|
229
|
+
vertical_layers? ? [item.y, item.height] : [item.x, item.width]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The generic router bends midway between the two nodes. Nodes lean
|
|
233
|
+
# toward one side of their layer, so a bend at a node's own edge can
|
|
234
|
+
# land inside a layer neighbour; bend midway between the layers.
|
|
235
|
+
def horizontal_gap(source, target, *)
|
|
236
|
+
layer_gap(source, target, :x, :width) || super
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def vertical_gap(source, target, *)
|
|
240
|
+
layer_gap(source, target, :y, :height) || super
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def layer_gap(source, target, coordinate, size)
|
|
244
|
+
return unless @routed_layers && vertical_layers? == (coordinate == :y)
|
|
245
|
+
|
|
246
|
+
bounds = layer_bounds
|
|
247
|
+
held = [source, target].map do |rectangle|
|
|
248
|
+
layer_holding(bounds, rectangle, coordinate, size)
|
|
249
|
+
end
|
|
250
|
+
midway(bounds, *held)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def midway(bounds, from, to)
|
|
254
|
+
return unless from && to && (from - to).abs == 1
|
|
255
|
+
|
|
256
|
+
earlier, later = [from, to].minmax
|
|
257
|
+
(bounds[earlier].last + bounds[later].first) / 2.0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def layer_bounds
|
|
261
|
+
@routed_layers.first.map do |items|
|
|
262
|
+
ranges = items.map { |item| along_range(item) }
|
|
263
|
+
[ranges.map(&:first).min, ranges.map(&:last).max]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def layer_holding(bounds, rectangle, coordinate, size)
|
|
268
|
+
low = rectangle.public_send(coordinate)
|
|
269
|
+
high = low + rectangle.public_send(size)
|
|
270
|
+
bounds.index do |first, last|
|
|
271
|
+
first && low >= first - 1e-6 && high <= last + 1e-6
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def cross_extent(item)
|
|
276
|
+
thickness = Layered::NodePlacer::EDGE_THICKNESS
|
|
277
|
+
return thickness if item.respond_to?(:dummy?)
|
|
278
|
+
|
|
279
|
+
(vertical_layers? ? item.width : item.height) || 0
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def cross_start(item)
|
|
283
|
+
start = (vertical_layers? ? item.x : item.y) || 0.0
|
|
284
|
+
return start unless item.respond_to?(:dummy?)
|
|
285
|
+
|
|
286
|
+
spare = cross_slot_size(item) - Layered::NodePlacer::EDGE_THICKNESS
|
|
287
|
+
start + (spare / 2.0)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def cross_slot_size(slot)
|
|
291
|
+
vertical_layers? ? slot.width : slot.height
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def port_cross_extent(port)
|
|
295
|
+
return 0 unless port.declared
|
|
296
|
+
|
|
297
|
+
(vertical_layers? ? port.declared.width : port.declared.height) || 0
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def apply_padding_with_dummies(graph)
|
|
301
|
+
shift_x, shift_y = padding_shift(graph)
|
|
302
|
+
shift_dummy_slots(shift_x, shift_y)
|
|
303
|
+
include_long_edges(graph)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# A long edge's lane can run beyond the nodes; the padding is
|
|
307
|
+
# measured from the lane, not from the nearest node.
|
|
308
|
+
def include_long_edges(graph)
|
|
309
|
+
return if @dummy_slots.empty?
|
|
310
|
+
|
|
311
|
+
vertical = %w[DOWN UP].include?(option("elk.direction"))
|
|
312
|
+
before, after = lane_overhang(graph, vertical)
|
|
313
|
+
.map { |amount| [amount, 0].max }
|
|
314
|
+
shift_cross(graph, vertical, before)
|
|
315
|
+
grow_cross(graph, vertical, before + after)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# How far the lanes reach past the padded content on each end of
|
|
319
|
+
# the cross axis; negative when they stay inside.
|
|
320
|
+
def lane_overhang(graph, vertical)
|
|
321
|
+
low, high = @dummy_slots.map { |slot| lane_centre(slot, vertical) }
|
|
322
|
+
.minmax
|
|
323
|
+
reach = Layered::NodePlacer::EDGE_THICKNESS / 2.0
|
|
324
|
+
start, stop = content_range(graph, vertical)
|
|
325
|
+
[start - (low - reach), high + reach - stop]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def content_range(graph, vertical)
|
|
329
|
+
lead, trail = padding.values_at(*cross_sides(vertical))
|
|
330
|
+
[lead, (vertical ? graph.width : graph.height) - trail]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def cross_sides(vertical)
|
|
334
|
+
vertical ? %i[left right] : %i[top bottom]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def lane_centre(slot, vertical)
|
|
338
|
+
vertical ? slot.x + (slot.width / 2.0) : slot.y + (slot.height / 2.0)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def shift_cross(graph, vertical, amount)
|
|
342
|
+
items = [*graph.children, *@dummy_slots]
|
|
343
|
+
items.each do |item|
|
|
344
|
+
if vertical
|
|
345
|
+
item.x += amount
|
|
346
|
+
else
|
|
347
|
+
item.y += amount
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def grow_cross(graph, vertical, amount)
|
|
353
|
+
return unless amount.positive?
|
|
354
|
+
|
|
355
|
+
if vertical
|
|
356
|
+
graph.width += amount
|
|
357
|
+
else
|
|
358
|
+
graph.height += amount
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def padding_shift(graph)
|
|
363
|
+
first = graph.children.first
|
|
364
|
+
before = first && [first.x, first.y]
|
|
42
365
|
apply_padding(graph)
|
|
366
|
+
return [0.0, 0.0] unless before
|
|
43
367
|
|
|
44
|
-
|
|
368
|
+
[first.x - before[0], first.y - before[1]]
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def shift_dummy_slots(shift_x, shift_y)
|
|
372
|
+
@dummy_slots.each do |slot|
|
|
373
|
+
slot.x += shift_x
|
|
374
|
+
slot.y += shift_y
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def dummy_slots(layers)
|
|
379
|
+
layers.flatten.select { |item| item.respond_to?(:dummy?) }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def add_long_edge_bends(graph, routed)
|
|
383
|
+
slots_by_edge = {}.compare_by_identity
|
|
384
|
+
@dummy_slots.to_a.each do |slot|
|
|
385
|
+
(slots_by_edge[slot.edge] ||= []) << slot
|
|
386
|
+
end
|
|
387
|
+
graph.edges.to_a.each do |edge|
|
|
388
|
+
next if routed.any? { |done| done.equal?(edge) }
|
|
389
|
+
|
|
390
|
+
add_edge_dummy_bends(edge, slots_by_edge[edge], graph)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def add_edge_dummy_bends(edge, slots, graph)
|
|
395
|
+
return if slots.nil? || slots.empty? || edge.sections.to_a.empty?
|
|
396
|
+
|
|
397
|
+
section = edge.sections.first
|
|
398
|
+
section.bend_points = ordered_dummy_anchors(
|
|
399
|
+
section.start_point, slots, graph
|
|
400
|
+
)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# A lone dummy is entered and left at its layer's edges; a single
|
|
404
|
+
# bend at its centre would cut across the nodes beside it.
|
|
405
|
+
def ordered_dummy_anchors(start_point, slots, graph)
|
|
406
|
+
return lone_dummy_anchors(start_point, slots.first, graph) if
|
|
407
|
+
slots.length == 1
|
|
408
|
+
|
|
409
|
+
ordered = slots.sort_by { |slot| squared_distance(start_point, slot) }
|
|
410
|
+
ordered.each_with_index.map do |slot, index|
|
|
411
|
+
dummy_anchor(slot, graph, anchor_for(index, ordered.length))
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# A reversed edge runs against the layer order, so the anchor nearer
|
|
416
|
+
# its start comes first.
|
|
417
|
+
def lone_dummy_anchors(start_point, slot, graph)
|
|
418
|
+
points = %i[leading trailing].map do |anchor|
|
|
419
|
+
dummy_anchor(slot, graph, anchor)
|
|
420
|
+
end
|
|
421
|
+
points.sort_by do |point|
|
|
422
|
+
((point.x - start_point.x)**2) + ((point.y - start_point.y)**2)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def anchor_for(index, length)
|
|
427
|
+
return :leading if index.zero?
|
|
428
|
+
return :trailing if index == length - 1
|
|
429
|
+
|
|
430
|
+
:center
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def squared_distance(point, slot)
|
|
434
|
+
((slot.x + (slot.width / 2.0) - point.x)**2) +
|
|
435
|
+
((slot.y + (slot.height / 2.0) - point.y)**2)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def dummy_anchor(slot, graph, anchor)
|
|
439
|
+
direction = resolver.get("elk.direction", graph)
|
|
440
|
+
return dummy_center(slot) if anchor == :center
|
|
441
|
+
return vertical_dummy_anchor(slot, direction, anchor) if
|
|
442
|
+
%w[DOWN UP].include?(direction)
|
|
443
|
+
|
|
444
|
+
Geometry::Point.new(
|
|
445
|
+
x: horizontal_anchor(slot, direction, anchor),
|
|
446
|
+
y: slot.y + (slot.height / 2.0),
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def vertical_dummy_anchor(slot, direction, anchor)
|
|
451
|
+
Geometry::Point.new(
|
|
452
|
+
x: slot.x + (slot.width / 2.0),
|
|
453
|
+
y: vertical_anchor(slot, direction, anchor),
|
|
454
|
+
)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def dummy_center(slot)
|
|
458
|
+
Geometry::Point.new(
|
|
459
|
+
x: slot.x + (slot.width / 2.0),
|
|
460
|
+
y: slot.y + (slot.height / 2.0),
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def horizontal_anchor(slot, direction, anchor)
|
|
465
|
+
leading, trailing = if direction == "LEFT"
|
|
466
|
+
[slot.x + slot.width, slot.x]
|
|
467
|
+
else
|
|
468
|
+
[slot.x, slot.x + slot.width]
|
|
469
|
+
end
|
|
470
|
+
anchor == :leading ? leading : trailing
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def vertical_anchor(slot, direction, anchor)
|
|
474
|
+
leading, trailing = if direction == "UP"
|
|
475
|
+
[slot.y + slot.height, slot.y]
|
|
476
|
+
else
|
|
477
|
+
[slot.y, slot.y + slot.height]
|
|
478
|
+
end
|
|
479
|
+
anchor == :leading ? leading : trailing
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def minimize_crossings(graph, layers, index)
|
|
483
|
+
minimizer = Layered::Components::Minimizer.new(
|
|
484
|
+
graph, layers, index, resolver, @level_size
|
|
485
|
+
)
|
|
486
|
+
minimizer.minimize.tap { @port_order = minimizer.port_order }
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def place_nodes(graph, layers, index)
|
|
490
|
+
direction = option("elk.direction")
|
|
491
|
+
direction = "RIGHT" if direction == "UNDEFINED"
|
|
492
|
+
|
|
493
|
+
placer = Layered::NodePlacer.new(
|
|
494
|
+
graph, layers,
|
|
495
|
+
direction: direction,
|
|
496
|
+
layer_spacing: option("elk.layered.spacing.nodeNodeBetweenLayers"),
|
|
497
|
+
node_spacing: node_spacing
|
|
498
|
+
)
|
|
499
|
+
placer.index = index
|
|
500
|
+
placer.port_order = @port_order
|
|
501
|
+
placer.orthogonal = get_edge_routing_style(graph) == "ORTHOGONAL"
|
|
502
|
+
placer.place_nodes
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def validate_edges(index)
|
|
506
|
+
index.edges.each do |edge|
|
|
507
|
+
validate_simple_edge!(edge)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# An id is optional on an edge. An edge without one is ANONYMOUS: it
|
|
512
|
+
# carries no handle, so `edge_label` falls back to its endpoints.
|
|
513
|
+
# `""` counts as no id, and this is the ONE place that decides it.
|
|
514
|
+
def anonymous?(edge)
|
|
515
|
+
edge.id.to_s.empty?
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def validate_simple_edge!(edge)
|
|
519
|
+
sources = edge.sources || []
|
|
520
|
+
targets = edge.targets || []
|
|
521
|
+
|
|
522
|
+
raise_missing_endpoint!(edge) if missing_endpoint?(sources, targets)
|
|
523
|
+
|
|
524
|
+
return if sources.length == 1 && targets.length == 1
|
|
525
|
+
|
|
526
|
+
raise_hyperedge!(edge)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def missing_endpoint?(sources, targets)
|
|
530
|
+
[sources, targets].any? do |endpoints|
|
|
531
|
+
endpoints.empty? || !endpoint_present?(endpoints.first)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def endpoint_present?(endpoint)
|
|
536
|
+
!endpoint.nil?
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# An edge id is optional in ELK, so every message below could read
|
|
540
|
+
# "(edge )" with nothing after it. The endpoints are the fallback
|
|
541
|
+
# handle -- not because an edge always has them (the very next
|
|
542
|
+
# method is raised when it does not, and `endpoint_list` answers
|
|
543
|
+
# "(no endpoints)" for that case), but because they are the only
|
|
544
|
+
# other thing a reader can use to find the edge in their input.
|
|
545
|
+
# Keep the three messages using one helper: fixing only some of
|
|
546
|
+
# them makes the rest look deliberate.
|
|
547
|
+
def edge_label(edge)
|
|
548
|
+
# `""` is truthy in Ruby, so a plain `if edge.id` here puts the
|
|
549
|
+
# empty message straight back. `anonymous?` is the single
|
|
550
|
+
# definition of "no id".
|
|
551
|
+
return edge.id unless anonymous?(edge)
|
|
552
|
+
|
|
553
|
+
"(none), #{endpoint_list(edge.sources)} -> " \
|
|
554
|
+
"#{endpoint_list(edge.targets)}"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def endpoint_list(endpoints)
|
|
558
|
+
named = (endpoints || []).compact
|
|
559
|
+
return "(no endpoints)" if named.empty?
|
|
560
|
+
|
|
561
|
+
named.map(&:inspect).join(", ")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def raise_missing_endpoint!(edge)
|
|
565
|
+
raise Elkrb::UnsupportedConfigurationException.new(
|
|
566
|
+
"layered requires non-empty edge endpoints " \
|
|
567
|
+
"(edge #{edge_label(edge)})",
|
|
568
|
+
option: "edge",
|
|
569
|
+
value: edge.id,
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def raise_hyperedge!(edge)
|
|
574
|
+
raise Elkrb::UnsupportedConfigurationException.new(
|
|
575
|
+
"layered does not support hyperedges " \
|
|
576
|
+
"(edge #{edge_label(edge)})",
|
|
577
|
+
option: "edge",
|
|
578
|
+
value: edge.id,
|
|
579
|
+
)
|
|
45
580
|
end
|
|
46
581
|
end
|
|
47
582
|
end
|