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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/Gemfile +13 -0
- data/README.adoc +1028 -0
- data/Rakefile +64 -0
- data/benchmarks/README.md +172 -0
- data/benchmarks/elkjs_benchmark.js +140 -0
- data/benchmarks/elkrb_benchmark.rb +145 -0
- data/benchmarks/fixtures/graphs.json +10777 -0
- data/benchmarks/generate_report.rb +241 -0
- data/benchmarks/generate_test_graphs.rb +154 -0
- data/benchmarks/results/elkrb_results.json +280 -0
- data/benchmarks/results/elkrb_summary.json +285 -0
- data/elkrb.gemspec +39 -0
- data/examples/dot_export_demo.rb +133 -0
- data/examples/hierarchical_graph.rb +19 -0
- data/examples/layout_constraints_demo.rb +272 -0
- data/examples/port_constraints_demo.rb +291 -0
- data/examples/self_loop_demo.rb +391 -0
- data/examples/simple_graph.rb +50 -0
- data/examples/spline_routing_demo.rb +235 -0
- data/exe/elkrb +8 -0
- data/lib/elkrb/cli.rb +224 -0
- data/lib/elkrb/commands/batch_command.rb +66 -0
- data/lib/elkrb/commands/convert_command.rb +130 -0
- data/lib/elkrb/commands/diagram_command.rb +208 -0
- data/lib/elkrb/commands/render_command.rb +52 -0
- data/lib/elkrb/commands/validate_command.rb +241 -0
- data/lib/elkrb/errors.rb +30 -0
- data/lib/elkrb/geometry/bezier.rb +163 -0
- data/lib/elkrb/geometry/dimension.rb +32 -0
- data/lib/elkrb/geometry/point.rb +68 -0
- data/lib/elkrb/geometry/rectangle.rb +86 -0
- data/lib/elkrb/geometry/vector.rb +67 -0
- data/lib/elkrb/graph/edge.rb +95 -0
- data/lib/elkrb/graph/graph.rb +90 -0
- data/lib/elkrb/graph/label.rb +45 -0
- data/lib/elkrb/graph/layout_options.rb +247 -0
- data/lib/elkrb/graph/node.rb +79 -0
- data/lib/elkrb/graph/node_constraints.rb +107 -0
- data/lib/elkrb/graph/port.rb +104 -0
- data/lib/elkrb/graphviz_wrapper.rb +133 -0
- data/lib/elkrb/layout/algorithm_registry.rb +57 -0
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +208 -0
- data/lib/elkrb/layout/algorithms/box.rb +47 -0
- data/lib/elkrb/layout/algorithms/disco.rb +206 -0
- data/lib/elkrb/layout/algorithms/fixed.rb +32 -0
- data/lib/elkrb/layout/algorithms/force.rb +165 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +86 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +96 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +77 -0
- data/lib/elkrb/layout/algorithms/layered.rb +49 -0
- data/lib/elkrb/layout/algorithms/libavoid.rb +389 -0
- data/lib/elkrb/layout/algorithms/mrtree.rb +144 -0
- data/lib/elkrb/layout/algorithms/radial.rb +64 -0
- data/lib/elkrb/layout/algorithms/random.rb +43 -0
- data/lib/elkrb/layout/algorithms/rectpacking.rb +93 -0
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +139 -0
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +117 -0
- data/lib/elkrb/layout/algorithms/stress.rb +176 -0
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +183 -0
- data/lib/elkrb/layout/algorithms/vertiflex.rb +174 -0
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +150 -0
- data/lib/elkrb/layout/constraints/base_constraint.rb +72 -0
- data/lib/elkrb/layout/constraints/constraint_processor.rb +134 -0
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +87 -0
- data/lib/elkrb/layout/constraints/layer_constraint.rb +71 -0
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +110 -0
- data/lib/elkrb/layout/edge_router.rb +935 -0
- data/lib/elkrb/layout/hierarchical_processor.rb +299 -0
- data/lib/elkrb/layout/label_placer.rb +338 -0
- data/lib/elkrb/layout/layout_engine.rb +170 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +173 -0
- data/lib/elkrb/options/elk_padding.rb +94 -0
- data/lib/elkrb/options/k_vector.rb +100 -0
- data/lib/elkrb/options/k_vector_chain.rb +135 -0
- data/lib/elkrb/parsers/elkt_parser.rb +248 -0
- data/lib/elkrb/serializers/dot_serializer.rb +339 -0
- data/lib/elkrb/serializers/elkt_serializer.rb +236 -0
- data/lib/elkrb/version.rb +5 -0
- data/lib/elkrb.rb +509 -0
- data/sig/elkrb/constraints.rbs +114 -0
- data/sig/elkrb/geometry.rbs +61 -0
- data/sig/elkrb/graph.rbs +112 -0
- data/sig/elkrb/layout.rbs +107 -0
- data/sig/elkrb/options.rbs +81 -0
- data/sig/elkrb.rbs +32 -0
- metadata +179 -0
data/lib/elkrb.rb
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "elkrb/version"
|
|
4
|
+
require_relative "elkrb/errors"
|
|
5
|
+
|
|
6
|
+
# Geometry
|
|
7
|
+
require_relative "elkrb/geometry/point"
|
|
8
|
+
require_relative "elkrb/geometry/dimension"
|
|
9
|
+
require_relative "elkrb/geometry/rectangle"
|
|
10
|
+
require_relative "elkrb/geometry/vector"
|
|
11
|
+
|
|
12
|
+
# Graph models
|
|
13
|
+
require_relative "elkrb/graph/layout_options"
|
|
14
|
+
require_relative "elkrb/graph/label"
|
|
15
|
+
require_relative "elkrb/graph/port"
|
|
16
|
+
require_relative "elkrb/graph/node_constraints"
|
|
17
|
+
require_relative "elkrb/graph/edge"
|
|
18
|
+
require_relative "elkrb/graph/node"
|
|
19
|
+
require_relative "elkrb/graph/graph"
|
|
20
|
+
|
|
21
|
+
# Serializers
|
|
22
|
+
require_relative "elkrb/serializers/dot_serializer"
|
|
23
|
+
|
|
24
|
+
# Options parsers
|
|
25
|
+
require_relative "elkrb/options/elk_padding"
|
|
26
|
+
require_relative "elkrb/options/k_vector"
|
|
27
|
+
require_relative "elkrb/options/k_vector_chain"
|
|
28
|
+
|
|
29
|
+
# Layout constraints
|
|
30
|
+
require_relative "elkrb/layout/constraints/base_constraint"
|
|
31
|
+
require_relative "elkrb/layout/constraints/fixed_position_constraint"
|
|
32
|
+
require_relative "elkrb/layout/constraints/alignment_constraint"
|
|
33
|
+
require_relative "elkrb/layout/constraints/layer_constraint"
|
|
34
|
+
require_relative "elkrb/layout/constraints/relative_position_constraint"
|
|
35
|
+
require_relative "elkrb/layout/constraints/constraint_processor"
|
|
36
|
+
|
|
37
|
+
# Layout engine
|
|
38
|
+
require_relative "elkrb/layout/algorithm_registry"
|
|
39
|
+
require_relative "elkrb/layout/layout_engine"
|
|
40
|
+
require_relative "elkrb/layout/algorithms/base_algorithm"
|
|
41
|
+
require_relative "elkrb/layout/algorithms/random"
|
|
42
|
+
require_relative "elkrb/layout/algorithms/fixed"
|
|
43
|
+
require_relative "elkrb/layout/algorithms/box"
|
|
44
|
+
require_relative "elkrb/layout/algorithms/layered"
|
|
45
|
+
require_relative "elkrb/layout/algorithms/force"
|
|
46
|
+
require_relative "elkrb/layout/algorithms/stress"
|
|
47
|
+
require_relative "elkrb/layout/algorithms/mrtree"
|
|
48
|
+
require_relative "elkrb/layout/algorithms/radial"
|
|
49
|
+
require_relative "elkrb/layout/algorithms/rectpacking"
|
|
50
|
+
require_relative "elkrb/layout/algorithms/topdown_packing"
|
|
51
|
+
require_relative "elkrb/layout/algorithms/disco"
|
|
52
|
+
require_relative "elkrb/layout/algorithms/spore_overlap"
|
|
53
|
+
require_relative "elkrb/layout/algorithms/spore_compaction"
|
|
54
|
+
require_relative "elkrb/layout/algorithms/libavoid"
|
|
55
|
+
require_relative "elkrb/layout/algorithms/vertiflex"
|
|
56
|
+
|
|
57
|
+
# ElkRb - Pure Ruby implementation of the Eclipse Layout Kernel
|
|
58
|
+
#
|
|
59
|
+
# ElkRb provides automatic graph layout algorithms for node-link diagrams.
|
|
60
|
+
# It implements 12 layout algorithms from the Eclipse Layout Kernel (ELK),
|
|
61
|
+
# supporting hierarchical graphs, port-based connections, and automatic
|
|
62
|
+
# label placement.
|
|
63
|
+
#
|
|
64
|
+
# @example Basic usage
|
|
65
|
+
# require 'elkrb'
|
|
66
|
+
#
|
|
67
|
+
# graph = {
|
|
68
|
+
# id: "root",
|
|
69
|
+
# layoutOptions: { "elk.algorithm" => "layered" },
|
|
70
|
+
# children: [
|
|
71
|
+
# { id: "n1", width: 100, height: 60 },
|
|
72
|
+
# { id: "n2", width: 100, height: 60 }
|
|
73
|
+
# ],
|
|
74
|
+
# edges: [
|
|
75
|
+
# { id: "e1", sources: ["n1"], targets: ["n2"] }
|
|
76
|
+
# ]
|
|
77
|
+
# }
|
|
78
|
+
#
|
|
79
|
+
# result = Elkrb.layout(graph)
|
|
80
|
+
# puts result[:children][0][:x] # Node positions computed
|
|
81
|
+
#
|
|
82
|
+
# @example Using model classes
|
|
83
|
+
# graph = Elkrb::Graph::Graph.new(id: "root")
|
|
84
|
+
# node = Elkrb::Graph::Node.new(id: "n1", width: 100, height: 60)
|
|
85
|
+
# graph.children = [node]
|
|
86
|
+
#
|
|
87
|
+
# result = Elkrb.layout(graph, algorithm: "force")
|
|
88
|
+
#
|
|
89
|
+
# @example Querying available algorithms
|
|
90
|
+
# Elkrb.known_layout_algorithms.each do |alg|
|
|
91
|
+
# puts "#{alg[:id]}: #{alg[:description]}"
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# @see https://www.eclipse.org/elk/ Eclipse Layout Kernel
|
|
95
|
+
# @see https://github.com/kieler/elkjs elkjs - JavaScript port
|
|
96
|
+
module Elkrb
|
|
97
|
+
# Register basic layout algorithms
|
|
98
|
+
Layout::AlgorithmRegistry.register(
|
|
99
|
+
"random",
|
|
100
|
+
Layout::Algorithms::Random,
|
|
101
|
+
{
|
|
102
|
+
name: "Random",
|
|
103
|
+
description: "Places nodes at random positions",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
Layout::AlgorithmRegistry.register(
|
|
108
|
+
"fixed",
|
|
109
|
+
Layout::Algorithms::Fixed,
|
|
110
|
+
{
|
|
111
|
+
name: "Fixed",
|
|
112
|
+
description: "Keeps nodes at their current positions",
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
Layout::AlgorithmRegistry.register(
|
|
117
|
+
"box",
|
|
118
|
+
Layout::Algorithms::Box,
|
|
119
|
+
{
|
|
120
|
+
name: "Box",
|
|
121
|
+
description: "Arranges nodes in a grid pattern",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
Layout::AlgorithmRegistry.register(
|
|
126
|
+
"layered",
|
|
127
|
+
Layout::Algorithms::LayeredAlgorithm,
|
|
128
|
+
{
|
|
129
|
+
name: "Layered (Sugiyama)",
|
|
130
|
+
description: "Hierarchical layout using the Sugiyama framework",
|
|
131
|
+
category: "hierarchical",
|
|
132
|
+
supports_hierarchy: true,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
Layout::AlgorithmRegistry.register(
|
|
137
|
+
"force",
|
|
138
|
+
Layout::Algorithms::Force,
|
|
139
|
+
{
|
|
140
|
+
name: "Force-Directed",
|
|
141
|
+
description: "Physics-based layout using attractive and repulsive forces",
|
|
142
|
+
category: "force",
|
|
143
|
+
supports_hierarchy: false,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
Layout::AlgorithmRegistry.register(
|
|
148
|
+
"stress",
|
|
149
|
+
Layout::Algorithms::Stress,
|
|
150
|
+
{
|
|
151
|
+
name: "Stress Minimization",
|
|
152
|
+
description: "High-quality layout using stress majorization",
|
|
153
|
+
category: "force",
|
|
154
|
+
supports_hierarchy: false,
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
Layout::AlgorithmRegistry.register(
|
|
159
|
+
"mrtree",
|
|
160
|
+
Layout::Algorithms::MRTree,
|
|
161
|
+
{
|
|
162
|
+
name: "Multi-Rooted Tree",
|
|
163
|
+
description: "Tree layout supporting multiple root nodes",
|
|
164
|
+
category: "hierarchical",
|
|
165
|
+
supports_hierarchy: true,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
Layout::AlgorithmRegistry.register(
|
|
170
|
+
"radial",
|
|
171
|
+
Layout::Algorithms::Radial,
|
|
172
|
+
{
|
|
173
|
+
name: "Radial",
|
|
174
|
+
description: "Circular/radial node arrangement",
|
|
175
|
+
category: "general",
|
|
176
|
+
supports_hierarchy: false,
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
Layout::AlgorithmRegistry.register(
|
|
181
|
+
"rectpacking",
|
|
182
|
+
Layout::Algorithms::RectPacking,
|
|
183
|
+
{
|
|
184
|
+
name: "Rectangle Packing",
|
|
185
|
+
description: "Efficient rectangle bin packing layout",
|
|
186
|
+
category: "packing",
|
|
187
|
+
supports_hierarchy: false,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
Layout::AlgorithmRegistry.register(
|
|
192
|
+
"topdownpacking",
|
|
193
|
+
Layout::Algorithms::TopdownPacking,
|
|
194
|
+
{
|
|
195
|
+
name: "TopdownPacking",
|
|
196
|
+
description: "Top-down rectangle packing with hierarchical space partitioning",
|
|
197
|
+
category: "packing",
|
|
198
|
+
supports_hierarchy: false,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
Layout::AlgorithmRegistry.register(
|
|
203
|
+
"disco",
|
|
204
|
+
Layout::Algorithms::Disco,
|
|
205
|
+
{
|
|
206
|
+
name: "DISCO",
|
|
207
|
+
description: "Layout for disconnected graph components",
|
|
208
|
+
category: "general",
|
|
209
|
+
supports_hierarchy: false,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
Layout::AlgorithmRegistry.register(
|
|
214
|
+
"spore_overlap",
|
|
215
|
+
Layout::Algorithms::SporeOverlap,
|
|
216
|
+
{
|
|
217
|
+
name: "SPOrE Overlap Removal",
|
|
218
|
+
description: "Removes node overlaps while preserving structure",
|
|
219
|
+
category: "optimization",
|
|
220
|
+
supports_hierarchy: false,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
Layout::AlgorithmRegistry.register(
|
|
225
|
+
"spore_compaction",
|
|
226
|
+
Layout::Algorithms::SporeCompaction,
|
|
227
|
+
{
|
|
228
|
+
name: "SPOrE Compaction",
|
|
229
|
+
description: "Compacts layout by removing whitespace",
|
|
230
|
+
category: "optimization",
|
|
231
|
+
supports_hierarchy: false,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
Layout::AlgorithmRegistry.register(
|
|
236
|
+
"libavoid",
|
|
237
|
+
Layout::Algorithms::Libavoid,
|
|
238
|
+
{
|
|
239
|
+
name: "Libavoid",
|
|
240
|
+
description: "Orthogonal connector routing with obstacle avoidance",
|
|
241
|
+
category: "routing",
|
|
242
|
+
supports_hierarchy: false,
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
Layout::AlgorithmRegistry.register(
|
|
247
|
+
"vertiflex",
|
|
248
|
+
Layout::Algorithms::VertiFlex,
|
|
249
|
+
{
|
|
250
|
+
name: "VertiFlex",
|
|
251
|
+
description: "Vertical flexible layout with column-based arrangement",
|
|
252
|
+
category: "layered",
|
|
253
|
+
supports_hierarchy: false,
|
|
254
|
+
experimental: true,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Performs layout on a graph using the specified algorithm.
|
|
259
|
+
#
|
|
260
|
+
# This is the main entry point for ElkRb. It accepts either a Hash or
|
|
261
|
+
# a Graph model object and applies the chosen layout algorithm to compute
|
|
262
|
+
# node positions and edge routes.
|
|
263
|
+
#
|
|
264
|
+
# @param graph [Hash, Graph::Graph] The graph to layout
|
|
265
|
+
# @param options [Hash] Layout options including:
|
|
266
|
+
# - :algorithm (String) - Algorithm name (default: "layered")
|
|
267
|
+
# - Algorithm-specific options (e.g., "elk.spacing.nodeNode")
|
|
268
|
+
# @return [Graph::Graph] The input graph with computed positions
|
|
269
|
+
#
|
|
270
|
+
# @example With hash input
|
|
271
|
+
# result = Elkrb.layout({
|
|
272
|
+
# id: "root",
|
|
273
|
+
# children: [{ id: "n1", width: 100, height: 60 }]
|
|
274
|
+
# })
|
|
275
|
+
#
|
|
276
|
+
# @example With specific algorithm
|
|
277
|
+
# result = Elkrb.layout(graph, algorithm: "force")
|
|
278
|
+
#
|
|
279
|
+
# @example With algorithm options
|
|
280
|
+
# result = Elkrb.layout(graph,
|
|
281
|
+
# algorithm: "layered",
|
|
282
|
+
# "elk.direction" => "DOWN",
|
|
283
|
+
# "elk.spacing.nodeNode" => 50
|
|
284
|
+
# )
|
|
285
|
+
def self.layout(graph, options = {})
|
|
286
|
+
Layout::LayoutEngine.layout(graph, options)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Exports a graph to Graphviz DOT format.
|
|
290
|
+
#
|
|
291
|
+
# This is a convenience method that delegates to the LayoutEngine.
|
|
292
|
+
# It serializes an ELK graph structure to DOT format that can be
|
|
293
|
+
# rendered by Graphviz or other DOT-compatible tools.
|
|
294
|
+
#
|
|
295
|
+
# @param graph [Hash, Graph::Graph] The graph to export
|
|
296
|
+
# @param options [Hash] Serialization options:
|
|
297
|
+
# - :directed (Boolean) - Whether graph is directed (default: true)
|
|
298
|
+
# - :rankdir (String) - Layout direction (TB, LR, BT, RL)
|
|
299
|
+
# - :graph_name (String) - Name for the graph (default: "G")
|
|
300
|
+
# - :graph_attrs (Hash) - Additional graph attributes
|
|
301
|
+
# - :node_attrs (Hash) - Default node attributes
|
|
302
|
+
# - :edge_attrs (Hash) - Default edge attributes
|
|
303
|
+
# @return [String] DOT format string
|
|
304
|
+
#
|
|
305
|
+
# @example Basic export
|
|
306
|
+
# dot = Elkrb.export_dot(graph)
|
|
307
|
+
# File.write("output.dot", dot)
|
|
308
|
+
#
|
|
309
|
+
# @example Layout then export
|
|
310
|
+
# graph = Elkrb.layout(graph, algorithm: "layered")
|
|
311
|
+
# dot = Elkrb.export_dot(graph, rankdir: "LR")
|
|
312
|
+
# File.write("output.dot", dot)
|
|
313
|
+
#
|
|
314
|
+
# @example With custom attributes
|
|
315
|
+
# dot = Elkrb.export_dot(graph,
|
|
316
|
+
# graph_name: "MyGraph",
|
|
317
|
+
# graph_attrs: { bgcolor: "lightgray" },
|
|
318
|
+
# node_attrs: { shape: "box", color: "blue" }
|
|
319
|
+
# )
|
|
320
|
+
def self.export_dot(graph, options = {})
|
|
321
|
+
Layout::LayoutEngine.export_dot(graph, options)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Registers a custom layout algorithm.
|
|
325
|
+
#
|
|
326
|
+
# This allows you to extend ElkRb with your own layout algorithms.
|
|
327
|
+
# The algorithm class must implement the layout interface (inherit from
|
|
328
|
+
# BaseAlgorithm or implement a compatible interface).
|
|
329
|
+
#
|
|
330
|
+
# @param name [String] Unique algorithm identifier
|
|
331
|
+
# @param algorithm_class [Class] Algorithm implementation class
|
|
332
|
+
# @param metadata [Hash] Optional metadata:
|
|
333
|
+
# - :name (String) - Display name
|
|
334
|
+
# - :description (String) - Brief description
|
|
335
|
+
# - :category (String) - Algorithm category
|
|
336
|
+
# - :supports_hierarchy (Boolean) - Hierarchical support
|
|
337
|
+
#
|
|
338
|
+
# @example Register custom algorithm
|
|
339
|
+
# class MyAlgorithm < Elkrb::Layout::Algorithms::BaseAlgorithm
|
|
340
|
+
# def layout_flat(graph, options = {})
|
|
341
|
+
# # Custom layout logic
|
|
342
|
+
# graph
|
|
343
|
+
# end
|
|
344
|
+
# end
|
|
345
|
+
#
|
|
346
|
+
# Elkrb.register_algorithm("my_algo", MyAlgorithm, {
|
|
347
|
+
# name: "My Algorithm",
|
|
348
|
+
# description: "Custom layout algorithm",
|
|
349
|
+
# category: "general"
|
|
350
|
+
# })
|
|
351
|
+
#
|
|
352
|
+
# # Use it
|
|
353
|
+
# Elkrb.layout(graph, algorithm: "my_algo")
|
|
354
|
+
def self.register_algorithm(name, algorithm_class, metadata = {})
|
|
355
|
+
Layout::AlgorithmRegistry.register(name, algorithm_class, metadata)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Returns metadata for all available layout algorithms.
|
|
359
|
+
#
|
|
360
|
+
# This includes the 12 built-in algorithms plus any custom algorithms
|
|
361
|
+
# registered via {register_algorithm}.
|
|
362
|
+
#
|
|
363
|
+
# @return [Array<Hash>] Array of algorithm metadata with keys:
|
|
364
|
+
# - :id (String) - Algorithm identifier
|
|
365
|
+
# - :name (String) - Display name
|
|
366
|
+
# - :description (String) - Brief description
|
|
367
|
+
# - :category (String) - Algorithm category
|
|
368
|
+
# - :supports_hierarchy (Boolean) - Whether it supports nested graphs
|
|
369
|
+
#
|
|
370
|
+
# @example List all algorithms
|
|
371
|
+
# Elkrb.known_layout_algorithms.each do |alg|
|
|
372
|
+
# puts "#{alg[:id]}: #{alg[:description]}"
|
|
373
|
+
# end
|
|
374
|
+
#
|
|
375
|
+
# @example Filter by category
|
|
376
|
+
# force_algorithms = Elkrb.known_layout_algorithms
|
|
377
|
+
# .select { |alg| alg[:category] == "force" }
|
|
378
|
+
def self.known_layout_algorithms
|
|
379
|
+
Layout::AlgorithmRegistry.all.map do |name, data|
|
|
380
|
+
{
|
|
381
|
+
id: name,
|
|
382
|
+
name: data[:metadata][:name] || name.capitalize,
|
|
383
|
+
description: data[:metadata][:description] || "",
|
|
384
|
+
category: data[:metadata][:category] || "general",
|
|
385
|
+
supports_hierarchy: data[:metadata][:supports_hierarchy] || false,
|
|
386
|
+
}
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Returns metadata for all supported layout options.
|
|
391
|
+
#
|
|
392
|
+
# This includes common options like algorithm selection, spacing,
|
|
393
|
+
# direction, and padding, as well as algorithm-specific options.
|
|
394
|
+
#
|
|
395
|
+
# @return [Hash{String => Hash}] Hash mapping option names to metadata.
|
|
396
|
+
# Each metadata hash contains:
|
|
397
|
+
# - :type (String) - Option value type
|
|
398
|
+
# - :description (String) - Brief description
|
|
399
|
+
# - :default - Default value
|
|
400
|
+
# - :values (Array, nil) - Allowed values for enum types
|
|
401
|
+
# - :parser (String, nil) - Parser class for complex types
|
|
402
|
+
#
|
|
403
|
+
# @example Query options
|
|
404
|
+
# options = Elkrb.known_layout_options
|
|
405
|
+
# puts options["elk.direction"][:description]
|
|
406
|
+
# # => "Overall direction of layout"
|
|
407
|
+
#
|
|
408
|
+
# @example Get allowed values
|
|
409
|
+
# directions = Elkrb.known_layout_options["elk.direction"][:values]
|
|
410
|
+
# # => ["UP", "DOWN", "LEFT", "RIGHT"]
|
|
411
|
+
def self.known_layout_options
|
|
412
|
+
{
|
|
413
|
+
"algorithm" => {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "The layout algorithm to use",
|
|
416
|
+
default: "layered",
|
|
417
|
+
values: Layout::AlgorithmRegistry.all.keys,
|
|
418
|
+
},
|
|
419
|
+
"elk.direction" => {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Overall direction of layout",
|
|
422
|
+
default: "RIGHT",
|
|
423
|
+
values: %w[UP DOWN LEFT RIGHT],
|
|
424
|
+
},
|
|
425
|
+
"elk.spacing.nodeNode" => {
|
|
426
|
+
type: "float",
|
|
427
|
+
description: "Spacing between nodes",
|
|
428
|
+
default: 20.0,
|
|
429
|
+
},
|
|
430
|
+
"elk.padding" => {
|
|
431
|
+
type: "ElkPadding",
|
|
432
|
+
description: "Padding around the graph",
|
|
433
|
+
default: "[left=12, top=12, right=12, bottom=12]",
|
|
434
|
+
parser: "Elkrb::Options::ElkPadding",
|
|
435
|
+
},
|
|
436
|
+
"position" => {
|
|
437
|
+
type: "KVector",
|
|
438
|
+
description: "Fixed position for nodes (when using fixed algorithm)",
|
|
439
|
+
default: nil,
|
|
440
|
+
parser: "Elkrb::Options::KVector",
|
|
441
|
+
},
|
|
442
|
+
"bendPoints" => {
|
|
443
|
+
type: "KVectorChain",
|
|
444
|
+
description: "Bend points for edges",
|
|
445
|
+
default: nil,
|
|
446
|
+
parser: "Elkrb::Options::KVectorChain",
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Returns metadata for layout option categories.
|
|
452
|
+
#
|
|
453
|
+
# Categories help organize the many layout options into logical groups.
|
|
454
|
+
# There are 9 categories covering different aspects of layout.
|
|
455
|
+
#
|
|
456
|
+
# @return [Hash{String => Hash}] Hash mapping category names to metadata.
|
|
457
|
+
# Each metadata hash contains:
|
|
458
|
+
# - :name (String) - Display name
|
|
459
|
+
# - :description (String) - Brief description
|
|
460
|
+
#
|
|
461
|
+
# @example List categories
|
|
462
|
+
# Elkrb.known_layout_categories.each do |id, info|
|
|
463
|
+
# puts "#{id}: #{info[:description]}"
|
|
464
|
+
# end
|
|
465
|
+
#
|
|
466
|
+
# @example Get specific category
|
|
467
|
+
# force_category = Elkrb.known_layout_categories["force"]
|
|
468
|
+
# puts force_category[:name] # => "Force-Directed"
|
|
469
|
+
def self.known_layout_categories
|
|
470
|
+
{
|
|
471
|
+
"general" => {
|
|
472
|
+
name: "General",
|
|
473
|
+
description: "General layout options",
|
|
474
|
+
},
|
|
475
|
+
"spacing" => {
|
|
476
|
+
name: "Spacing",
|
|
477
|
+
description: "Options for controlling spacing between elements",
|
|
478
|
+
},
|
|
479
|
+
"alignment" => {
|
|
480
|
+
name: "Alignment",
|
|
481
|
+
description: "Options for controlling element alignment",
|
|
482
|
+
},
|
|
483
|
+
"direction" => {
|
|
484
|
+
name: "Direction",
|
|
485
|
+
description: "Options for controlling layout direction",
|
|
486
|
+
},
|
|
487
|
+
"ports" => {
|
|
488
|
+
name: "Ports",
|
|
489
|
+
description: "Options for port handling",
|
|
490
|
+
},
|
|
491
|
+
"edges" => {
|
|
492
|
+
name: "Edges",
|
|
493
|
+
description: "Options for edge routing",
|
|
494
|
+
},
|
|
495
|
+
"hierarchical" => {
|
|
496
|
+
name: "Hierarchical",
|
|
497
|
+
description: "Options for hierarchical layouts",
|
|
498
|
+
},
|
|
499
|
+
"force" => {
|
|
500
|
+
name: "Force-Directed",
|
|
501
|
+
description: "Options for force-directed layouts",
|
|
502
|
+
},
|
|
503
|
+
"optimization" => {
|
|
504
|
+
name: "Optimization",
|
|
505
|
+
description: "Options for layout optimization algorithms",
|
|
506
|
+
},
|
|
507
|
+
}
|
|
508
|
+
end
|
|
509
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Type signatures for ElkRb layout constraints
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Graph
|
|
5
|
+
# Relative offset for positioning
|
|
6
|
+
class RelativeOffset < Lutaml::Model::Serializable
|
|
7
|
+
attr_accessor x: Float
|
|
8
|
+
attr_accessor y: Float
|
|
9
|
+
|
|
10
|
+
def initialize: (?x: Float, ?y: Float) -> void
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Node positioning constraints
|
|
14
|
+
class NodeConstraints < Lutaml::Model::Serializable
|
|
15
|
+
attr_accessor fixed_position: bool
|
|
16
|
+
attr_accessor layer: Integer?
|
|
17
|
+
attr_accessor align_group: String?
|
|
18
|
+
attr_accessor align_direction: String?
|
|
19
|
+
attr_accessor relative_to: String?
|
|
20
|
+
attr_accessor relative_offset: RelativeOffset?
|
|
21
|
+
attr_accessor position_priority: Integer
|
|
22
|
+
|
|
23
|
+
HORIZONTAL: String
|
|
24
|
+
VERTICAL: String
|
|
25
|
+
ALIGN_DIRECTIONS: Array[String]
|
|
26
|
+
|
|
27
|
+
def initialize: (
|
|
28
|
+
?fixed_position: bool,
|
|
29
|
+
?layer: Integer?,
|
|
30
|
+
?align_group: String?,
|
|
31
|
+
?align_direction: String?,
|
|
32
|
+
?relative_to: String?,
|
|
33
|
+
?relative_offset: RelativeOffset?,
|
|
34
|
+
?position_priority: Integer
|
|
35
|
+
) -> void
|
|
36
|
+
|
|
37
|
+
def align_direction=: (String? value) -> String?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module Layout
|
|
42
|
+
module Constraints
|
|
43
|
+
# Base constraint class
|
|
44
|
+
class BaseConstraint
|
|
45
|
+
def apply: (Graph::Graph graph) -> Graph::Graph
|
|
46
|
+
def validate: (Graph::Graph graph) -> Array[String]
|
|
47
|
+
def applies_to?: (Graph::Node node) -> bool
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def find_node: (Graph::Graph graph, String node_id) -> Graph::Node?
|
|
52
|
+
def all_nodes: (Graph::Graph graph) -> Array[Graph::Node]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Fixed position constraint
|
|
56
|
+
class FixedPositionConstraint < BaseConstraint
|
|
57
|
+
def apply: (Graph::Graph graph) -> Graph::Graph
|
|
58
|
+
def validate: (Graph::Graph graph) -> Array[String]
|
|
59
|
+
def restore_fixed_positions: (Graph::Graph graph) -> Graph::Graph
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Alignment constraint
|
|
63
|
+
class AlignmentConstraint < BaseConstraint
|
|
64
|
+
def apply: (Graph::Graph graph) -> Graph::Graph
|
|
65
|
+
def validate: (Graph::Graph graph) -> Array[String]
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def group_by_alignment: (Array[Graph::Node] nodes) -> Hash[[String, String], Array[Graph::Node]]
|
|
70
|
+
def align_nodes: (Array[Graph::Node] nodes, String direction) -> void
|
|
71
|
+
def align_horizontally: (Array[Graph::Node] nodes) -> void
|
|
72
|
+
def align_vertically: (Array[Graph::Node] nodes) -> void
|
|
73
|
+
def validate_group_alignment: (Array[Graph::Node] nodes, String group_name, String direction) -> Array[String]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Layer constraint
|
|
77
|
+
class LayerConstraint < BaseConstraint
|
|
78
|
+
def apply: (Graph::Graph graph) -> Graph::Graph
|
|
79
|
+
def validate: (Graph::Graph graph) -> Array[String]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Relative position constraint
|
|
83
|
+
class RelativePositionConstraint < BaseConstraint
|
|
84
|
+
def apply: (Graph::Graph graph) -> Graph::Graph
|
|
85
|
+
def validate: (Graph::Graph graph) -> Array[String]
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def apply_relative_position: (Graph::Node node, Graph::Graph graph) -> void
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Constraint processor
|
|
93
|
+
class ConstraintProcessor
|
|
94
|
+
PRE_LAYOUT_CONSTRAINTS: Array[singleton(BaseConstraint)]
|
|
95
|
+
POST_LAYOUT_CONSTRAINTS: Array[singleton(BaseConstraint)]
|
|
96
|
+
|
|
97
|
+
@pre_constraints: Array[BaseConstraint]
|
|
98
|
+
@post_constraints: Array[BaseConstraint]
|
|
99
|
+
@all_constraints: Array[BaseConstraint]
|
|
100
|
+
|
|
101
|
+
def initialize: () -> void
|
|
102
|
+
def apply_pre_layout: (Graph::Graph graph) -> Graph::Graph
|
|
103
|
+
def enforce_post_layout: (Graph::Graph graph) -> Graph::Graph
|
|
104
|
+
def apply_all: (Graph::Graph graph) -> Graph::Graph
|
|
105
|
+
def validate_all: (Graph::Graph graph) -> Array[String]
|
|
106
|
+
def has_constraints?: (Graph::Graph graph) -> bool
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def has_constraints_recursive?: (Graph::Node node) -> bool
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Elkrb
|
|
2
|
+
module Geometry
|
|
3
|
+
class Point
|
|
4
|
+
attr_reader x: Float
|
|
5
|
+
attr_reader y: Float
|
|
6
|
+
|
|
7
|
+
def initialize: (Float x, Float y) -> void
|
|
8
|
+
def +: (Point other) -> Point
|
|
9
|
+
def -: (Point other) -> Point
|
|
10
|
+
def *: (Float scalar) -> Point
|
|
11
|
+
def /: (Float scalar) -> Point
|
|
12
|
+
def distance_to: (Point other) -> Float
|
|
13
|
+
def ==: (untyped other) -> bool
|
|
14
|
+
def to_h: () -> Hash[Symbol, Float]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Dimension
|
|
18
|
+
attr_reader width: Float
|
|
19
|
+
attr_reader height: Float
|
|
20
|
+
|
|
21
|
+
def initialize: (Float width, Float height) -> void
|
|
22
|
+
def area: () -> Float
|
|
23
|
+
def ==: (untyped other) -> bool
|
|
24
|
+
def to_h: () -> Hash[Symbol, Float]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Rectangle
|
|
28
|
+
attr_reader x: Float
|
|
29
|
+
attr_reader y: Float
|
|
30
|
+
attr_reader width: Float
|
|
31
|
+
attr_reader height: Float
|
|
32
|
+
|
|
33
|
+
def initialize: (Float x, Float y, Float width, Float height) -> void
|
|
34
|
+
def left: () -> Float
|
|
35
|
+
def right: () -> Float
|
|
36
|
+
def top: () -> Float
|
|
37
|
+
def bottom: () -> Float
|
|
38
|
+
def center: () -> Point
|
|
39
|
+
def contains?: (Point | Rectangle point_or_rect) -> bool
|
|
40
|
+
def intersects?: (Rectangle other) -> bool
|
|
41
|
+
def ==: (untyped other) -> bool
|
|
42
|
+
def to_h: () -> Hash[Symbol, Float]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Vector
|
|
46
|
+
attr_reader x: Float
|
|
47
|
+
attr_reader y: Float
|
|
48
|
+
|
|
49
|
+
def initialize: (Float x, Float y) -> void
|
|
50
|
+
def +: (Vector other) -> Vector
|
|
51
|
+
def -: (Vector other) -> Vector
|
|
52
|
+
def *: (Float scalar) -> Vector
|
|
53
|
+
def /: (Float scalar) -> Vector
|
|
54
|
+
def magnitude: () -> Float
|
|
55
|
+
def normalize: () -> Vector
|
|
56
|
+
def dot: (Vector other) -> Float
|
|
57
|
+
def ==: (untyped other) -> bool
|
|
58
|
+
def to_h: () -> Hash[Symbol, Float]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|