elkrb 1.0.2 → 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
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../options/resolver"
|
|
4
|
+
|
|
3
5
|
module Elkrb
|
|
4
6
|
module Layout
|
|
5
7
|
# Module to add automatic label placement to layout algorithms.
|
|
6
8
|
# Handles positioning of node labels, edge labels, and port labels.
|
|
9
|
+
# The including class sets @resolver, as BaseAlgorithm does.
|
|
7
10
|
module LabelPlacer
|
|
11
|
+
OPPOSITE_SIDE = { left: :right, right: :left, top: :bottom, bottom: :top }.freeze
|
|
12
|
+
private_constant :OPPOSITE_SIDE
|
|
13
|
+
|
|
8
14
|
# Place all labels in the graph after layout is complete.
|
|
9
15
|
#
|
|
10
16
|
# @param graph [Graph::Graph] The laid out graph
|
|
@@ -16,115 +22,135 @@ module Elkrb
|
|
|
16
22
|
|
|
17
23
|
# Place edge labels
|
|
18
24
|
place_edge_labels(graph) if graph.edges
|
|
19
|
-
|
|
20
|
-
# Recursively place labels in hierarchical graphs
|
|
21
|
-
place_hierarchical_labels(graph) if graph.hierarchical?
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
private
|
|
25
28
|
|
|
26
|
-
#
|
|
29
|
+
# ELK treats a missing width/height as 0 (compound nodes and
|
|
30
|
+
# deserialized labels commonly omit size).
|
|
31
|
+
def width_of(element)
|
|
32
|
+
element.width || 0.0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def height_of(element)
|
|
36
|
+
element.height || 0.0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Place labels for all nodes in the graph. Node and port labels are
|
|
40
|
+
# owner-relative: (0, 0) is the owner's top-left corner.
|
|
27
41
|
def place_node_labels(graph)
|
|
28
42
|
graph.children.each do |node|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
if node.labels && !node.labels.empty?
|
|
44
|
+
node.labels.each_with_index do |label, index|
|
|
45
|
+
place_node_label(node, label, index)
|
|
46
|
+
end
|
|
33
47
|
end
|
|
34
48
|
|
|
35
|
-
# Place port labels if node has ports
|
|
36
49
|
place_port_labels(node) if node.ports && !node.ports.empty?
|
|
37
50
|
end
|
|
38
51
|
end
|
|
39
52
|
|
|
40
|
-
# Place a single node label.
|
|
53
|
+
# Place a single node label. Without a placement option the label is
|
|
54
|
+
# not moved, as in ELK: it keeps its coordinates, or (0, 0) if it has
|
|
55
|
+
# none.
|
|
41
56
|
def place_node_label(node, label, index = 0)
|
|
42
|
-
placement = label_placement_option(node, "
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
when /OUTSIDE/
|
|
49
|
-
place_label_outside_node(node, label, placement, index)
|
|
50
|
-
else
|
|
51
|
-
# Default: center inside
|
|
52
|
-
place_label_center(node, label)
|
|
57
|
+
placement = label_placement_option(node, "elk.nodeLabels.placement")
|
|
58
|
+
tokens = placement_tokens(placement)
|
|
59
|
+
if tokens.empty?
|
|
60
|
+
label.x ||= 0.0
|
|
61
|
+
label.y ||= 0.0
|
|
62
|
+
return
|
|
53
63
|
end
|
|
54
|
-
end
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
when /BOTTOM/
|
|
62
|
-
place_label_inside_bottom(node, label, index)
|
|
63
|
-
when /LEFT/
|
|
64
|
-
place_label_inside_left(node, label, index)
|
|
65
|
-
when /RIGHT/
|
|
66
|
-
place_label_inside_right(node, label, index)
|
|
65
|
+
vertical = vertical_of(tokens)
|
|
66
|
+
horizontal = horizontal_of(tokens)
|
|
67
|
+
|
|
68
|
+
if tokens.include?("OUTSIDE")
|
|
69
|
+
place_label_outside_node(node, label, vertical, horizontal, index)
|
|
67
70
|
else
|
|
68
|
-
|
|
71
|
+
place_label_inside_node(node, label, vertical, horizontal, index)
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
when /TOP/
|
|
78
|
-
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
79
|
-
label.y = node.y - label.height - margin
|
|
80
|
-
when /BOTTOM/
|
|
81
|
-
label.x = node.x + ((node.width - label.width) / 2.0)
|
|
82
|
-
label.y = node.y + node.height + margin
|
|
83
|
-
when /LEFT/
|
|
84
|
-
label.x = node.x - label.width - margin
|
|
85
|
-
label.y = node.y + ((node.height - label.height) / 2.0)
|
|
86
|
-
when /RIGHT/
|
|
87
|
-
label.x = node.x + node.width + margin
|
|
88
|
-
label.y = node.y + ((node.height - label.height) / 2.0)
|
|
89
|
-
end
|
|
75
|
+
# The upcased tokens of a placement value. Accepts ELK's
|
|
76
|
+
# "[H_CENTER,V_CENTER,INSIDE]" and the space-separated
|
|
77
|
+
# "INSIDE V_CENTER H_CENTER".
|
|
78
|
+
def placement_tokens(placement)
|
|
79
|
+
placement.to_s.upcase.split(/[\s,\[\]]+/).reject(&:empty?)
|
|
90
80
|
end
|
|
91
81
|
|
|
92
|
-
#
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
# :top, :center or :bottom. V_TOP is also spelled TOP.
|
|
83
|
+
def vertical_of(tokens)
|
|
84
|
+
return :top if tokens.intersect?(%w[V_TOP TOP])
|
|
85
|
+
return :bottom if tokens.intersect?(%w[V_BOTTOM BOTTOM])
|
|
96
86
|
|
|
97
|
-
|
|
98
|
-
label.y = node.y + padding + y_offset
|
|
87
|
+
:center
|
|
99
88
|
end
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
# :left, :center or :right. H_LEFT is also spelled LEFT.
|
|
91
|
+
def horizontal_of(tokens)
|
|
92
|
+
return :left if tokens.intersect?(%w[H_LEFT LEFT])
|
|
93
|
+
return :right if tokens.intersect?(%w[H_RIGHT RIGHT])
|
|
104
94
|
|
|
105
|
-
|
|
106
|
-
label.y = node.y + node.height - label.height - padding - y_offset
|
|
95
|
+
:center
|
|
107
96
|
end
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
# Place a label inside the node, relative to the node's origin.
|
|
99
|
+
def place_label_inside_node(node, label, vertical, horizontal, index)
|
|
110
100
|
padding = label_padding_option(node)
|
|
111
|
-
|
|
101
|
+
offset = index * (height_of(label) + padding)
|
|
102
|
+
|
|
103
|
+
label.x = inside_x(node, label, horizontal, padding)
|
|
104
|
+
label.y =
|
|
105
|
+
case vertical
|
|
106
|
+
when :top then padding + offset
|
|
107
|
+
when :bottom then height_of(node) - height_of(label) - padding - offset
|
|
108
|
+
else (height_of(node) - height_of(label)) / 2.0
|
|
109
|
+
end
|
|
110
|
+
end
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
def inside_x(node, label, horizontal, padding)
|
|
113
|
+
case horizontal
|
|
114
|
+
when :left then padding
|
|
115
|
+
when :right then width_of(node) - width_of(label) - padding
|
|
116
|
+
else (width_of(node) - width_of(label)) / 2.0
|
|
117
|
+
end
|
|
115
118
|
end
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
# Place a label outside the node, relative to the node's origin. Above
|
|
121
|
+
# or below when a vertical side is named, otherwise beside. Naming no
|
|
122
|
+
# side at all places it above.
|
|
123
|
+
def place_label_outside_node(node, label, vertical, horizontal, index)
|
|
124
|
+
margin = label_margin_option(node)
|
|
125
|
+
offset = index * (height_of(label) + margin)
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
if vertical == :center && horizontal != :center
|
|
128
|
+
place_label_beside_node(node, label, horizontal, margin)
|
|
129
|
+
elsif vertical == :bottom
|
|
130
|
+
label.x = outside_x(node, label, horizontal)
|
|
131
|
+
label.y = height_of(node) + margin + offset
|
|
132
|
+
else
|
|
133
|
+
label.x = outside_x(node, label, horizontal)
|
|
134
|
+
label.y = -height_of(label) - margin - offset
|
|
135
|
+
end
|
|
123
136
|
end
|
|
124
137
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
def outside_x(node, label, horizontal)
|
|
139
|
+
case horizontal
|
|
140
|
+
when :left then 0.0
|
|
141
|
+
when :right then width_of(node) - width_of(label)
|
|
142
|
+
else (width_of(node) - width_of(label)) / 2.0
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def place_label_beside_node(node, label, horizontal, margin)
|
|
147
|
+
label.x =
|
|
148
|
+
if horizontal == :left
|
|
149
|
+
-width_of(label) - margin
|
|
150
|
+
else
|
|
151
|
+
width_of(node) + margin
|
|
152
|
+
end
|
|
153
|
+
label.y = (height_of(node) - height_of(label)) / 2.0
|
|
128
154
|
end
|
|
129
155
|
|
|
130
156
|
# Place labels for all ports on a node.
|
|
@@ -138,27 +164,16 @@ module Elkrb
|
|
|
138
164
|
end
|
|
139
165
|
end
|
|
140
166
|
|
|
141
|
-
# Place a port label.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
placement = label_placement_option(port, "port.label.placement") ||
|
|
167
|
+
# Place a port label, relative to the port's origin. OUTSIDE (the
|
|
168
|
+
# default) puts it beside the port away from the node, INSIDE on the
|
|
169
|
+
# node's side of the port.
|
|
170
|
+
def place_port_label(node, port, label, index)
|
|
171
|
+
placement = label_placement_option(port, "elk.portLabels.placement") ||
|
|
148
172
|
"OUTSIDE"
|
|
173
|
+
side = port_side(node, port)
|
|
174
|
+
side = OPPOSITE_SIDE.fetch(side) if placement_tokens(placement).include?("INSIDE")
|
|
149
175
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
case placement.upcase
|
|
153
|
-
when /INSIDE/
|
|
154
|
-
# Place inside port (if port is large enough)
|
|
155
|
-
label.x = port_x + ((port.width - label.width) / 2.0)
|
|
156
|
-
label.y = port_y + ((port.height - label.height) / 2.0)
|
|
157
|
-
else
|
|
158
|
-
# Default: outside, positioned based on port side
|
|
159
|
-
side = port_side(node, port)
|
|
160
|
-
place_port_label_by_side(label, port_x, port_y, port, side, margin)
|
|
161
|
-
end
|
|
176
|
+
place_port_label_by_side(label, port, side, label_margin_option(port), index)
|
|
162
177
|
end
|
|
163
178
|
|
|
164
179
|
# Determine which side of the node the port is on.
|
|
@@ -168,9 +183,9 @@ module Elkrb
|
|
|
168
183
|
|
|
169
184
|
# Check which edge the port is closest to
|
|
170
185
|
left_dist = port_x
|
|
171
|
-
right_dist = node
|
|
186
|
+
right_dist = width_of(node) - port_x
|
|
172
187
|
top_dist = port_y
|
|
173
|
-
bottom_dist = node
|
|
188
|
+
bottom_dist = height_of(node) - port_y
|
|
174
189
|
|
|
175
190
|
min_dist = [left_dist, right_dist, top_dist, bottom_dist].min
|
|
176
191
|
|
|
@@ -182,21 +197,21 @@ module Elkrb
|
|
|
182
197
|
end
|
|
183
198
|
end
|
|
184
199
|
|
|
185
|
-
# Place port label
|
|
186
|
-
|
|
200
|
+
# Place a port label on the given side of the port. Stacked labels
|
|
201
|
+
# step along the side by their own height.
|
|
202
|
+
def place_port_label_by_side(label, port, side, margin, index)
|
|
203
|
+
step = index * (height_of(label) + margin)
|
|
204
|
+
|
|
187
205
|
case side
|
|
188
|
-
when :left
|
|
189
|
-
label.x =
|
|
190
|
-
label.y =
|
|
191
|
-
when :right
|
|
192
|
-
label.x = port_x + port.width + margin
|
|
193
|
-
label.y = port_y + ((port.height - label.height) / 2.0)
|
|
206
|
+
when :left, :right
|
|
207
|
+
label.x = side == :left ? -width_of(label) - margin : width_of(port) + margin
|
|
208
|
+
label.y = ((height_of(port) - height_of(label)) / 2.0) + step
|
|
194
209
|
when :top
|
|
195
|
-
label.x =
|
|
196
|
-
label.y =
|
|
210
|
+
label.x = (width_of(port) - width_of(label)) / 2.0
|
|
211
|
+
label.y = -height_of(label) - margin - step
|
|
197
212
|
when :bottom
|
|
198
|
-
label.x =
|
|
199
|
-
label.y =
|
|
213
|
+
label.x = (width_of(port) - width_of(label)) / 2.0
|
|
214
|
+
label.y = height_of(port) + margin + step
|
|
200
215
|
end
|
|
201
216
|
end
|
|
202
217
|
|
|
@@ -215,33 +230,37 @@ module Elkrb
|
|
|
215
230
|
def place_edge_label(edge, label, index)
|
|
216
231
|
# Get edge path (sections with bend points)
|
|
217
232
|
if edge.sections && !edge.sections.empty?
|
|
218
|
-
place_edge_label_on_section(edge.sections.first, label, index)
|
|
233
|
+
place_edge_label_on_section(edge, edge.sections.first, label, index)
|
|
219
234
|
else
|
|
220
235
|
# No sections, estimate from source/target
|
|
221
236
|
place_edge_label_estimated(edge, label, index)
|
|
222
237
|
end
|
|
223
238
|
end
|
|
224
239
|
|
|
225
|
-
# Place edge label on an edge section.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
# Place an edge label on an edge section, in the section's frame. The
|
|
241
|
+
# label's own elk.edgeLabels.placement wins over the edge's.
|
|
242
|
+
def place_edge_label_on_section(edge, section, label, index)
|
|
243
|
+
tokens = placement_tokens(
|
|
244
|
+
@resolver.get("elk.edgeLabels.placement", label, edge),
|
|
245
|
+
)
|
|
246
|
+
offset = index * (height_of(label) + 2) # Stack multiple labels
|
|
247
|
+
|
|
248
|
+
anchor =
|
|
249
|
+
if tokens.include?("HEAD")
|
|
250
|
+
above(section.end_point, label)
|
|
251
|
+
elsif tokens.include?("TAIL")
|
|
252
|
+
above(section.start_point, label)
|
|
253
|
+
else
|
|
254
|
+
calculate_edge_center(section)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
label.x = anchor[:x] - (width_of(label) / 2.0)
|
|
258
|
+
label.y = anchor[:y] - (height_of(label) / 2.0) + offset
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# HEAD and TAIL labels sit just above the end point they name.
|
|
262
|
+
def above(point, label)
|
|
263
|
+
{ x: point.x, y: point.y - (height_of(label) / 2.0) - 5 }
|
|
245
264
|
end
|
|
246
265
|
|
|
247
266
|
# Calculate the center point of an edge section.
|
|
@@ -262,6 +281,8 @@ module Elkrb
|
|
|
262
281
|
total_length += length
|
|
263
282
|
end
|
|
264
283
|
|
|
284
|
+
return { x: points.first.x, y: points.first.y } if total_length.zero?
|
|
285
|
+
|
|
265
286
|
# Find point at half the total length
|
|
266
287
|
target_length = total_length / 2.0
|
|
267
288
|
current_length = 0.0
|
|
@@ -269,7 +290,7 @@ module Elkrb
|
|
|
269
290
|
(0...lengths.length).each do |i|
|
|
270
291
|
if current_length + lengths[i] >= target_length
|
|
271
292
|
# Interpolate between points[i] and points[i+1]
|
|
272
|
-
ratio = (target_length - current_length) / lengths[i]
|
|
293
|
+
ratio = lengths[i].zero? ? 0.0 : (target_length - current_length) / lengths[i]
|
|
273
294
|
p1 = points[i]
|
|
274
295
|
p2 = points[i + 1]
|
|
275
296
|
|
|
@@ -294,44 +315,31 @@ module Elkrb
|
|
|
294
315
|
label.y = 0
|
|
295
316
|
end
|
|
296
317
|
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
child_graph = Graph::Graph.new(
|
|
306
|
-
children: node.children,
|
|
307
|
-
edges: node.edges,
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
# Recursively place labels
|
|
311
|
-
place_labels(child_graph)
|
|
318
|
+
# Get the label placement an element names, or nil. The element's own
|
|
319
|
+
# options come before the call's: for a port, `label.placement` is one
|
|
320
|
+
# of them, though the registry aliases it to nodes. Because it is that
|
|
321
|
+
# alias, a port that names elk.nodeLabels.placement is read the same way.
|
|
322
|
+
def label_placement_option(element, option_id)
|
|
323
|
+
[option_id, "label.placement"].each do |id|
|
|
324
|
+
own = own_option(id, element)
|
|
325
|
+
return own if own
|
|
312
326
|
end
|
|
327
|
+
@resolver.get(option_id, element, default: nil)
|
|
313
328
|
end
|
|
314
329
|
|
|
315
|
-
#
|
|
316
|
-
def
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
element.layout_options.properties&.[](option_key) ||
|
|
320
|
-
element.layout_options.properties&.[]("label.placement")
|
|
330
|
+
# An element's own option, ignoring the call's options.
|
|
331
|
+
def own_option(id, element)
|
|
332
|
+
(@own_options ||= Options::Resolver.new).get(id, element, default: nil)
|
|
321
333
|
end
|
|
322
334
|
|
|
323
335
|
# Get label padding option.
|
|
324
336
|
def label_padding_option(element)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
element.layout_options.properties&.[]("label.padding") || 5.0
|
|
337
|
+
@resolver.get("label.padding", element)
|
|
328
338
|
end
|
|
329
339
|
|
|
330
340
|
# Get label margin option.
|
|
331
341
|
def label_margin_option(element)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
element.layout_options.properties&.[]("label.margin") || 5.0
|
|
342
|
+
@resolver.get("label.margin", element)
|
|
335
343
|
end
|
|
336
344
|
end
|
|
337
345
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "algorithm_registry"
|
|
4
|
+
require_relative "../options/registry"
|
|
5
|
+
require_relative "../options/resolver"
|
|
4
6
|
|
|
5
7
|
module Elkrb
|
|
6
8
|
module Layout
|
|
@@ -45,11 +47,24 @@ module Elkrb
|
|
|
45
47
|
# appropriate algorithm based on options, and computes node positions
|
|
46
48
|
# and edge routes.
|
|
47
49
|
#
|
|
48
|
-
# The algorithm is
|
|
49
|
-
# 1.
|
|
50
|
-
#
|
|
50
|
+
# The algorithm is the first of these that names one:
|
|
51
|
+
# 1. The graph's own selector -- layoutOptions under "elk.algorithm",
|
|
52
|
+
# "algorithm" or the "org.eclipse.elk.algorithm" long form, then
|
|
53
|
+
# properties
|
|
54
|
+
# 2. The :algorithm call option, under a Symbol or String key
|
|
51
55
|
# 3. Default: "layered"
|
|
52
56
|
#
|
|
57
|
+
# A graph that pins an algorithm keeps it; the caller's option is
|
|
58
|
+
# the fallback for graphs that pin none.
|
|
59
|
+
#
|
|
60
|
+
# With the call option strict: true, an option in the graph's
|
|
61
|
+
# layoutOptions that elkrb does not know, or does not fully honour,
|
|
62
|
+
# raises Elkrb::Error instead of logging a warning. So does a
|
|
63
|
+
# registered option in an element's properties or in +options+ that
|
|
64
|
+
# is not fully honoured; an unknown property name or key in +options+
|
|
65
|
+
# is not reported. An option only some algorithms read is accepted
|
|
66
|
+
# where the algorithm laying out its element is one of them.
|
|
67
|
+
#
|
|
53
68
|
# @param graph [Hash, Elkrb::Graph::Graph] The graph to layout. Can be:
|
|
54
69
|
# - A Hash with keys: :id, :children, :edges, :layoutOptions
|
|
55
70
|
# - A Graph model object
|
|
@@ -57,7 +72,9 @@ module Elkrb
|
|
|
57
72
|
# - :algorithm (String) - Algorithm name (layered, force, etc.)
|
|
58
73
|
# - Algorithm-specific options
|
|
59
74
|
# @return [Elkrb::Graph::Graph] The input graph with computed positions
|
|
60
|
-
# @raise [
|
|
75
|
+
# @raise [ArgumentError] If graph is neither a Hash nor a Graph::Graph
|
|
76
|
+
# @raise [Elkrb::AlgorithmNotFoundError] If the algorithm is not found
|
|
77
|
+
# @raise [Elkrb::Error] In strict mode, for an unknown or unhonoured option
|
|
61
78
|
#
|
|
62
79
|
# @example With specific algorithm
|
|
63
80
|
# result = Elkrb::Layout::LayoutEngine.layout(
|
|
@@ -66,27 +83,20 @@ module Elkrb
|
|
|
66
83
|
# "elk.force.repulsion" => 5.0
|
|
67
84
|
# )
|
|
68
85
|
#
|
|
69
|
-
#
|
|
70
|
-
# result = Elkrb::Layout::LayoutEngine.layout(
|
|
71
|
-
# graph,
|
|
72
|
-
# algorithm: "layered",
|
|
73
|
-
# hierarchical: true
|
|
74
|
-
# )
|
|
86
|
+
# Hierarchy is detected from compound nodes; no call flag is needed.
|
|
75
87
|
def layout(graph, options = {})
|
|
76
|
-
|
|
77
|
-
graph = convert_to_graph(graph) if graph.is_a?(Hash)
|
|
88
|
+
graph = graph_argument(graph)
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
algorithm_name =
|
|
81
|
-
options["algorithm"] ||
|
|
82
|
-
"layered"
|
|
90
|
+
resolver = Options::Resolver.new(options)
|
|
91
|
+
algorithm_name = resolver.get("elk.algorithm", graph)
|
|
83
92
|
|
|
84
93
|
algorithm_class = AlgorithmRegistry.get(algorithm_name)
|
|
94
|
+
raise AlgorithmNotFoundError, algorithm_name unless algorithm_class
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
resolver.report_unhonoured(
|
|
97
|
+
graph, algorithm_name: AlgorithmRegistry.method(:registered_name)
|
|
98
|
+
)
|
|
88
99
|
|
|
89
|
-
# Create and run algorithm with options
|
|
90
100
|
algorithm = algorithm_class.new(options)
|
|
91
101
|
algorithm.layout(graph)
|
|
92
102
|
|
|
@@ -104,6 +114,7 @@ module Elkrb
|
|
|
104
114
|
# - :description (String) - Brief description
|
|
105
115
|
# - :category (String, nil) - Algorithm category (e.g., "hierarchical", "force")
|
|
106
116
|
# - :supports_hierarchy (Boolean, nil) - Whether it supports hierarchical graphs
|
|
117
|
+
# - :supported_options (Array<String>) - Canonical option ids scoped to this algorithm
|
|
107
118
|
#
|
|
108
119
|
# @example List all algorithms
|
|
109
120
|
# algorithms = Elkrb::Layout::LayoutEngine.known_layout_algorithms
|
|
@@ -152,11 +163,10 @@ module Elkrb
|
|
|
152
163
|
|
|
153
164
|
# Returns metadata for all supported layout options.
|
|
154
165
|
#
|
|
155
|
-
# @return [
|
|
156
|
-
#
|
|
166
|
+
# @return [Hash{String => Hash}] Same shape and content as
|
|
167
|
+
# Elkrb.known_layout_options, keyed by canonical id.
|
|
157
168
|
def known_layout_options
|
|
158
|
-
|
|
159
|
-
[]
|
|
169
|
+
Options::Registry.render_known_options(algorithm_values: AlgorithmRegistry.available_algorithms)
|
|
160
170
|
end
|
|
161
171
|
|
|
162
172
|
private
|
|
@@ -164,6 +174,19 @@ module Elkrb
|
|
|
164
174
|
def convert_to_graph(hash)
|
|
165
175
|
Graph::Graph.from_hash(hash)
|
|
166
176
|
end
|
|
177
|
+
|
|
178
|
+
# Takes the two documented shapes and nothing else. The check lives
|
|
179
|
+
# here, not in Elkrb.layout, so the CLI and every other caller of the
|
|
180
|
+
# engine get the same clear error instead of a NoMethodError deep
|
|
181
|
+
# inside an algorithm.
|
|
182
|
+
def graph_argument(graph)
|
|
183
|
+
return convert_to_graph(graph) if graph.is_a?(::Hash)
|
|
184
|
+
return graph if graph.is_a?(Graph::Graph)
|
|
185
|
+
|
|
186
|
+
raise ArgumentError,
|
|
187
|
+
"graph must be a Hash or Elkrb::Graph::Graph, " \
|
|
188
|
+
"got #{graph.class}"
|
|
189
|
+
end
|
|
167
190
|
end
|
|
168
191
|
end
|
|
169
192
|
end
|