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
@@ -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
- # Single node - place at origin
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
- # Calculate radius based on node count and sizes
24
- radius = calculate_radius(graph.children)
22
+ rings = rings(graph)
23
+ centre(rings.fetch(0).first)
24
+ place_rings(rings)
25
25
 
26
- # Calculate center point
27
- center_x = radius
28
- center_y = radius
26
+ apply_padding(graph)
27
+
28
+ graph
29
+ end
29
30
 
30
- # Arrange nodes in a circle
31
- angle_step = (2 * Math::PI) / graph.children.size
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
- graph.children.each_with_index do |node, index|
34
- angle = index * angle_step
43
+ def edge_tree(graph, index)
44
+ adjacent = Hash.new { |hash, key| hash[key] = [] }
45
+ incoming = Hash.new(0)
35
46
 
36
- # Calculate position on circle
37
- # Adjust for node size to center the node
38
- node.x = center_x + (radius * Math.cos(angle)) - (node.width / 2.0)
39
- node.y = center_y + (radius * Math.sin(angle)) - (node.height / 2.0)
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
- apply_padding(graph)
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
- graph
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
- private
74
+ def breadth_first_depths(root, adjacent)
75
+ depths = { root.id => 0 }
76
+ queue = [root]
48
77
 
49
- def calculate_radius(nodes)
50
- # Base radius on number of nodes and their average size
51
- avg_width = nodes.sum(&:width) / nodes.size.to_f
52
- avg_height = nodes.sum(&:height) / nodes.size.to_f
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
- # Ensure enough space for all nodes
56
- min_radius = (nodes.size * avg_size) / (2 * Math::PI)
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
- # Add some extra spacing
59
- [min_radius * 1.2, 100.0].max
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
- # Useful for initial layouts or testing.
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
- # Get configuration
17
- aspect_ratio = option("aspect_ratio", 1.6).to_f
18
- spacing = node_spacing
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
- # Calculate total area needed
21
- total_area = graph.children.sum do |node|
22
- (node.width + spacing) * (node.height + spacing)
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
- # Calculate bounds
26
- width = Math.sqrt(total_area * aspect_ratio)
27
- height = width / aspect_ratio
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
- # Position nodes randomly
95
+ index = NodeIndex.build(graph)
96
+ grouped = edges_by_source(graph, index)
30
97
  graph.children.each do |node|
31
- node.x = rand * (width - node.width)
32
- node.y = rand * (height - node.height)
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
- # Apply padding and set graph dimensions
36
- apply_padding(graph)
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
- graph
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 = node_spacing
37
-
38
- # Sort nodes by height (tallest first) for better packing
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, current_shelf, spacing)
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 can_fit_on_shelf?(node, shelf, _spacing)
72
- # First node always fits
73
- return true if shelf[:nodes].empty?
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
- # Check if adding this node would make the shelf too tall
76
- # (we want relatively uniform shelf heights)
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[:nodes] << node
87
- shelf[:width] += node.width + spacing
88
- shelf[:height] = [shelf[:height], node.height].max
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