elkrb 1.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 (89) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +11 -0
  4. data/Gemfile +13 -0
  5. data/README.adoc +1028 -0
  6. data/Rakefile +64 -0
  7. data/benchmarks/README.md +172 -0
  8. data/benchmarks/elkjs_benchmark.js +140 -0
  9. data/benchmarks/elkrb_benchmark.rb +145 -0
  10. data/benchmarks/fixtures/graphs.json +10777 -0
  11. data/benchmarks/generate_report.rb +241 -0
  12. data/benchmarks/generate_test_graphs.rb +154 -0
  13. data/benchmarks/results/elkrb_results.json +280 -0
  14. data/benchmarks/results/elkrb_summary.json +285 -0
  15. data/elkrb.gemspec +39 -0
  16. data/examples/dot_export_demo.rb +133 -0
  17. data/examples/hierarchical_graph.rb +19 -0
  18. data/examples/layout_constraints_demo.rb +272 -0
  19. data/examples/port_constraints_demo.rb +291 -0
  20. data/examples/self_loop_demo.rb +391 -0
  21. data/examples/simple_graph.rb +50 -0
  22. data/examples/spline_routing_demo.rb +235 -0
  23. data/exe/elkrb +8 -0
  24. data/lib/elkrb/cli.rb +224 -0
  25. data/lib/elkrb/commands/batch_command.rb +66 -0
  26. data/lib/elkrb/commands/convert_command.rb +130 -0
  27. data/lib/elkrb/commands/diagram_command.rb +208 -0
  28. data/lib/elkrb/commands/render_command.rb +52 -0
  29. data/lib/elkrb/commands/validate_command.rb +241 -0
  30. data/lib/elkrb/errors.rb +30 -0
  31. data/lib/elkrb/geometry/bezier.rb +163 -0
  32. data/lib/elkrb/geometry/dimension.rb +32 -0
  33. data/lib/elkrb/geometry/point.rb +68 -0
  34. data/lib/elkrb/geometry/rectangle.rb +86 -0
  35. data/lib/elkrb/geometry/vector.rb +67 -0
  36. data/lib/elkrb/graph/edge.rb +95 -0
  37. data/lib/elkrb/graph/graph.rb +90 -0
  38. data/lib/elkrb/graph/label.rb +45 -0
  39. data/lib/elkrb/graph/layout_options.rb +247 -0
  40. data/lib/elkrb/graph/node.rb +79 -0
  41. data/lib/elkrb/graph/node_constraints.rb +107 -0
  42. data/lib/elkrb/graph/port.rb +104 -0
  43. data/lib/elkrb/graphviz_wrapper.rb +133 -0
  44. data/lib/elkrb/layout/algorithm_registry.rb +57 -0
  45. data/lib/elkrb/layout/algorithms/base_algorithm.rb +208 -0
  46. data/lib/elkrb/layout/algorithms/box.rb +47 -0
  47. data/lib/elkrb/layout/algorithms/disco.rb +206 -0
  48. data/lib/elkrb/layout/algorithms/fixed.rb +32 -0
  49. data/lib/elkrb/layout/algorithms/force.rb +165 -0
  50. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +86 -0
  51. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +96 -0
  52. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +77 -0
  53. data/lib/elkrb/layout/algorithms/layered.rb +49 -0
  54. data/lib/elkrb/layout/algorithms/libavoid.rb +389 -0
  55. data/lib/elkrb/layout/algorithms/mrtree.rb +144 -0
  56. data/lib/elkrb/layout/algorithms/radial.rb +64 -0
  57. data/lib/elkrb/layout/algorithms/random.rb +43 -0
  58. data/lib/elkrb/layout/algorithms/rectpacking.rb +93 -0
  59. data/lib/elkrb/layout/algorithms/spore_compaction.rb +139 -0
  60. data/lib/elkrb/layout/algorithms/spore_overlap.rb +117 -0
  61. data/lib/elkrb/layout/algorithms/stress.rb +176 -0
  62. data/lib/elkrb/layout/algorithms/topdown_packing.rb +183 -0
  63. data/lib/elkrb/layout/algorithms/vertiflex.rb +174 -0
  64. data/lib/elkrb/layout/constraints/alignment_constraint.rb +150 -0
  65. data/lib/elkrb/layout/constraints/base_constraint.rb +72 -0
  66. data/lib/elkrb/layout/constraints/constraint_processor.rb +134 -0
  67. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +87 -0
  68. data/lib/elkrb/layout/constraints/layer_constraint.rb +71 -0
  69. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +110 -0
  70. data/lib/elkrb/layout/edge_router.rb +935 -0
  71. data/lib/elkrb/layout/hierarchical_processor.rb +299 -0
  72. data/lib/elkrb/layout/label_placer.rb +338 -0
  73. data/lib/elkrb/layout/layout_engine.rb +170 -0
  74. data/lib/elkrb/layout/port_constraint_processor.rb +173 -0
  75. data/lib/elkrb/options/elk_padding.rb +94 -0
  76. data/lib/elkrb/options/k_vector.rb +100 -0
  77. data/lib/elkrb/options/k_vector_chain.rb +135 -0
  78. data/lib/elkrb/parsers/elkt_parser.rb +248 -0
  79. data/lib/elkrb/serializers/dot_serializer.rb +339 -0
  80. data/lib/elkrb/serializers/elkt_serializer.rb +236 -0
  81. data/lib/elkrb/version.rb +5 -0
  82. data/lib/elkrb.rb +509 -0
  83. data/sig/elkrb/constraints.rbs +114 -0
  84. data/sig/elkrb/geometry.rbs +61 -0
  85. data/sig/elkrb/graph.rbs +112 -0
  86. data/sig/elkrb/layout.rbs +107 -0
  87. data/sig/elkrb/options.rbs +81 -0
  88. data/sig/elkrb.rbs +32 -0
  89. metadata +179 -0
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/elkrb"
5
+
6
+ # Example 1: Simple Self-loop
7
+ puts "=" * 70
8
+ puts "Example 1: Simple Self-loop with Default Settings"
9
+ puts "=" * 70
10
+
11
+ graph1 = Elkrb::Graph::Graph.new(
12
+ id: "simple_self_loop",
13
+ children: [
14
+ Elkrb::Graph::Node.new(
15
+ id: "state1",
16
+ width: 100.0,
17
+ height: 60.0,
18
+ ),
19
+ ],
20
+ edges: [
21
+ Elkrb::Graph::Edge.new(
22
+ id: "loop1",
23
+ sources: ["state1"],
24
+ targets: ["state1"],
25
+ ),
26
+ ],
27
+ layout_options: Elkrb::Graph::LayoutOptions.new(
28
+ algorithm: "layered",
29
+ ),
30
+ )
31
+
32
+ result1 = Elkrb.layout(graph1)
33
+ puts "\nNode position: (#{result1.children[0].x}, #{result1.children[0].y})"
34
+ puts "Self-loop section: #{result1.edges[0].sections.length} section(s)"
35
+ puts "Bend points: #{result1.edges[0].sections[0].bend_points.length}"
36
+ puts "Start: (#{result1.edges[0].sections[0].start_point.x.round(1)}, " \
37
+ "#{result1.edges[0].sections[0].start_point.y.round(1)})"
38
+ puts "End: (#{result1.edges[0].sections[0].end_point.x.round(1)}, " \
39
+ "#{result1.edges[0].sections[0].end_point.y.round(1)})"
40
+
41
+ # Example 2: Self-loop with Custom Side
42
+ puts "\n#{'=' * 70}"
43
+ puts "Example 2: Self-loop on Different Sides"
44
+ puts "=" * 70
45
+
46
+ %w[EAST WEST NORTH SOUTH].each do |side|
47
+ graph = Elkrb::Graph::Graph.new(
48
+ id: "self_loop_#{side.downcase}",
49
+ children: [
50
+ Elkrb::Graph::Node.new(
51
+ id: "node",
52
+ width: 80.0,
53
+ height: 60.0,
54
+ ),
55
+ ],
56
+ edges: [
57
+ Elkrb::Graph::Edge.new(
58
+ id: "loop",
59
+ sources: ["node"],
60
+ targets: ["node"],
61
+ layout_options: Elkrb::Graph::LayoutOptions.new(
62
+ "elk.selfLoopSide" => side,
63
+ ),
64
+ ),
65
+ ],
66
+ layout_options: Elkrb::Graph::LayoutOptions.new(
67
+ algorithm: "layered",
68
+ ),
69
+ )
70
+
71
+ result = Elkrb.layout(graph)
72
+ section = result.edges[0].sections[0]
73
+ puts "\n#{side} side:"
74
+ puts " Start: (#{section.start_point.x.round(1)}, " \
75
+ "#{section.start_point.y.round(1)})"
76
+ puts " Bend points: #{section.bend_points.length}"
77
+ end
78
+
79
+ # Example 3: Multiple Self-loops on Same Node
80
+ puts "\n#{'=' * 70}"
81
+ puts "Example 3: Multiple Self-loops on Same Node"
82
+ puts "=" * 70
83
+
84
+ graph3 = Elkrb::Graph::Graph.new(
85
+ id: "multiple_self_loops",
86
+ children: [
87
+ Elkrb::Graph::Node.new(
88
+ id: "state",
89
+ width: 100.0,
90
+ height: 80.0,
91
+ ),
92
+ ],
93
+ edges: [
94
+ Elkrb::Graph::Edge.new(
95
+ id: "loop1",
96
+ sources: ["state"],
97
+ targets: ["state"],
98
+ ),
99
+ Elkrb::Graph::Edge.new(
100
+ id: "loop2",
101
+ sources: ["state"],
102
+ targets: ["state"],
103
+ ),
104
+ Elkrb::Graph::Edge.new(
105
+ id: "loop3",
106
+ sources: ["state"],
107
+ targets: ["state"],
108
+ ),
109
+ ],
110
+ layout_options: Elkrb::Graph::LayoutOptions.new(
111
+ algorithm: "layered",
112
+ ),
113
+ )
114
+
115
+ result3 = Elkrb.layout(graph3)
116
+ result3.edges.each_with_index do |edge, i|
117
+ section = edge.sections[0]
118
+ first_bend = section.bend_points[0]
119
+ puts "\nLoop #{i + 1} (#{edge.id}):"
120
+ puts " First bend point x: #{first_bend.x.round(1)}"
121
+ puts " Offset from node edge: " \
122
+ "#{(first_bend.x - result3.children[0].x -
123
+ result3.children[0].width).round(1)}"
124
+ end
125
+
126
+ # Example 4: Self-loop with Spline Routing
127
+ puts "\n#{'=' * 70}"
128
+ puts "Example 4: Self-loop with Spline (Curved) Routing"
129
+ puts "=" * 70
130
+
131
+ graph4 = Elkrb::Graph::Graph.new(
132
+ id: "spline_self_loop",
133
+ children: [
134
+ Elkrb::Graph::Node.new(
135
+ id: "node",
136
+ width: 100.0,
137
+ height: 60.0,
138
+ ),
139
+ ],
140
+ edges: [
141
+ Elkrb::Graph::Edge.new(
142
+ id: "curved_loop",
143
+ sources: ["node"],
144
+ targets: ["node"],
145
+ ),
146
+ ],
147
+ layout_options: Elkrb::Graph::LayoutOptions.new(
148
+ algorithm: "layered",
149
+ edge_routing: "SPLINES",
150
+ ),
151
+ )
152
+
153
+ result4 = Elkrb.layout(graph4)
154
+ section4 = result4.edges[0].sections[0]
155
+ puts "\nSpline self-loop:"
156
+ puts " Control points: #{section4.bend_points.length}"
157
+ puts " Control point 1: (#{section4.bend_points[0].x.round(1)}, " \
158
+ "#{section4.bend_points[0].y.round(1)})"
159
+ puts " Control point 2: (#{section4.bend_points[1].x.round(1)}, " \
160
+ "#{section4.bend_points[1].y.round(1)})"
161
+
162
+ # Example 5: State Machine with Self-loops
163
+ puts "\n#{'=' * 70}"
164
+ puts "Example 5: State Machine with Self-loops and Transitions"
165
+ puts "=" * 70
166
+
167
+ graph5 = Elkrb::Graph::Graph.new(
168
+ id: "state_machine",
169
+ children: [
170
+ Elkrb::Graph::Node.new(
171
+ id: "idle",
172
+ width: 80.0,
173
+ height: 50.0,
174
+ ),
175
+ Elkrb::Graph::Node.new(
176
+ id: "running",
177
+ width: 80.0,
178
+ height: 50.0,
179
+ ),
180
+ Elkrb::Graph::Node.new(
181
+ id: "error",
182
+ width: 80.0,
183
+ height: 50.0,
184
+ ),
185
+ ],
186
+ edges: [
187
+ # Self-loops (states that can transition to themselves)
188
+ Elkrb::Graph::Edge.new(
189
+ id: "idle_wait",
190
+ sources: ["idle"],
191
+ targets: ["idle"],
192
+ ),
193
+ Elkrb::Graph::Edge.new(
194
+ id: "running_continue",
195
+ sources: ["running"],
196
+ targets: ["running"],
197
+ ),
198
+ # Regular transitions
199
+ Elkrb::Graph::Edge.new(
200
+ id: "start",
201
+ sources: ["idle"],
202
+ targets: ["running"],
203
+ ),
204
+ Elkrb::Graph::Edge.new(
205
+ id: "fail",
206
+ sources: ["running"],
207
+ targets: ["error"],
208
+ ),
209
+ Elkrb::Graph::Edge.new(
210
+ id: "reset",
211
+ sources: ["error"],
212
+ targets: ["idle"],
213
+ ),
214
+ ],
215
+ layout_options: Elkrb::Graph::LayoutOptions.new(
216
+ algorithm: "layered",
217
+ direction: "RIGHT",
218
+ ),
219
+ )
220
+
221
+ result5 = Elkrb.layout(graph5)
222
+ puts "\nState machine layout:"
223
+ result5.children.each do |node|
224
+ puts " #{node.id}: (#{node.x.round(1)}, #{node.y.round(1)})"
225
+ end
226
+ puts "\nEdges:"
227
+ result5.edges.each do |edge|
228
+ is_self_loop = edge.sources[0] == edge.targets[0]
229
+ puts " #{edge.id}: #{is_self_loop ? 'self-loop' : 'transition'}"
230
+ end
231
+
232
+ # Example 6: Self-loop with Ports
233
+ puts "\n#{'=' * 70}"
234
+ puts "Example 6: Self-loop Between Ports"
235
+ puts "=" * 70
236
+
237
+ graph6 = Elkrb::Graph::Graph.new(
238
+ id: "port_self_loop",
239
+ children: [
240
+ Elkrb::Graph::Node.new(
241
+ id: "component",
242
+ width: 120.0,
243
+ height: 80.0,
244
+ ports: [
245
+ Elkrb::Graph::Port.new(
246
+ id: "out1",
247
+ x: 120.0,
248
+ y: 20.0,
249
+ side: "EAST",
250
+ ),
251
+ Elkrb::Graph::Port.new(
252
+ id: "in1",
253
+ x: 120.0,
254
+ y: 60.0,
255
+ side: "EAST",
256
+ ),
257
+ ],
258
+ ),
259
+ ],
260
+ edges: [
261
+ Elkrb::Graph::Edge.new(
262
+ id: "feedback",
263
+ sources: ["out1"],
264
+ targets: ["in1"],
265
+ ),
266
+ ],
267
+ layout_options: Elkrb::Graph::LayoutOptions.new(
268
+ algorithm: "layered",
269
+ ),
270
+ )
271
+
272
+ result6 = Elkrb.layout(graph6)
273
+ section6 = result6.edges[0].sections[0]
274
+ puts "\nPort-based self-loop:"
275
+ puts " Start (out1): (#{section6.start_point.x.round(1)}, " \
276
+ "#{section6.start_point.y.round(1)})"
277
+ puts " End (in1): (#{section6.end_point.x.round(1)}, " \
278
+ "#{section6.end_point.y.round(1)})"
279
+ puts " Bend points: #{section6.bend_points.length}"
280
+
281
+ # Example 7: Finite Automaton
282
+ puts "\n#{'=' * 70}"
283
+ puts "Example 7: Finite Automaton with Self-loops"
284
+ puts "=" * 70
285
+
286
+ graph7 = Elkrb::Graph::Graph.new(
287
+ id: "finite_automaton",
288
+ children: [
289
+ Elkrb::Graph::Node.new(
290
+ id: "q0",
291
+ width: 60.0,
292
+ height: 60.0,
293
+ ),
294
+ Elkrb::Graph::Node.new(
295
+ id: "q1",
296
+ width: 60.0,
297
+ height: 60.0,
298
+ ),
299
+ Elkrb::Graph::Node.new(
300
+ id: "q2",
301
+ width: 60.0,
302
+ height: 60.0,
303
+ ),
304
+ ],
305
+ edges: [
306
+ # Self-loops for states reading same symbol
307
+ Elkrb::Graph::Edge.new(
308
+ id: "q0_0",
309
+ sources: ["q0"],
310
+ targets: ["q0"],
311
+ layout_options: Elkrb::Graph::LayoutOptions.new(
312
+ "elk.selfLoopSide" => "NORTH",
313
+ ),
314
+ ),
315
+ Elkrb::Graph::Edge.new(
316
+ id: "q1_1",
317
+ sources: ["q1"],
318
+ targets: ["q1"],
319
+ layout_options: Elkrb::Graph::LayoutOptions.new(
320
+ "elk.selfLoopSide" => "NORTH",
321
+ ),
322
+ ),
323
+ # State transitions
324
+ Elkrb::Graph::Edge.new(
325
+ id: "q0_to_q1",
326
+ sources: ["q0"],
327
+ targets: ["q1"],
328
+ ),
329
+ Elkrb::Graph::Edge.new(
330
+ id: "q1_to_q2",
331
+ sources: ["q1"],
332
+ targets: ["q2"],
333
+ ),
334
+ ],
335
+ layout_options: Elkrb::Graph::LayoutOptions.new(
336
+ algorithm: "layered",
337
+ direction: "RIGHT",
338
+ ),
339
+ )
340
+
341
+ result7 = Elkrb.layout(graph7)
342
+ puts "\nFinite automaton states:"
343
+ result7.children.each do |node|
344
+ puts " #{node.id}: (#{node.x.round(1)}, #{node.y.round(1)})"
345
+ end
346
+
347
+ self_loops = result7.edges.select { |e| e.sources[0] == e.targets[0] }
348
+ puts "\nSelf-loops (same symbol transitions):"
349
+ self_loops.each do |edge|
350
+ section = edge.sections[0]
351
+ puts " #{edge.id}: #{section.bend_points.length} bend points"
352
+ end
353
+
354
+ # Example 8: Mixed Routing Styles
355
+ puts "\n#{'=' * 70}"
356
+ puts "Example 8: Comparing Routing Styles"
357
+ puts "=" * 70
358
+
359
+ %w[ORTHOGONAL SPLINES POLYLINE].each do |style|
360
+ graph = Elkrb::Graph::Graph.new(
361
+ id: "style_#{style.downcase}",
362
+ children: [
363
+ Elkrb::Graph::Node.new(
364
+ id: "node",
365
+ width: 80.0,
366
+ height: 60.0,
367
+ ),
368
+ ],
369
+ edges: [
370
+ Elkrb::Graph::Edge.new(
371
+ id: "loop",
372
+ sources: ["node"],
373
+ targets: ["node"],
374
+ ),
375
+ ],
376
+ layout_options: Elkrb::Graph::LayoutOptions.new(
377
+ algorithm: "layered",
378
+ edge_routing: style,
379
+ ),
380
+ )
381
+
382
+ result = Elkrb.layout(graph)
383
+ section = result.edges[0].sections[0]
384
+ puts "\n#{style} routing:"
385
+ puts " Bend points: #{section.bend_points.length}"
386
+ puts " Path length: #{section.length.round(2)}"
387
+ end
388
+
389
+ puts "\n#{'=' * 70}"
390
+ puts "All examples completed successfully!"
391
+ puts "=" * 70
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "elkrb"
6
+
7
+ # Simple graph layout example
8
+ # This demonstrates the basic usage of ElkRb with a simple node graph
9
+
10
+ # Create a graph with 4 nodes connected in sequence
11
+ graph = Elkrb::Graph.new(
12
+ id: "root",
13
+ children: [
14
+ { id: "n1", width: 100, height: 50 },
15
+ { id: "n2", width: 100, height: 50 },
16
+ { id: "n3", width: 100, height: 50 },
17
+ { id: "n4", width: 100, height: 50 },
18
+ ],
19
+ edges: [
20
+ { id: "e1", sources: ["n1"], targets: ["n2"] },
21
+ { id: "e2", sources: ["n2"], targets: ["n3"] },
22
+ { id: "e3", sources: ["n3"], targets: ["n4"] },
23
+ ],
24
+ )
25
+
26
+ # Configure layout options
27
+ graph.layout_options = {
28
+ "algorithm" => "layered",
29
+ "elk.direction" => "DOWN",
30
+ "spacing.nodeNode" => 50,
31
+ }
32
+
33
+ # Perform layout
34
+ engine = Elkrb::Layout::LayoutEngine.new
35
+ result = engine.layout(graph)
36
+
37
+ # Display results
38
+ puts "Graph layout completed!"
39
+ puts "=" * 60
40
+ puts "Root dimensions: #{result.width}x#{result.height}"
41
+ puts "\nNode positions:"
42
+ result.children.each do |node|
43
+ puts " #{node.id}: (#{node.x}, #{node.y})"
44
+ end
45
+
46
+ puts "\nEdge routing:"
47
+ result.edges.each do |edge|
48
+ puts " #{edge.id}: #{edge.sections&.first&.start_point} -> " \
49
+ "#{edge.sections&.first&.end_point}"
50
+ end
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Spline Edge Routing Demo
5
+ #
6
+ # This example demonstrates the use of smooth Bezier curve routing for edges
7
+ # in ElkRb, comparing different routing styles.
8
+
9
+ require_relative "../lib/elkrb"
10
+
11
+ def create_sample_graph(routing_style, curvature = 0.5)
12
+ Elkrb::Graph::Graph.new(
13
+ id: "root",
14
+ layout_options: Elkrb::Graph::LayoutOptions.new(
15
+ algorithm: "layered",
16
+ direction: "RIGHT",
17
+ edge_routing: routing_style,
18
+ spline_curvature: curvature,
19
+ spacing_node_node: 80.0,
20
+ ),
21
+ children: [
22
+ Elkrb::Graph::Node.new(
23
+ id: "n1",
24
+ width: 80,
25
+ height: 40,
26
+ labels: [
27
+ Elkrb::Graph::Label.new(text: "Start", width: 40, height: 15),
28
+ ],
29
+ ),
30
+ Elkrb::Graph::Node.new(
31
+ id: "n2",
32
+ width: 80,
33
+ height: 40,
34
+ labels: [
35
+ Elkrb::Graph::Label.new(text: "Process", width: 50, height: 15),
36
+ ],
37
+ ),
38
+ Elkrb::Graph::Node.new(
39
+ id: "n3",
40
+ width: 80,
41
+ height: 40,
42
+ labels: [
43
+ Elkrb::Graph::Label.new(text: "Decision", width: 55, height: 15),
44
+ ],
45
+ ),
46
+ Elkrb::Graph::Node.new(
47
+ id: "n4",
48
+ width: 80,
49
+ height: 40,
50
+ labels: [
51
+ Elkrb::Graph::Label.new(text: "End", width: 30, height: 15),
52
+ ],
53
+ ),
54
+ ],
55
+ edges: [
56
+ Elkrb::Graph::Edge.new(
57
+ id: "e1",
58
+ sources: ["n1"],
59
+ targets: ["n2"],
60
+ ),
61
+ Elkrb::Graph::Edge.new(
62
+ id: "e2",
63
+ sources: ["n2"],
64
+ targets: ["n3"],
65
+ ),
66
+ Elkrb::Graph::Edge.new(
67
+ id: "e3",
68
+ sources: ["n3"],
69
+ targets: ["n4"],
70
+ ),
71
+ Elkrb::Graph::Edge.new(
72
+ id: "e4",
73
+ sources: ["n3"],
74
+ targets: ["n2"],
75
+ labels: [
76
+ Elkrb::Graph::Label.new(text: "retry", width: 30, height: 15),
77
+ ],
78
+ ),
79
+ ],
80
+ )
81
+ end
82
+
83
+ def print_edge_info(edge, style_name)
84
+ section = edge.sections&.first
85
+ return unless section
86
+
87
+ puts "\n Edge #{edge.id} (#{style_name}):"
88
+ puts " Start: (#{section.start_point.x.round(1)}, " \
89
+ "#{section.start_point.y.round(1)})"
90
+
91
+ if section.bend_points&.any?
92
+ section.bend_points.each_with_index do |point, i|
93
+ puts " Control #{i + 1}: (#{point.x.round(1)}, #{point.y.round(1)})"
94
+ end
95
+ end
96
+
97
+ puts " End: (#{section.end_point.x.round(1)}, " \
98
+ "#{section.end_point.y.round(1)})"
99
+ puts " Length: #{section.length.round(2)}"
100
+ end
101
+
102
+ puts "=" * 70
103
+ puts "ElkRb Spline Edge Routing Demo"
104
+ puts "=" * 70
105
+
106
+ # Demo 1: Orthogonal Routing (Default)
107
+ puts "\n1. ORTHOGONAL ROUTING (90-degree bends)"
108
+ puts "-" * 70
109
+
110
+ graph1 = create_sample_graph("ORTHOGONAL")
111
+ result1 = Elkrb::Layout::LayoutEngine.layout(graph1)
112
+
113
+ puts "\nGraph dimensions: #{result1.width.round(1)} x #{result1.height.round(1)}"
114
+ result1.edges.each { |edge| print_edge_info(edge, "ORTHOGONAL") }
115
+
116
+ # Demo 2: Polyline Routing
117
+ puts "\n\n2. POLYLINE ROUTING (Straight lines)"
118
+ puts "-" * 70
119
+
120
+ graph2 = create_sample_graph("POLYLINE")
121
+ result2 = Elkrb::Layout::LayoutEngine.layout(graph2)
122
+
123
+ puts "\nGraph dimensions: #{result2.width.round(1)} x #{result2.height.round(1)}"
124
+ result2.edges.each { |edge| print_edge_info(edge, "POLYLINE") }
125
+
126
+ # Demo 3: Spline Routing (Smooth curves - Default curvature)
127
+ puts "\n\n3. SPLINE ROUTING (Smooth curves - curvature: 0.5)"
128
+ puts "-" * 70
129
+
130
+ graph3 = create_sample_graph("SPLINES", 0.5)
131
+ result3 = Elkrb::Layout::LayoutEngine.layout(graph3)
132
+
133
+ puts "\nGraph dimensions: #{result3.width.round(1)} x #{result3.height.round(1)}"
134
+ result3.edges.each { |edge| print_edge_info(edge, "SPLINES") }
135
+
136
+ # Demo 4: Spline Routing with Low Curvature
137
+ puts "\n\n4. SPLINE ROUTING (Gentle curves - curvature: 0.2)"
138
+ puts "-" * 70
139
+
140
+ graph4 = create_sample_graph("SPLINES", 0.2)
141
+ result4 = Elkrb::Layout::LayoutEngine.layout(graph4)
142
+
143
+ puts "\nGraph dimensions: #{result4.width.round(1)} x #{result4.height.round(1)}"
144
+ result4.edges.each { |edge| print_edge_info(edge, "SPLINES 0.2") }
145
+
146
+ # Demo 5: Spline Routing with High Curvature
147
+ puts "\n\n5. SPLINE ROUTING (Strong curves - curvature: 0.9)"
148
+ puts "-" * 70
149
+
150
+ graph5 = create_sample_graph("SPLINES", 0.9)
151
+ result5 = Elkrb::Layout::LayoutEngine.layout(graph5)
152
+
153
+ puts "\nGraph dimensions: #{result5.width.round(1)} x #{result5.height.round(1)}"
154
+ result5.edges.each { |edge| print_edge_info(edge, "SPLINES 0.9") }
155
+
156
+ # Demo 6: Port-based Spline Routing
157
+ puts "\n\n6. PORT-BASED SPLINE ROUTING"
158
+ puts "-" * 70
159
+
160
+ port_graph = Elkrb::Graph::Graph.new(
161
+ id: "port_demo",
162
+ layout_options: Elkrb::Graph::LayoutOptions.new(
163
+ algorithm: "layered",
164
+ direction: "RIGHT",
165
+ edge_routing: "SPLINES",
166
+ spline_curvature: 0.6,
167
+ ),
168
+ children: [
169
+ Elkrb::Graph::Node.new(
170
+ id: "source",
171
+ width: 100,
172
+ height: 60,
173
+ ports: [
174
+ Elkrb::Graph::Port.new(id: "out1", x: 100, y: 15),
175
+ Elkrb::Graph::Port.new(id: "out2", x: 100, y: 45),
176
+ ],
177
+ labels: [
178
+ Elkrb::Graph::Label.new(text: "Source", width: 45, height: 15),
179
+ ],
180
+ ),
181
+ Elkrb::Graph::Node.new(
182
+ id: "target",
183
+ width: 100,
184
+ height: 60,
185
+ ports: [
186
+ Elkrb::Graph::Port.new(id: "in1", x: 0, y: 15),
187
+ Elkrb::Graph::Port.new(id: "in2", x: 0, y: 45),
188
+ ],
189
+ labels: [
190
+ Elkrb::Graph::Label.new(text: "Target", width: 45, height: 15),
191
+ ],
192
+ ),
193
+ ],
194
+ edges: [
195
+ Elkrb::Graph::Edge.new(
196
+ id: "pe1",
197
+ sources: ["out1"],
198
+ targets: ["in1"],
199
+ ),
200
+ Elkrb::Graph::Edge.new(
201
+ id: "pe2",
202
+ sources: ["out2"],
203
+ targets: ["in2"],
204
+ ),
205
+ ],
206
+ )
207
+
208
+ port_result = Elkrb::Layout::LayoutEngine.layout(port_graph)
209
+ puts "\nGraph dimensions: #{port_result.width.round(1)} x " \
210
+ "#{port_result.height.round(1)}"
211
+ port_result.edges.each { |edge| print_edge_info(edge, "PORT SPLINES") }
212
+
213
+ # Comparison Summary
214
+ puts "\n\n#{'=' * 70}"
215
+ puts "COMPARISON SUMMARY"
216
+ puts "=" * 70
217
+
218
+ puts "\nRouting Style Characteristics:"
219
+ puts " • ORTHOGONAL: Right-angle bends, clear paths, traditional look"
220
+ puts " • POLYLINE: Direct straight lines, minimal space, technical look"
221
+ puts " • SPLINES: Smooth curves, aesthetic appeal, natural flow"
222
+
223
+ puts "\nSpline Curvature Guide:"
224
+ puts " • 0.0-0.3: Gentle curves, closer to straight lines"
225
+ puts " • 0.4-0.6: Moderate curves, balanced appearance (recommended)"
226
+ puts " • 0.7-1.0: Strong curves, very pronounced arcs"
227
+
228
+ puts "\nWhen to Use Each Style:"
229
+ puts " • ORTHOGONAL: Technical diagrams, flowcharts, circuit diagrams"
230
+ puts " • POLYLINE: Simple connections, space-constrained layouts"
231
+ puts " • SPLINES: Presentation diagrams, organic layouts, visual appeal"
232
+
233
+ puts "\n#{'=' * 70}"
234
+ puts "Demo complete! See SPLINE_ROUTING_GUIDE.md for more information."
235
+ puts "=" * 70
data/exe/elkrb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "elkrb"
6
+ require "elkrb/cli"
7
+
8
+ Elkrb::Cli.start(ARGV)