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,935 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../geometry/point"
|
|
4
|
+
require_relative "../geometry/bezier"
|
|
5
|
+
require_relative "../graph/edge"
|
|
6
|
+
|
|
7
|
+
module Elkrb
|
|
8
|
+
module Layout
|
|
9
|
+
# Provides edge routing functionality for layout algorithms
|
|
10
|
+
module EdgeRouter
|
|
11
|
+
# Route edges in a graph using specified routing style
|
|
12
|
+
# @param graph [Graph::Graph] The graph to route edges for
|
|
13
|
+
# @param node_map [Hash] Map of node IDs to node objects
|
|
14
|
+
# @param routing_style [String] Routing style (ORTHOGONAL, POLYLINE,
|
|
15
|
+
# SPLINES)
|
|
16
|
+
def route_edges(graph, node_map = nil, routing_style = nil)
|
|
17
|
+
node_map ||= build_node_map(graph)
|
|
18
|
+
routing_style ||= get_routing_style(graph)
|
|
19
|
+
|
|
20
|
+
graph.edges&.each do |edge|
|
|
21
|
+
if self_loop?(edge)
|
|
22
|
+
route_self_loop(edge, node_map, graph, routing_style)
|
|
23
|
+
else
|
|
24
|
+
route_edge_with_style(edge, node_map, graph, routing_style)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Route a single edge
|
|
30
|
+
# @param edge [Graph::Edge] The edge to route
|
|
31
|
+
# @param node_map [Hash] Map of node IDs to node objects
|
|
32
|
+
# @param graph [Graph::Graph] The containing graph
|
|
33
|
+
def route_edge(edge, node_map, _graph)
|
|
34
|
+
return unless edge.sources&.any? && edge.targets&.any?
|
|
35
|
+
|
|
36
|
+
# Get source and target nodes
|
|
37
|
+
source_id = edge.sources.first
|
|
38
|
+
target_id = edge.targets.first
|
|
39
|
+
|
|
40
|
+
source_node = node_map[source_id]
|
|
41
|
+
target_node = node_map[target_id]
|
|
42
|
+
|
|
43
|
+
# If nodes not found, sources/targets might be port IDs
|
|
44
|
+
# Try to find nodes that contain these ports
|
|
45
|
+
source_node ||= find_node_with_port(node_map.values, source_id)
|
|
46
|
+
target_node ||= find_node_with_port(node_map.values, target_id)
|
|
47
|
+
|
|
48
|
+
return unless source_node && target_node
|
|
49
|
+
|
|
50
|
+
# Create edge section if not exists
|
|
51
|
+
edge.sections ||= []
|
|
52
|
+
if edge.sections.empty?
|
|
53
|
+
edge.sections << Graph::EdgeSection.new(
|
|
54
|
+
id: "#{edge.id}_section_0",
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
section = edge.sections.first
|
|
59
|
+
|
|
60
|
+
# Calculate routing points based on port-awareness
|
|
61
|
+
if edge_uses_ports?(edge, source_node, target_node)
|
|
62
|
+
route_with_ports(section, edge, source_node, target_node)
|
|
63
|
+
else
|
|
64
|
+
route_node_to_node(section, source_node, target_node, edge)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Build a map of node IDs to node objects
|
|
71
|
+
def build_node_map(graph)
|
|
72
|
+
map = {}
|
|
73
|
+
graph.children&.each do |node|
|
|
74
|
+
map[node.id] = node
|
|
75
|
+
end
|
|
76
|
+
map
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Find node that contains a port with the given ID
|
|
80
|
+
def find_node_with_port(nodes, port_id)
|
|
81
|
+
nodes.find do |node|
|
|
82
|
+
node.ports&.any? { |port| port.id == port_id }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if edge should use port-based routing
|
|
87
|
+
def edge_uses_ports?(_edge, source_node, target_node)
|
|
88
|
+
# Check if nodes have ports
|
|
89
|
+
has_source_ports = source_node.ports&.any?
|
|
90
|
+
has_target_ports = target_node.ports&.any?
|
|
91
|
+
|
|
92
|
+
has_source_ports || has_target_ports
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Route edge using port positions
|
|
96
|
+
def route_with_ports(section, edge, source_node, target_node)
|
|
97
|
+
# Get port positions or fallback to node center
|
|
98
|
+
source_port = find_port_by_id(edge.sources.first, source_node)
|
|
99
|
+
target_port = find_port_by_id(edge.targets.first, target_node)
|
|
100
|
+
|
|
101
|
+
start_point = get_port_position(
|
|
102
|
+
edge.sources.first,
|
|
103
|
+
source_node,
|
|
104
|
+
:outgoing,
|
|
105
|
+
)
|
|
106
|
+
end_point = get_port_position(
|
|
107
|
+
edge.targets.first,
|
|
108
|
+
target_node,
|
|
109
|
+
:incoming,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
section.start_point = start_point
|
|
113
|
+
section.end_point = end_point
|
|
114
|
+
section.bend_points ||= []
|
|
115
|
+
|
|
116
|
+
# Add intelligent bend points based on port sides
|
|
117
|
+
if source_port && target_port
|
|
118
|
+
add_port_aware_bend_points(
|
|
119
|
+
section,
|
|
120
|
+
start_point,
|
|
121
|
+
end_point,
|
|
122
|
+
source_port,
|
|
123
|
+
target_port,
|
|
124
|
+
)
|
|
125
|
+
elsif should_use_orthogonal_routing?(edge)
|
|
126
|
+
add_orthogonal_bend_points(section, start_point, end_point)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Route edge from node center to node center
|
|
131
|
+
def route_node_to_node(section, source_node, target_node, edge = nil)
|
|
132
|
+
start_point = get_node_center(source_node)
|
|
133
|
+
end_point = get_node_center(target_node)
|
|
134
|
+
|
|
135
|
+
section.start_point = start_point
|
|
136
|
+
section.end_point = end_point
|
|
137
|
+
section.bend_points ||= []
|
|
138
|
+
|
|
139
|
+
# Add orthogonal routing if configured
|
|
140
|
+
if edge && should_use_orthogonal_routing?(edge)
|
|
141
|
+
add_orthogonal_bend_points(section, start_point, end_point)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Find port by ID
|
|
146
|
+
def find_port_by_id(port_id, node)
|
|
147
|
+
node.ports&.find { |p| p.id == port_id }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get position of a port or node center
|
|
151
|
+
def get_port_position(port_id, node, _direction)
|
|
152
|
+
# Try to find port
|
|
153
|
+
port = find_port_by_id(port_id, node)
|
|
154
|
+
|
|
155
|
+
if port
|
|
156
|
+
# Port position is relative to node position
|
|
157
|
+
Geometry::Point.new(
|
|
158
|
+
x: (node.x || 0.0) + (port.x || 0.0),
|
|
159
|
+
y: (node.y || 0.0) + (port.y || 0.0),
|
|
160
|
+
)
|
|
161
|
+
else
|
|
162
|
+
# Fallback to node center
|
|
163
|
+
get_node_center(node)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get center point of a node
|
|
168
|
+
def get_node_center(node)
|
|
169
|
+
x = (node.x || 0.0) + ((node.width || 0.0) / 2.0)
|
|
170
|
+
y = (node.y || 0.0) + ((node.height || 0.0) / 2.0)
|
|
171
|
+
Geometry::Point.new(x: x, y: y)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Check if orthogonal routing should be used
|
|
175
|
+
def should_use_orthogonal_routing?(edge)
|
|
176
|
+
edge.layout_options&.[]("edge.routing") == "orthogonal"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Add intelligent bend points based on port sides
|
|
180
|
+
def add_port_aware_bend_points(section, start_point, end_point,
|
|
181
|
+
source_port, target_port)
|
|
182
|
+
source_side = source_port.side
|
|
183
|
+
target_side = target_port.side
|
|
184
|
+
|
|
185
|
+
# Calculate bend points based on port side combinations
|
|
186
|
+
case [source_side, target_side]
|
|
187
|
+
when ["EAST", "WEST"], ["WEST", "EAST"]
|
|
188
|
+
# Horizontal connection: add midpoint
|
|
189
|
+
add_horizontal_bend_points(section, start_point, end_point)
|
|
190
|
+
when ["NORTH", "SOUTH"], ["SOUTH", "NORTH"]
|
|
191
|
+
# Vertical connection: add midpoint
|
|
192
|
+
add_vertical_bend_points(section, start_point, end_point)
|
|
193
|
+
when ["EAST", "NORTH"], ["EAST", "SOUTH"],
|
|
194
|
+
["WEST", "NORTH"], ["WEST", "SOUTH"]
|
|
195
|
+
# Horizontal to vertical
|
|
196
|
+
add_horizontal_then_vertical(section, start_point, end_point)
|
|
197
|
+
when ["NORTH", "EAST"], ["NORTH", "WEST"],
|
|
198
|
+
["SOUTH", "EAST"], ["SOUTH", "WEST"]
|
|
199
|
+
# Vertical to horizontal
|
|
200
|
+
add_vertical_then_horizontal(section, start_point, end_point)
|
|
201
|
+
else
|
|
202
|
+
# Default orthogonal routing
|
|
203
|
+
add_orthogonal_bend_points(section, start_point, end_point)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Add horizontal bend points (for horizontal connections)
|
|
208
|
+
def add_horizontal_bend_points(section, start_point, end_point)
|
|
209
|
+
mid_x = (start_point.x + end_point.x) / 2.0
|
|
210
|
+
section.add_bend_point(mid_x, start_point.y)
|
|
211
|
+
section.add_bend_point(mid_x, end_point.y)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Add vertical bend points (for vertical connections)
|
|
215
|
+
def add_vertical_bend_points(section, start_point, end_point)
|
|
216
|
+
mid_y = (start_point.y + end_point.y) / 2.0
|
|
217
|
+
section.add_bend_point(start_point.x, mid_y)
|
|
218
|
+
section.add_bend_point(end_point.x, mid_y)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Route horizontal then vertical
|
|
222
|
+
def add_horizontal_then_vertical(section, start_point, end_point)
|
|
223
|
+
section.add_bend_point(end_point.x, start_point.y)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Route vertical then horizontal
|
|
227
|
+
def add_vertical_then_horizontal(section, start_point, end_point)
|
|
228
|
+
section.add_bend_point(start_point.x, end_point.y)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Add orthogonal (right-angle) bend points
|
|
232
|
+
def add_orthogonal_bend_points(section, start_point, end_point)
|
|
233
|
+
# Simple orthogonal routing: horizontal then vertical
|
|
234
|
+
mid_x = (start_point.x + end_point.x) / 2.0
|
|
235
|
+
|
|
236
|
+
# Add bend points for orthogonal path
|
|
237
|
+
section.add_bend_point(mid_x, start_point.y)
|
|
238
|
+
section.add_bend_point(mid_x, end_point.y)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Get routing style from graph options
|
|
242
|
+
def get_routing_style(graph)
|
|
243
|
+
return "ORTHOGONAL" unless graph.layout_options
|
|
244
|
+
|
|
245
|
+
style = graph.layout_options["elk.edgeRouting"] ||
|
|
246
|
+
graph.layout_options["edgeRouting"] ||
|
|
247
|
+
graph.layout_options.edge_routing
|
|
248
|
+
|
|
249
|
+
style ? style.to_s.upcase : "ORTHOGONAL"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Route edge with specified routing style
|
|
253
|
+
def route_edge_with_style(edge, node_map, graph, routing_style)
|
|
254
|
+
case routing_style
|
|
255
|
+
when "SPLINES"
|
|
256
|
+
route_spline_edge(edge, node_map, graph)
|
|
257
|
+
when "POLYLINE"
|
|
258
|
+
route_polyline_edge(edge, node_map, graph)
|
|
259
|
+
else
|
|
260
|
+
route_edge(edge, node_map, graph)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Route edge with polyline (straight segments) style
|
|
265
|
+
def route_polyline_edge(edge, node_map, graph)
|
|
266
|
+
# Polyline is just direct routing without bend points
|
|
267
|
+
route_edge(edge, node_map, graph)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Route edge with spline (curved) style
|
|
271
|
+
def route_spline_edge(edge, node_map, _graph)
|
|
272
|
+
return unless edge.sources&.any? && edge.targets&.any?
|
|
273
|
+
|
|
274
|
+
source_id = edge.sources.first
|
|
275
|
+
target_id = edge.targets.first
|
|
276
|
+
|
|
277
|
+
source_node = node_map[source_id]
|
|
278
|
+
target_node = node_map[target_id]
|
|
279
|
+
|
|
280
|
+
source_node ||= find_node_with_port(node_map.values, source_id)
|
|
281
|
+
target_node ||= find_node_with_port(node_map.values, target_id)
|
|
282
|
+
|
|
283
|
+
return unless source_node && target_node
|
|
284
|
+
|
|
285
|
+
# Create edge section if not exists
|
|
286
|
+
edge.sections ||= []
|
|
287
|
+
if edge.sections.empty?
|
|
288
|
+
edge.sections << Graph::EdgeSection.new(
|
|
289
|
+
id: "#{edge.id}_section_0",
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
section = edge.sections.first
|
|
294
|
+
|
|
295
|
+
# Calculate spline routing
|
|
296
|
+
if edge_uses_ports?(edge, source_node, target_node)
|
|
297
|
+
route_spline_with_ports(section, edge, source_node, target_node)
|
|
298
|
+
else
|
|
299
|
+
route_spline_node_to_node(section, edge, source_node, target_node)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Route spline edge using port positions
|
|
304
|
+
def route_spline_with_ports(section, edge, source_node, target_node)
|
|
305
|
+
start_point = get_port_position(
|
|
306
|
+
edge.sources.first,
|
|
307
|
+
source_node,
|
|
308
|
+
:outgoing,
|
|
309
|
+
)
|
|
310
|
+
end_point = get_port_position(
|
|
311
|
+
edge.targets.first,
|
|
312
|
+
target_node,
|
|
313
|
+
:incoming,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
section.start_point = start_point
|
|
317
|
+
section.end_point = end_point
|
|
318
|
+
|
|
319
|
+
# Calculate control points for spline
|
|
320
|
+
add_spline_control_points(section, start_point, end_point, edge)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Route spline edge from node center to node center
|
|
324
|
+
def route_spline_node_to_node(section, edge, source_node, target_node)
|
|
325
|
+
start_point = get_node_center(source_node)
|
|
326
|
+
end_point = get_node_center(target_node)
|
|
327
|
+
|
|
328
|
+
section.start_point = start_point
|
|
329
|
+
section.end_point = end_point
|
|
330
|
+
|
|
331
|
+
# Calculate control points for spline
|
|
332
|
+
add_spline_control_points(section, start_point, end_point, edge)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Add Bezier control points to create smooth spline
|
|
336
|
+
def add_spline_control_points(section, start_point, end_point, edge)
|
|
337
|
+
# Get curvature setting
|
|
338
|
+
curvature = get_spline_curvature(edge)
|
|
339
|
+
|
|
340
|
+
# Calculate control points
|
|
341
|
+
control_points = calculate_spline_controls(
|
|
342
|
+
start_point,
|
|
343
|
+
end_point,
|
|
344
|
+
curvature,
|
|
345
|
+
edge,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Store control points as bend points
|
|
349
|
+
section.bend_points ||= []
|
|
350
|
+
section.bend_points = control_points
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Get spline curvature from options
|
|
354
|
+
def get_spline_curvature(edge)
|
|
355
|
+
return 0.5 unless edge.layout_options
|
|
356
|
+
|
|
357
|
+
curvature = edge.layout_options["elk.spline.curvature"] ||
|
|
358
|
+
edge.layout_options["spline.curvature"]
|
|
359
|
+
|
|
360
|
+
curvature ? curvature.to_f : 0.5
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Calculate Bezier control points for smooth curves
|
|
364
|
+
def calculate_spline_controls(start_point, end_point, curvature, edge)
|
|
365
|
+
# Determine routing direction from edge or graph options
|
|
366
|
+
direction = get_routing_direction(edge)
|
|
367
|
+
|
|
368
|
+
case direction
|
|
369
|
+
when "HORIZONTAL", "RIGHT", "LEFT"
|
|
370
|
+
Geometry::Bezier.horizontal_control_points(
|
|
371
|
+
start_point,
|
|
372
|
+
end_point,
|
|
373
|
+
curvature,
|
|
374
|
+
)
|
|
375
|
+
when "VERTICAL", "DOWN", "UP"
|
|
376
|
+
Geometry::Bezier.vertical_control_points(
|
|
377
|
+
start_point,
|
|
378
|
+
end_point,
|
|
379
|
+
curvature,
|
|
380
|
+
)
|
|
381
|
+
else
|
|
382
|
+
# Default: perpendicular control points
|
|
383
|
+
Geometry::Bezier.calculate_control_points(
|
|
384
|
+
start_point,
|
|
385
|
+
end_point,
|
|
386
|
+
curvature,
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Get routing direction from edge options
|
|
392
|
+
def get_routing_direction(edge)
|
|
393
|
+
return nil unless edge.layout_options
|
|
394
|
+
|
|
395
|
+
edge.layout_options["elk.direction"] ||
|
|
396
|
+
edge.layout_options["direction"] ||
|
|
397
|
+
nil
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Check if edge is a self-loop (source == target)
|
|
401
|
+
def self_loop?(edge)
|
|
402
|
+
sources = edge.sources || []
|
|
403
|
+
targets = edge.targets || []
|
|
404
|
+
|
|
405
|
+
return false if sources.empty? || targets.empty?
|
|
406
|
+
|
|
407
|
+
sources.first == targets.first
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Route a self-loop edge
|
|
411
|
+
def route_self_loop(edge, node_map, graph, routing_style)
|
|
412
|
+
node_id = edge.sources.first
|
|
413
|
+
node = node_map[node_id]
|
|
414
|
+
|
|
415
|
+
# If not found, might be a port ID
|
|
416
|
+
node ||= find_node_with_port(node_map.values, node_id)
|
|
417
|
+
|
|
418
|
+
return unless node
|
|
419
|
+
|
|
420
|
+
# Get self-loop index for multiple loops on same node
|
|
421
|
+
loop_index = get_self_loop_index(edge, node, graph)
|
|
422
|
+
|
|
423
|
+
# Create edge section if not exists
|
|
424
|
+
edge.sections ||= []
|
|
425
|
+
if edge.sections.empty?
|
|
426
|
+
edge.sections << Graph::EdgeSection.new(
|
|
427
|
+
id: "#{edge.id}_section_0",
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
section = edge.sections.first
|
|
432
|
+
|
|
433
|
+
# Check if edge uses ports
|
|
434
|
+
if edge_uses_ports_for_self_loop?(edge, node)
|
|
435
|
+
route_self_loop_with_ports(section, edge, node, loop_index,
|
|
436
|
+
routing_style)
|
|
437
|
+
else
|
|
438
|
+
# Route based on style
|
|
439
|
+
case routing_style
|
|
440
|
+
when "SPLINES"
|
|
441
|
+
route_spline_self_loop(section, edge, node, loop_index)
|
|
442
|
+
when "POLYLINE"
|
|
443
|
+
route_polyline_self_loop(section, edge, node, loop_index)
|
|
444
|
+
else
|
|
445
|
+
route_orthogonal_self_loop(section, edge, node, loop_index)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Get self-loop index for multiple loops on same node
|
|
451
|
+
def get_self_loop_index(edge, node, graph)
|
|
452
|
+
return 0 unless graph.edges
|
|
453
|
+
|
|
454
|
+
# Find all self-loops on this node
|
|
455
|
+
self_loops = graph.edges.select do |e|
|
|
456
|
+
self_loop?(e) && e.sources&.first == node.id
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Return index of current edge
|
|
460
|
+
self_loops.index(edge) || 0
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Route orthogonal self-loop (rectangular path)
|
|
464
|
+
def route_orthogonal_self_loop(section, edge, node, loop_index)
|
|
465
|
+
# Calculate offset based on loop index
|
|
466
|
+
offset = calculate_loop_offset(loop_index)
|
|
467
|
+
|
|
468
|
+
# Get self-loop side
|
|
469
|
+
side = get_self_loop_side(edge, node)
|
|
470
|
+
|
|
471
|
+
# Calculate dimensions
|
|
472
|
+
width = ((node.width || 50.0) * 0.4) + offset
|
|
473
|
+
height = ((node.height || 50.0) * 0.4) + offset
|
|
474
|
+
|
|
475
|
+
# Calculate start/end points based on side
|
|
476
|
+
case side
|
|
477
|
+
when "EAST"
|
|
478
|
+
route_east_self_loop(section, node, width, height)
|
|
479
|
+
when "WEST"
|
|
480
|
+
route_west_self_loop(section, node, width, height)
|
|
481
|
+
when "NORTH"
|
|
482
|
+
route_north_self_loop(section, node, width, height)
|
|
483
|
+
when "SOUTH"
|
|
484
|
+
route_south_self_loop(section, node, width, height)
|
|
485
|
+
else
|
|
486
|
+
# Default: EAST
|
|
487
|
+
route_east_self_loop(section, node, width, height)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Route self-loop on EAST side
|
|
492
|
+
def route_east_self_loop(section, node, width, height)
|
|
493
|
+
node_x = node.x || 0.0
|
|
494
|
+
node_y = node.y || 0.0
|
|
495
|
+
node_width = node.width || 50.0
|
|
496
|
+
node_height = node.height || 50.0
|
|
497
|
+
|
|
498
|
+
# Start point (right middle of node)
|
|
499
|
+
start_x = node_x + node_width
|
|
500
|
+
start_y = node_y + (node_height / 2.0)
|
|
501
|
+
|
|
502
|
+
# End point (slightly below start)
|
|
503
|
+
end_x = start_x
|
|
504
|
+
end_y = start_y + 10.0
|
|
505
|
+
|
|
506
|
+
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
507
|
+
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
508
|
+
|
|
509
|
+
# Bend points forming rectangular loop
|
|
510
|
+
section.bend_points = [
|
|
511
|
+
Geometry::Point.new(x: start_x + width, y: start_y),
|
|
512
|
+
Geometry::Point.new(x: start_x + width, y: start_y - height),
|
|
513
|
+
Geometry::Point.new(x: start_x + width, y: start_y + height),
|
|
514
|
+
Geometry::Point.new(x: end_x, y: end_y - 5.0),
|
|
515
|
+
]
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Route self-loop on WEST side
|
|
519
|
+
def route_west_self_loop(section, node, width, height)
|
|
520
|
+
node_x = node.x || 0.0
|
|
521
|
+
node_y = node.y || 0.0
|
|
522
|
+
node_height = node.height || 50.0
|
|
523
|
+
|
|
524
|
+
# Start point (left middle of node)
|
|
525
|
+
start_x = node_x
|
|
526
|
+
start_y = node_y + (node_height / 2.0)
|
|
527
|
+
|
|
528
|
+
# End point (slightly below start)
|
|
529
|
+
end_x = start_x
|
|
530
|
+
end_y = start_y + 10.0
|
|
531
|
+
|
|
532
|
+
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
533
|
+
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
534
|
+
|
|
535
|
+
# Bend points forming rectangular loop
|
|
536
|
+
section.bend_points = [
|
|
537
|
+
Geometry::Point.new(x: start_x - width, y: start_y),
|
|
538
|
+
Geometry::Point.new(x: start_x - width, y: start_y - height),
|
|
539
|
+
Geometry::Point.new(x: start_x - width, y: start_y + height),
|
|
540
|
+
Geometry::Point.new(x: end_x, y: end_y - 5.0),
|
|
541
|
+
]
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Route self-loop on NORTH side
|
|
545
|
+
def route_north_self_loop(section, node, width, height)
|
|
546
|
+
node_x = node.x || 0.0
|
|
547
|
+
node_y = node.y || 0.0
|
|
548
|
+
node_width = node.width || 50.0
|
|
549
|
+
|
|
550
|
+
# Start point (top middle of node)
|
|
551
|
+
start_x = node_x + (node_width / 2.0)
|
|
552
|
+
start_y = node_y
|
|
553
|
+
|
|
554
|
+
# End point (slightly to the right of start)
|
|
555
|
+
end_x = start_x + 10.0
|
|
556
|
+
end_y = start_y
|
|
557
|
+
|
|
558
|
+
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
559
|
+
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
560
|
+
|
|
561
|
+
# Bend points forming rectangular loop
|
|
562
|
+
section.bend_points = [
|
|
563
|
+
Geometry::Point.new(x: start_x, y: start_y - height),
|
|
564
|
+
Geometry::Point.new(x: start_x - width, y: start_y - height),
|
|
565
|
+
Geometry::Point.new(x: start_x + width, y: start_y - height),
|
|
566
|
+
Geometry::Point.new(x: end_x - 5.0, y: end_y),
|
|
567
|
+
]
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Route self-loop on SOUTH side
|
|
571
|
+
def route_south_self_loop(section, node, width, height)
|
|
572
|
+
node_x = node.x || 0.0
|
|
573
|
+
node_y = node.y || 0.0
|
|
574
|
+
node_width = node.width || 50.0
|
|
575
|
+
node_height = node.height || 50.0
|
|
576
|
+
|
|
577
|
+
# Start point (bottom middle of node)
|
|
578
|
+
start_x = node_x + (node_width / 2.0)
|
|
579
|
+
start_y = node_y + node_height
|
|
580
|
+
|
|
581
|
+
# End point (slightly to the right of start)
|
|
582
|
+
end_x = start_x + 10.0
|
|
583
|
+
end_y = start_y
|
|
584
|
+
|
|
585
|
+
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
586
|
+
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
587
|
+
|
|
588
|
+
# Bend points forming rectangular loop
|
|
589
|
+
section.bend_points = [
|
|
590
|
+
Geometry::Point.new(x: start_x, y: start_y + height),
|
|
591
|
+
Geometry::Point.new(x: start_x - width, y: start_y + height),
|
|
592
|
+
Geometry::Point.new(x: start_x + width, y: start_y + height),
|
|
593
|
+
Geometry::Point.new(x: end_x - 5.0, y: end_y),
|
|
594
|
+
]
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Route spline self-loop (curved path)
|
|
598
|
+
def route_spline_self_loop(section, edge, node, loop_index)
|
|
599
|
+
# Calculate offset based on loop index
|
|
600
|
+
offset = calculate_loop_offset(loop_index)
|
|
601
|
+
|
|
602
|
+
# Get self-loop side
|
|
603
|
+
side = get_self_loop_side(edge, node)
|
|
604
|
+
|
|
605
|
+
node_x = node.x || 0.0
|
|
606
|
+
node_y = node.y || 0.0
|
|
607
|
+
node_width = node.width || 50.0
|
|
608
|
+
node_height = node.height || 50.0
|
|
609
|
+
|
|
610
|
+
# Calculate radius based on offset
|
|
611
|
+
radius = ((node_width + node_height) / 4.0) + offset
|
|
612
|
+
|
|
613
|
+
case side
|
|
614
|
+
when "EAST"
|
|
615
|
+
start_x = node_x + node_width
|
|
616
|
+
start_y = node_y + (node_height / 2.0)
|
|
617
|
+
end_x = start_x
|
|
618
|
+
end_y = start_y + 10.0
|
|
619
|
+
|
|
620
|
+
# Control points for circular arc on right side
|
|
621
|
+
control1 = Geometry::Point.new(
|
|
622
|
+
x: start_x + radius,
|
|
623
|
+
y: start_y - radius,
|
|
624
|
+
)
|
|
625
|
+
control2 = Geometry::Point.new(
|
|
626
|
+
x: start_x + radius,
|
|
627
|
+
y: start_y + radius,
|
|
628
|
+
)
|
|
629
|
+
when "WEST"
|
|
630
|
+
start_x = node_x
|
|
631
|
+
start_y = node_y + (node_height / 2.0)
|
|
632
|
+
end_x = start_x
|
|
633
|
+
end_y = start_y + 10.0
|
|
634
|
+
|
|
635
|
+
# Control points for circular arc on left side
|
|
636
|
+
control1 = Geometry::Point.new(
|
|
637
|
+
x: start_x - radius,
|
|
638
|
+
y: start_y - radius,
|
|
639
|
+
)
|
|
640
|
+
control2 = Geometry::Point.new(
|
|
641
|
+
x: start_x - radius,
|
|
642
|
+
y: start_y + radius,
|
|
643
|
+
)
|
|
644
|
+
when "NORTH"
|
|
645
|
+
start_x = node_x + (node_width / 2.0)
|
|
646
|
+
start_y = node_y
|
|
647
|
+
end_x = start_x + 10.0
|
|
648
|
+
end_y = start_y
|
|
649
|
+
|
|
650
|
+
# Control points for circular arc on top
|
|
651
|
+
control1 = Geometry::Point.new(
|
|
652
|
+
x: start_x - radius,
|
|
653
|
+
y: start_y - radius,
|
|
654
|
+
)
|
|
655
|
+
control2 = Geometry::Point.new(
|
|
656
|
+
x: start_x + radius,
|
|
657
|
+
y: start_y - radius,
|
|
658
|
+
)
|
|
659
|
+
when "SOUTH"
|
|
660
|
+
start_x = node_x + (node_width / 2.0)
|
|
661
|
+
start_y = node_y + node_height
|
|
662
|
+
end_x = start_x + 10.0
|
|
663
|
+
end_y = start_y
|
|
664
|
+
|
|
665
|
+
# Control points for circular arc on bottom
|
|
666
|
+
control1 = Geometry::Point.new(
|
|
667
|
+
x: start_x - radius,
|
|
668
|
+
y: start_y + radius,
|
|
669
|
+
)
|
|
670
|
+
control2 = Geometry::Point.new(
|
|
671
|
+
x: start_x + radius,
|
|
672
|
+
y: start_y + radius,
|
|
673
|
+
)
|
|
674
|
+
else
|
|
675
|
+
# Default: EAST
|
|
676
|
+
start_x = node_x + node_width
|
|
677
|
+
start_y = node_y + (node_height / 2.0)
|
|
678
|
+
end_x = start_x
|
|
679
|
+
end_y = start_y + 10.0
|
|
680
|
+
|
|
681
|
+
control1 = Geometry::Point.new(
|
|
682
|
+
x: start_x + radius,
|
|
683
|
+
y: start_y - radius,
|
|
684
|
+
)
|
|
685
|
+
control2 = Geometry::Point.new(
|
|
686
|
+
x: start_x + radius,
|
|
687
|
+
y: start_y + radius,
|
|
688
|
+
)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
692
|
+
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
693
|
+
section.bend_points = [control1, control2]
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Route polyline self-loop (simple path)
|
|
697
|
+
def route_polyline_self_loop(section, edge, node, loop_index)
|
|
698
|
+
# For polyline, use orthogonal routing
|
|
699
|
+
route_orthogonal_self_loop(section, edge, node, loop_index)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Calculate offset for multiple self-loops on same node
|
|
703
|
+
def calculate_loop_offset(loop_index)
|
|
704
|
+
base_offset = 20.0
|
|
705
|
+
base_offset * (loop_index + 1)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Get self-loop side from options or default
|
|
709
|
+
def get_self_loop_side(edge, node)
|
|
710
|
+
# Check edge layout options first
|
|
711
|
+
if edge.layout_options
|
|
712
|
+
side = edge.layout_options["elk.selfLoopSide"] ||
|
|
713
|
+
edge.layout_options["selfLoopSide"]
|
|
714
|
+
return side if side
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Check node layout options
|
|
718
|
+
if node.layout_options
|
|
719
|
+
side = node.layout_options["elk.selfLoopSide"] ||
|
|
720
|
+
node.layout_options["selfLoopSide"]
|
|
721
|
+
return side if side
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# Default: EAST
|
|
725
|
+
"EAST"
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Check if self-loop edge uses ports
|
|
729
|
+
def edge_uses_ports_for_self_loop?(edge, node)
|
|
730
|
+
return false unless node.ports&.any?
|
|
731
|
+
|
|
732
|
+
source_id = edge.sources&.first
|
|
733
|
+
target_id = edge.targets&.first
|
|
734
|
+
|
|
735
|
+
return false unless source_id && target_id
|
|
736
|
+
|
|
737
|
+
# Check if source or target is a port ID
|
|
738
|
+
source_port = find_port_by_id(source_id, node)
|
|
739
|
+
target_port = find_port_by_id(target_id, node)
|
|
740
|
+
|
|
741
|
+
!!(source_port || target_port)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Route self-loop with ports
|
|
745
|
+
def route_self_loop_with_ports(section, edge, node, loop_index,
|
|
746
|
+
routing_style)
|
|
747
|
+
source_id = edge.sources.first
|
|
748
|
+
target_id = edge.targets.first
|
|
749
|
+
|
|
750
|
+
source_port = find_port_by_id(source_id, node)
|
|
751
|
+
target_port = find_port_by_id(target_id, node)
|
|
752
|
+
|
|
753
|
+
# Get port positions
|
|
754
|
+
start_point = if source_port
|
|
755
|
+
get_port_absolute_position(source_port, node)
|
|
756
|
+
else
|
|
757
|
+
get_node_center(node)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
end_point = if target_port
|
|
761
|
+
get_port_absolute_position(target_port, node)
|
|
762
|
+
else
|
|
763
|
+
get_node_center(node)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
section.start_point = start_point
|
|
767
|
+
section.end_point = end_point
|
|
768
|
+
|
|
769
|
+
# Calculate offset
|
|
770
|
+
offset = calculate_loop_offset(loop_index)
|
|
771
|
+
|
|
772
|
+
# Route between ports with appropriate style
|
|
773
|
+
if source_port && target_port
|
|
774
|
+
route_port_to_port_self_loop(
|
|
775
|
+
section,
|
|
776
|
+
start_point,
|
|
777
|
+
end_point,
|
|
778
|
+
source_port,
|
|
779
|
+
target_port,
|
|
780
|
+
offset,
|
|
781
|
+
routing_style,
|
|
782
|
+
)
|
|
783
|
+
else
|
|
784
|
+
# Fallback to regular self-loop routing
|
|
785
|
+
case routing_style
|
|
786
|
+
when "SPLINES"
|
|
787
|
+
route_spline_self_loop(section, edge, node, loop_index)
|
|
788
|
+
else
|
|
789
|
+
route_orthogonal_self_loop(section, edge, node, loop_index)
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Get absolute position of a port
|
|
795
|
+
def get_port_absolute_position(port, node)
|
|
796
|
+
Geometry::Point.new(
|
|
797
|
+
x: (node.x || 0.0) + (port.x || 0.0),
|
|
798
|
+
y: (node.y || 0.0) + (port.y || 0.0),
|
|
799
|
+
)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Route self-loop from port to port
|
|
803
|
+
def route_port_to_port_self_loop(section, start_point, end_point,
|
|
804
|
+
source_port, target_port, offset,
|
|
805
|
+
routing_style)
|
|
806
|
+
# Calculate midpoint for loop
|
|
807
|
+
(start_point.x + end_point.x) / 2.0
|
|
808
|
+
(start_point.y + end_point.y) / 2.0
|
|
809
|
+
|
|
810
|
+
# Determine loop direction based on port sides
|
|
811
|
+
source_side = source_port.side || "EAST"
|
|
812
|
+
target_side = target_port.side || "EAST"
|
|
813
|
+
|
|
814
|
+
if routing_style == "SPLINES"
|
|
815
|
+
# Create smooth curve between ports
|
|
816
|
+
route_spline_port_self_loop(
|
|
817
|
+
section,
|
|
818
|
+
start_point,
|
|
819
|
+
end_point,
|
|
820
|
+
source_side,
|
|
821
|
+
target_side,
|
|
822
|
+
offset,
|
|
823
|
+
)
|
|
824
|
+
else
|
|
825
|
+
# Create orthogonal path between ports
|
|
826
|
+
route_orthogonal_port_self_loop(
|
|
827
|
+
section,
|
|
828
|
+
start_point,
|
|
829
|
+
end_point,
|
|
830
|
+
source_side,
|
|
831
|
+
target_side,
|
|
832
|
+
offset,
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# Route orthogonal self-loop between ports
|
|
838
|
+
def route_orthogonal_port_self_loop(section, start_point, end_point,
|
|
839
|
+
source_side, target_side, offset)
|
|
840
|
+
section.bend_points = []
|
|
841
|
+
|
|
842
|
+
# Create bend points based on port sides
|
|
843
|
+
case [source_side, target_side]
|
|
844
|
+
when ["EAST", "EAST"], ["WEST", "WEST"]
|
|
845
|
+
# Both on same vertical side - create horizontal loop
|
|
846
|
+
extension = offset + 30.0
|
|
847
|
+
mid_y = (start_point.y + end_point.y) / 2.0
|
|
848
|
+
|
|
849
|
+
if source_side == "EAST"
|
|
850
|
+
section.add_bend_point(start_point.x + extension, start_point.y)
|
|
851
|
+
section.add_bend_point(start_point.x + extension, mid_y)
|
|
852
|
+
section.add_bend_point(end_point.x + extension, end_point.y)
|
|
853
|
+
else
|
|
854
|
+
section.add_bend_point(start_point.x - extension, start_point.y)
|
|
855
|
+
section.add_bend_point(start_point.x - extension, mid_y)
|
|
856
|
+
section.add_bend_point(end_point.x - extension, end_point.y)
|
|
857
|
+
end
|
|
858
|
+
when ["NORTH", "NORTH"], ["SOUTH", "SOUTH"]
|
|
859
|
+
# Both on same horizontal side - create vertical loop
|
|
860
|
+
extension = offset + 30.0
|
|
861
|
+
mid_x = (start_point.x + end_point.x) / 2.0
|
|
862
|
+
|
|
863
|
+
if source_side == "NORTH"
|
|
864
|
+
section.add_bend_point(start_point.x, start_point.y - extension)
|
|
865
|
+
section.add_bend_point(mid_x, start_point.y - extension)
|
|
866
|
+
section.add_bend_point(end_point.x, end_point.y - extension)
|
|
867
|
+
else
|
|
868
|
+
section.add_bend_point(start_point.x, start_point.y + extension)
|
|
869
|
+
section.add_bend_point(mid_x, start_point.y + extension)
|
|
870
|
+
section.add_bend_point(end_point.x, end_point.y + extension)
|
|
871
|
+
end
|
|
872
|
+
else
|
|
873
|
+
# Different sides - create L-shaped path
|
|
874
|
+
mid_x = (start_point.x + end_point.x) / 2.0
|
|
875
|
+
section.add_bend_point(mid_x, start_point.y)
|
|
876
|
+
section.add_bend_point(mid_x, end_point.y)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# Route spline self-loop between ports
|
|
881
|
+
def route_spline_port_self_loop(section, start_point, end_point,
|
|
882
|
+
source_side, target_side, offset)
|
|
883
|
+
# Create Bezier curve control points
|
|
884
|
+
extension = offset + 30.0
|
|
885
|
+
|
|
886
|
+
case [source_side, target_side]
|
|
887
|
+
when ["EAST", "EAST"]
|
|
888
|
+
control1 = Geometry::Point.new(
|
|
889
|
+
x: start_point.x + extension,
|
|
890
|
+
y: start_point.y,
|
|
891
|
+
)
|
|
892
|
+
control2 = Geometry::Point.new(
|
|
893
|
+
x: end_point.x + extension,
|
|
894
|
+
y: end_point.y,
|
|
895
|
+
)
|
|
896
|
+
when ["WEST", "WEST"]
|
|
897
|
+
control1 = Geometry::Point.new(
|
|
898
|
+
x: start_point.x - extension,
|
|
899
|
+
y: start_point.y,
|
|
900
|
+
)
|
|
901
|
+
control2 = Geometry::Point.new(
|
|
902
|
+
x: end_point.x - extension,
|
|
903
|
+
y: end_point.y,
|
|
904
|
+
)
|
|
905
|
+
when ["NORTH", "NORTH"]
|
|
906
|
+
control1 = Geometry::Point.new(
|
|
907
|
+
x: start_point.x,
|
|
908
|
+
y: start_point.y - extension,
|
|
909
|
+
)
|
|
910
|
+
control2 = Geometry::Point.new(
|
|
911
|
+
x: end_point.x,
|
|
912
|
+
y: end_point.y - extension,
|
|
913
|
+
)
|
|
914
|
+
when ["SOUTH", "SOUTH"]
|
|
915
|
+
control1 = Geometry::Point.new(
|
|
916
|
+
x: start_point.x,
|
|
917
|
+
y: start_point.y + extension,
|
|
918
|
+
)
|
|
919
|
+
control2 = Geometry::Point.new(
|
|
920
|
+
x: end_point.x,
|
|
921
|
+
y: end_point.y + extension,
|
|
922
|
+
)
|
|
923
|
+
else
|
|
924
|
+
# Default perpendicular control points
|
|
925
|
+
mid_x = (start_point.x + end_point.x) / 2.0
|
|
926
|
+
(start_point.y + end_point.y) / 2.0
|
|
927
|
+
control1 = Geometry::Point.new(x: mid_x, y: start_point.y)
|
|
928
|
+
control2 = Geometry::Point.new(x: mid_x, y: end_point.y)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
section.bend_points = [control1, control2]
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
end
|