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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "../geometry/point"
5
+
6
+ module Elkrb
7
+ module Graph
8
+ # Represents a section of an edge with routing information
9
+ class EdgeSection < Lutaml::Model::Serializable
10
+ attribute :id, :string
11
+ attribute :start_point, Geometry::Point
12
+ attribute :end_point, Geometry::Point
13
+ attribute :bend_points, Geometry::Point, collection: true
14
+ attribute :incoming_shape, :string
15
+ attribute :outgoing_shape, :string
16
+
17
+ json do
18
+ map "id", to: :id
19
+ map "startPoint", to: :start_point
20
+ map "endPoint", to: :end_point
21
+ map "bendPoints", to: :bend_points
22
+ map "incomingShape", to: :incoming_shape
23
+ map "outgoingShape", to: :outgoing_shape
24
+ end
25
+
26
+ yaml do
27
+ map "id", to: :id
28
+ map "start_point", to: :start_point
29
+ map "end_point", to: :end_point
30
+ map "bend_points", to: :bend_points
31
+ map "incoming_shape", to: :incoming_shape
32
+ map "outgoing_shape", to: :outgoing_shape
33
+ end
34
+
35
+ def initialize(**attributes)
36
+ super
37
+ @bend_points ||= []
38
+ end
39
+
40
+ # Add a bend point to this section
41
+ def add_bend_point(x, y)
42
+ @bend_points ||= []
43
+ @bend_points << Geometry::Point.new(x: x, y: y)
44
+ end
45
+
46
+ # Get total length of this section
47
+ def length
48
+ return 0.0 if !start_point || !end_point
49
+
50
+ total = 0.0
51
+ points = [start_point] + (bend_points || []) + [end_point]
52
+
53
+ (0...(points.length - 1)).each do |i|
54
+ p1 = points[i]
55
+ p2 = points[i + 1]
56
+ dx = p2.x - p1.x
57
+ dy = p2.y - p1.y
58
+ total += Math.sqrt((dx * dx) + (dy * dy))
59
+ end
60
+
61
+ total
62
+ end
63
+ end
64
+
65
+ class Edge < Lutaml::Model::Serializable
66
+ attribute :id, :string
67
+ attribute :sources, :string, collection: true
68
+ attribute :targets, :string, collection: true
69
+ attribute :labels, Label, collection: true
70
+ attribute :sections, EdgeSection, collection: true
71
+ attribute :layout_options, LayoutOptions
72
+ attribute :properties, :hash
73
+
74
+ json do
75
+ map "id", to: :id
76
+ map "sources", to: :sources
77
+ map "targets", to: :targets
78
+ map "labels", to: :labels
79
+ map "sections", to: :sections
80
+ map "layoutOptions", to: :layout_options
81
+ map "properties", to: :properties
82
+ end
83
+
84
+ yaml do
85
+ map "id", to: :id
86
+ map "sources", to: :sources
87
+ map "targets", to: :targets
88
+ map "labels", to: :labels
89
+ map "sections", to: :sections
90
+ map "layout_options", to: :layout_options
91
+ map "properties", to: :properties
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Elkrb
6
+ module Graph
7
+ class Graph < Lutaml::Model::Serializable
8
+ attribute :id, :string
9
+ attribute :x, :float
10
+ attribute :y, :float
11
+ attribute :width, :float
12
+ attribute :height, :float
13
+ attribute :children, Node, collection: true
14
+ attribute :edges, Edge, collection: true
15
+ attribute :layout_options, LayoutOptions
16
+ attribute :properties, :hash
17
+
18
+ json do
19
+ map "id", to: :id
20
+ map "x", to: :x
21
+ map "y", to: :y
22
+ map "width", to: :width
23
+ map "height", to: :height
24
+ map "children", to: :children
25
+ map "edges", to: :edges
26
+ map "layoutOptions", to: :layout_options
27
+ map "properties", to: :properties
28
+ end
29
+
30
+ yaml do
31
+ map "id", to: :id
32
+ map "x", to: :x
33
+ map "y", to: :y
34
+ map "width", to: :width
35
+ map "height", to: :height
36
+ map "children", to: :children
37
+ map "edges", to: :edges
38
+ map "layout_options", to: :layout_options
39
+ map "properties", to: :properties
40
+ end
41
+
42
+ def initialize(**attributes)
43
+ super
44
+ @id ||= "root"
45
+ @x ||= 0.0
46
+ @y ||= 0.0
47
+ @width ||= 0.0
48
+ @height ||= 0.0
49
+ @children ||= []
50
+ @edges ||= []
51
+ @properties ||= {}
52
+ @layout_options ||= LayoutOptions.new
53
+ end
54
+
55
+ def find_node(node_id)
56
+ @children.each do |child|
57
+ found = child.find_node(node_id)
58
+ return found if found
59
+ end
60
+ nil
61
+ end
62
+
63
+ def all_nodes
64
+ nodes = []
65
+ @children.each do |child|
66
+ nodes.concat(child.all_nodes)
67
+ end
68
+ nodes
69
+ end
70
+
71
+ def all_edges
72
+ edges = @edges.dup
73
+ @children.each do |child|
74
+ edges.concat(child.edges) if child.respond_to?(:edges)
75
+ next unless child.respond_to?(:children)
76
+
77
+ child.children.each do |grandchild|
78
+ edges.concat(grandchild.all_edges) if
79
+ grandchild.respond_to?(:all_edges)
80
+ end
81
+ end
82
+ edges
83
+ end
84
+
85
+ def hierarchical?
86
+ @children.any?(&:hierarchical?)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Elkrb
6
+ module Graph
7
+ class Label < Lutaml::Model::Serializable
8
+ attribute :id, :string
9
+ attribute :text, :string
10
+ attribute :x, :float
11
+ attribute :y, :float
12
+ attribute :width, :float
13
+ attribute :height, :float
14
+ attribute :layout_options, LayoutOptions
15
+
16
+ json do
17
+ map "id", to: :id
18
+ map "text", to: :text
19
+ map "x", to: :x
20
+ map "y", to: :y
21
+ map "width", to: :width
22
+ map "height", to: :height
23
+ map "layoutOptions", to: :layout_options
24
+ end
25
+
26
+ yaml do
27
+ map "id", to: :id
28
+ map "text", to: :text
29
+ map "x", to: :x
30
+ map "y", to: :y
31
+ map "width", to: :width
32
+ map "height", to: :height
33
+ map "layout_options", to: :layout_options
34
+ end
35
+
36
+ def initialize(**attributes)
37
+ super
38
+ @x ||= 0.0
39
+ @y ||= 0.0
40
+ @width ||= 0.0
41
+ @height ||= 0.0
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Elkrb
6
+ module Graph
7
+ class LayoutOptions < Lutaml::Model::Serializable
8
+ attribute :algorithm, :string
9
+ attribute :direction, :string
10
+ attribute :spacing_node_node, :float
11
+ attribute :spacing_edge_node, :float
12
+ attribute :spacing_edge_edge, :float
13
+ attribute :spacing_node_label, :float
14
+ attribute :edge_routing, :string
15
+ attribute :spline_curvature, :float
16
+ attribute :spline_segments, :integer
17
+ attribute :hierarchical, :boolean
18
+ attribute :interactive_layout, :boolean
19
+ attribute :aspect_ratio, :float
20
+ attribute :node_placement_strategy, :string
21
+ attribute :crossing_minimization_strategy, :string
22
+ attribute :layer_constraint, :string
23
+ attribute :cycle_breaking_strategy, :string
24
+ attribute :properties, :hash
25
+
26
+ json do
27
+ map "algorithm", to: :algorithm
28
+ map "direction", to: :direction
29
+ map "spacing.nodeNode", to: :spacing_node_node
30
+ map "spacing.edgeNode", to: :spacing_edge_node
31
+ map "spacing.edgeEdge", to: :spacing_edge_edge
32
+ map "spacing.nodeLabel", to: :spacing_node_label
33
+ map "edgeRouting", to: :edge_routing
34
+ map "elk.edgeRouting", to: :edge_routing
35
+ map "spline.curvature", to: :spline_curvature
36
+ map "elk.spline.curvature", to: :spline_curvature
37
+ map "spline.segments", to: :spline_segments
38
+ map "elk.spline.segments", to: :spline_segments
39
+ map "hierarchical", to: :hierarchical
40
+ map "interactiveLayout", to: :interactive_layout
41
+ map "aspectRatio", to: :aspect_ratio
42
+ map "nodePlacement.strategy", to: :node_placement_strategy
43
+ map "crossingMinimization.strategy",
44
+ to: :crossing_minimization_strategy
45
+ map "layerConstraint", to: :layer_constraint
46
+ map "cycleBreaking.strategy", to: :cycle_breaking_strategy
47
+ map "properties", to: :properties
48
+ end
49
+
50
+ yaml do
51
+ map "algorithm", to: :algorithm
52
+ map "direction", to: :direction
53
+ map "spacing_node_node", to: :spacing_node_node
54
+ map "spacing_edge_node", to: :spacing_edge_node
55
+ map "spacing_edge_edge", to: :spacing_edge_edge
56
+ map "spacing_node_label", to: :spacing_node_label
57
+ map "edge_routing", to: :edge_routing
58
+ map "spline_curvature", to: :spline_curvature
59
+ map "spline_segments", to: :spline_segments
60
+ map "hierarchical", to: :hierarchical
61
+ map "interactive_layout", to: :interactive_layout
62
+ map "aspect_ratio", to: :aspect_ratio
63
+ map "node_placement_strategy", to: :node_placement_strategy
64
+ map "crossing_minimization_strategy",
65
+ to: :crossing_minimization_strategy
66
+ map "layer_constraint", to: :layer_constraint
67
+ map "cycle_breaking_strategy", to: :cycle_breaking_strategy
68
+ map "properties", to: :properties
69
+ end
70
+
71
+ def initialize(hash_or_attrs = {}, **attributes)
72
+ # Handle both hash argument and keyword arguments
73
+ if hash_or_attrs.is_a?(Hash) && attributes.empty?
74
+ # Plain hash passed as first argument
75
+ # Skip calling super and set defaults manually
76
+ @properties = hash_or_attrs.transform_keys(&:to_s)
77
+ @algorithm = nil
78
+ @direction = nil
79
+ @spacing_node_node = nil
80
+ @spacing_edge_node = nil
81
+ @spacing_edge_edge = nil
82
+ @spacing_node_label = nil
83
+ @edge_routing = nil
84
+ @spline_curvature = nil
85
+ @spline_segments = nil
86
+ @hierarchical = nil
87
+ @interactive_layout = nil
88
+ @aspect_ratio = nil
89
+ @node_placement_strategy = nil
90
+ @crossing_minimization_strategy = nil
91
+ @layer_constraint = nil
92
+ @cycle_breaking_strategy = nil
93
+ else
94
+ # Keyword arguments
95
+ super(**attributes)
96
+ @properties ||= {}
97
+ end
98
+ end
99
+
100
+ def []=(key, value)
101
+ @properties[key.to_s] = value
102
+ end
103
+
104
+ def [](key)
105
+ @properties[key.to_s]
106
+ end
107
+
108
+ def merge(other_options)
109
+ return self unless other_options
110
+
111
+ other_options.each do |key, value|
112
+ self[key] = value
113
+ end
114
+ self
115
+ end
116
+
117
+ # Port constraint options
118
+
119
+ # Get port constraints setting
120
+ #
121
+ # Port constraint values:
122
+ # - "UNDEFINED" - No constraints (default)
123
+ # - "FIXED_SIDE" - Port sides are fixed
124
+ # - "FIXED_ORDER" - Port sides and order are fixed
125
+ # - "FIXED_POS" - Port positions are completely fixed
126
+ #
127
+ # @return [String] The port constraints setting
128
+ def port_constraints
129
+ properties["elk.portConstraints"] ||
130
+ properties["portConstraints"] ||
131
+ "UNDEFINED"
132
+ end
133
+
134
+ # Set port constraints
135
+ #
136
+ # @param value [String] The port constraints value
137
+ def port_constraints=(value)
138
+ properties["elk.portConstraints"] = value
139
+ end
140
+
141
+ # Get port side assignment setting
142
+ #
143
+ # Port side assignment values:
144
+ # - "AUTOMATIC" - Auto-detect from position (default)
145
+ # - "MANUAL" - Use explicitly specified sides
146
+ #
147
+ # @return [String] The port side assignment setting
148
+ def port_side_assignment
149
+ properties["elk.portSideAssignment"] ||
150
+ properties["portSideAssignment"] ||
151
+ "AUTOMATIC"
152
+ end
153
+
154
+ # Set port side assignment
155
+ #
156
+ # @param value [String] The port side assignment value
157
+ def port_side_assignment=(value)
158
+ properties["elk.portSideAssignment"] = value
159
+ end
160
+
161
+ # Get port ordering setting
162
+ #
163
+ # Port ordering values:
164
+ # - "DEFAULT" - Algorithm-specific default
165
+ # - "INDEX" - Use port index attribute
166
+ # - "OFFSET" - Use port offset/position
167
+ #
168
+ # @return [String] The port ordering setting
169
+ def port_ordering
170
+ properties["elk.portOrdering"] ||
171
+ properties["portOrdering"] ||
172
+ "DEFAULT"
173
+ end
174
+
175
+ # Set port ordering
176
+ #
177
+ # @param value [String] The port ordering value
178
+ def port_ordering=(value)
179
+ properties["elk.portOrdering"] = value
180
+ end
181
+
182
+ # Self-loop options
183
+
184
+ # Get self-loop side setting
185
+ #
186
+ # Self-loop side values:
187
+ # - "EAST" - Loop extends to the right (default)
188
+ # - "WEST" - Loop extends to the left
189
+ # - "NORTH" - Loop extends upward
190
+ # - "SOUTH" - Loop extends downward
191
+ #
192
+ # @return [String] The self-loop side setting
193
+ def self_loop_side
194
+ properties["elk.selfLoopSide"] ||
195
+ properties["selfLoopSide"] ||
196
+ "EAST"
197
+ end
198
+
199
+ # Set self-loop side
200
+ #
201
+ # @param value [String] The self-loop side value
202
+ def self_loop_side=(value)
203
+ properties["elk.selfLoopSide"] = value
204
+ end
205
+
206
+ # Get self-loop offset setting
207
+ #
208
+ # Controls the base distance the self-loop extends from the node.
209
+ # Multiple self-loops on the same node will use increasing offsets.
210
+ #
211
+ # @return [Float] The self-loop offset (default: 20.0)
212
+ def self_loop_offset
213
+ (properties["elk.selfLoopOffset"] ||
214
+ properties["selfLoopOffset"] ||
215
+ 20.0).to_f
216
+ end
217
+
218
+ # Set self-loop offset
219
+ #
220
+ # @param value [Float] The self-loop offset value
221
+ def self_loop_offset=(value)
222
+ properties["elk.selfLoopOffset"] = value
223
+ end
224
+
225
+ # Get self-loop routing style
226
+ #
227
+ # Self-loop routing values:
228
+ # - "ORTHOGONAL" - Rectangular path with 90-degree corners (default)
229
+ # - "SPLINES" - Smooth curved path using Bezier curves
230
+ # - "POLYLINE" - Simple polyline path
231
+ #
232
+ # @return [String] The self-loop routing style
233
+ def self_loop_routing
234
+ properties["elk.selfLoopRouting"] ||
235
+ properties["selfLoopRouting"] ||
236
+ "ORTHOGONAL"
237
+ end
238
+
239
+ # Set self-loop routing style
240
+ #
241
+ # @param value [String] The self-loop routing style value
242
+ def self_loop_routing=(value)
243
+ properties["elk.selfLoopRouting"] = value
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "node_constraints"
5
+
6
+ module Elkrb
7
+ module Graph
8
+ class Node < Lutaml::Model::Serializable
9
+ attribute :id, :string
10
+ attribute :x, :float
11
+ attribute :y, :float
12
+ attribute :width, :float
13
+ attribute :height, :float
14
+ attribute :labels, Label, collection: true
15
+ attribute :ports, Port, collection: true
16
+ attribute :children, Node, collection: true
17
+ attribute :edges, Edge, collection: true
18
+ attribute :layout_options, LayoutOptions
19
+ attribute :constraints, NodeConstraints
20
+ attribute :properties, :hash
21
+
22
+ json do
23
+ map "id", to: :id
24
+ map "x", to: :x
25
+ map "y", to: :y
26
+ map "width", to: :width
27
+ map "height", to: :height
28
+ map "labels", to: :labels
29
+ map "ports", to: :ports
30
+ map "children", to: :children
31
+ map "edges", to: :edges
32
+ map "layoutOptions", to: :layout_options
33
+ map "constraints", to: :constraints
34
+ map "properties", to: :properties
35
+ end
36
+
37
+ yaml do
38
+ map "id", to: :id
39
+ map "x", to: :x
40
+ map "y", to: :y
41
+ map "width", to: :width
42
+ map "height", to: :height
43
+ map "labels", to: :labels
44
+ map "ports", to: :ports
45
+ map "children", to: :children
46
+ map "edges", to: :edges
47
+ map "layout_options", to: :layout_options
48
+ map "constraints", to: :constraints
49
+ map "properties", to: :properties
50
+ end
51
+
52
+ def hierarchical?
53
+ @children && !@children.empty?
54
+ end
55
+
56
+ def find_node(node_id)
57
+ return self if @id == node_id
58
+
59
+ return nil unless @children
60
+
61
+ @children.each do |child|
62
+ found = child.find_node(node_id)
63
+ return found if found
64
+ end
65
+ nil
66
+ end
67
+
68
+ def all_nodes
69
+ nodes = [self]
70
+ return nodes unless @children
71
+
72
+ @children.each do |child|
73
+ nodes.concat(child.all_nodes)
74
+ end
75
+ nodes
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Elkrb
6
+ module Graph
7
+ # Relative offset for positioning
8
+ #
9
+ # Specifies x and y offset from a reference node.
10
+ #
11
+ # @example
12
+ # offset = RelativeOffset.new(x: 100, y: 50)
13
+ # # Position 100px right, 50px down from reference
14
+ class RelativeOffset < Lutaml::Model::Serializable
15
+ attribute :x, :float, default: -> { 0.0 }
16
+ attribute :y, :float, default: -> { 0.0 }
17
+
18
+ json do
19
+ map "x", to: :x
20
+ map "y", to: :y
21
+ end
22
+
23
+ yaml do
24
+ map "x", to: :x
25
+ map "y", to: :y
26
+ end
27
+ end
28
+
29
+ # Node positioning constraints
30
+ #
31
+ # Allows precise control over node placement through various constraint types:
32
+ # - Fixed position: Lock node at specific coordinates
33
+ # - Alignment: Align nodes horizontally or vertically
34
+ # - Layer: Force node into specific layer (for layered algorithm)
35
+ # - Relative position: Position relative to another node
36
+ #
37
+ # @example Fixed position constraint
38
+ # constraints = NodeConstraints.new(fixed_position: true)
39
+ # node.constraints = constraints
40
+ # node.x = 100
41
+ # node.y = 200
42
+ # # Node won't move during layout
43
+ #
44
+ # @example Alignment constraint
45
+ # constraints = NodeConstraints.new(
46
+ # align_group: "databases",
47
+ # align_direction: "horizontal"
48
+ # )
49
+ # # All nodes in "databases" group will align horizontally
50
+ #
51
+ # @example Layer constraint
52
+ # constraints = NodeConstraints.new(layer: 2)
53
+ # # Node forced into layer 2 (for layered algorithm)
54
+ #
55
+ # @example Relative position constraint
56
+ # offset = RelativeOffset.new(x: 150, y: 0)
57
+ # constraints = NodeConstraints.new(
58
+ # relative_to: "backend_service",
59
+ # relative_offset: offset
60
+ # )
61
+ # # Node positioned 150px right of backend_service
62
+ class NodeConstraints < Lutaml::Model::Serializable
63
+ attribute :fixed_position, :boolean, default: -> { false }
64
+ attribute :layer, :integer
65
+ attribute :align_group, :string
66
+ attribute :align_direction, :string
67
+ attribute :relative_to, :string
68
+ attribute :relative_offset, RelativeOffset
69
+ attribute :position_priority, :integer, default: -> { 0 }
70
+
71
+ json do
72
+ map "fixedPosition", to: :fixed_position
73
+ map "layer", to: :layer
74
+ map "alignGroup", to: :align_group
75
+ map "alignDirection", to: :align_direction
76
+ map "relativeTo", to: :relative_to
77
+ map "relativeOffset", to: :relative_offset
78
+ map "positionPriority", to: :position_priority
79
+ end
80
+
81
+ yaml do
82
+ map "fixedPosition", to: :fixed_position
83
+ map "layer", to: :layer
84
+ map "alignGroup", to: :align_group
85
+ map "alignDirection", to: :align_direction
86
+ map "relativeTo", to: :relative_to
87
+ map "relativeOffset", to: :relative_offset
88
+ map "positionPriority", to: :position_priority
89
+ end
90
+
91
+ # Valid alignment directions
92
+ HORIZONTAL = "horizontal"
93
+ VERTICAL = "vertical"
94
+ ALIGN_DIRECTIONS = [HORIZONTAL, VERTICAL].freeze
95
+
96
+ # Validate alignment direction
97
+ def align_direction=(value)
98
+ if value && !ALIGN_DIRECTIONS.include?(value.to_s.downcase)
99
+ raise ArgumentError,
100
+ "Invalid align_direction: #{value}. " \
101
+ "Must be #{ALIGN_DIRECTIONS.join(' or ')}"
102
+ end
103
+ @align_direction = value&.to_s&.downcase
104
+ end
105
+ end
106
+ end
107
+ end