elkrb 1.0.2 → 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,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_algorithm"
|
|
4
|
+
require_relative "../node_index"
|
|
4
5
|
|
|
5
6
|
module Elkrb
|
|
6
7
|
module Layout
|
|
@@ -10,53 +11,211 @@ module Elkrb
|
|
|
10
11
|
# Arranges nodes in a circular/radial pattern around a center point.
|
|
11
12
|
class Radial < BaseAlgorithm
|
|
12
13
|
def layout_flat(graph, _options = {})
|
|
13
|
-
return graph if graph.children.empty?
|
|
14
|
+
return graph if graph.children.nil? || graph.children.empty?
|
|
14
15
|
|
|
15
16
|
if graph.children.size == 1
|
|
16
|
-
|
|
17
|
-
graph.children.first.x = 0.0
|
|
18
|
-
graph.children.first.y = 0.0
|
|
17
|
+
centre(graph.children.first)
|
|
19
18
|
apply_padding(graph)
|
|
20
19
|
return graph
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
rings = rings(graph)
|
|
23
|
+
centre(rings.fetch(0).first)
|
|
24
|
+
place_rings(rings)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
apply_padding(graph)
|
|
27
|
+
|
|
28
|
+
graph
|
|
29
|
+
end
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def rings(graph)
|
|
34
|
+
index = NodeIndex.build(graph)
|
|
35
|
+
adjacent, incoming = edge_tree(graph, index)
|
|
36
|
+
root = radial_root(graph, incoming)
|
|
37
|
+
depths = breadth_first_depths(root, adjacent)
|
|
38
|
+
assign_unreached(graph.children, depths)
|
|
39
|
+
|
|
40
|
+
graph.children.group_by { |node| depths.fetch(node.id) }
|
|
41
|
+
end
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
def edge_tree(graph, index)
|
|
44
|
+
adjacent = Hash.new { |hash, key| hash[key] = [] }
|
|
45
|
+
incoming = Hash.new(0)
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
(graph.edges || []).each do |edge|
|
|
48
|
+
edge_connections(edge, index).each do |source, target|
|
|
49
|
+
adjacent[source.id] << target
|
|
50
|
+
incoming[target.id] += 1
|
|
51
|
+
end
|
|
40
52
|
end
|
|
53
|
+
adjacent.each_value { |nodes| nodes.uniq!(&:id) }
|
|
54
|
+
[adjacent, incoming]
|
|
55
|
+
end
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
def edge_connections(edge, index)
|
|
58
|
+
sources = index.endpoint_nodes(edge.sources)
|
|
59
|
+
targets = index.endpoint_nodes(edge.targets)
|
|
60
|
+
sources.product(targets).reject do |source, target|
|
|
61
|
+
source.id == target.id
|
|
62
|
+
end
|
|
63
|
+
end
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
def radial_root(graph, incoming)
|
|
66
|
+
return graph.children.first if option(
|
|
67
|
+
"elk.radial.centerOnRoot", default: false
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
graph.children.find { |node| incoming[node.id].zero? } ||
|
|
71
|
+
graph.children.first
|
|
45
72
|
end
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
def breadth_first_depths(root, adjacent)
|
|
75
|
+
depths = { root.id => 0 }
|
|
76
|
+
queue = [root]
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
avg_size = (avg_width + avg_height) / 2.0
|
|
78
|
+
until queue.empty?
|
|
79
|
+
node = queue.shift
|
|
80
|
+
adjacent[node.id].each do |child|
|
|
81
|
+
next if depths.key?(child.id)
|
|
54
82
|
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
depths[child.id] = depths.fetch(node.id) + 1
|
|
84
|
+
queue << child
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
depths
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def assign_unreached(nodes, depths)
|
|
91
|
+
outer = depths.values.max || 0
|
|
92
|
+
nodes.each do |node|
|
|
93
|
+
next if depths.key?(node.id)
|
|
94
|
+
|
|
95
|
+
depths[node.id] = outer + 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def centre(node)
|
|
100
|
+
node.x = -(node.width || 0.0) / 2.0
|
|
101
|
+
node.y = -(node.height || 0.0) / 2.0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def place_rings(rings)
|
|
105
|
+
placed = rings.fetch(0).dup
|
|
106
|
+
base = option(
|
|
107
|
+
"elk.radial.radius", default: automatic_radius(rings)
|
|
108
|
+
).to_f
|
|
109
|
+
|
|
110
|
+
rings.keys.sort.drop(1).each do |depth|
|
|
111
|
+
nodes = rings.fetch(depth)
|
|
112
|
+
radius = base * depth
|
|
113
|
+
radius = grow_until_separate(nodes, placed, radius)
|
|
114
|
+
position_ring(nodes, radius)
|
|
115
|
+
placed.concat(nodes)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def automatic_radius(rings)
|
|
120
|
+
extent = rings.values.flatten.map do |node|
|
|
121
|
+
[node.width || 0.0, node.height || 0.0].max
|
|
122
|
+
end.max || 0.0
|
|
123
|
+
extent * Math.sqrt(2.0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def grow_until_separate(nodes, placed, radius)
|
|
127
|
+
loop do
|
|
128
|
+
position_ring(nodes, radius)
|
|
129
|
+
return radius unless overlaps?(nodes, placed)
|
|
130
|
+
|
|
131
|
+
radius += [node_spacing, 1.0].max
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def position_ring(nodes, radius)
|
|
136
|
+
step = (2 * Math::PI) / nodes.size
|
|
137
|
+
offset = step / 2.0
|
|
138
|
+
nodes.each_with_index do |node, index|
|
|
139
|
+
angle = (index * step) + offset
|
|
140
|
+
node.x = (radius * Math.cos(angle)) - ((node.width || 0.0) / 2.0)
|
|
141
|
+
node.y = (radius * Math.sin(angle)) - ((node.height || 0.0) / 2.0)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def overlaps?(ring, placed)
|
|
146
|
+
candidates = ring.combination(2).to_a
|
|
147
|
+
candidates.concat(ring.product(placed))
|
|
148
|
+
candidates.any? { |left, right| overlap?(left, right) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def overlap?(left, right)
|
|
152
|
+
horizontal_overlap?(left, right) && vertical_overlap?(left, right)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def horizontal_overlap?(left, right)
|
|
156
|
+
left.x < right.x + node_width(right) &&
|
|
157
|
+
right.x < left.x + node_width(left)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def vertical_overlap?(left, right)
|
|
161
|
+
left.y < right.y + node_height(right) &&
|
|
162
|
+
right.y < left.y + node_height(left)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The shared router starts node-to-node edges at node centres. Radial
|
|
166
|
+
# edges are straight rays, so clip each end to its node rectangle.
|
|
167
|
+
def apply_edge_routing(graph)
|
|
168
|
+
super
|
|
169
|
+
index = NodeIndex.build(graph)
|
|
170
|
+
|
|
171
|
+
(graph.edges || []).each do |edge|
|
|
172
|
+
source = index.endpoint_nodes(edge.sources).first
|
|
173
|
+
target = index.endpoint_nodes(edge.targets).first
|
|
174
|
+
next unless source && target && source.id != target.id
|
|
175
|
+
|
|
176
|
+
section = reset_section(edge, graph)
|
|
177
|
+
section.start_point = radial_endpoint_position(
|
|
178
|
+
edge.sources.first, source, target, :outgoing
|
|
179
|
+
)
|
|
180
|
+
section.end_point = radial_endpoint_position(
|
|
181
|
+
edge.targets.first, target, source, :incoming
|
|
182
|
+
)
|
|
183
|
+
section.bend_points = []
|
|
184
|
+
edge.container = graph.id
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def radial_endpoint_position(endpoint_id, node, other, direction)
|
|
189
|
+
return border_towards(node, other) unless
|
|
190
|
+
find_port_by_id(endpoint_id, node)
|
|
191
|
+
|
|
192
|
+
get_port_position(endpoint_id, node, direction)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def border_towards(node, other)
|
|
196
|
+
center_x = node.x + (node_width(node) / 2.0)
|
|
197
|
+
center_y = node.y + (node_height(node) / 2.0)
|
|
198
|
+
other_x = other.x + (node_width(other) / 2.0)
|
|
199
|
+
other_y = other.y + (node_height(other) / 2.0)
|
|
200
|
+
delta_x = other_x - center_x
|
|
201
|
+
delta_y = other_y - center_y
|
|
202
|
+
scales = []
|
|
203
|
+
scales << (node_width(node) / 2.0 / delta_x.abs) unless delta_x.zero?
|
|
204
|
+
scales << (node_height(node) / 2.0 / delta_y.abs) unless delta_y.zero?
|
|
205
|
+
scale = scales.min || 0.0
|
|
206
|
+
|
|
207
|
+
Geometry::Point.new(
|
|
208
|
+
x: center_x + (delta_x * scale),
|
|
209
|
+
y: center_y + (delta_y * scale),
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def node_width(node)
|
|
214
|
+
node.width || 0.0
|
|
215
|
+
end
|
|
57
216
|
|
|
58
|
-
|
|
59
|
-
|
|
217
|
+
def node_height(node)
|
|
218
|
+
node.height || 0.0
|
|
60
219
|
end
|
|
61
220
|
end
|
|
62
221
|
end
|
|
@@ -1,41 +1,155 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_algorithm"
|
|
4
|
+
require_relative "../java_random"
|
|
5
|
+
require_relative "../node_index"
|
|
4
6
|
|
|
5
7
|
module Elkrb
|
|
6
8
|
module Layout
|
|
7
9
|
module Algorithms
|
|
8
10
|
# Random layout algorithm
|
|
9
11
|
#
|
|
10
|
-
# Places nodes at random positions within a bounded area
|
|
11
|
-
#
|
|
12
|
+
# Places nodes at random positions within a bounded area, following
|
|
13
|
+
# Java ELK's RandomLayoutProvider draw for draw: for one seed the node
|
|
14
|
+
# positions and edge bend points are the ones Java ELK produces. That
|
|
15
|
+
# includes its quirks, which are kept on purpose:
|
|
16
|
+
# - an edge's start and end are both placed on its SOURCE node
|
|
17
|
+
# (right side, bottom side), never on the target;
|
|
18
|
+
# - the graph size adds the padding twice.
|
|
12
19
|
class Random < BaseAlgorithm
|
|
20
|
+
# Java ELK's own defaults for this algorithm; the registry's are the
|
|
21
|
+
# general ones.
|
|
22
|
+
DEFAULT_SPACING = 15.0
|
|
23
|
+
DEFAULT_PADDING = { left: 15.0, right: 15.0, top: 15.0,
|
|
24
|
+
bottom: 15.0 }.freeze
|
|
25
|
+
# Bend points scatter by this share of the start-to-end distance.
|
|
26
|
+
# The value is the float32 nearest 0.2, as in Java ELK.
|
|
27
|
+
BEND_SCATTER = 0.20000000298023224
|
|
28
|
+
|
|
13
29
|
def layout_flat(graph, _options = {})
|
|
14
30
|
return graph if graph.children.nil? || graph.children.empty?
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
@random = JavaRandom.new(option("elk.randomSeed").to_i)
|
|
33
|
+
@pad = random_padding
|
|
34
|
+
width, height = area_size(graph)
|
|
35
|
+
|
|
36
|
+
graph.children.each do |node|
|
|
37
|
+
node.x = @pad[:left] + spread(width - node_width(node))
|
|
38
|
+
node.y = @pad[:top] + spread(height - node_height(node))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@bounds = [width + @pad[:left] + @pad[:right],
|
|
42
|
+
height + @pad[:top] + @pad[:bottom]]
|
|
43
|
+
graph.width = @bounds[0] + @pad[:left] + @pad[:right]
|
|
44
|
+
graph.height = @bounds[1] + @pad[:top] + @pad[:bottom]
|
|
45
|
+
graph
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def spread(room)
|
|
51
|
+
@random.next_double * room
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def random_padding
|
|
55
|
+
given = option("elk.padding", default: nil)
|
|
56
|
+
given ? given.to_h : DEFAULT_PADDING
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Java ELK sizes the area from the node areas plus a spacing term per
|
|
60
|
+
# node and edge, then stretches it to the aspect ratio.
|
|
61
|
+
def area_size(graph)
|
|
62
|
+
spacing = option("elk.spacing.nodeNode",
|
|
63
|
+
default: DEFAULT_SPACING).to_f
|
|
64
|
+
aspect = option("elk.aspectRatio").to_f
|
|
65
|
+
index = NodeIndex.build(graph)
|
|
66
|
+
node_area = graph.children.sum { |n| node_width(n) * node_height(n) }
|
|
67
|
+
edge_count = 1 + edges_by_source(graph, index).values.sum(&:size)
|
|
68
|
+
side = Math.sqrt(node_area + (2 * spacing * spacing * edge_count *
|
|
69
|
+
graph.children.size))
|
|
70
|
+
[[side * aspect, graph.children.map { |n| node_width(n) }.max].max,
|
|
71
|
+
[side / aspect, graph.children.map { |n| node_height(n) }.max].max]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def node_width(node)
|
|
75
|
+
node.width || 0.0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def node_height(node)
|
|
79
|
+
node.height || 0.0
|
|
80
|
+
end
|
|
19
81
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
82
|
+
# Edges whose source is a node of this level, keyed by that node.
|
|
83
|
+
def edges_by_source(graph, index)
|
|
84
|
+
(graph.edges || []).each_with_object({}) do |edge, grouped|
|
|
85
|
+
source = index.endpoint_nodes(edge.sources).first
|
|
86
|
+
(grouped[source.id] ||= []) << edge if source
|
|
23
87
|
end
|
|
88
|
+
end
|
|
24
89
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
90
|
+
# Edges are scattered after the nodes, from the same random stream,
|
|
91
|
+
# node by node.
|
|
92
|
+
def apply_edge_routing(graph)
|
|
93
|
+
return unless @random
|
|
28
94
|
|
|
29
|
-
|
|
95
|
+
index = NodeIndex.build(graph)
|
|
96
|
+
grouped = edges_by_source(graph, index)
|
|
30
97
|
graph.children.each do |node|
|
|
31
|
-
node.
|
|
32
|
-
|
|
98
|
+
(grouped[node.id] || []).each do |edge|
|
|
99
|
+
edge.container ||= graph.id
|
|
100
|
+
next unless same_level?(edge, index)
|
|
101
|
+
|
|
102
|
+
scatter_edge(edge, node)
|
|
103
|
+
end
|
|
33
104
|
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def same_level?(edge, index)
|
|
108
|
+
ids = Array(edge.sources) + Array(edge.targets)
|
|
109
|
+
ids.all? { |id| index.node(id) }
|
|
110
|
+
end
|
|
34
111
|
|
|
35
|
-
|
|
36
|
-
|
|
112
|
+
def scatter_edge(edge, node)
|
|
113
|
+
half_w = node_width(node) / 2.0
|
|
114
|
+
half_h = node_height(node) / 2.0
|
|
115
|
+
center_x = node.x + half_w
|
|
116
|
+
center_y = node.y + half_h
|
|
117
|
+
inside = half_w.positive? && half_h.positive?
|
|
118
|
+
start = [inside ? center_x + half_w : center_x, center_y]
|
|
119
|
+
finish = [center_x, inside ? center_y + half_h : center_y]
|
|
37
120
|
|
|
38
|
-
|
|
121
|
+
section = Graph::EdgeSection.new(id: "#{edge.id}_s0")
|
|
122
|
+
section.start_point = point(*start)
|
|
123
|
+
section.end_point = point(*finish)
|
|
124
|
+
section.bend_points = bend_points(start, finish)
|
|
125
|
+
edge.sections = [section]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Java ELK draws the bend count from the source and target node
|
|
129
|
+
# being the same node, so there is always one more than nextInt(5).
|
|
130
|
+
def bend_points(start, finish)
|
|
131
|
+
count = @random.next_int(5) + 1
|
|
132
|
+
dx = finish[0] - start[0]
|
|
133
|
+
dy = finish[1] - start[1]
|
|
134
|
+
scatter = Math.sqrt((dx * dx) + (dy * dy)) * BEND_SCATTER
|
|
135
|
+
x, y = start
|
|
136
|
+
Array.new(count) do
|
|
137
|
+
x += dx / (count + 1)
|
|
138
|
+
y += dy / (count + 1)
|
|
139
|
+
point(scattered(x, scatter, @bounds[0]),
|
|
140
|
+
scattered(y, scatter, @bounds[1]))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def scattered(coordinate, scatter, limit)
|
|
145
|
+
value = coordinate + (@random.next_float * scatter) - (scatter / 2)
|
|
146
|
+
return 1.0 if value.negative?
|
|
147
|
+
|
|
148
|
+
value > limit ? limit - 1.0 : value
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def point(horizontal, vertical)
|
|
152
|
+
Geometry::Point.new(x: horizontal, y: vertical)
|
|
39
153
|
end
|
|
40
154
|
end
|
|
41
155
|
end
|
|
@@ -20,7 +20,6 @@ module Elkrb
|
|
|
20
20
|
return graph
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Pack rectangles using shelf algorithm
|
|
24
23
|
pack_rectangles(graph.children)
|
|
25
24
|
|
|
26
25
|
apply_padding(graph)
|
|
@@ -33,48 +32,31 @@ module Elkrb
|
|
|
33
32
|
def pack_rectangles(nodes)
|
|
34
33
|
return if nodes.empty?
|
|
35
34
|
|
|
36
|
-
spacing =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sorted_nodes = nodes.sort_by { |n| -n.height }
|
|
40
|
-
|
|
41
|
-
# Initialize first shelf
|
|
42
|
-
shelves = []
|
|
43
|
-
current_shelf = {
|
|
44
|
-
y: 0.0,
|
|
45
|
-
height: 0.0,
|
|
46
|
-
width: 0.0,
|
|
47
|
-
nodes: [],
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
sorted_nodes.each do |node|
|
|
51
|
-
# Try to fit on current shelf
|
|
52
|
-
unless can_fit_on_shelf?(node, current_shelf, spacing)
|
|
53
|
-
# Start a new shelf
|
|
54
|
-
shelves << current_shelf unless current_shelf[:nodes].empty?
|
|
55
|
-
|
|
56
|
-
current_shelf = {
|
|
57
|
-
y: shelves.empty? ? 0.0 : shelves.last[:y] + shelves.last[:height] + spacing,
|
|
58
|
-
height: node.height,
|
|
59
|
-
width: 0.0,
|
|
60
|
-
nodes: [],
|
|
61
|
-
}
|
|
35
|
+
spacing = option("elk.spacing.nodeNode", default: 15.0).to_f
|
|
36
|
+
budget = packing_width(nodes, spacing)
|
|
37
|
+
shelf = new_shelf(0.0)
|
|
62
38
|
|
|
39
|
+
nodes.each do |node|
|
|
40
|
+
if shelf[:width].positive? &&
|
|
41
|
+
(shelf[:width] + (node.width || 0.0)) > budget
|
|
42
|
+
shelf = new_shelf(
|
|
43
|
+
shelf[:y] + shelf[:height] + spacing,
|
|
44
|
+
)
|
|
63
45
|
end
|
|
64
|
-
place_on_shelf(node,
|
|
46
|
+
place_on_shelf(node, shelf, spacing)
|
|
65
47
|
end
|
|
66
|
-
|
|
67
|
-
# Add the last shelf
|
|
68
|
-
shelves << current_shelf unless current_shelf[:nodes].empty?
|
|
69
48
|
end
|
|
70
49
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
def packing_width(nodes, spacing)
|
|
51
|
+
area = nodes.sum do |node|
|
|
52
|
+
((node.width || 0.0) + (2 * spacing)) *
|
|
53
|
+
((node.height || 0.0) + (2 * spacing))
|
|
54
|
+
end
|
|
55
|
+
Math.sqrt(area * option("elk.aspectRatio").to_f)
|
|
56
|
+
end
|
|
74
57
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
shelf[:height] >= node.height * 0.8
|
|
58
|
+
def new_shelf(y_position)
|
|
59
|
+
{ y: y_position, height: 0.0, width: 0.0 }
|
|
78
60
|
end
|
|
79
61
|
|
|
80
62
|
def place_on_shelf(node, shelf, spacing)
|
|
@@ -83,9 +65,15 @@ module Elkrb
|
|
|
83
65
|
node.y = shelf[:y]
|
|
84
66
|
|
|
85
67
|
# Update shelf dimensions
|
|
86
|
-
shelf[:
|
|
87
|
-
shelf[:
|
|
88
|
-
|
|
68
|
+
shelf[:width] += (node.width || 0.0) + spacing
|
|
69
|
+
shelf[:height] = [shelf[:height], node.height || 0.0].max
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def padding
|
|
73
|
+
option(
|
|
74
|
+
"elk.padding",
|
|
75
|
+
default: { left: 15.0, top: 15.0, right: 15.0, bottom: 15.0 },
|
|
76
|
+
).to_h
|
|
89
77
|
end
|
|
90
78
|
end
|
|
91
79
|
end
|