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.
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,299 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "algorithm_registry"
4
+ require_relative "../options/resolver"
5
+
3
6
  module Elkrb
4
7
  module Layout
5
- # Module to add hierarchical graph layout support to algorithms.
6
- # Handles recursive layout, parent-child constraints, and cross-hierarchy
7
- # edges.
8
+ # Sizes compound nodes bottom-up, one graph level per algorithm instance.
8
9
  module HierarchicalProcessor
9
- # Layout a graph and all its hierarchical children recursively.
10
- #
11
- # @param graph [Graph::Graph] The graph to layout
12
- # @param options [Hash] Layout options
13
- # @return [Graph::Graph] The laid out graph
14
- def layout_hierarchical(graph, options = {})
15
- return layout_flat(graph, options) unless graph.hierarchical?
16
-
17
- # First, recursively layout all child nodes
18
- layout_children_recursively(graph)
19
-
20
- # Then layout the top-level graph
21
- layout_flat(graph, options)
22
-
23
- # Apply parent constraints
24
- apply_parent_constraints(graph)
25
-
26
- # Handle cross-hierarchy edges
27
- handle_cross_hierarchy_edges(graph)
28
-
29
- # Update parent bounds
30
- update_parent_bounds(graph)
31
-
32
- graph
33
- end
34
-
35
- private
36
-
37
- # Layout all children of nodes in the graph recursively.
38
- def layout_children_recursively(graph)
39
- return unless graph.children
40
-
10
+ # Layout each compound as its own graph before its parent level.
11
+ def size_compound_children(graph)
12
+ child_resolver = Options::Resolver.new
41
13
  graph.children.each do |node|
42
14
  next unless node.hierarchical?
43
15
 
44
- # Create a temporary graph for the node's children
45
16
  child_graph = create_child_graph(node)
17
+ pin = child_resolver.get("elk.algorithm", child_graph, default: nil)
18
+ algorithm = pin ? AlgorithmRegistry.get(pin) : self.class
19
+ raise AlgorithmNotFoundError.new(pin) unless algorithm
46
20
 
47
- # Recursively layout the child graph
48
- layout_hierarchical(child_graph, extract_node_options(node))
49
-
50
- # Apply the layout back to the node
51
- apply_child_layout(node, child_graph)
21
+ algorithm.new(@options).layout(child_graph)
22
+ node.width = child_graph.width
23
+ node.height = child_graph.height
52
24
  end
53
25
  end
54
26
 
55
- # Layout a flat (non-hierarchical) graph using the base algorithm.
56
- def layout_flat(graph, options = {})
57
- # This should be implemented by the including algorithm class
58
- raise NotImplementedError,
59
- "#{self.class.name} must implement #layout_flat"
60
- end
27
+ private
61
28
 
62
- # Create a temporary graph from a node's children.
29
+ # Build the graph view shared by compound sizing and nested edge routing.
63
30
  def create_child_graph(node)
31
+ (node.edges || []).each { |edge| edge.container = node.id }
64
32
  Graph::Graph.new(
65
33
  id: "#{node.id}_children",
66
- children: node.children || [],
67
- edges: node.edges || [],
34
+ children: node.children,
35
+ edges: node.edges,
68
36
  layout_options: node.layout_options,
37
+ properties: node.properties,
69
38
  )
70
39
  end
