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
|
@@ -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
|