elkrb 1.0.0 → 2.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 +4 -4
- data/CHANGELOG.adoc +364 -0
- data/LICENSE +25 -0
- data/README.adoc +695 -557
- data/exe/elkrb +36 -2
- data/lib/elkrb/atomic_destination.rb +53 -0
- data/lib/elkrb/best_effort_write.rb +49 -0
- data/lib/elkrb/cli/layout_flag_options.rb +54 -0
- data/lib/elkrb/cli.rb +155 -82
- data/lib/elkrb/command_resolver.rb +186 -0
- data/lib/elkrb/commands/batch_command.rb +28 -5
- data/lib/elkrb/commands/convert_command.rb +22 -52
- data/lib/elkrb/commands/diagram_command.rb +234 -102
- data/lib/elkrb/commands/options_command.rb +87 -0
- data/lib/elkrb/commands/render_command.rb +6 -1
- data/lib/elkrb/commands/validate_command.rb +146 -91
- data/lib/elkrb/errors.rb +20 -1
- data/lib/elkrb/format_sniffer.rb +362 -0
- data/lib/elkrb/geometry/bezier.rb +14 -4
- data/lib/elkrb/geometry/point.rb +5 -6
- data/lib/elkrb/geometry/rectangle.rb +2 -2
- data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
- data/lib/elkrb/graph/edge.rb +89 -3
- data/lib/elkrb/graph/graph.rb +43 -15
- data/lib/elkrb/graph/label.rb +16 -2
- data/lib/elkrb/graph/node.rb +13 -2
- data/lib/elkrb/graph/node_constraints.rb +139 -8
- data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
- data/lib/elkrb/graph/port.rb +29 -21
- data/lib/elkrb/graph/read_only_mapping.rb +21 -0
- data/lib/elkrb/graphviz_wrapper.rb +152 -44
- data/lib/elkrb/layout/algorithm_registry.rb +65 -8
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
- data/lib/elkrb/layout/algorithms/box.rb +47 -20
- data/lib/elkrb/layout/algorithms/disco.rb +150 -61
- data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
- data/lib/elkrb/layout/algorithms/force.rb +145 -86
- data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
- data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
- data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
- data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
- data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
- data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
- data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
- data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
- data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
- data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
- data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
- data/lib/elkrb/layout/algorithms/layered.rb +548 -13
- data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
- data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
- data/lib/elkrb/layout/algorithms/radial.rb +188 -29
- data/lib/elkrb/layout/algorithms/random.rb +131 -17
- data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
- data/lib/elkrb/layout/algorithms/stress.rb +391 -84
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
- data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
- data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
- data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
- data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
- data/lib/elkrb/layout/edge_router.rb +533 -337
- data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
- data/lib/elkrb/layout/java_random.rb +59 -0
- data/lib/elkrb/layout/label_placer.rb +165 -157
- data/lib/elkrb/layout/layout_engine.rb +46 -23
- data/lib/elkrb/layout/node_index.rb +149 -0
- data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
- data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
- data/lib/elkrb/layout/polyomino/grid.rb +106 -0
- data/lib/elkrb/layout/polyomino/packer.rb +95 -0
- data/lib/elkrb/layout/polyomino/piece.rb +52 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
- data/lib/elkrb/layout/spore/body.rb +65 -0
- data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
- data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
- data/lib/elkrb/layout/spore/gap.rb +76 -0
- data/lib/elkrb/layout/spore/geometry.rb +104 -0
- data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
- data/lib/elkrb/layout/spore/placement.rb +57 -0
- data/lib/elkrb/layout/spore/rays.rb +119 -0
- data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
- data/lib/elkrb/layout/spore/triangulation.rb +159 -0
- data/lib/elkrb/layout_flags.rb +69 -0
- data/lib/elkrb/options/decimal.rb +50 -0
- data/lib/elkrb/options/element_options.rb +43 -0
- data/lib/elkrb/options/elk_padding.rb +20 -14
- data/lib/elkrb/options/k_vector.rb +6 -7
- data/lib/elkrb/options/k_vector_chain.rb +12 -20
- data/lib/elkrb/options/option_map.rb +85 -0
- data/lib/elkrb/options/registry.rb +335 -0
- data/lib/elkrb/options/resolver.rb +151 -0
- data/lib/elkrb/options/spellings.rb +62 -0
- data/lib/elkrb/options/unhonoured_report.rb +194 -0
- data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
- data/lib/elkrb/parsers/elkt/parser.rb +424 -0
- data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
- data/lib/elkrb/parsers/elkt/token.rb +40 -0
- data/lib/elkrb/parsers/elkt_parser.rb +32 -230
- data/lib/elkrb/serializers/dot_serializer.rb +342 -54
- data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
- data/lib/elkrb/version.rb +1 -1
- data/lib/elkrb.rb +60 -61
- metadata +71 -49
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Rakefile +0 -64
- data/benchmarks/README.md +0 -172
- data/benchmarks/elkjs_benchmark.js +0 -140
- data/benchmarks/elkrb_benchmark.rb +0 -145
- data/benchmarks/fixtures/graphs.json +0 -10777
- data/benchmarks/generate_report.rb +0 -241
- data/benchmarks/generate_test_graphs.rb +0 -154
- data/benchmarks/results/elkrb_results.json +0 -280
- data/benchmarks/results/elkrb_summary.json +0 -285
- data/elkrb.gemspec +0 -39
- data/examples/dot_export_demo.rb +0 -133
- data/examples/hierarchical_graph.rb +0 -19
- data/examples/layout_constraints_demo.rb +0 -272
- data/examples/port_constraints_demo.rb +0 -291
- data/examples/self_loop_demo.rb +0 -391
- data/examples/simple_graph.rb +0 -50
- data/examples/spline_routing_demo.rb +0 -235
- data/lib/elkrb/graph/layout_options.rb +0 -247
- data/sig/elkrb/constraints.rbs +0 -114
- data/sig/elkrb/geometry.rbs +0 -61
- data/sig/elkrb/graph.rbs +0 -112
- data/sig/elkrb/layout.rbs +0 -107
- data/sig/elkrb/options.rbs +0 -81
- data/sig/elkrb.rbs +0 -32
|
@@ -2,165 +2,298 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../geometry/point"
|
|
4
4
|
require_relative "../geometry/bezier"
|
|
5
|
+
require_relative "../geometry/rectangle"
|
|
5
6
|
require_relative "../graph/edge"
|
|
7
|
+
require_relative "node_index"
|
|
6
8
|
|
|
7
9
|
module Elkrb
|
|
8
10
|
module Layout
|
|
9
|
-
# Provides edge routing functionality for layout algorithms
|
|
11
|
+
# Provides edge routing functionality for layout algorithms.
|
|
12
|
+
#
|
|
13
|
+
# The including class sets @resolver (an Options::Resolver), as
|
|
14
|
+
# BaseAlgorithm does; every option read goes through it.
|
|
10
15
|
module EdgeRouter
|
|
11
16
|
# Route edges in a graph using specified routing style
|
|
12
17
|
# @param graph [Graph::Graph] The graph to route edges for
|
|
13
|
-
# @param node_map [
|
|
18
|
+
# @param node_map [NodeIndex] Resolves node/port ids to owning
|
|
19
|
+
# nodes for this level
|
|
14
20
|
# @param routing_style [String] Routing style (ORTHOGONAL, POLYLINE,
|
|
15
21
|
# SPLINES)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
# A plain `{id => node}` Hash was the documented node_map before
|
|
23
|
+
# NodeIndex existed, and both public entry points still take one.
|
|
24
|
+
# Only #node is ever asked of a node_map, so that is all this wraps.
|
|
25
|
+
class HashNodeMap
|
|
26
|
+
def initialize(nodes)
|
|
27
|
+
@nodes = nodes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A caller-supplied Hash is keyed by node id, but an edge endpoint may
|
|
31
|
+
# name a PORT. Fall back to the node owning that port, the way the
|
|
32
|
+
# pre-NodeIndex router did — otherwise a ported edge silently ends up
|
|
33
|
+
# with no sections at all.
|
|
34
|
+
def node(id)
|
|
35
|
+
@nodes[id] || owner_of_port(id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def owner_of_port(id)
|
|
41
|
+
@nodes.each_value.find { |node| owns_port?(node, id) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def owns_port?(node, id)
|
|
45
|
+
return false unless node.respond_to?(:ports)
|
|
46
|
+
|
|
47
|
+
node.ports&.any? { |port| port.id == id } || false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
private_constant :HashNodeMap
|
|
51
|
+
|
|
52
|
+
def coerce_node_map(node_map)
|
|
53
|
+
node_map.is_a?(::Hash) ? HashNodeMap.new(node_map) : node_map
|
|
54
|
+
end
|
|
19
55
|
|
|
56
|
+
def route_edges(graph, node_map = nil, routing_style = nil)
|
|
57
|
+
node_map = coerce_node_map(node_map || NodeIndex.build(graph))
|
|
20
58
|
graph.edges&.each do |edge|
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
style = routing_style || get_edge_routing_style(graph, edge)
|
|
60
|
+
if self_loop?(edge, node_map)
|
|
61
|
+
route_self_loop(edge, node_map, graph, style)
|
|
23
62
|
else
|
|
24
|
-
route_edge_with_style(edge, node_map, graph,
|
|
63
|
+
route_edge_with_style(edge, node_map, graph, style)
|
|
25
64
|
end
|
|
26
65
|
end
|
|
27
66
|
end
|
|
28
67
|
|
|
29
68
|
# Route a single edge
|
|
30
69
|
# @param edge [Graph::Edge] The edge to route
|
|
31
|
-
# @param node_map [
|
|
70
|
+
# @param node_map [NodeIndex] Resolves node/port ids to owning
|
|
71
|
+
# nodes for this level
|
|
32
72
|
# @param graph [Graph::Graph] The containing graph
|
|
33
|
-
def route_edge(edge, node_map,
|
|
73
|
+
def route_edge(edge, node_map, graph, routing_style = nil)
|
|
34
74
|
return unless edge.sources&.any? && edge.targets&.any?
|
|
35
75
|
|
|
76
|
+
node_map = coerce_node_map(node_map)
|
|
77
|
+
|
|
36
78
|
# Get source and target nodes
|
|
37
79
|
source_id = edge.sources.first
|
|
38
80
|
target_id = edge.targets.first
|
|
39
81
|
|
|
40
|
-
source_node = node_map
|
|
41
|
-
target_node = node_map
|
|
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)
|
|
82
|
+
source_node = node_map.node(source_id)
|
|
83
|
+
target_node = node_map.node(target_id)
|
|
47
84
|
|
|
48
85
|
return unless source_node && target_node
|
|
49
86
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
87
|
+
section = reset_section(edge, graph)
|
|
88
|
+
routing_style ||= get_edge_routing_style(graph, edge)
|
|
59
89
|
|
|
60
90
|
# Calculate routing points based on port-awareness
|
|
61
91
|
if edge_uses_ports?(edge, source_node, target_node)
|
|
62
|
-
route_with_ports(section, edge, source_node, target_node
|
|
92
|
+
route_with_ports(section, edge, source_node, target_node,
|
|
93
|
+
routing_style, graph)
|
|
63
94
|
else
|
|
64
|
-
route_node_to_node(section, source_node, target_node,
|
|
95
|
+
route_node_to_node(section, source_node, target_node, routing_style)
|
|
65
96
|
end
|
|
66
97
|
end
|
|
67
98
|
|
|
68
99
|
private
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
def route_cross_level_edges(graph)
|
|
102
|
+
hierarchy = @resolver.get("elk.hierarchyHandling", graph)
|
|
103
|
+
return unless hierarchy == "INCLUDE_CHILDREN"
|
|
104
|
+
|
|
105
|
+
deep_index = NodeIndex.build_deep(graph)
|
|
106
|
+
level_index = NodeIndex.build(graph)
|
|
107
|
+
graph.edges&.each do |edge|
|
|
108
|
+
route_cross_level_edge(edge, graph, level_index, deep_index)
|
|
75
109
|
end
|
|
76
|
-
map
|
|
77
110
|
end
|
|
78
111
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
def route_cross_level_edge(edge, graph, level_index, deep_index)
|
|
113
|
+
return unless edge.sources&.any? && edge.targets&.any?
|
|
114
|
+
|
|
115
|
+
source = unique_deep_endpoint(deep_index, edge.sources.first)
|
|
116
|
+
target = unique_deep_endpoint(deep_index, edge.targets.first)
|
|
117
|
+
return unless source && target
|
|
118
|
+
return if self_loop?(edge, level_index)
|
|
119
|
+
return if level_index.node(edge.sources.first) &&
|
|
120
|
+
level_index.node(edge.targets.first)
|
|
121
|
+
|
|
122
|
+
start_point, end_point = absolute_endpoint_points(
|
|
123
|
+
edge, source, target
|
|
124
|
+
)
|
|
125
|
+
section = reset_section(edge, graph)
|
|
126
|
+
section.start_point = start_point
|
|
127
|
+
section.end_point = end_point
|
|
128
|
+
section.bend_points = []
|
|
129
|
+
route_cross_level_bends(section, edge, graph)
|
|
84
130
|
end
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
def unique_deep_endpoint(deep_index, id)
|
|
133
|
+
matches = deep_index[id] || []
|
|
134
|
+
if matches.length > 1
|
|
135
|
+
raise Elkrb::ValidationError, "ambiguous id across levels: #{id}"
|
|
136
|
+
end
|
|
91
137
|
|
|
92
|
-
|
|
138
|
+
matches.first
|
|
93
139
|
end
|
|
94
140
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
141
|
+
def absolute_endpoint_points(edge, source, target)
|
|
142
|
+
source_node, source_offset = source
|
|
143
|
+
target_node, target_offset = target
|
|
144
|
+
source_rect = offset_rectangle(source_node, source_offset)
|
|
145
|
+
target_rect = offset_rectangle(target_node, target_offset)
|
|
98
146
|
source_port = find_port_by_id(edge.sources.first, source_node)
|
|
99
147
|
target_port = find_port_by_id(edge.targets.first, target_node)
|
|
148
|
+
source_anchor = absolute_endpoint_anchor(source_node, source_offset,
|
|
149
|
+
source_port, source_rect)
|
|
150
|
+
target_anchor = absolute_endpoint_anchor(target_node, target_offset,
|
|
151
|
+
target_port, target_rect)
|
|
100
152
|
|
|
101
|
-
start_point =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
start_point = if source_port
|
|
154
|
+
source_anchor
|
|
155
|
+
else
|
|
156
|
+
clip_to_border(source_rect, source_anchor,
|
|
157
|
+
target_anchor)
|
|
158
|
+
end
|
|
159
|
+
end_point = if target_port
|
|
160
|
+
target_anchor
|
|
161
|
+
else
|
|
162
|
+
clip_to_border(target_rect, target_anchor,
|
|
163
|
+
source_anchor)
|
|
164
|
+
end
|
|
165
|
+
[start_point, end_point]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def offset_rectangle(node, offset)
|
|
169
|
+
Geometry::Rectangle.new(
|
|
170
|
+
(node.x || 0.0) + offset.x,
|
|
171
|
+
(node.y || 0.0) + offset.y,
|
|
172
|
+
node.width || 0.0,
|
|
173
|
+
node.height || 0.0,
|
|
110
174
|
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def absolute_endpoint_anchor(node, offset, port, rectangle)
|
|
178
|
+
return rectangle.center unless port
|
|
179
|
+
|
|
180
|
+
point = get_port_position(port.id, node, nil)
|
|
181
|
+
Geometry::Point.new(x: offset.x + point.x, y: offset.y + point.y)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def route_cross_level_bends(section, edge, graph)
|
|
185
|
+
case get_edge_routing_style(graph, edge)
|
|
186
|
+
when "ORTHOGONAL"
|
|
187
|
+
add_orthogonal_bend_points(section, section.start_point,
|
|
188
|
+
section.end_point)
|
|
189
|
+
when "SPLINES"
|
|
190
|
+
add_spline_control_points(section, section.start_point,
|
|
191
|
+
section.end_point, edge)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check if edge should use port-based routing
|
|
196
|
+
def edge_uses_ports?(edge, source_node, target_node)
|
|
197
|
+
find_port_by_id(edge.sources.first, source_node) ||
|
|
198
|
+
find_port_by_id(edge.targets.first, target_node)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Route edge using port positions
|
|
202
|
+
def route_with_ports(section, edge, source_node, target_node,
|
|
203
|
+
routing_style, graph)
|
|
204
|
+
start_point, end_point = endpoint_points(edge, source_node, target_node)
|
|
111
205
|
|
|
112
206
|
section.start_point = start_point
|
|
113
207
|
section.end_point = end_point
|
|
114
|
-
section.bend_points
|
|
208
|
+
section.bend_points = []
|
|
115
209
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
section,
|
|
120
|
-
start_point,
|
|
121
|
-
end_point,
|
|
122
|
-
source_port,
|
|
123
|
-
target_port,
|
|
210
|
+
if routing_style == "ORTHOGONAL"
|
|
211
|
+
add_orthogonal_bend_points_between_nodes(
|
|
212
|
+
section, start_point, end_point, source_node, target_node, graph
|
|
124
213
|
)
|
|
125
|
-
elsif should_use_orthogonal_routing?(edge)
|
|
126
|
-
add_orthogonal_bend_points(section, start_point, end_point)
|
|
127
214
|
end
|
|
128
215
|
end
|
|
129
216
|
|
|
130
|
-
# Route edge from
|
|
131
|
-
def route_node_to_node(section, source_node, target_node,
|
|
132
|
-
|
|
133
|
-
|
|
217
|
+
# Route edge from source border to target border
|
|
218
|
+
def route_node_to_node(section, source_node, target_node, routing_style)
|
|
219
|
+
source_rect = node_rectangle(source_node)
|
|
220
|
+
target_rect = node_rectangle(target_node)
|
|
221
|
+
source_center = source_rect.center
|
|
222
|
+
target_center = target_rect.center
|
|
223
|
+
start_point = clip_to_border(source_rect, source_center, target_center)
|
|
224
|
+
end_point = clip_to_border(target_rect, target_center, source_center)
|
|
134
225
|
|
|
135
226
|
section.start_point = start_point
|
|
136
227
|
section.end_point = end_point
|
|
137
|
-
section.bend_points
|
|
228
|
+
section.bend_points = []
|
|
138
229
|
|
|
139
|
-
|
|
140
|
-
if edge && should_use_orthogonal_routing?(edge)
|
|
230
|
+
if routing_style == "ORTHOGONAL"
|
|
141
231
|
add_orthogonal_bend_points(section, start_point, end_point)
|
|
142
232
|
end
|
|
143
233
|
end
|
|
144
234
|
|
|
235
|
+
def node_rectangle(node)
|
|
236
|
+
Geometry::Rectangle.new(
|
|
237
|
+
node.x || 0.0,
|
|
238
|
+
node.y || 0.0,
|
|
239
|
+
node.width || 0.0,
|
|
240
|
+
node.height || 0.0,
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Intersect the ray from a rectangle's centre towards another point
|
|
245
|
+
# with the rectangle border.
|
|
246
|
+
def clip_to_border(rect, from, to)
|
|
247
|
+
dx = to.x - from.x
|
|
248
|
+
dy = to.y - from.y
|
|
249
|
+
return Geometry::Point.new(x: from.x, y: from.y) if dx.zero? && dy.zero?
|
|
250
|
+
|
|
251
|
+
scales = []
|
|
252
|
+
scales << ((rect.width / 2.0) / dx.abs) unless dx.zero?
|
|
253
|
+
scales << ((rect.height / 2.0) / dy.abs) unless dy.zero?
|
|
254
|
+
scale = scales.min
|
|
255
|
+
|
|
256
|
+
Geometry::Point.new(
|
|
257
|
+
x: from.x + (dx * scale),
|
|
258
|
+
y: from.y + (dy * scale),
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def reset_section(edge, graph)
|
|
263
|
+
section = Graph::EdgeSection.new(
|
|
264
|
+
id: "#{edge.id}_s0",
|
|
265
|
+
incoming_shape: edge.sources.first,
|
|
266
|
+
outgoing_shape: edge.targets.first,
|
|
267
|
+
)
|
|
268
|
+
edge.sections = [section]
|
|
269
|
+
edge.container ||= graph.id
|
|
270
|
+
section
|
|
271
|
+
end
|
|
272
|
+
|
|
145
273
|
# Find port by ID
|
|
146
274
|
def find_port_by_id(port_id, node)
|
|
147
275
|
node.ports&.find { |p| p.id == port_id }
|
|
148
276
|
end
|
|
149
277
|
|
|
150
|
-
# Get
|
|
278
|
+
# Get the port's outer-border anchor, or the node center.
|
|
151
279
|
def get_port_position(port_id, node, _direction)
|
|
152
|
-
# Try to find port
|
|
153
280
|
port = find_port_by_id(port_id, node)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
281
|
+
return get_node_center(node) unless port
|
|
282
|
+
|
|
283
|
+
x = (node.x || 0.0) + (port.x || 0.0)
|
|
284
|
+
y = (node.y || 0.0) + (port.y || 0.0)
|
|
285
|
+
width = port.width || 0.0
|
|
286
|
+
height = port.height || 0.0
|
|
287
|
+
case port.side
|
|
288
|
+
when Graph::Port::NORTH
|
|
289
|
+
Geometry::Point.new(x: x + (width / 2.0), y: y)
|
|
290
|
+
when Graph::Port::SOUTH
|
|
291
|
+
Geometry::Point.new(x: x + (width / 2.0), y: y + height)
|
|
292
|
+
when Graph::Port::WEST
|
|
293
|
+
Geometry::Point.new(x: x, y: y + (height / 2.0))
|
|
294
|
+
when Graph::Port::EAST
|
|
295
|
+
Geometry::Point.new(x: x + width, y: y + (height / 2.0))
|
|
296
|
+
else Geometry::Point.new(x: x, y: y)
|
|
164
297
|
end
|
|
165
298
|
end
|
|
166
299
|
|
|
@@ -171,37 +304,27 @@ module Elkrb
|
|
|
171
304
|
Geometry::Point.new(x: x, y: y)
|
|
172
305
|
end
|
|
173
306
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
edge.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
source_port, target_port)
|
|
182
|
-
source_side = source_port.side
|
|
183
|
-
target_side = target_port.side
|
|
307
|
+
def endpoint_points(edge, source_node, target_node)
|
|
308
|
+
source_id = edge.sources.first
|
|
309
|
+
target_id = edge.targets.first
|
|
310
|
+
source_port = find_port_by_id(source_id, source_node)
|
|
311
|
+
target_port = find_port_by_id(target_id, target_node)
|
|
312
|
+
source_anchor = get_port_position(source_id, source_node, :outgoing)
|
|
313
|
+
target_anchor = get_port_position(target_id, target_node, :incoming)
|
|
184
314
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
315
|
+
start_point = if source_port
|
|
316
|
+
source_anchor
|
|
317
|
+
else
|
|
318
|
+
clip_to_border(node_rectangle(source_node),
|
|
319
|
+
source_anchor, target_anchor)
|
|
320
|
+
end
|
|
321
|
+
end_point = if target_port
|
|
322
|
+
target_anchor
|
|
323
|
+
else
|
|
324
|
+
clip_to_border(node_rectangle(target_node),
|
|
325
|
+
target_anchor, source_anchor)
|
|
326
|
+
end
|
|
327
|
+
[start_point, end_point]
|
|
205
328
|
end
|
|
206
329
|
|
|
207
330
|
# Add horizontal bend points (for horizontal connections)
|
|
@@ -218,35 +341,75 @@ module Elkrb
|
|
|
218
341
|
section.add_bend_point(end_point.x, mid_y)
|
|
219
342
|
end
|
|
220
343
|
|
|
221
|
-
#
|
|
222
|
-
def
|
|
223
|
-
|
|
344
|
+
# Add orthogonal (right-angle) bend points
|
|
345
|
+
def add_orthogonal_bend_points(section, start_point, end_point)
|
|
346
|
+
return if start_point.x == end_point.x || start_point.y == end_point.y
|
|
347
|
+
|
|
348
|
+
dx = (end_point.x - start_point.x).abs
|
|
349
|
+
dy = (end_point.y - start_point.y).abs
|
|
350
|
+
if dx >= dy
|
|
351
|
+
add_horizontal_bend_points(section, start_point, end_point)
|
|
352
|
+
else
|
|
353
|
+
add_vertical_bend_points(section, start_point, end_point)
|
|
354
|
+
end
|
|
224
355
|
end
|
|
225
356
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
357
|
+
def add_orthogonal_bend_points_between_nodes(
|
|
358
|
+
section, start_point, end_point, source_node, target_node, graph
|
|
359
|
+
)
|
|
360
|
+
return if start_point.x == end_point.x || start_point.y == end_point.y
|
|
361
|
+
|
|
362
|
+
source = node_rectangle(source_node)
|
|
363
|
+
target = node_rectangle(target_node)
|
|
364
|
+
|
|
365
|
+
if horizontal_routing?(source, target, graph)
|
|
366
|
+
mid_x = horizontal_gap(source, target, start_point, end_point)
|
|
367
|
+
section.add_bend_point(mid_x, start_point.y)
|
|
368
|
+
section.add_bend_point(mid_x, end_point.y)
|
|
369
|
+
else
|
|
370
|
+
mid_y = vertical_gap(source, target, start_point, end_point)
|
|
371
|
+
section.add_bend_point(start_point.x, mid_y)
|
|
372
|
+
section.add_bend_point(end_point.x, mid_y)
|
|
373
|
+
end
|
|
229
374
|
end
|
|
230
375
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
376
|
+
def horizontal_routing?(source, target, graph)
|
|
377
|
+
direction = @resolver.get("elk.direction", graph)
|
|
378
|
+
return true if %w[LEFT RIGHT].include?(direction)
|
|
379
|
+
return false if %w[DOWN UP].include?(direction)
|
|
235
380
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
381
|
+
(target.center.x - source.center.x).abs >=
|
|
382
|
+
(target.center.y - source.center.y).abs
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def horizontal_gap(source, target, start_point, end_point)
|
|
386
|
+
return (source.x + source.width + target.x) / 2.0 if
|
|
387
|
+
source.x + source.width <= target.x
|
|
388
|
+
return (target.x + target.width + source.x) / 2.0 if
|
|
389
|
+
target.x + target.width <= source.x
|
|
390
|
+
|
|
391
|
+
(start_point.x + end_point.x) / 2.0
|
|
239
392
|
end
|
|
240
393
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
394
|
+
def vertical_gap(source, target, start_point, end_point)
|
|
395
|
+
return (source.y + source.height + target.y) / 2.0 if
|
|
396
|
+
source.y + source.height <= target.y
|
|
397
|
+
return (target.y + target.height + source.y) / 2.0 if
|
|
398
|
+
target.y + target.height <= source.y
|
|
244
399
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
graph.layout_options.edge_routing
|
|
400
|
+
(start_point.y + end_point.y) / 2.0
|
|
401
|
+
end
|
|
248
402
|
|
|
249
|
-
|
|
403
|
+
# Get edge routing style from graph options
|
|
404
|
+
#
|
|
405
|
+
# @param graph [Elkrb::Graph::Graph] The graph
|
|
406
|
+
# @return [String] Routing style (ORTHOGONAL, POLYLINE, SPLINES)
|
|
407
|
+
def get_edge_routing_style(graph, edge = nil)
|
|
408
|
+
elements = edge ? [edge, graph] : [graph]
|
|
409
|
+
style = @resolver.get("elk.edgeRouting", *elements)
|
|
410
|
+
|
|
411
|
+
style = style.to_s.upcase
|
|
412
|
+
style == "UNDEFINED" ? "ORTHOGONAL" : style
|
|
250
413
|
end
|
|
251
414
|
|
|
252
415
|
# Route edge with specified routing style
|
|
@@ -257,40 +420,28 @@ module Elkrb
|
|
|
257
420
|
when "POLYLINE"
|
|
258
421
|
route_polyline_edge(edge, node_map, graph)
|
|
259
422
|
else
|
|
260
|
-
route_edge(edge, node_map, graph)
|
|
423
|
+
route_edge(edge, node_map, graph, "ORTHOGONAL")
|
|
261
424
|
end
|
|
262
425
|
end
|
|
263
426
|
|
|
264
427
|
# Route edge with polyline (straight segments) style
|
|
265
428
|
def route_polyline_edge(edge, node_map, graph)
|
|
266
|
-
|
|
267
|
-
route_edge(edge, node_map, graph)
|
|
429
|
+
route_edge(edge, node_map, graph, "POLYLINE")
|
|
268
430
|
end
|
|
269
431
|
|
|
270
432
|
# Route edge with spline (curved) style
|
|
271
|
-
def route_spline_edge(edge, node_map,
|
|
433
|
+
def route_spline_edge(edge, node_map, graph)
|
|
272
434
|
return unless edge.sources&.any? && edge.targets&.any?
|
|
273
435
|
|
|
274
436
|
source_id = edge.sources.first
|
|
275
437
|
target_id = edge.targets.first
|
|
276
438
|
|
|
277
|
-
source_node = node_map
|
|
278
|
-
target_node = node_map
|
|
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)
|
|
439
|
+
source_node = node_map.node(source_id)
|
|
440
|
+
target_node = node_map.node(target_id)
|
|
282
441
|
|
|
283
442
|
return unless source_node && target_node
|
|
284
443
|
|
|
285
|
-
|
|
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
|
|
444
|
+
section = reset_section(edge, graph)
|
|
294
445
|
|
|
295
446
|
# Calculate spline routing
|
|
296
447
|
if edge_uses_ports?(edge, source_node, target_node)
|
|
@@ -302,16 +453,7 @@ module Elkrb
|
|
|
302
453
|
|
|
303
454
|
# Route spline edge using port positions
|
|
304
455
|
def route_spline_with_ports(section, edge, source_node, target_node)
|
|
305
|
-
start_point =
|
|
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
|
-
)
|
|
456
|
+
start_point, end_point = endpoint_points(edge, source_node, target_node)
|
|
315
457
|
|
|
316
458
|
section.start_point = start_point
|
|
317
459
|
section.end_point = end_point
|
|
@@ -322,8 +464,12 @@ module Elkrb
|
|
|
322
464
|
|
|
323
465
|
# Route spline edge from node center to node center
|
|
324
466
|
def route_spline_node_to_node(section, edge, source_node, target_node)
|
|
325
|
-
|
|
326
|
-
|
|
467
|
+
source_rect = node_rectangle(source_node)
|
|
468
|
+
target_rect = node_rectangle(target_node)
|
|
469
|
+
source_center = source_rect.center
|
|
470
|
+
target_center = target_rect.center
|
|
471
|
+
start_point = clip_to_border(source_rect, source_center, target_center)
|
|
472
|
+
end_point = clip_to_border(target_rect, target_center, source_center)
|
|
327
473
|
|
|
328
474
|
section.start_point = start_point
|
|
329
475
|
section.end_point = end_point
|
|
@@ -352,17 +498,12 @@ module Elkrb
|
|
|
352
498
|
|
|
353
499
|
# Get spline curvature from options
|
|
354
500
|
def get_spline_curvature(edge)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
curvature = edge.layout_options["elk.spline.curvature"] ||
|
|
358
|
-
edge.layout_options["spline.curvature"]
|
|
359
|
-
|
|
360
|
-
curvature ? curvature.to_f : 0.5
|
|
501
|
+
@resolver.get("elk.spline.curvature", edge)
|
|
361
502
|
end
|
|
362
503
|
|
|
363
504
|
# Calculate Bezier control points for smooth curves
|
|
364
505
|
def calculate_spline_controls(start_point, end_point, curvature, edge)
|
|
365
|
-
# Determine routing direction from edge
|
|
506
|
+
# Determine routing direction from the edge's options
|
|
366
507
|
direction = get_routing_direction(edge)
|
|
367
508
|
|
|
368
509
|
case direction
|
|
@@ -390,45 +531,34 @@ module Elkrb
|
|
|
390
531
|
|
|
391
532
|
# Get routing direction from edge options
|
|
392
533
|
def get_routing_direction(edge)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
edge.layout_options["elk.direction"] ||
|
|
396
|
-
edge.layout_options["direction"] ||
|
|
397
|
-
nil
|
|
534
|
+
@resolver.get("elk.direction", edge, default: nil)
|
|
398
535
|
end
|
|
399
536
|
|
|
400
|
-
# Check if edge
|
|
401
|
-
def self_loop?(edge)
|
|
537
|
+
# Check if an edge's endpoints belong to the same node.
|
|
538
|
+
def self_loop?(edge, node_map = nil)
|
|
402
539
|
sources = edge.sources || []
|
|
403
540
|
targets = edge.targets || []
|
|
404
541
|
|
|
405
542
|
return false if sources.empty? || targets.empty?
|
|
543
|
+
return sources.first == targets.first unless node_map
|
|
544
|
+
|
|
545
|
+
source_node = node_map.node(sources.first)
|
|
546
|
+
target_node = node_map.node(targets.first)
|
|
547
|
+
return sources.first == targets.first unless source_node || target_node
|
|
406
548
|
|
|
407
|
-
|
|
549
|
+
source_node&.equal?(target_node)
|
|
408
550
|
end
|
|
409
551
|
|
|
410
552
|
# Route a self-loop edge
|
|
411
553
|
def route_self_loop(edge, node_map, graph, routing_style)
|
|
412
554
|
node_id = edge.sources.first
|
|
413
|
-
node = node_map
|
|
414
|
-
|
|
415
|
-
# If not found, might be a port ID
|
|
416
|
-
node ||= find_node_with_port(node_map.values, node_id)
|
|
555
|
+
node = node_map.node(node_id)
|
|
417
556
|
|
|
418
557
|
return unless node
|
|
419
558
|
|
|
420
559
|
# 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
|
|
560
|
+
loop_index = get_self_loop_index(edge, node, graph, node_map)
|
|
561
|
+
section = reset_section(edge, graph)
|
|
432
562
|
|
|
433
563
|
# Check if edge uses ports
|
|
434
564
|
if edge_uses_ports_for_self_loop?(edge, node)
|
|
@@ -440,39 +570,42 @@ module Elkrb
|
|
|
440
570
|
when "SPLINES"
|
|
441
571
|
route_spline_self_loop(section, edge, node, loop_index)
|
|
442
572
|
when "POLYLINE"
|
|
443
|
-
route_polyline_self_loop(section, edge, node, loop_index)
|
|
573
|
+
route_polyline_self_loop(section, edge, node, loop_index, graph)
|
|
444
574
|
else
|
|
445
|
-
route_orthogonal_self_loop(
|
|
575
|
+
route_orthogonal_self_loop(
|
|
576
|
+
section, edge, node, loop_index, graph
|
|
577
|
+
)
|
|
446
578
|
end
|
|
447
579
|
end
|
|
448
580
|
end
|
|
449
581
|
|
|
450
582
|
# Get self-loop index for multiple loops on same node
|
|
451
|
-
def get_self_loop_index(edge, node, graph
|
|
583
|
+
def get_self_loop_index(edge, node, graph,
|
|
584
|
+
node_map = NodeIndex.build(graph))
|
|
452
585
|
return 0 unless graph.edges
|
|
453
586
|
|
|
454
|
-
# Find all self-loops on this node
|
|
455
587
|
self_loops = graph.edges.select do |e|
|
|
456
|
-
|
|
588
|
+
source_node = node_map.node(e.sources&.first)
|
|
589
|
+
self_loop?(e, node_map) && source_node.equal?(node)
|
|
457
590
|
end
|
|
458
591
|
|
|
459
|
-
|
|
460
|
-
self_loops.index(edge) || 0
|
|
592
|
+
self_loops.index { |candidate| candidate.equal?(edge) } || 0
|
|
461
593
|
end
|
|
462
594
|
|
|
463
595
|
# Route orthogonal self-loop (rectangular path)
|
|
464
|
-
def route_orthogonal_self_loop(section, edge, node, loop_index
|
|
596
|
+
def route_orthogonal_self_loop(section, edge, node, loop_index,
|
|
597
|
+
graph = nil)
|
|
465
598
|
# Calculate offset based on loop index
|
|
466
|
-
offset = calculate_loop_offset(loop_index)
|
|
599
|
+
offset = calculate_loop_offset(loop_index, edge, node)
|
|
467
600
|
|
|
468
601
|
# Get self-loop side
|
|
469
|
-
side = get_self_loop_side(edge, node)
|
|
470
|
-
|
|
471
602
|
# Calculate dimensions
|
|
472
603
|
width = ((node.width || 50.0) * 0.4) + offset
|
|
473
604
|
height = ((node.height || 50.0) * 0.4) + offset
|
|
605
|
+
side = unobstructed_self_loop_side(
|
|
606
|
+
get_self_loop_side(edge, node), node, width, height, graph
|
|
607
|
+
)
|
|
474
608
|
|
|
475
|
-
# Calculate start/end points based on side
|
|
476
609
|
case side
|
|
477
610
|
when "EAST"
|
|
478
611
|
route_east_self_loop(section, node, width, height)
|
|
@@ -483,121 +616,143 @@ module Elkrb
|
|
|
483
616
|
when "SOUTH"
|
|
484
617
|
route_south_self_loop(section, node, width, height)
|
|
485
618
|
else
|
|
486
|
-
# Default: EAST
|
|
487
619
|
route_east_self_loop(section, node, width, height)
|
|
488
620
|
end
|
|
489
621
|
end
|
|
490
622
|
|
|
623
|
+
def unobstructed_self_loop_side(preferred, node, width, height, graph)
|
|
624
|
+
return preferred unless graph
|
|
625
|
+
|
|
626
|
+
([preferred] + %w[EAST WEST NORTH SOUTH]).uniq.find do |side|
|
|
627
|
+
self_loop_side_clear?(side, node, width, height, graph)
|
|
628
|
+
end || preferred
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def self_loop_side_clear?(side, node, width, height, graph)
|
|
632
|
+
loop_box = self_loop_box(side, node, width, height)
|
|
633
|
+
graph.children.to_a.none? do |candidate|
|
|
634
|
+
next false if candidate.equal?(node)
|
|
635
|
+
|
|
636
|
+
rectangles_overlap?(loop_box, node_rectangle(candidate))
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def self_loop_box(side, node, width, height)
|
|
641
|
+
x = node.x || 0.0
|
|
642
|
+
y = node.y || 0.0
|
|
643
|
+
node_width = node.width || 50.0
|
|
644
|
+
node_height = node.height || 50.0
|
|
645
|
+
case side
|
|
646
|
+
when "WEST"
|
|
647
|
+
Geometry::Rectangle.new(x - width, y + (node_height / 2.0) - 5.0,
|
|
648
|
+
width, 10.0)
|
|
649
|
+
when "NORTH"
|
|
650
|
+
Geometry::Rectangle.new(x + (node_width / 2.0) - 5.0, y - height,
|
|
651
|
+
10.0, height)
|
|
652
|
+
when "SOUTH"
|
|
653
|
+
Geometry::Rectangle.new(
|
|
654
|
+
x + (node_width / 2.0) - 5.0, y + node_height, 10.0, height
|
|
655
|
+
)
|
|
656
|
+
else
|
|
657
|
+
Geometry::Rectangle.new(
|
|
658
|
+
x + node_width, y + (node_height / 2.0) - 5.0, width, 10.0
|
|
659
|
+
)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def rectangles_overlap?(first, second)
|
|
664
|
+
first.x < second.x + second.width &&
|
|
665
|
+
first.x + first.width > second.x &&
|
|
666
|
+
first.y < second.y + second.height &&
|
|
667
|
+
first.y + first.height > second.y
|
|
668
|
+
end
|
|
669
|
+
|
|
491
670
|
# Route self-loop on EAST side
|
|
492
|
-
def route_east_self_loop(section, node, width,
|
|
671
|
+
def route_east_self_loop(section, node, width, _height)
|
|
493
672
|
node_x = node.x || 0.0
|
|
494
673
|
node_y = node.y || 0.0
|
|
495
674
|
node_width = node.width || 50.0
|
|
496
675
|
node_height = node.height || 50.0
|
|
497
676
|
|
|
498
|
-
# Start point (right middle of node)
|
|
499
677
|
start_x = node_x + node_width
|
|
500
|
-
start_y = node_y + (node_height / 2.0)
|
|
501
|
-
|
|
502
|
-
# End point (slightly below start)
|
|
678
|
+
start_y = node_y + (node_height / 2.0) - 5.0
|
|
503
679
|
end_x = start_x
|
|
504
680
|
end_y = start_y + 10.0
|
|
505
681
|
|
|
506
682
|
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
507
683
|
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
508
684
|
|
|
509
|
-
# Bend points forming rectangular loop
|
|
510
685
|
section.bend_points = [
|
|
511
686
|
Geometry::Point.new(x: start_x + width, y: start_y),
|
|
512
|
-
Geometry::Point.new(x: start_x + width, y:
|
|
513
|
-
Geometry::Point.new(x: start_x + width, y: start_y + height),
|
|
514
|
-
Geometry::Point.new(x: end_x, y: end_y - 5.0),
|
|
687
|
+
Geometry::Point.new(x: start_x + width, y: end_y),
|
|
515
688
|
]
|
|
516
689
|
end
|
|
517
690
|
|
|
518
691
|
# Route self-loop on WEST side
|
|
519
|
-
def route_west_self_loop(section, node, width,
|
|
692
|
+
def route_west_self_loop(section, node, width, _height)
|
|
520
693
|
node_x = node.x || 0.0
|
|
521
694
|
node_y = node.y || 0.0
|
|
522
695
|
node_height = node.height || 50.0
|
|
523
696
|
|
|
524
|
-
# Start point (left middle of node)
|
|
525
697
|
start_x = node_x
|
|
526
|
-
start_y = node_y + (node_height / 2.0)
|
|
527
|
-
|
|
528
|
-
# End point (slightly below start)
|
|
698
|
+
start_y = node_y + (node_height / 2.0) - 5.0
|
|
529
699
|
end_x = start_x
|
|
530
700
|
end_y = start_y + 10.0
|
|
531
701
|
|
|
532
702
|
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
533
703
|
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
534
704
|
|
|
535
|
-
# Bend points forming rectangular loop
|
|
536
705
|
section.bend_points = [
|
|
537
706
|
Geometry::Point.new(x: start_x - width, y: start_y),
|
|
538
|
-
Geometry::Point.new(x: start_x - width, y:
|
|
539
|
-
Geometry::Point.new(x: start_x - width, y: start_y + height),
|
|
540
|
-
Geometry::Point.new(x: end_x, y: end_y - 5.0),
|
|
707
|
+
Geometry::Point.new(x: start_x - width, y: end_y),
|
|
541
708
|
]
|
|
542
709
|
end
|
|
543
710
|
|
|
544
711
|
# Route self-loop on NORTH side
|
|
545
|
-
def route_north_self_loop(section, node,
|
|
712
|
+
def route_north_self_loop(section, node, _width, height)
|
|
546
713
|
node_x = node.x || 0.0
|
|
547
714
|
node_y = node.y || 0.0
|
|
548
715
|
node_width = node.width || 50.0
|
|
549
716
|
|
|
550
|
-
|
|
551
|
-
start_x = node_x + (node_width / 2.0)
|
|
717
|
+
start_x = node_x + (node_width / 2.0) - 5.0
|
|
552
718
|
start_y = node_y
|
|
553
|
-
|
|
554
|
-
# End point (slightly to the right of start)
|
|
555
719
|
end_x = start_x + 10.0
|
|
556
720
|
end_y = start_y
|
|
557
721
|
|
|
558
722
|
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
559
723
|
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
560
724
|
|
|
561
|
-
# Bend points forming rectangular loop
|
|
562
725
|
section.bend_points = [
|
|
563
726
|
Geometry::Point.new(x: start_x, y: start_y - height),
|
|
564
|
-
Geometry::Point.new(x:
|
|
565
|
-
Geometry::Point.new(x: start_x + width, y: start_y - height),
|
|
566
|
-
Geometry::Point.new(x: end_x - 5.0, y: end_y),
|
|
727
|
+
Geometry::Point.new(x: end_x, y: start_y - height),
|
|
567
728
|
]
|
|
568
729
|
end
|
|
569
730
|
|
|
570
731
|
# Route self-loop on SOUTH side
|
|
571
|
-
def route_south_self_loop(section, node,
|
|
732
|
+
def route_south_self_loop(section, node, _width, height)
|
|
572
733
|
node_x = node.x || 0.0
|
|
573
734
|
node_y = node.y || 0.0
|
|
574
735
|
node_width = node.width || 50.0
|
|
575
736
|
node_height = node.height || 50.0
|
|
576
737
|
|
|
577
|
-
|
|
578
|
-
start_x = node_x + (node_width / 2.0)
|
|
738
|
+
start_x = node_x + (node_width / 2.0) - 5.0
|
|
579
739
|
start_y = node_y + node_height
|
|
580
|
-
|
|
581
|
-
# End point (slightly to the right of start)
|
|
582
740
|
end_x = start_x + 10.0
|
|
583
741
|
end_y = start_y
|
|
584
742
|
|
|
585
743
|
section.start_point = Geometry::Point.new(x: start_x, y: start_y)
|
|
586
744
|
section.end_point = Geometry::Point.new(x: end_x, y: end_y)
|
|
587
745
|
|
|
588
|
-
# Bend points forming rectangular loop
|
|
589
746
|
section.bend_points = [
|
|
590
747
|
Geometry::Point.new(x: start_x, y: start_y + height),
|
|
591
|
-
Geometry::Point.new(x:
|
|
592
|
-
Geometry::Point.new(x: start_x + width, y: start_y + height),
|
|
593
|
-
Geometry::Point.new(x: end_x - 5.0, y: end_y),
|
|
748
|
+
Geometry::Point.new(x: end_x, y: start_y + height),
|
|
594
749
|
]
|
|
595
750
|
end
|
|
596
751
|
|
|
597
752
|
# Route spline self-loop (curved path)
|
|
598
753
|
def route_spline_self_loop(section, edge, node, loop_index)
|
|
599
754
|
# Calculate offset based on loop index
|
|
600
|
-
offset = calculate_loop_offset(loop_index)
|
|
755
|
+
offset = calculate_loop_offset(loop_index, edge, node)
|
|
601
756
|
|
|
602
757
|
# Get self-loop side
|
|
603
758
|
side = get_self_loop_side(edge, node)
|
|
@@ -613,7 +768,7 @@ module Elkrb
|
|
|
613
768
|
case side
|
|
614
769
|
when "EAST"
|
|
615
770
|
start_x = node_x + node_width
|
|
616
|
-
start_y = node_y + (node_height / 2.0)
|
|
771
|
+
start_y = node_y + (node_height / 2.0) - 5.0
|
|
617
772
|
end_x = start_x
|
|
618
773
|
end_y = start_y + 10.0
|
|
619
774
|
|
|
@@ -628,7 +783,7 @@ module Elkrb
|
|
|
628
783
|
)
|
|
629
784
|
when "WEST"
|
|
630
785
|
start_x = node_x
|
|
631
|
-
start_y = node_y + (node_height / 2.0)
|
|
786
|
+
start_y = node_y + (node_height / 2.0) - 5.0
|
|
632
787
|
end_x = start_x
|
|
633
788
|
end_y = start_y + 10.0
|
|
634
789
|
|
|
@@ -642,7 +797,7 @@ module Elkrb
|
|
|
642
797
|
y: start_y + radius,
|
|
643
798
|
)
|
|
644
799
|
when "NORTH"
|
|
645
|
-
start_x = node_x + (node_width / 2.0)
|
|
800
|
+
start_x = node_x + (node_width / 2.0) - 5.0
|
|
646
801
|
start_y = node_y
|
|
647
802
|
end_x = start_x + 10.0
|
|
648
803
|
end_y = start_y
|
|
@@ -657,7 +812,7 @@ module Elkrb
|
|
|
657
812
|
y: start_y - radius,
|
|
658
813
|
)
|
|
659
814
|
when "SOUTH"
|
|
660
|
-
start_x = node_x + (node_width / 2.0)
|
|
815
|
+
start_x = node_x + (node_width / 2.0) - 5.0
|
|
661
816
|
start_y = node_y + node_height
|
|
662
817
|
end_x = start_x + 10.0
|
|
663
818
|
end_y = start_y
|
|
@@ -674,7 +829,7 @@ module Elkrb
|
|
|
674
829
|
else
|
|
675
830
|
# Default: EAST
|
|
676
831
|
start_x = node_x + node_width
|
|
677
|
-
start_y = node_y + (node_height / 2.0)
|
|
832
|
+
start_y = node_y + (node_height / 2.0) - 5.0
|
|
678
833
|
end_x = start_x
|
|
679
834
|
end_y = start_y + 10.0
|
|
680
835
|
|
|
@@ -694,35 +849,21 @@ module Elkrb
|
|
|
694
849
|
end
|
|
695
850
|
|
|
696
851
|
# Route polyline self-loop (simple path)
|
|
697
|
-
def route_polyline_self_loop(section, edge, node, loop_index
|
|
852
|
+
def route_polyline_self_loop(section, edge, node, loop_index,
|
|
853
|
+
graph = nil)
|
|
698
854
|
# For polyline, use orthogonal routing
|
|
699
|
-
route_orthogonal_self_loop(section, edge, node, loop_index)
|
|
855
|
+
route_orthogonal_self_loop(section, edge, node, loop_index, graph)
|
|
700
856
|
end
|
|
701
857
|
|
|
702
858
|
# Calculate offset for multiple self-loops on same node
|
|
703
|
-
def calculate_loop_offset(loop_index)
|
|
704
|
-
base_offset =
|
|
859
|
+
def calculate_loop_offset(loop_index, edge, node)
|
|
860
|
+
base_offset = @resolver.get("elk.selfLoopOffset", edge, node)
|
|
705
861
|
base_offset * (loop_index + 1)
|
|
706
862
|
end
|
|
707
863
|
|
|
708
864
|
# Get self-loop side from options or default
|
|
709
865
|
def get_self_loop_side(edge, node)
|
|
710
|
-
|
|
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"
|
|
866
|
+
@resolver.get("elk.selfLoopSide", edge, node)
|
|
726
867
|
end
|
|
727
868
|
|
|
728
869
|
# Check if self-loop edge uses ports
|
|
@@ -750,27 +891,12 @@ module Elkrb
|
|
|
750
891
|
source_port = find_port_by_id(source_id, node)
|
|
751
892
|
target_port = find_port_by_id(target_id, node)
|
|
752
893
|
|
|
753
|
-
|
|
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
|
|
894
|
+
offset = calculate_loop_offset(loop_index, edge, node)
|
|
773
895
|
if source_port && target_port
|
|
896
|
+
start_point = get_port_absolute_position(source_port, node)
|
|
897
|
+
end_point = get_port_absolute_position(target_port, node)
|
|
898
|
+
section.start_point = start_point
|
|
899
|
+
section.end_point = end_point
|
|
774
900
|
route_port_to_port_self_loop(
|
|
775
901
|
section,
|
|
776
902
|
start_point,
|
|
@@ -781,33 +907,52 @@ module Elkrb
|
|
|
781
907
|
routing_style,
|
|
782
908
|
)
|
|
783
909
|
else
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
910
|
+
port = source_port || target_port
|
|
911
|
+
port_point = get_port_absolute_position(port, node)
|
|
912
|
+
side = port.side || get_self_loop_side(edge, node)
|
|
913
|
+
node_x = node.x || 0.0
|
|
914
|
+
node_y = node.y || 0.0
|
|
915
|
+
node_width = node.width || 0.0
|
|
916
|
+
node_height = node.height || 0.0
|
|
917
|
+
node_point = case side
|
|
918
|
+
when "WEST", "EAST"
|
|
919
|
+
candidate = [port_point.y + 10.0,
|
|
920
|
+
node_y + node_height].min
|
|
921
|
+
Geometry::Point.new(
|
|
922
|
+
x: side == "EAST" ? node_x + node_width : node_x,
|
|
923
|
+
y: candidate,
|
|
924
|
+
)
|
|
925
|
+
else
|
|
926
|
+
candidate = [port_point.x + 10.0,
|
|
927
|
+
node_x + node_width].min
|
|
928
|
+
Geometry::Point.new(
|
|
929
|
+
x: candidate,
|
|
930
|
+
y: side == "SOUTH" ? node_y + node_height : node_y,
|
|
931
|
+
)
|
|
932
|
+
end
|
|
933
|
+
start_point = source_port ? port_point : node_point
|
|
934
|
+
end_point = target_port ? port_point : node_point
|
|
935
|
+
section.start_point = start_point
|
|
936
|
+
section.end_point = end_point
|
|
937
|
+
if routing_style == "SPLINES"
|
|
938
|
+
route_spline_port_self_loop(section, start_point, end_point,
|
|
939
|
+
side, side, offset)
|
|
788
940
|
else
|
|
789
|
-
|
|
941
|
+
route_orthogonal_port_self_loop(section, start_point, end_point,
|
|
942
|
+
side, side, offset)
|
|
790
943
|
end
|
|
791
944
|
end
|
|
792
945
|
end
|
|
793
946
|
|
|
794
947
|
# Get absolute position of a port
|
|
795
948
|
def get_port_absolute_position(port, node)
|
|
796
|
-
|
|
797
|
-
x: (node.x || 0.0) + (port.x || 0.0),
|
|
798
|
-
y: (node.y || 0.0) + (port.y || 0.0),
|
|
799
|
-
)
|
|
949
|
+
get_port_position(port.id, node, nil)
|
|
800
950
|
end
|
|
801
951
|
|
|
802
952
|
# Route self-loop from port to port
|
|
803
953
|
def route_port_to_port_self_loop(section, start_point, end_point,
|
|
804
954
|
source_port, target_port, offset,
|
|
805
955
|
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
956
|
source_side = source_port.side || "EAST"
|
|
812
957
|
target_side = target_port.side || "EAST"
|
|
813
958
|
|
|
@@ -837,43 +982,84 @@ module Elkrb
|
|
|
837
982
|
# Route orthogonal self-loop between ports
|
|
838
983
|
def route_orthogonal_port_self_loop(section, start_point, end_point,
|
|
839
984
|
source_side, target_side, offset)
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
# Create bend points based on port sides
|
|
985
|
+
extension = offset + 30.0
|
|
843
986
|
case [source_side, target_side]
|
|
844
987
|
when ["EAST", "EAST"], ["WEST", "WEST"]
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
section.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
section.add_bend_point(end_point.x - extension, end_point.y)
|
|
988
|
+
direction = source_side == "EAST" ? 1 : -1
|
|
989
|
+
outer_x = start_point.x + (direction * extension)
|
|
990
|
+
section.bend_points = [
|
|
991
|
+
Geometry::Point.new(x: outer_x, y: start_point.y),
|
|
992
|
+
Geometry::Point.new(x: outer_x, y: end_point.y),
|
|
993
|
+
]
|
|
994
|
+
if start_point.x == end_point.x && start_point.y == end_point.y
|
|
995
|
+
section.bend_points[1].y += 10.0
|
|
996
|
+
section.bend_points << Geometry::Point.new(
|
|
997
|
+
x: end_point.x, y: end_point.y + 10.0,
|
|
998
|
+
)
|
|
857
999
|
end
|
|
858
1000
|
when ["NORTH", "NORTH"], ["SOUTH", "SOUTH"]
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
section.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
section.add_bend_point(end_point.x, end_point.y + extension)
|
|
1001
|
+
direction = source_side == "SOUTH" ? 1 : -1
|
|
1002
|
+
outer_y = start_point.y + (direction * extension)
|
|
1003
|
+
section.bend_points = [
|
|
1004
|
+
Geometry::Point.new(x: start_point.x, y: outer_y),
|
|
1005
|
+
Geometry::Point.new(x: end_point.x, y: outer_y),
|
|
1006
|
+
]
|
|
1007
|
+
if start_point.x == end_point.x && start_point.y == end_point.y
|
|
1008
|
+
section.bend_points[1].x += 10.0
|
|
1009
|
+
section.bend_points << Geometry::Point.new(
|
|
1010
|
+
x: end_point.x + 10.0, y: end_point.y,
|
|
1011
|
+
)
|
|
871
1012
|
end
|
|
872
1013
|
else
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1014
|
+
deltas = {
|
|
1015
|
+
"EAST" => [extension, 0.0],
|
|
1016
|
+
"WEST" => [-extension, 0.0],
|
|
1017
|
+
"SOUTH" => [0.0, extension],
|
|
1018
|
+
"NORTH" => [0.0, -extension],
|
|
1019
|
+
}
|
|
1020
|
+
source_delta = deltas.fetch(source_side, [0.0, 0.0])
|
|
1021
|
+
target_delta = deltas.fetch(target_side, [0.0, 0.0])
|
|
1022
|
+
source_outer = Geometry::Point.new(
|
|
1023
|
+
x: start_point.x + source_delta[0],
|
|
1024
|
+
y: start_point.y + source_delta[1],
|
|
1025
|
+
)
|
|
1026
|
+
target_outer = Geometry::Point.new(
|
|
1027
|
+
x: end_point.x + target_delta[0],
|
|
1028
|
+
y: end_point.y + target_delta[1],
|
|
1029
|
+
)
|
|
1030
|
+
opposite = [["EAST", "WEST"], ["WEST", "EAST"],
|
|
1031
|
+
["NORTH", "SOUTH"], ["SOUTH", "NORTH"]]
|
|
1032
|
+
.include?([source_side, target_side])
|
|
1033
|
+
vertical_sides = %w[EAST WEST].include?(source_side)
|
|
1034
|
+
section.bend_points = if opposite && vertical_sides
|
|
1035
|
+
corridor = [source_outer.y,
|
|
1036
|
+
target_outer.y].min - extension
|
|
1037
|
+
[source_outer,
|
|
1038
|
+
Geometry::Point.new(x: source_outer.x,
|
|
1039
|
+
y: corridor),
|
|
1040
|
+
Geometry::Point.new(x: target_outer.x,
|
|
1041
|
+
y: corridor),
|
|
1042
|
+
target_outer]
|
|
1043
|
+
elsif opposite
|
|
1044
|
+
corridor = [source_outer.x,
|
|
1045
|
+
target_outer.x].max + extension
|
|
1046
|
+
[source_outer,
|
|
1047
|
+
Geometry::Point.new(x: corridor,
|
|
1048
|
+
y: source_outer.y),
|
|
1049
|
+
Geometry::Point.new(x: corridor,
|
|
1050
|
+
y: target_outer.y),
|
|
1051
|
+
target_outer]
|
|
1052
|
+
elsif vertical_sides
|
|
1053
|
+
[source_outer,
|
|
1054
|
+
Geometry::Point.new(x: source_outer.x,
|
|
1055
|
+
y: target_outer.y),
|
|
1056
|
+
target_outer]
|
|
1057
|
+
else
|
|
1058
|
+
[source_outer,
|
|
1059
|
+
Geometry::Point.new(x: target_outer.x,
|
|
1060
|
+
y: source_outer.y),
|
|
1061
|
+
target_outer]
|
|
1062
|
+
end
|
|
877
1063
|
end
|
|
878
1064
|
end
|
|
879
1065
|
|
|
@@ -928,6 +1114,16 @@ module Elkrb
|
|
|
928
1114
|
control2 = Geometry::Point.new(x: mid_x, y: end_point.y)
|
|
929
1115
|
end
|
|
930
1116
|
|
|
1117
|
+
if start_point.x == end_point.x && start_point.y == end_point.y
|
|
1118
|
+
if %w[EAST WEST].include?(source_side)
|
|
1119
|
+
control1.y -= 5.0
|
|
1120
|
+
control2.y += 5.0
|
|
1121
|
+
else
|
|
1122
|
+
control1.x -= 5.0
|
|
1123
|
+
control2.x += 5.0
|
|
1124
|
+
end
|
|
1125
|
+
end
|
|
1126
|
+
|
|
931
1127
|
section.bend_points = [control1, control2]
|
|
932
1128
|
end
|
|
933
1129
|
end
|