71
-
72
- # Extract layout options from a node.
73
- def extract_node_options(node)
74
- return {} unless node.layout_options
75
-
76
- node.layout_options.properties || {}
77
- end
78
-
79
- # Apply the child graph layout back to the parent node.
80
- def apply_child_layout(node, child_graph)
81
- # Copy positions from child_graph children to node children
82
- return unless child_graph.children
83
-
84
- child_graph.children.each_with_index do |child, index|
85
- if node.children && node.children[index]
86
- node.children[index].x = child.x
87
- node.children[index].y = child.y
88
- node.children[index].width = child.width
89
- node.children[index].height = child.height
90
- end
91
- end
92
- end
93
-
94
- # Apply parent constraints to ensure children fit within parent bounds.
95
- def apply_parent_constraints(graph)
96
- return unless graph.children
97
-
98
- graph.children.each do |node|
99
- next unless node.hierarchical?
100
-
101
- # Get padding from node options
102
- padding = get_padding(node)
103
-
104
- # Adjust child positions to account for padding
105
- adjust_children_for_padding(node, padding)
106
-
107
- # Recursively apply to nested children
108
- if node.children
109
- temp_graph = Graph::Graph.new(children: node.children)
110
- apply_parent_constraints(temp_graph)
111
- end
112
- end
113
- end
114
-
115
- # Get padding for a node from its layout options.
116
- def get_padding(node)
117
- return default_padding unless node.layout_options
118
-
119
- padding_option = node.layout_options.properties&.[]("padding") ||
120
- node.layout_options.properties&.[]("elk.padding")
121
-
122
- return default_padding unless padding_option
123
-
124
- parse_padding(padding_option)
125
- end
126
-
127
- # Default padding values.
128
- def default_padding
129
- { top: 12.0, right: 12.0, bottom: 12.0, left: 12.0 }
130
- end
131
-
132
- # Parse padding from various formats.
133
- def parse_padding(padding)
134
- case padding
135
- when Hash
136
- {
137
- top: padding[:top] || padding["top"] || 12.0,
138
- right: padding[:right] || padding["right"] || 12.0,
139
- bottom: padding[:bottom] || padding["bottom"] || 12.0,
140
- left: padding[:left] || padding["left"] || 12.0,
141
- }
142
- when Numeric
143
- { top: padding, right: padding, bottom: padding, left: padding }
144
- else
145
- default_padding
146
- end
147
- end
148
-
149
- # Adjust children positions to account for parent padding.
150
- def adjust_children_for_padding(node, padding)
151
- return unless node.children
152
-
153
- node.children.each do |child|
154
- child.x = (child.x || 0.0) + padding[:left]
155
- child.y = (child.y || 0.0) + padding[:top]
156
- end
157
- end
158
-
159
- # Handle edges that cross hierarchy boundaries.
160
- def handle_cross_hierarchy_edges(graph)
161
- return unless graph.edges
162
-
163
- graph.edges.each do |edge|
164
- # Get source and target IDs from edge
165
- source_id = edge.sources&.first
166
- target_id = edge.targets&.first
167
-
168
- next unless source_id && target_id
169
-
170
- source_node = find_node_in_hierarchy(graph, source_id)
171
- target_node = find_node_in_hierarchy(graph, target_id)
172
-
173
- next unless source_node && target_node
174
-
175
- # Check if edge crosses hierarchy levels
176
- if crosses_hierarchy?(source_node, target_node, graph)
177
- adjust_edge_for_hierarchy(edge, source_node, target_node)
178
- end
179
- end
180
- end
181
-
182
- # Find a node anywhere in the graph hierarchy.
183
- def find_node_in_hierarchy(graph, node_id)
184
- return nil unless node_id
185
-
186
- # Handle both direct node IDs and port IDs
187
- node_id_str = node_id.is_a?(String) ? node_id : node_id.to_s
188
- graph.find_node(node_id_str)
189
- end
190
-
191
- # Check if an edge crosses hierarchy boundaries.
192
- def crosses_hierarchy?(source_node, target_node, graph)
193
- return false unless source_node && target_node
194
-
195
- source_depth = node_depth(source_node, graph)
196
- target_depth = node_depth(target_node, graph)
197
-
198
- source_depth != target_depth
199
- end
200
-
201
- # Calculate the depth of a node in the hierarchy.
202
- def node_depth(node, graph, depth = 0)
203
- return depth if graph.children&.include?(node)
204
-
205
- graph.children&.each do |child|
206
- if child.hierarchical? && child.children
207
- child_graph = Graph::Graph.new(children: child.children)
208
- found_depth = node_depth(node, child_graph, depth + 1)
209
- return found_depth if found_depth > depth
210
- end
211
- end
212
-
213
- depth
214
- end
215
-
216
- # Adjust edge routing for cross-hierarchy edges.
217
- def adjust_edge_for_hierarchy(edge, _source_node, _target_node)
218
- # Add additional bend points to route around parent boundaries
219
- # This is a simplified version - could be enhanced with proper routing
220
- return unless edge.sections && !edge.sections.empty?
221
-
222
- section = edge.sections.first
223
-
224
- # Calculate midpoint
225
- mid_x = (section.start_point.x + section.end_point.x) / 2.0
226
- mid_y = (section.start_point.y + section.end_point.y) / 2.0
227
-
228
- # Add a bend point at the midpoint
229
- section.add_bend_point(mid_x, mid_y)
230
- end
231
-
232
- # Update parent node bounds to contain all children.
233
- def update_parent_bounds(graph)
234
- return unless graph.children
235
-
236
- graph.children.each do |node|
237
- next unless node.hierarchical?
238
-
239
- # Recursively update nested children first
240
- if node.children
241
- temp_graph = Graph::Graph.new(children: node.children)
242
- update_parent_bounds(temp_graph)
243
- end
244
-
245
- # Calculate bounds from children
246
- bounds = calculate_children_bounds(node)
247
-
248
- # Get padding
249
- padding = get_padding(node)
250
-
251
- # Update node dimensions
252
- node.width = bounds[:width] + padding[:left] + padding[:right]
253
- node.height = bounds[:height] + padding[:top] + padding[:bottom]
254
-
255
- # Update position if needed (ensure children are at positive coords)
256
- if bounds[:min_x].negative?
257
- offset_x = -bounds[:min_x] + padding[:left]
258
- node.children&.each { |child| child.x = (child.x || 0) + offset_x }
259
- end
260
-
261
- if bounds[:min_y].negative?
262
- offset_y = -bounds[:min_y] + padding[:top]
263
- node.children&.each { |child| child.y = (child.y || 0) + offset_y }
264
- end
265
- end
266
- end
267
-
268
- # Calculate the bounding box of a node's children.
269
- def calculate_children_bounds(node)
270
- return { min_x: 0, min_y: 0, width: 0, height: 0 } unless
271
- node.children && !node.children.empty?
272
-
273
- min_x = Float::INFINITY
274
- min_y = Float::INFINITY
275
- max_x = -Float::INFINITY
276
- max_y = -Float::INFINITY
277
-
278
- node.children.each do |child|
279
- x = child.x || 0.0
280
- y = child.y || 0.0
281
- w = child.width || 0.0
282
- h = child.height || 0.0
283
-
284
- min_x = [min_x, x].min
285
- min_y = [min_y, y].min
286
- max_x = [max_x, x + w].max
287
- max_y = [max_y, y + h].max
288
- end
289
-
290
- {
291
- min_x: min_x,
292
- min_y: min_y,
293
- width: max_x - min_x,
294
- height: max_y - min_y,
295
- }
296
- end
297
40
  end
