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,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
# Module to add hierarchical graph layout support to algorithms.
|
|
6
|
+
# Handles recursive layout, parent-child constraints, and cross-hierarchy
|
|
7
|
+
# edges.
|
|
8
|
+
module HierarchicalProcessor
|
|
9
|
+
# Layout a graph and all its hierarchical children recursively.
|
|
10
|
+
#
|
|
11
|
+
# @param graph [Graph::Graph] The graph to layout
|
|
12
|
+
# @param options [Hash] Layout options
|
|
13
|
+
# @return [Graph::Graph] The laid out graph
|
|
14
|
+
def layout_hierarchical(graph, options = {})
|
|
15
|
+
return layout_flat(graph, options) unless graph.hierarchical?
|
|
16
|
+
|
|
17
|
+
# First, recursively layout all child nodes
|
|
18
|
+
layout_children_recursively(graph)
|
|
19
|
+
|
|
20
|
+
# Then layout the top-level graph
|
|
21
|
+
layout_flat(graph, options)
|
|
22
|
+
|
|
23
|
+
# Apply parent constraints
|
|
24
|
+
apply_parent_constraints(graph)
|
|
25
|
+
|
|
26
|
+
# Handle cross-hierarchy edges
|
|
27
|
+
handle_cross_hierarchy_edges(graph)
|
|
28
|
+
|
|
29
|
+
# Update parent bounds
|
|
30
|
+
update_parent_bounds(graph)
|
|
31
|
+
|
|
32
|
+
graph
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Layout all children of nodes in the graph recursively.
|
|
38
|
+
def layout_children_recursively(graph)
|
|
39
|
+
return unless graph.children
|
|
40
|
+
|
|
41
|
+
graph.children.each do |node|
|
|
42
|
+
next unless node.hierarchical?
|
|
43
|
+
|
|
44
|
+
# Create a temporary graph for the node's children
|
|
45
|
+
child_graph = create_child_graph(node)
|
|
46
|
+
|
|
47
|
+
# Recursively layout the child graph
|
|
48
|
+
layout_hierarchical(child_graph, extract_node_options(node))
|
|
49
|
+
|
|
50
|
+
# Apply the layout back to the node
|
|
51
|
+
apply_child_layout(node, child_graph)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Layout a flat (non-hierarchical) graph using the base algorithm.
|
|
56
|
+
def layout_flat(graph, options = {})
|
|
57
|
+
# This should be implemented by the including algorithm class
|
|
58
|
+
raise NotImplementedError,
|
|
59
|
+
"#{self.class.name} must implement #layout_flat"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create a temporary graph from a node's children.
|
|
63
|
+
def create_child_graph(node)
|
|
64
|
+
Graph::Graph.new(
|
|
65
|
+
id: "#{node.id}_children",
|
|
66
|
+
children: node.children || [],
|
|
67
|
+
edges: node.edges || [],
|
|
68
|
+
layout_options: node.layout_options,
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Extract layout options from a node.
|
|
73
|
+
def extract_node_options(node)
|
|
74
|
+
return {} unless node.layout_options
|
|
75
|
+
|
|
76
|
+
node.layout_options.properties || {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Apply the child graph layout back to the parent node.
|
|
80
|
+
def apply_child_layout(node, child_graph)
|
|
81
|
+
# Copy positions from child_graph children to node children
|
|
82
|
+
return unless child_graph.children
|
|
83
|
+
|
|
84
|
+
child_graph.children.each_with_index do |child, index|
|
|
85
|
+
if node.children && node.children[index]
|
|
86
|
+
node.children[index].x = child.x
|
|
87
|
+
node.children[index].y = child.y
|
|
88
|
+
node.children[index].width = child.width
|
|
89
|
+
node.children[index].height = child.height
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Apply parent constraints to ensure children fit within parent bounds.
|
|
95
|
+
def apply_parent_constraints(graph)
|
|
96
|
+
return unless graph.children
|
|
97
|
+
|
|
98
|
+
graph.children.each do |node|
|
|
99
|
+
next unless node.hierarchical?
|
|
100
|
+
|
|
101
|
+
# Get padding from node options
|
|
102
|
+
padding = get_padding(node)
|
|
103
|
+
|
|
104
|
+
# Adjust child positions to account for padding
|
|
105
|
+
adjust_children_for_padding(node, padding)
|
|
106
|
+
|
|
107
|
+
# Recursively apply to nested children
|
|
108
|
+
if node.children
|
|
109
|
+
temp_graph = Graph::Graph.new(children: node.children)
|
|
110
|
+
apply_parent_constraints(temp_graph)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get padding for a node from its layout options.
|
|
116
|
+
def get_padding(node)
|
|
117
|
+
return default_padding unless node.layout_options
|
|
118
|
+
|
|
119
|
+
padding_option = node.layout_options.properties&.[]("padding") ||
|
|
120
|
+
node.layout_options.properties&.[]("elk.padding")
|
|
121
|
+
|
|
122
|
+
return default_padding unless padding_option
|
|
123
|
+
|
|
124
|
+
parse_padding(padding_option)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Default padding values.
|
|
128
|
+
def default_padding
|
|
129
|
+
{ top: 12.0, right: 12.0, bottom: 12.0, left: 12.0 }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Parse padding from various formats.
|
|
133
|
+
def parse_padding(padding)
|
|
134
|
+
case padding
|
|
135
|
+
when Hash
|
|
136
|
+
{
|
|
137
|
+
top: padding[:top] || padding["top"] || 12.0,
|
|
138
|
+
right: padding[:right] || padding["right"] || 12.0,
|
|
139
|
+
bottom: padding[:bottom] || padding["bottom"] || 12.0,
|
|
140
|
+
left: padding[:left] || padding["left"] || 12.0,
|
|
141
|
+
}
|
|
142
|
+
when Numeric
|
|
143
|
+
{ top: padding, right: padding, bottom: padding, left: padding }
|
|
144
|
+
else
|
|
145
|
+
default_padding
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Adjust children positions to account for parent padding.
|
|
150
|
+
def adjust_children_for_padding(node, padding)
|
|
151
|
+
return unless node.children
|
|
152
|
+
|
|
153
|
+
node.children.each do |child|
|
|
154
|
+
child.x = (child.x || 0.0) + padding[:left]
|
|
155
|
+
child.y = (child.y || 0.0) + padding[:top]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Handle edges that cross hierarchy boundaries.
|
|
160
|
+
def handle_cross_hierarchy_edges(graph)
|
|
161
|
+
return unless graph.edges
|
|
162
|
+
|
|
163
|
+
graph.edges.each do |edge|
|
|
164
|
+
# Get source and target IDs from edge
|
|
165
|
+
source_id = edge.sources&.first
|
|
166
|
+
target_id = edge.targets&.first
|
|
167
|
+
|
|
168
|
+
next unless source_id && target_id
|
|
169
|
+
|
|
170
|
+
source_node = find_node_in_hierarchy(graph, source_id)
|
|
171
|
+
target_node = find_node_in_hierarchy(graph, target_id)
|
|
172
|
+
|
|
173
|
+
next unless source_node && target_node
|
|
174
|
+
|
|
175
|
+
# Check if edge crosses hierarchy levels
|
|
176
|
+
if crosses_hierarchy?(source_node, target_node, graph)
|
|
177
|
+
adjust_edge_for_hierarchy(edge, source_node, target_node)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Find a node anywhere in the graph hierarchy.
|
|
183
|
+
def find_node_in_hierarchy(graph, node_id)
|
|
184
|
+
return nil unless node_id
|
|
185
|
+
|
|
186
|
+
# Handle both direct node IDs and port IDs
|
|
187
|
+
node_id_str = node_id.is_a?(String) ? node_id : node_id.to_s
|
|
188
|
+
graph.find_node(node_id_str)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if an edge crosses hierarchy boundaries.
|
|
192
|
+
def crosses_hierarchy?(source_node, target_node, graph)
|
|
193
|
+
return false unless source_node && target_node
|
|
194
|
+
|
|
195
|
+
source_depth = node_depth(source_node, graph)
|
|
196
|
+
target_depth = node_depth(target_node, graph)
|
|
197
|
+
|
|
198
|
+
source_depth != target_depth
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Calculate the depth of a node in the hierarchy.
|
|
202
|
+
def node_depth(node, graph, depth = 0)
|
|
203
|
+
return depth if graph.children&.include?(node)
|
|
204
|
+
|
|
205
|
+
graph.children&.each do |child|
|
|
206
|
+
if child.hierarchical? && child.children
|
|
207
|
+
child_graph = Graph::Graph.new(children: child.children)
|
|
208
|
+
found_depth = node_depth(node, child_graph, depth + 1)
|
|
209
|
+
return found_depth if found_depth > depth
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
depth
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Adjust edge routing for cross-hierarchy edges.
|
|
217
|
+
def adjust_edge_for_hierarchy(edge, _source_node, _target_node)
|
|
218
|
+
# Add additional bend points to route around parent boundaries
|
|
219
|
+
# This is a simplified version - could be enhanced with proper routing
|
|
220
|
+
return unless edge.sections && !edge.sections.empty?
|
|
221
|
+
|
|
222
|
+
section = edge.sections.first
|
|
223
|
+
|
|
224
|
+
# Calculate midpoint
|
|
225
|
+
mid_x = (section.start_point.x + section.end_point.x) / 2.0
|
|
226
|
+
mid_y = (section.start_point.y + section.end_point.y) / 2.0
|
|
227
|
+
|
|
228
|
+
# Add a bend point at the midpoint
|
|
229
|
+
section.add_bend_point(mid_x, mid_y)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Update parent node bounds to contain all children.
|
|
233
|
+
def update_parent_bounds(graph)
|
|
234
|
+
return unless graph.children
|
|
235
|
+
|
|
236
|
+
graph.children.each do |node|
|
|
237
|
+
next unless node.hierarchical?
|
|
238
|
+
|
|
239
|
+
# Recursively update nested children first
|
|
240
|
+
if node.children
|
|
241
|
+
temp_graph = Graph::Graph.new(children: node.children)
|
|
242
|
+
update_parent_bounds(temp_graph)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Calculate bounds from children
|
|
246
|
+
bounds = calculate_children_bounds(node)
|
|
247
|
+
|
|
248
|
+
# Get padding
|
|
249
|
+
padding = get_padding(node)
|
|
250
|
+
|
|
251
|
+
# Update node dimensions
|
|
252
|
+
node.width = bounds[:width] + padding[:left] + padding[:right]
|
|
253
|
+
node.height = bounds[:height] + padding[:top] + padding[:bottom]
|
|
254
|
+
|
|
255
|
+
# Update position if needed (ensure children are at positive coords)
|
|
256
|
+
if bounds[:min_x].negative?
|
|
257
|
+
offset_x = -bounds[:min_x] + padding[:left]
|
|
258
|
+
node.children&.each { |child| child.x = (child.x || 0) + offset_x }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
if bounds[:min_y].negative?
|
|
262
|
+
offset_y = -bounds[:min_y] + padding[:top]
|
|
263
|
+
node.children&.each { |child| child.y = (child.y || 0) + offset_y }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Calculate the bounding box of a node's children.
|
|
269
|
+
def calculate_children_bounds(node)
|
|
270
|
+
return { min_x: 0, min_y: 0, width: 0, height: 0 } unless
|
|
271
|
+
node.children && !node.children.empty?
|
|
272
|
+
|
|
273
|
+
min_x = Float::INFINITY
|
|
274
|
+
min_y = Float::INFINITY
|
|
275
|
+
max_x = -Float::INFINITY
|
|
276
|
+
max_y = -Float::INFINITY
|
|
277
|
+
|
|
278
|
+
node.children.each do |child|
|
|
279
|
+
x = child.x || 0.0
|
|
280
|
+
y = child.y || 0.0
|
|
281
|
+
w = child.width || 0.0
|
|
282
|
+
h = child.height || 0.0
|
|
283
|
+
|
|
284
|
+
min_x = [min_x, x].min
|
|
285
|
+
min_y = [min_y, y].min
|
|
286
|
+
max_x = [max_x, x + w].max
|
|
287
|
+
max_y = [max_y, y + h].max
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
{
|
|
291
|
+
min_x: min_x,
|
|
292
|
+
min_y: min_y,
|
|
293
|
+
width: max_x - min_x,
|
|
294
|
+
height: max_y - min_y,
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Layout
|
|
5
|
+
# Module to add automatic label placement to layout algorithms.
|
|
6
|
+
# Handles positioning of node labels, edge labels, and port labels.
|
|
7
|
+
module LabelPlacer
|
|
8
|
+
# Place all labels in the graph after layout is complete.
|
|
9
|
+
#
|
|
10
|
+
# @param graph [Graph::Graph] The laid out graph
|
|
11
|
+
def place_labels(graph)
|
|
12
|
+
return unless graph
|
|
13
|
+
|
|
14
|
+
# Place node labels
|
|
15
|
+
place_node_labels(graph) if graph.children
|
|
16
|
+
|
|
17
|
+
# Place edge labels
|
|
18
|
+
place_edge_labels(graph) if graph.edges
|
|
19
|
+
|
|
20
|
+
# Recursively place labels in hierarchical graphs
|
|
21
|
+
place_hierarchical_labels(graph) if graph.hierarchical?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Place labels for all nodes in the graph.
|
|
27
|
+
def place_node_labels(graph)
|
|
28
|
+
graph.children.each do |node|
|
|
29
|
+
next unless node.labels && !node.labels.empty?
|
|
30
|
+
|
|
31
|
+
node.labels.each_with_index do |label, index|
|
|
32
|
+
place_node_label(node, label, index)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Place port labels if node has ports
|
|
36
|
+
place_port_labels(node) if node.ports && !node.ports.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Place a single node label.
|
|
41
|
+
def place_node_label(node, label, index = 0)
|
|
42
|
+
placement = label_placement_option(node, "node.label.placement") ||
|
|
43
|
+
"INSIDE CENTER"
|
|
44
|
+
|
|
45
|
+
case placement.upcase
|
|
46
|
+
when /INSIDE/
|
|
47
|
+
place_label_inside_node(node, label, placement, index)
|
|
48
|
+
when /OUTSIDE/
|
|
49
|
+
place_label_outside_node(node, label, placement, index)
|
|
50
|
+
else
|
|
51
|
+
# Default: center inside
|
|
52
|
+
place_label_center(node, label)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Place label inside the node bounds.
|
|
57
|
+
def place_label_inside_node(node, label, placement, index)
|
|
58
|
+
case placement.upcase
|
|
59
|
+
when /TOP/
|
|
60
|
+
place_label_inside_top(node, label, index)
|
|
61
|
+
when /BOTTOM/
|
|
62
|
+
place_label_inside_bottom(node, label, index)
|
|
63
|
+
when /LEFT/
|
|
64
|
+
place_label_inside_left(node, label, index)
|
|
65
|
+
when /RIGHT/
|
|
66
|
+
place_label_inside_right(node, label, index)
|
|
67
|
+
else
|
|
68
|
+
place_label_center(node, label)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Place label outside the node bounds.
|
|
73
|
+
def place_label_outside_node(node, label, placement, _index)
|
|
74
|
+
margin = label_margin_option(node)
|
|
75
|
+
|
|
76
|
+
case placement.upcase
|
|
77
|
+
when /TOP/
|
|
78
|
+
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
79
|
+
label.y = node.y - label.height - margin
|
|
80
|
+
when /BOTTOM/
|
|
81
|
+
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
82
|
+
label.y = node.y + node.height + margin
|
|
83
|
+
when /LEFT/
|
|
84
|
+
label.x = node.x - label.width - margin
|
|
85
|
+
label.y = node.y + ((node.height - label.height) / 2.0)
|
|
86
|
+
when /RIGHT/
|
|
87
|
+
label.x = node.x + node.width + margin
|
|
88
|
+
label.y = node.y + ((node.height - label.height) / 2.0)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Place label at various inside positions.
|
|
93
|
+
def place_label_inside_top(node, label, index)
|
|
94
|
+
padding = label_padding_option(node)
|
|
95
|
+
y_offset = index * (label.height + padding)
|
|
96
|
+
|
|
97
|
+
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
98
|
+
label.y = node.y + padding + y_offset
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def place_label_inside_bottom(node, label, index)
|
|
102
|
+
padding = label_padding_option(node)
|
|
103
|
+
y_offset = index * (label.height + padding)
|
|
104
|
+
|
|
105
|
+
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
106
|
+
label.y = node.y + node.height - label.height - padding - y_offset
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def place_label_inside_left(node, label, index)
|
|
110
|
+
padding = label_padding_option(node)
|
|
111
|
+
y_offset = index * (label.height + padding)
|
|
112
|
+
|
|
113
|
+
label.x = node.x + padding
|
|
114
|
+
label.y = node.y + padding + y_offset
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def place_label_inside_right(node, label, index)
|
|
118
|
+
padding = label_padding_option(node)
|
|
119
|
+
y_offset = index * (label.height + padding)
|
|
120
|
+
|
|
121
|
+
label.x = node.x + node.width - label.width - padding
|
|
122
|
+
label.y = node.y + padding + y_offset
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def place_label_center(node, label)
|
|
126
|
+
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
127
|
+
label.y = node.y + ((node.height - label.height) / 2.0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Place labels for all ports on a node.
|
|
131
|
+
def place_port_labels(node)
|
|
132
|
+
node.ports.each do |port|
|
|
133
|
+
next unless port.labels && !port.labels.empty?
|
|
134
|
+
|
|
135
|
+
port.labels.each_with_index do |label, index|
|
|
136
|
+
place_port_label(node, port, label, index)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Place a port label.
|
|
142
|
+
def place_port_label(node, port, label, _index)
|
|
143
|
+
# Port position relative to node
|
|
144
|
+
port_x = node.x + (port.x || 0)
|
|
145
|
+
port_y = node.y + (port.y || 0)
|
|
146
|
+
|
|
147
|
+
placement = label_placement_option(port, "port.label.placement") ||
|
|
148
|
+
"OUTSIDE"
|
|
149
|
+
|
|
150
|
+
margin = label_margin_option(port)
|
|
151
|
+
|
|
152
|
+
case placement.upcase
|
|
153
|
+
when /INSIDE/
|
|
154
|
+
# Place inside port (if port is large enough)
|
|
155
|
+
label.x = port_x + ((port.width - label.width) / 2.0)
|
|
156
|
+
label.y = port_y + ((port.height - label.height) / 2.0)
|
|
157
|
+
else
|
|
158
|
+
# Default: outside, positioned based on port side
|
|
159
|
+
side = port_side(node, port)
|
|
160
|
+
place_port_label_by_side(label, port_x, port_y, port, side, margin)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Determine which side of the node the port is on.
|
|
165
|
+
def port_side(node, port)
|
|
166
|
+
port_x = port.x || 0
|
|
167
|
+
port_y = port.y || 0
|
|
168
|
+
|
|
169
|
+
# Check which edge the port is closest to
|
|
170
|
+
left_dist = port_x
|
|
171
|
+
right_dist = node.width - port_x
|
|
172
|
+
top_dist = port_y
|
|
173
|
+
bottom_dist = node.height - port_y
|
|
174
|
+
|
|
175
|
+
min_dist = [left_dist, right_dist, top_dist, bottom_dist].min
|
|
176
|
+
|
|
177
|
+
case min_dist
|
|
178
|
+
when left_dist then :left
|
|
179
|
+
when right_dist then :right
|
|
180
|
+
when top_dist then :top
|
|
181
|
+
else :bottom
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Place port label based on port side.
|
|
186
|
+
def place_port_label_by_side(label, port_x, port_y, port, side, margin)
|
|
187
|
+
case side
|
|
188
|
+
when :left
|
|
189
|
+
label.x = port_x - label.width - margin
|
|
190
|
+
label.y = port_y + ((port.height - label.height) / 2.0)
|
|
191
|
+
when :right
|
|
192
|
+
label.x = port_x + port.width + margin
|
|
193
|
+
label.y = port_y + ((port.height - label.height) / 2.0)
|
|
194
|
+
when :top
|
|
195
|
+
label.x = port_x + ((port.width - label.width) / 2.0)
|
|
196
|
+
label.y = port_y - label.height - margin
|
|
197
|
+
when :bottom
|
|
198
|
+
label.x = port_x + ((port.width - label.width) / 2.0)
|
|
199
|
+
label.y = port_y + port.height + margin
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Place labels for all edges in the graph.
|
|
204
|
+
def place_edge_labels(graph)
|
|
205
|
+
graph.edges.each do |edge|
|
|
206
|
+
next unless edge.labels && !edge.labels.empty?
|
|
207
|
+
|
|
208
|
+
edge.labels.each_with_index do |label, index|
|
|
209
|
+
place_edge_label(edge, label, index)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Place a single edge label.
|
|
215
|
+
def place_edge_label(edge, label, index)
|
|
216
|
+
# Get edge path (sections with bend points)
|
|
217
|
+
if edge.sections && !edge.sections.empty?
|
|
218
|
+
place_edge_label_on_section(edge.sections.first, label, index)
|
|
219
|
+
else
|
|
220
|
+
# No sections, estimate from source/target
|
|
221
|
+
place_edge_label_estimated(edge, label, index)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Place edge label on an edge section.
|
|
226
|
+
def place_edge_label_on_section(section, label, index)
|
|
227
|
+
# Calculate center point of the edge path
|
|
228
|
+
center = calculate_edge_center(section)
|
|
229
|
+
|
|
230
|
+
placement = "CENTER" # Could be configurable
|
|
231
|
+
|
|
232
|
+
offset = index * (label.height + 2) # Stack multiple labels
|
|
233
|
+
|
|
234
|
+
case placement.upcase
|
|
235
|
+
when /CENTER/
|
|
236
|
+
label.x = center[:x] - (label.width / 2.0)
|
|
237
|
+
label.y = center[:y] - (label.height / 2.0) + offset
|
|
238
|
+
when /HEAD/
|
|
239
|
+
label.x = section.end_point.x - (label.width / 2.0)
|
|
240
|
+
label.y = section.end_point.y - label.height - 5 + offset
|
|
241
|
+
when /TAIL/
|
|
242
|
+
label.x = section.start_point.x - (label.width / 2.0)
|
|
243
|
+
label.y = section.start_point.y - label.height - 5 + offset
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Calculate the center point of an edge section.
|
|
248
|
+
def calculate_edge_center(section)
|
|
249
|
+
points = [section.start_point]
|
|
250
|
+
points.concat(section.bend_points) if section.bend_points
|
|
251
|
+
points << section.end_point
|
|
252
|
+
|
|
253
|
+
# Find midpoint along the path
|
|
254
|
+
total_length = 0.0
|
|
255
|
+
lengths = []
|
|
256
|
+
|
|
257
|
+
(0...(points.length - 1)).each do |i|
|
|
258
|
+
p1 = points[i]
|
|
259
|
+
p2 = points[i + 1]
|
|
260
|
+
length = Math.sqrt(((p2.x - p1.x)**2) + ((p2.y - p1.y)**2))
|
|
261
|
+
lengths << length
|
|
262
|
+
total_length += length
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Find point at half the total length
|
|
266
|
+
target_length = total_length / 2.0
|
|
267
|
+
current_length = 0.0
|
|
268
|
+
|
|
269
|
+
(0...lengths.length).each do |i|
|
|
270
|
+
if current_length + lengths[i] >= target_length
|
|
271
|
+
# Interpolate between points[i] and points[i+1]
|
|
272
|
+
ratio = (target_length - current_length) / lengths[i]
|
|
273
|
+
p1 = points[i]
|
|
274
|
+
p2 = points[i + 1]
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
x: p1.x + (ratio * (p2.x - p1.x)),
|
|
278
|
+
y: p1.y + (ratio * (p2.y - p1.y)),
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
current_length += lengths[i]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Fallback: use middle point
|
|
285
|
+
mid_point = points[points.length / 2]
|
|
286
|
+
{ x: mid_point.x, y: mid_point.y }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Estimate edge label position when no sections available.
|
|
290
|
+
def place_edge_label_estimated(_edge, label, _index)
|
|
291
|
+
# This is a fallback - in practice edges should have sections
|
|
292
|
+
# after routing, but we handle the case anyway
|
|
293
|
+
label.x = 0
|
|
294
|
+
label.y = 0
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Place labels in hierarchical child nodes.
|
|
298
|
+
def place_hierarchical_labels(graph)
|
|
299
|
+
return unless graph.children
|
|
300
|
+
|
|
301
|
+
graph.children.each do |node|
|
|
302
|
+
next unless node.hierarchical?
|
|
303
|
+
|
|
304
|
+
# Create temporary graph for children
|
|
305
|
+
child_graph = Graph::Graph.new(
|
|
306
|
+
children: node.children,
|
|
307
|
+
edges: node.edges,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Recursively place labels
|
|
311
|
+
place_labels(child_graph)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Get label placement option from node/port layout options.
|
|
316
|
+
def label_placement_option(element, option_key)
|
|
317
|
+
return nil unless element.layout_options
|
|
318
|
+
|
|
319
|
+
element.layout_options.properties&.[](option_key) ||
|
|
320
|
+
element.layout_options.properties&.[]("label.placement")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Get label padding option.
|
|
324
|
+
def label_padding_option(element)
|
|
325
|
+
return 5.0 unless element.layout_options
|
|
326
|
+
|
|
327
|
+
element.layout_options.properties&.[]("label.padding") || 5.0
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Get label margin option.
|
|
331
|
+
def label_margin_option(element)
|
|
332
|
+
return 5.0 unless element.layout_options
|
|
333
|
+
|
|
334
|
+
element.layout_options.properties&.[]("label.margin") || 5.0
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|