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,272 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/elkrb"
5
+
6
+ puts "=" * 70
7
+ puts "ElkRb Layout Constraints Demonstration"
8
+ puts "=" * 70
9
+ puts
10
+
11
+ # Example 1: Fixed Position Constraint
12
+ puts "Example 1: Fixed Position - API Gateway stays at top center"
13
+ puts "-" * 70
14
+
15
+ graph1 = Elkrb::Graph::Graph.new(id: "fixed_example")
16
+
17
+ # API Gateway fixed at (500, 100)
18
+ gateway = Elkrb::Graph::Node.new(
19
+ id: "gateway",
20
+ width: 120,
21
+ height: 60,
22
+ x: 500,
23
+ y: 100,
24
+ constraints: Elkrb::Graph::NodeConstraints.new(fixed_position: true),
25
+ )
26
+
27
+ # Other services positioned automatically
28
+ auth = Elkrb::Graph::Node.new(id: "auth", width: 100, height: 60)
29
+ user = Elkrb::Graph::Node.new(id: "user", width: 100, height: 60)
30
+
31
+ graph1.children = [gateway, auth, user]
32
+ graph1.edges = [
33
+ Elkrb::Graph::Edge.new(id: "e1", sources: ["gateway"], targets: ["auth"]),
34
+ Elkrb::Graph::Edge.new(id: "e2", sources: ["gateway"], targets: ["user"]),
35
+ ]
36
+
37
+ result1 = Elkrb.layout(graph1, algorithm: "layered")
38
+
39
+ puts "Gateway position: (#{result1.children[0].x}, #{result1.children[0].y})"
40
+ puts " Expected: (500.0, 100.0)"
41
+ puts " Status: #{result1.children[0].x == 500 && result1.children[0].y == 100 ? '✓' : '✗'}"
42
+ puts
43
+
44
+ # Example 2: Alignment Constraint
45
+ puts "Example 2: Alignment - All databases aligned horizontally"
46
+ puts "-" * 70
47
+
48
+ graph2 = Elkrb::Graph::Graph.new(id: "alignment_example")
49
+
50
+ # Three databases, all aligned horizontally
51
+ db1 = Elkrb::Graph::Node.new(
52
+ id: "db1",
53
+ width: 80,
54
+ height: 50,
55
+ constraints: Elkrb::Graph::NodeConstraints.new(
56
+ align_group: "databases",
57
+ align_direction: "horizontal",
58
+ ),
59
+ )
60
+
61
+ db2 = Elkrb::Graph::Node.new(
62
+ id: "db2",
63
+ width: 80,
64
+ height: 50,
65
+ constraints: Elkrb::Graph::NodeConstraints.new(
66
+ align_group: "databases",
67
+ align_direction: "horizontal",
68
+ ),
69
+ )
70
+
71
+ db3 = Elkrb::Graph::Node.new(
72
+ id: "db3",
73
+ width: 80,
74
+ height: 50,
75
+ constraints: Elkrb::Graph::NodeConstraints.new(
76
+ align_group: "databases",
77
+ align_direction: "horizontal",
78
+ ),
79
+ )
80
+
81
+ graph2.children = [db1, db2, db3]
82
+
83
+ result2 = Elkrb.layout(graph2, algorithm: "box")
84
+
85
+ y1 = result2.children[0].y
86
+ y2 = result2.children[1].y
87
+ y3 = result2.children[2].y
88
+
89
+ puts "Database positions:"
90
+ puts " db1 y: #{y1}"
91
+ puts " db2 y: #{y2}"
92
+ puts " db3 y: #{y3}"
93
+ puts " Aligned: #{y1 == y2 && y2 == y3 ? '✓' : '✗'}"
94
+ puts
95
+
96
+ # Example 3: Layer Constraint
97
+ puts "Example 3: Layer - Three-tier architecture"
98
+ puts "-" * 70
99
+
100
+ graph3 = Elkrb::Graph::Graph.new(id: "layer_example")
101
+
102
+ # Tier 0: Frontend
103
+ frontend = Elkrb::Graph::Node.new(
104
+ id: "frontend",
105
+ width: 100,
106
+ height: 60,
107
+ constraints: Elkrb::Graph::NodeConstraints.new(layer: 0),
108
+ )
109
+
110
+ # Tier 1: Backend
111
+ backend = Elkrb::Graph::Node.new(
112
+ id: "backend",
113
+ width: 100,
114
+ height: 60,
115
+ constraints: Elkrb::Graph::NodeConstraints.new(layer: 1),
116
+ )
117
+
118
+ # Tier 2: Database
119
+ database = Elkrb::Graph::Node.new(
120
+ id: "database",
121
+ width: 100,
122
+ height: 60,
123
+ constraints: Elkrb::Graph::NodeConstraints.new(layer: 2),
124
+ )
125
+
126
+ graph3.children = [frontend, backend, database]
127
+ graph3.edges = [
128
+ Elkrb::Graph::Edge.new(id: "e1", sources: ["frontend"], targets: ["backend"]),
129
+ Elkrb::Graph::Edge.new(id: "e2", sources: ["backend"], targets: ["database"]),
130
+ ]
131
+
132
+ result3 = Elkrb.layout(graph3, algorithm: "layered")
133
+
134
+ puts "Layer positions (y coordinates):"
135
+ puts " Frontend (layer 0): y = #{result3.children[0].y.round(1)}"
136
+ puts " Backend (layer 1): y = #{result3.children[1].y.round(1)}"
137
+ puts " Database (layer 2): y = #{result3.children[2].y.round(1)}"
138
+ puts " Ordered correctly: #{result3.children[0].y < result3.children[1].y && result3.children[1].y < result3.children[2].y ? '✓' : '✗'}"
139
+ puts
140
+
141
+ # Example 4: Relative Position Constraint
142
+ puts "Example 4: Relative Position - API next to backend service"
143
+ puts "-" * 70
144
+
145
+ graph4 = Elkrb::Graph::Graph.new(id: "relative_example")
146
+
147
+ # Backend service (positioned by algorithm)
148
+ backend_svc = Elkrb::Graph::Node.new(
149
+ id: "backend_svc",
150
+ width: 100,
151
+ height: 60,
152
+ )
153
+
154
+ # API positioned 150px to the right of backend
155
+ offset = Elkrb::Graph::RelativeOffset.new(x: 150, y: 0)
156
+ api = Elkrb::Graph::Node.new(
157
+ id: "api",
158
+ width: 100,
159
+ height: 60,
160
+ constraints: Elkrb::Graph::NodeConstraints.new(
161
+ relative_to: "backend_svc",
162
+ relative_offset: offset,
163
+ ),
164
+ )
165
+
166
+ graph4.children = [backend_svc, api]
167
+
168
+ result4 = Elkrb.layout(graph4, algorithm: "box")
169
+
170
+ backend_x = result4.children[0].x
171
+ api_x = result4.children[1].x
172
+ expected_offset = 150
173
+
174
+ puts "Backend position: x = #{backend_x}"
175
+ puts "API position: x = #{api_x}"
176
+ puts "Offset: #{(api_x - backend_x).round(1)} (expected: #{expected_offset})"
177
+ puts "Correct offset: #{(api_x - backend_x - expected_offset).abs < 0.1 ? '✓' : '✗'}"
178
+ puts
179
+
180
+ # Example 5: Combined Constraints
181
+ puts "Example 5: Combined - Microservices architecture with multiple constraints"
182
+ puts "-" * 70
183
+
184
+ graph5 = Elkrb::Graph::Graph.new(id: "microservices")
185
+
186
+ # API Gateway (fixed position, layer 0)
187
+ gateway5 = Elkrb::Graph::Node.new(
188
+ id: "api_gateway",
189
+ width: 120,
190
+ height: 60,
191
+ x: 500,
192
+ y: 100,
193
+ constraints: Elkrb::Graph::NodeConstraints.new(
194
+ fixed_position: true,
195
+ layer: 0,
196
+ ),
197
+ )
198
+
199
+ # Backend services (aligned, layer 1)
200
+ auth5 = Elkrb::Graph::Node.new(
201
+ id: "auth_svc",
202
+ width: 100,
203
+ height: 60,
204
+ constraints: Elkrb::Graph::NodeConstraints.new(
205
+ layer: 1,
206
+ align_group: "backend",
207
+ align_direction: "horizontal",
208
+ ),
209
+ )
210
+
211
+ user5 = Elkrb::Graph::Node.new(
212
+ id: "user_svc",
213
+ width: 100,
214
+ height: 60,
215
+ constraints: Elkrb::Graph::NodeConstraints.new(
216
+ layer: 1,
217
+ align_group: "backend",
218
+ align_direction: "horizontal",
219
+ ),
220
+ )
221
+
222
+ # Databases (aligned, layer 2, relative to services)
223
+ auth_db = Elkrb::Graph::Node.new(
224
+ id: "auth_db",
225
+ width: 80,
226
+ height: 50,
227
+ constraints: Elkrb::Graph::NodeConstraints.new(
228
+ layer: 2,
229
+ align_group: "databases",
230
+ align_direction: "horizontal",
231
+ ),
232
+ )
233
+
234
+ user_db = Elkrb::Graph::Node.new(
235
+ id: "user_db",
236
+ width: 80,
237
+ height: 50,
238
+ constraints: Elkrb::Graph::NodeConstraints.new(
239
+ layer: 2,
240
+ align_group: "databases",
241
+ align_direction: "horizontal",
242
+ ),
243
+ )
244
+
245
+ graph5.children = [gateway5, auth5, user5, auth_db, user_db]
246
+ graph5.edges = [
247
+ Elkrb::Graph::Edge.new(id: "e1", sources: ["api_gateway"],
248
+ targets: ["auth_svc"]),
249
+ Elkrb::Graph::Edge.new(id: "e2", sources: ["api_gateway"],
250
+ targets: ["user_svc"]),
251
+ Elkrb::Graph::Edge.new(id: "e3", sources: ["auth_svc"], targets: ["auth_db"]),
252
+ Elkrb::Graph::Edge.new(id: "e4", sources: ["user_svc"], targets: ["user_db"]),
253
+ ]
254
+
255
+ result5 = Elkrb.layout(graph5, algorithm: "layered", "elk.direction" => "DOWN")
256
+
257
+ puts "Results:"
258
+ puts " Gateway (fixed): (#{result5.children[0].x}, #{result5.children[0].y})"
259
+ puts " Backend services:"
260
+ puts " auth: y = #{result5.children[1].y.round(1)}"
261
+ puts " user: y = #{result5.children[2].y.round(1)}"
262
+ puts " Aligned: #{result5.children[1].y == result5.children[2].y ? '✓' : '✗'}"
263
+ puts " Databases:"
264
+ puts " auth_db: y = #{result5.children[3].y.round(1)}"
265
+ puts " user_db: y = #{result5.children[4].y.round(1)}"
266
+ puts " Aligned: #{result5.children[3].y == result5.children[4].y ? '✓' : '✗'}"
267
+ puts
268
+
269
+ puts "=" * 70
270
+ puts "All examples completed successfully!"
271
+ puts "See docs/LAYOUT_CONSTRAINTS_GUIDE.md for detailed documentation"
272
+ puts "=" * 70
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/elkrb"
5
+
6
+ # Example 1: Manual Port Sides
7
+ puts "=" * 60
8
+ puts "Example 1: Manual Port Sides"
9
+ puts "=" * 60
10
+
11
+ graph1 = Elkrb::Graph::Graph.new(id: "g1")
12
+
13
+ # Create a node with ports on specific sides
14
+ node1 = Elkrb::Graph::Node.new(
15
+ id: "n1",
16
+ width: 100,
17
+ height: 60,
18
+ x: 50,
19
+ y: 50,
20
+ )
21
+
22
+ # Input ports on the left (WEST)
23
+ node1.ports = [
24
+ Elkrb::Graph::Port.new(id: "in1", side: "WEST", index: 0),
25
+ Elkrb::Graph::Port.new(id: "in2", side: "WEST", index: 1),
26
+ ]
27
+
28
+ # Output port on the right (EAST)
29
+ node2 = Elkrb::Graph::Node.new(
30
+ id: "n2",
31
+ width: 100,
32
+ height: 60,
33
+ x: 200,
34
+ y: 50,
35
+ )
36
+ node2.ports = [
37
+ Elkrb::Graph::Port.new(id: "out1", side: "EAST", index: 0),
38
+ ]
39
+
40
+ graph1.children = [node1, node2]
41
+
42
+ # Create edges connecting the ports
43
+ graph1.edges = [
44
+ Elkrb::Graph::Edge.new(
45
+ id: "e1",
46
+ sources: ["in1"],
47
+ targets: ["out1"],
48
+ ),
49
+ ]
50
+
51
+ # Apply layout
52
+ engine = Elkrb::Layout::LayoutEngine.new
53
+ result1 = engine.layout(graph1)
54
+
55
+ puts "\nNode 1 Ports (after layout):"
56
+ result1.children[0].ports.each do |port|
57
+ puts " #{port.id}: side=#{port.side}, x=#{port.x}, y=#{port.y}, " \
58
+ "index=#{port.index}"
59
+ end
60
+
61
+ puts "\nNode 2 Ports (after layout):"
62
+ result1.children[1].ports.each do |port|
63
+ puts " #{port.id}: side=#{port.side}, x=#{port.x}, y=#{port.y}, " \
64
+ "index=#{port.index}"
65
+ end
66
+
67
+ # Example 2: Automatic Side Detection
68
+ puts "\n"
69
+ puts "=" * 60
70
+ puts "Example 2: Automatic Side Detection from Positions"
71
+ puts "=" * 60
72
+
73
+ graph2 = Elkrb::Graph::Graph.new(id: "g2")
74
+
75
+ # Create node with ports at specific positions
76
+ # The layout engine will detect which side each port belongs to
77
+ node3 = Elkrb::Graph::Node.new(
78
+ id: "n3",
79
+ width: 120,
80
+ height: 80,
81
+ x: 50,
82
+ y: 50,
83
+ )
84
+
85
+ node3.ports = [
86
+ Elkrb::Graph::Port.new(id: "p1", x: 60, y: 0), # Top - should detect NORTH
87
+ Elkrb::Graph::Port.new(id: "p2", x: 60, y: 80), # Bottom - should detect SOUTH
88
+ Elkrb::Graph::Port.new(id: "p3", x: 0, y: 40), # Left - should detect WEST
89
+ Elkrb::Graph::Port.new(id: "p4", x: 120, y: 40), # Right - should detect EAST
90
+ ]
91
+
92
+ graph2.children = [node3]
93
+
94
+ # Apply layout
95
+ result2 = engine.layout(graph2)
96
+
97
+ puts "\nPort Side Detection Results:"
98
+ result2.children[0].ports.each do |port|
99
+ puts " #{port.id}: detected side=#{port.side}, position=(#{port.x}, #{port.y})"
100
+ end
101
+
102
+ # Example 3: Port Ordering
103
+ puts "\n"
104
+ puts "=" * 60
105
+ puts "Example 3: Port Ordering within Sides"
106
+ puts "=" * 60
107
+
108
+ graph3 = Elkrb::Graph::Graph.new(id: "g3")
109
+
110
+ # Create node with multiple ports on the same side
111
+ node4 = Elkrb::Graph::Node.new(
112
+ id: "n4",
113
+ width: 150,
114
+ height: 100,
115
+ x: 50,
116
+ y: 50,
117
+ )
118
+
119
+ # Multiple ports on NORTH side with explicit ordering
120
+ node4.ports = [
121
+ Elkrb::Graph::Port.new(id: "north3", side: "NORTH", index: 2),
122
+ Elkrb::Graph::Port.new(id: "north1", side: "NORTH", index: 0),
123
+ Elkrb::Graph::Port.new(id: "north2", side: "NORTH", index: 1),
124
+ Elkrb::Graph::Port.new(id: "south1", side: "SOUTH", index: 0),
125
+ Elkrb::Graph::Port.new(id: "south2", side: "SOUTH", index: 1),
126
+ ]
127
+
128
+ graph3.children = [node4]
129
+
130
+ # Apply layout
131
+ result3 = engine.layout(graph3)
132
+
133
+ puts "\nNORTH Side Ports (ordered):"
134
+ north_ports = result3.children[0].ports.select { |p| p.side == "NORTH" }
135
+ north_ports.sort_by(&:index).each do |port|
136
+ puts " #{port.id}: index=#{port.index}, x=#{port.x.round(2)}, " \
137
+ "offset=#{port.offset.round(2)}"
138
+ end
139
+
140
+ puts "\nSOUTH Side Ports (ordered):"
141
+ south_ports = result3.children[0].ports.select { |p| p.side == "SOUTH" }
142
+ south_ports.sort_by(&:index).each do |port|
143
+ puts " #{port.id}: index=#{port.index}, x=#{port.x.round(2)}, " \
144
+ "offset=#{port.offset.round(2)}"
145
+ end
146
+
147
+ # Example 4: Port Constraints in Layout Options
148
+ puts "\n"
149
+ puts "=" * 60
150
+ puts "Example 4: Port Constraints via Layout Options"
151
+ puts "=" * 60
152
+
153
+ graph4 = Elkrb::Graph::Graph.new(id: "g4")
154
+
155
+ # Set port constraint options
156
+ graph4.layout_options = Elkrb::Graph::LayoutOptions.new
157
+ graph4.layout_options["elk.portConstraints"] = "FIXED_SIDE"
158
+ graph4.layout_options["elk.portSideAssignment"] = "AUTOMATIC"
159
+ graph4.layout_options["elk.portOrdering"] = "INDEX"
160
+
161
+ puts "\nLayout Options:"
162
+ puts " Port Constraints: #{graph4.layout_options.port_constraints}"
163
+ puts " Port Side Assignment: #{graph4.layout_options.port_side_assignment}"
164
+ puts " Port Ordering: #{graph4.layout_options.port_ordering}"
165
+
166
+ # Create node with ports
167
+ node5 = Elkrb::Graph::Node.new(
168
+ id: "n5",
169
+ width: 100,
170
+ height: 60,
171
+ x: 50,
172
+ y: 50,
173
+ )
174
+
175
+ node5.ports = [
176
+ Elkrb::Graph::Port.new(id: "p1", x: 25, y: 0),
177
+ Elkrb::Graph::Port.new(id: "p2", x: 75, y: 0),
178
+ Elkrb::Graph::Port.new(id: "p3", x: 0, y: 30),
179
+ ]
180
+
181
+ graph4.children = [node5]
182
+
183
+ # Apply layout
184
+ result4 = engine.layout(graph4)
185
+
186
+ puts "\nPorts after layout with constraints:"
187
+ result4.children[0].ports.each do |port|
188
+ puts " #{port.id}: side=#{port.side}, index=#{port.index}, " \
189
+ "pos=(#{port.x.round(2)}, #{port.y.round(2)})"
190
+ end
191
+
192
+ # Example 5: Complete Diagram with Port-Based Routing
193
+ puts "\n"
194
+ puts "=" * 60
195
+ puts "Example 5: Complete Diagram with Port-Based Routing"
196
+ puts "=" * 60
197
+
198
+ graph5 = Elkrb::Graph::Graph.new(id: "g5")
199
+ graph5.layout_options = Elkrb::Graph::LayoutOptions.new
200
+ graph5.layout_options["elk.algorithm"] = "layered"
201
+ graph5.layout_options["elk.edgeRouting"] = "ORTHOGONAL"
202
+
203
+ # Create multiple nodes with ports
204
+ nodeA = Elkrb::Graph::Node.new(
205
+ id: "A",
206
+ width: 80,
207
+ height: 50,
208
+ labels: [Elkrb::Graph::Label.new(text: "Node A")],
209
+ )
210
+ nodeA.ports = [
211
+ Elkrb::Graph::Port.new(id: "a_out1", side: "EAST", index: 0),
212
+ Elkrb::Graph::Port.new(id: "a_out2", side: "EAST", index: 1),
213
+ ]
214
+
215
+ nodeB = Elkrb::Graph::Node.new(
216
+ id: "B",
217
+ width: 80,
218
+ height: 50,
219
+ labels: [Elkrb::Graph::Label.new(text: "Node B")],
220
+ )
221
+ nodeB.ports = [
222
+ Elkrb::Graph::Port.new(id: "b_in", side: "WEST", index: 0),
223
+ Elkrb::Graph::Port.new(id: "b_out", side: "EAST", index: 0),
224
+ ]
225
+
226
+ nodeC = Elkrb::Graph::Node.new(
227
+ id: "C",
228
+ width: 80,
229
+ height: 50,
230
+ labels: [Elkrb::Graph::Label.new(text: "Node C")],
231
+ )
232
+ nodeC.ports = [
233
+ Elkrb::Graph::Port.new(id: "c_in1", side: "WEST", index: 0),
234
+ Elkrb::Graph::Port.new(id: "c_in2", side: "WEST", index: 1),
235
+ ]
236
+
237
+ graph5.children = [nodeA, nodeB, nodeC]
238
+
239
+ # Connect nodes via ports
240
+ graph5.edges = [
241
+ Elkrb::Graph::Edge.new(
242
+ id: "e1",
243
+ sources: ["a_out1"],
244
+ targets: ["b_in"],
245
+ ),
246
+ Elkrb::Graph::Edge.new(
247
+ id: "e2",
248
+ sources: ["a_out2"],
249
+ targets: ["c_in1"],
250
+ ),
251
+ Elkrb::Graph::Edge.new(
252
+ id: "e3",
253
+ sources: ["b_out"],
254
+ targets: ["c_in2"],
255
+ ),
256
+ ]
257
+
258
+ # Apply layout
259
+ result5 = engine.layout(graph5)
260
+
261
+ puts "\nFinal Node Positions:"
262
+ result5.children.each do |node|
263
+ puts " #{node.id}: (#{node.x.round(2)}, #{node.y.round(2)})"
264
+ node.ports.each do |port|
265
+ abs_x = node.x + port.x
266
+ abs_y = node.y + port.y
267
+ puts " #{port.id}: side=#{port.side}, " \
268
+ "absolute_pos=(#{abs_x.round(2)}, #{abs_y.round(2)})"
269
+ end
270
+ end
271
+
272
+ puts "\nEdge Routing:"
273
+ result5.edges.each do |edge|
274
+ section = edge.sections&.first
275
+ next unless section
276
+
277
+ puts " #{edge.id}:"
278
+ puts " Start: (#{section.start_point.x.round(2)}, " \
279
+ "#{section.start_point.y.round(2)})"
280
+ if section.bend_points&.any?
281
+ section.bend_points.each_with_index do |bp, idx|
282
+ puts " Bend #{idx + 1}: (#{bp.x.round(2)}, #{bp.y.round(2)})"
283
+ end
284
+ end
285
+ puts " End: (#{section.end_point.x.round(2)}, " \
286
+ "#{section.end_point.y.round(2)})"
287
+ end
288
+
289
+ puts "\n#{'=' * 60}"
290
+ puts "Port Constraints Demo Complete!"
291
+ puts "=" * 60