298
41
  end
299
42
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Layout
5
+ # java.util.Random's 48-bit linear congruential generator. Java ELK draws
6
+ # its random layouts from it, so the same seed only gives the same
7
+ # numbers here if the generator is the same one.
8
+ class JavaRandom
9
+ MULTIPLIER = 0x5DEECE66D
10
+ ADDEND = 0xB
11
+ MASK = (1 << 48) - 1
12
+ INT_LIMIT = 1 << 31
13
+
14
+ def initialize(seed)
15
+ self.seed = seed
16
+ end
17
+
18
+ # Restarts the sequence, like Random#setSeed.
19
+ def seed=(seed)
20
+ @state = (seed ^ MULTIPLIER) & MASK
21
+ end
22
+
23
+ # The top `bits` bits of the next state, as a Java int (bits <= 32).
24
+ def next_bits(bits)
25
+ @state = ((@state * MULTIPLIER) + ADDEND) & MASK
26
+ value = @state >> (48 - bits)
27
+ value >= INT_LIMIT ? value - (1 << 32) : value
28
+ end
29
+
30
+ # A signed 64-bit integer, like Random#nextLong.
31
+ def next_long
32
+ value = (next_bits(32) << 32) + next_bits(32)
33
+ value >= 1 << 63 ? value - (1 << 64) : value
34
+ end
35
+
36
+ # A float in [0, 1) with 24 random bits, like Random#nextFloat.
37
+ def next_float
38
+ next_bits(24) / 16_777_216.0
39
+ end
40
+
41
+ # A double in [0, 1) with 53 random bits, like Random#nextDouble.
42
+ def next_double
43
+ ((next_bits(26) << 27) + next_bits(27)) / 9_007_199_254_740_992.0
44
+ end
45
+
46
+ # An integer in [0, bound), like Random#nextInt(bound).
47
+ def next_int(bound)
48
+ raise ArgumentError, "bound must be positive" unless bound.positive?
49
+ return (bound * next_bits(31)) >> 31 if bound.nobits?(bound - 1)
50
+
51
+ loop do
52
+ bits = next_bits(31)
53
+ value = bits % bound
54
+ return value if bits - value + (bound - 1) < INT_LIMIT
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end