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
|
@@ -17,10 +17,46 @@ module Elkrb
|
|
|
17
17
|
# The algorithm calculates grid dimensions to approximate a square
|
|
18
18
|
# aspect ratio, then places nodes from left to right, top to bottom.
|
|
19
19
|
class TopdownPacking < BaseAlgorithm
|
|
20
|
+
# Fallback cell size when no node anywhere declares a usable width
|
|
21
|
+
# or height. Mirrors node_spacing's own 20.0 default rather than
|
|
22
|
+
# inventing a new constant.
|
|
23
|
+
DEFAULT_CELL_SIZE = 20.0
|
|
24
|
+
|
|
20
25
|
def initialize(options = {})
|
|
21
26
|
super
|
|
22
27
|
end
|
|
23
28
|
|
|
29
|
+
# A declared width or height is only usable when it is finite and
|
|
30
|
+
# positive. NaN and Infinity are valid YAML float literals (`.nan`,
|
|
31
|
+
# `.inf`; JSON's parser rejects the bare form) that would otherwise
|
|
32
|
+
# propagate into every downstream position and crash
|
|
33
|
+
# calculate_bounding_box's Array#max, whose NaN-vs-NaN comparison
|
|
34
|
+
# raises ArgumentError. A non-positive value is rejected too --
|
|
35
|
+
# port_constraint_processor.rb already treats a node as size-less
|
|
36
|
+
# unless both dimensions are `positive?`, and letting a negative
|
|
37
|
+
# declared value reach this node's own box (rather than every
|
|
38
|
+
# node's shared, uniformly-averaged cell size, as before this
|
|
39
|
+
# file's declared-size-preserving change) inverts its box and
|
|
40
|
+
# produces a false-negative overlap check against its neighbours.
|
|
41
|
+
# No type check beyond nil-safety: Node's width/height are declared
|
|
42
|
+
# `attribute :width, :float` (node.rb), so lutaml casts every
|
|
43
|
+
# assignment to a Float or nil before it can ever reach here -- a
|
|
44
|
+
# value that responds to #finite? and is neither nil nor a Numeric
|
|
45
|
+
# is not a reachable input. A public class method -- it depends on
|
|
46
|
+
# nothing but its argument, matching the pure-helper convention
|
|
47
|
+
# already used elsewhere for options parsing (e.g. KVector.parse).
|
|
48
|
+
# Keep this as a class method with the instance delegate below --
|
|
49
|
+
# a plain private instance method here trips reek's UtilityFunction
|
|
50
|
+
# check.
|
|
51
|
+
#
|
|
52
|
+
# @param value [Float, nil] The node's declared width or height
|
|
53
|
+
# @return [Float, nil] The value if usable, nil otherwise
|
|
54
|
+
def self.finite_dimension(value)
|
|
55
|
+
return nil unless value&.finite? && value.positive?
|
|
56
|
+
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
24
60
|
# Layout nodes in a grid using top-down packing
|
|
25
61
|
#
|
|
26
62
|
# @param graph [Elkrb::Graph::Graph] The graph to layout
|
|
@@ -96,24 +132,34 @@ module Elkrb
|
|
|
96
132
|
height: 0.0 }
|
|
97
133
|
end
|
|
98
134
|
|
|
99
|
-
# Get
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
target_aspect_ratio = get_option(layout_opts,
|
|
104
|
-
"topdownpacking.aspectRatio", 1.0).to_f
|
|
135
|
+
# Get target aspect ratio (default: 1.0 for square cells). A
|
|
136
|
+
# non-positive ratio falls back to square rather than reaching the
|
|
137
|
+
# division below; the resolver already rejects NaN and Infinity.
|
|
138
|
+
target_aspect_ratio = resolver.get("topdownpacking.aspectRatio", graph)
|
|
105
139
|
target_aspect_ratio = 1.0 if target_aspect_ratio <= 0.0
|
|
106
140
|
|
|
107
141
|
# Calculate node dimensions based on aspect ratio
|
|
108
142
|
# We can either use specified dimensions or calculate from available space
|
|
109
|
-
node_width_opt =
|
|
143
|
+
node_width_opt = finite_dimension(resolver.get("topdownpacking.nodeWidth", graph))
|
|
110
144
|
if node_width_opt
|
|
111
|
-
node_width = node_width_opt
|
|
145
|
+
node_width = node_width_opt
|
|
112
146
|
node_height = node_width / target_aspect_ratio
|
|
113
147
|
else
|
|
114
|
-
# Calculate from node sizes to maintain proportions
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
# Calculate from node sizes to maintain proportions. A size-less,
|
|
149
|
+
# non-finite (NaN/Infinity, both valid YAML float literals) or
|
|
150
|
+
# non-positive node contributes 0.0 rather than crashing the
|
|
151
|
+
# average, matching the nil-handling convention in box.rb.
|
|
152
|
+
avg_width = nodes.sum { |node| finite_dimension(node.width) || 0.0 } / nodes.size.to_f
|
|
153
|
+
avg_height = nodes.sum { |node| finite_dimension(node.height) || 0.0 } / nodes.size.to_f
|
|
154
|
+
|
|
155
|
+
# When no node anywhere declares a usable size, both averages
|
|
156
|
+
# are zero and every node would silently collapse to a 0x0
|
|
157
|
+
# footprint. Fall back to a default cell size instead of
|
|
158
|
+
# discarding every node.
|
|
159
|
+
if avg_width.zero? && avg_height.zero?
|
|
160
|
+
avg_width = DEFAULT_CELL_SIZE
|
|
161
|
+
avg_height = DEFAULT_CELL_SIZE
|
|
162
|
+
end
|
|
117
163
|
|
|
118
164
|
# Use the larger dimension as base
|
|
119
165
|
if avg_width >= avg_height
|
|
@@ -128,20 +174,24 @@ module Elkrb
|
|
|
128
174
|
{ width: node_width, height: node_height }
|
|
129
175
|
end
|
|
130
176
|
|
|
131
|
-
#
|
|
177
|
+
# Instance-side delegate to the class method above, so callers in
|
|
178
|
+
# this file read as one private helper rather than repeating
|
|
179
|
+
# `self.class` at every call site.
|
|
132
180
|
#
|
|
133
|
-
# @param
|
|
134
|
-
# @
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
181
|
+
# @param value [Object] The node's declared width or height
|
|
182
|
+
# @return [Numeric, nil] The value if usable, nil otherwise
|
|
183
|
+
def finite_dimension(value)
|
|
184
|
+
self.class.finite_dimension(value)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Ports were placed on the declared size before layout. A declared
|
|
188
|
+
# size that is replaced here (size-less, non-positive or non-finite)
|
|
189
|
+
# leaves them on edges the node no longer has, or never placed.
|
|
190
|
+
#
|
|
191
|
+
# @param node [Elkrb::Graph::Node] The node, already resized
|
|
192
|
+
# @param declared [Array<Numeric, nil>] Its declared width and height
|
|
193
|
+
def replace_ports_for_resized_node(node, declared)
|
|
194
|
+
replace_node_ports(node) unless declared.all? { |dim| finite_dimension(dim) }
|
|
145
195
|
end
|
|
146
196
|
|
|
147
197
|
# Place nodes in grid positions
|
|
@@ -155,25 +205,39 @@ module Elkrb
|
|
|
155
205
|
current_x = 0.0
|
|
156
206
|
current_y = 0.0
|
|
157
207
|
current_col = 0
|
|
208
|
+
row_height = 0.0
|
|
158
209
|
|
|
159
210
|
nodes.each do |node|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
211
|
+
# Only a node with a usable (finite, positive, non-nil) declared
|
|
212
|
+
# width/height keeps it -- which means it may be larger than
|
|
213
|
+
# the cell, so advancing by the fixed cell size (rather than
|
|
214
|
+
# the node's own size) would let it overlap its neighbours. A
|
|
215
|
+
# size-less, non-finite (NaN/Infinity) or non-positive declared
|
|
216
|
+
# value takes the grid cell size instead of reaching position
|
|
217
|
+
# math as-is.
|
|
218
|
+
declared_width = node.width
|
|
219
|
+
declared_height = node.height
|
|
220
|
+
actual_width = (node.width = finite_dimension(declared_width) || node_width)
|
|
221
|
+
actual_height = (node.height = finite_dimension(declared_height) || node_height)
|
|
222
|
+
replace_ports_for_resized_node(node, [declared_width, declared_height])
|
|
163
223
|
|
|
164
224
|
# Set node position
|
|
165
225
|
node.x = current_x
|
|
166
226
|
node.y = current_y
|
|
167
227
|
|
|
168
|
-
|
|
228
|
+
row_height = actual_height if actual_height > row_height
|
|
229
|
+
|
|
230
|
+
# Advance to next position, past this node's own footprint
|
|
169
231
|
current_col += 1
|
|
170
|
-
current_x +=
|
|
232
|
+
current_x += actual_width + spacing
|
|
171
233
|
|
|
172
|
-
# Move to next row if we've filled the current row
|
|
234
|
+
# Move to next row if we've filled the current row, past the
|
|
235
|
+
# tallest node actually placed in it
|
|
173
236
|
if current_col >= cols
|
|
174
237
|
current_x = 0.0
|
|
175
|
-
current_y +=
|
|
238
|
+
current_y += row_height + spacing
|
|
176
239
|
current_col = 0
|
|
240
|
+
row_height = 0.0
|
|
177
241
|
end
|
|
178
242
|
end
|
|
179
243
|
end
|
|
@@ -7,15 +7,12 @@ module Elkrb
|
|
|
7
7
|
module Algorithms
|
|
8
8
|
# VertiFlex layout algorithm
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# Approximates a column-grid layout: it distributes nodes into a fixed
|
|
11
|
+
# number of vertical columns (round-robin or sequential) and stacks
|
|
12
|
+
# each column top to bottom, sizing each column to its widest node.
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# column.
|
|
17
|
-
#
|
|
18
|
-
# This is an experimental algorithm matching the Java ELK implementation.
|
|
14
|
+
# It never reads the graph's edges -- node positions are identical
|
|
15
|
+
# whether edges are present or not.
|
|
19
16
|
class VertiFlex < BaseAlgorithm
|
|
20
17
|
def initialize(options = {})
|
|
21
18
|
super
|
|
@@ -38,34 +35,18 @@ module Elkrb
|
|
|
38
35
|
end
|
|
39
36
|
|
|
40
37
|
nodes = graph.children
|
|
41
|
-
layout_opts = graph.layout_options || {}
|
|
42
38
|
|
|
43
39
|
# Get layout options
|
|
44
|
-
column_count =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
column_spacing = get_option(
|
|
49
|
-
layout_opts,
|
|
50
|
-
"vertiflex.columnSpacing",
|
|
51
|
-
50.0,
|
|
52
|
-
).to_f
|
|
53
|
-
|
|
54
|
-
vertical_spacing = get_option(
|
|
55
|
-
layout_opts,
|
|
56
|
-
"vertiflex.verticalSpacing",
|
|
57
|
-
30.0,
|
|
58
|
-
).to_f
|
|
40
|
+
column_count = [resolver.get("vertiflex.columnCount", graph), 1].max
|
|
41
|
+
column_spacing = resolver.get("vertiflex.columnSpacing", graph)
|
|
42
|
+
vertical_spacing = resolver.get("vertiflex.verticalSpacing", graph)
|
|
59
43
|
|
|
60
44
|
# Override with elk.spacing.nodeNode if present
|
|
61
|
-
node_node_spacing =
|
|
62
|
-
|
|
45
|
+
node_node_spacing = resolver.get("elk.spacing.nodeNode", graph,
|
|
46
|
+
default: nil)
|
|
47
|
+
vertical_spacing = node_node_spacing if node_node_spacing
|
|
63
48
|
|
|
64
|
-
balance_columns =
|
|
65
|
-
layout_opts,
|
|
66
|
-
"vertiflex.balanceColumns",
|
|
67
|
-
true,
|
|
68
|
-
)
|
|
49
|
+
balance_columns = resolver.get("vertiflex.balanceColumns", graph)
|
|
69
50
|
|
|
70
51
|
# Distribute nodes into columns
|
|
71
52
|
columns = distribute_nodes(nodes, column_count, balance_columns)
|
|
@@ -80,22 +61,6 @@ module Elkrb
|
|
|
80
61
|
|
|
81
62
|
private
|
|
82
63
|
|
|
83
|
-
# Get option value from layout options or default
|
|
84
|
-
#
|
|
85
|
-
# @param layout_opts [Hash, LayoutOptions] The layout options
|
|
86
|
-
# @param key [String] The option key
|
|
87
|
-
# @param default [Object] The default value
|
|
88
|
-
# @return [Object] The option value or default
|
|
89
|
-
def get_option(layout_opts, key, default = nil)
|
|
90
|
-
return default unless layout_opts
|
|
91
|
-
|
|
92
|
-
value = if layout_opts.respond_to?(:[])
|
|
93
|
-
layout_opts[key]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
value.nil? ? default : value
|
|
97
|
-
end
|
|
98
|
-
|
|
99
64
|
# Distribute nodes into columns
|
|
100
65
|
#
|
|
101
66
|
# @param nodes [Array<Elkrb::Graph::Node>] Nodes to distribute
|
|
@@ -31,7 +31,17 @@ module Elkrb
|
|
|
31
31
|
# align_direction: "vertical"
|
|
32
32
|
# )
|
|
33
33
|
# # Both nodes will have same x coordinate
|
|
34
|
+
#
|
|
35
|
+
# Members that the average would stack on top of each other are moved
|
|
36
|
+
# apart along the other axis, in their current order.
|
|
34
37
|
class AlignmentConstraint < BaseConstraint
|
|
38
|
+
# @param spacing [Numeric] minimum gap kept between group members
|
|
39
|
+
# along the axis that is not aligned
|
|
40
|
+
def initialize(spacing: 0.0)
|
|
41
|
+
super()
|
|
42
|
+
@spacing = spacing
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
# Apply alignment constraint
|
|
36
46
|
#
|
|
37
47
|
# Groups nodes by align_group and aligns them according to
|
|
@@ -102,26 +112,53 @@ module Elkrb
|
|
|
102
112
|
end
|
|
103
113
|
end
|
|
104
114
|
|
|
105
|
-
# Align nodes horizontally (same y)
|
|
115
|
+
# Align nodes horizontally (same y), then space them along x
|
|
106
116
|
def align_horizontally(nodes)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
nodes.each do |node|
|
|
111
|
-
node.y = avg_y
|
|
112
|
-
end
|
|
117
|
+
placed = align_to_average(nodes, :y)
|
|
118
|
+
space_apart(placed, :x, :width)
|
|
113
119
|
end
|
|
114
120
|
|
|
115
|
-
# Align nodes vertically (same x)
|
|
121
|
+
# Align nodes vertically (same x), then space them along y
|
|
116
122
|
def align_vertically(nodes)
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
placed = align_to_average(nodes, :x)
|
|
124
|
+
space_apart(placed, :y, :height)
|
|
125
|
+
end
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
# Moves every node that has a coordinate on +axis+ to the average of
|
|
128
|
+
# them. A node without one is left alone: it has nothing to align.
|
|
129
|
+
#
|
|
130
|
+
# @return [Array<Graph::Node>] the nodes that were aligned
|
|
131
|
+
def align_to_average(nodes, axis)
|
|
132
|
+
placed = nodes.reject { |node| node.public_send(axis).nil? }
|
|
133
|
+
return placed if placed.empty?
|
|
134
|
+
|
|
135
|
+
average = placed.sum { |node| node.public_send(axis) } /
|
|
136
|
+
placed.length.to_f
|
|
137
|
+
placed.each { |node| node.public_send(:"#{axis}=", average) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Pushes members apart along +axis+ so none overlaps the one before
|
|
141
|
+
# it. Order is the nodes' current order along the axis; ties keep
|
|
142
|
+
# the order they were given in.
|
|
143
|
+
def space_apart(nodes, axis, size)
|
|
144
|
+
ordered = nodes.each_with_index.sort_by do |node, index|
|
|
145
|
+
[coordinate(node, axis), index]
|
|
146
|
+
end.map(&:first)
|
|
147
|
+
|
|
148
|
+
ordered.each_cons(2) do |previous, node|
|
|
149
|
+
floor = coordinate(previous, axis) +
|
|
150
|
+
coordinate(previous, size) + @spacing
|
|
151
|
+
next unless coordinate(node, axis) < floor
|
|
152
|
+
|
|
153
|
+
node.public_send(:"#{axis}=", floor)
|
|
122
154
|
end
|
|
123
155
|
end
|
|
124
156
|
|
|
157
|
+
# A coordinate or size that has not been set counts as 0.
|
|
158
|
+
def coordinate(node, attribute)
|
|
159
|
+
node.public_send(attribute) || 0.0
|
|
160
|
+
end
|
|
161
|
+
|
|
125
162
|
# Validate group alignment
|
|
126
163
|
def validate_group_alignment(nodes, group_name, direction)
|
|
127
164
|
errors = []
|
|
@@ -32,14 +32,6 @@ module Elkrb
|
|
|
32
32
|
[]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Check if constraint applies to this node
|
|
36
|
-
#
|
|
37
|
-
# @param node [Graph::Node] The node to check
|
|
38
|
-
# @return [Boolean] True if constraint applies
|
|
39
|
-
def applies_to?(node)
|
|
40
|
-
node.constraints.present?
|
|
41
|
-
end
|
|
42
|
-
|
|
43
35
|
protected
|
|
44
36
|
|
|
45
37
|
# Find node by ID in graph
|
|
@@ -50,21 +42,17 @@ module Elkrb
|
|
|
50
42
|
def find_node(graph, node_id)
|
|
51
43
|
return nil unless graph.children
|
|
52
44
|
|
|
53
|
-
graph.children.
|
|
54
|
-
found = node.find_node(node_id)
|
|
55
|
-
return found if found
|
|
56
|
-
end
|
|
57
|
-
nil
|
|
45
|
+
graph.children.find { |node| node.id == node_id }
|
|
58
46
|
end
|
|
59
47
|
|
|
60
|
-
# Get all nodes
|
|
48
|
+
# Get all nodes at this hierarchy level.
|
|
61
49
|
#
|
|
62
50
|
# @param graph [Graph::Graph] The graph
|
|
63
51
|
# @return [Array<Graph::Node>] All nodes
|
|
64
52
|
def all_nodes(graph)
|
|
65
53
|
return [] unless graph.children
|
|
66
54
|
|
|
67
|
-
graph.children
|
|
55
|
+
graph.children
|
|
68
56
|
end
|
|
69
57
|
end
|
|
70
58
|
end
|
|
@@ -14,7 +14,7 @@ module Elkrb
|
|
|
14
14
|
# Orchestrates the application and validation of all layout constraints.
|
|
15
15
|
# Constraints are applied in a specific order to handle dependencies:
|
|
16
16
|
# 1. Fixed position (locks nodes)
|
|
17
|
-
# 2. Layer constraints (
|
|
17
|
+
# 2. Layer constraints (checked; the layered algorithm honours them)
|
|
18
18
|
# 3. Relative position (depends on reference nodes)
|
|
19
19
|
# 4. Alignment (adjusts positions)
|
|
20
20
|
#
|
|
@@ -30,15 +30,14 @@ module Elkrb
|
|
|
30
30
|
LayerConstraint,
|
|
31
31
|
].freeze
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
AlignmentConstraint,
|
|
37
|
-
].freeze
|
|
38
|
-
|
|
39
|
-
def initialize
|
|
33
|
+
# @param spacing [Numeric] minimum gap alignment keeps between the
|
|
34
|
+
# members of one group along the axis it does not align
|
|
35
|
+
def initialize(spacing: 0.0)
|
|
40
36
|
@pre_constraints = PRE_LAYOUT_CONSTRAINTS.map(&:new)
|
|
41
|
-
@post_constraints =
|
|
37
|
+
@post_constraints = [
|
|
38
|
+
RelativePositionConstraint.new,
|
|
39
|
+
AlignmentConstraint.new(spacing: spacing),
|
|
40
|
+
]
|
|
42
41
|
@all_constraints = (@pre_constraints + @post_constraints)
|
|
43
42
|
end
|
|
44
43
|
|
|
@@ -113,20 +112,7 @@ module Elkrb
|
|
|
113
112
|
def has_constraints?(graph)
|
|
114
113
|
return false unless graph.children
|
|
115
114
|
|
|
116
|
-
graph.children.any?
|
|
117
|
-
has_constraints_recursive?(node)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
# Check if node or its children have constraints
|
|
124
|
-
def has_constraints_recursive?(node)
|
|
125
|
-
return true if node.constraints
|
|
126
|
-
|
|
127
|
-
return false unless node.children
|
|
128
|
-
|
|
129
|
-
node.children.any? { |child| has_constraints_recursive?(child) }
|
|
115
|
+
graph.children.any?(&:constraints)
|
|
130
116
|
end
|
|
131
117
|
end
|
|
132
118
|
end
|
|
@@ -16,6 +16,11 @@ module Elkrb
|
|
|
16
16
|
# node.constraints = NodeConstraints.new(fixed_position: true)
|
|
17
17
|
# # After layout, node remains at (500, 800)
|
|
18
18
|
class FixedPositionConstraint < BaseConstraint
|
|
19
|
+
def initialize
|
|
20
|
+
super
|
|
21
|
+
@originals = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
# Apply fixed position constraint (pre-layout)
|
|
20
25
|
#
|
|
21
26
|
# Stores original positions of fixed nodes so they can be
|
|
@@ -28,11 +33,7 @@ module Elkrb
|
|
|
28
33
|
next unless node.constraints&.fixed_position
|
|
29
34
|
next if node.x.nil? || node.y.nil?
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
node.properties ||= {}
|
|
33
|
-
node.properties["_constraint_fixed"] = true
|
|
34
|
-
node.properties["_constraint_original_x"] = node.x
|
|
35
|
-
node.properties["_constraint_original_y"] = node.y
|
|
36
|
+
@originals[node.id] = [node.x, node.y]
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
graph
|
|
@@ -47,11 +48,10 @@ module Elkrb
|
|
|
47
48
|
# @return [Graph::Graph] The modified graph
|
|
48
49
|
def restore_fixed_positions(graph)
|
|
49
50
|
all_nodes(graph).each do |node|
|
|
50
|
-
|
|
51
|
+
original = fixed_original(node)
|
|
52
|
+
next unless original
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
node.x = node.properties["_constraint_original_x"]
|
|
54
|
-
node.y = node.properties["_constraint_original_y"]
|
|
54
|
+
node.x, node.y = original
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
graph
|
|
@@ -67,10 +67,8 @@ module Elkrb
|
|
|
67
67
|
errors = []
|
|
68
68
|
|
|
69
69
|
all_nodes(graph).each do |node|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
original_x = node.properties["_constraint_original_x"]
|
|
73
|
-
original_y = node.properties["_constraint_original_y"]
|
|
70
|
+
original_x, original_y = fixed_original(node)
|
|
71
|
+
next unless original_x
|
|
74
72
|
|
|
75
73
|
if node.x != original_x || node.y != original_y
|
|
76
74
|
errors << "Node '#{node.id}' has fixed_position constraint " \
|
|
@@ -81,6 +79,17 @@ module Elkrb
|
|
|
81
79
|
|
|
82
80
|
errors
|
|
83
81
|
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# The recorded position of a node that is still fixed, or nil. The
|
|
86
|
+
# constraint is read from the node every time, so clearing
|
|
87
|
+
# fixed_position releases a node recorded earlier.
|
|
88
|
+
def fixed_original(node)
|
|
89
|
+
return unless node.constraints&.fixed_position
|
|
90
|
+
|
|
91
|
+
@originals[node.id]
|
|
92
|
+
end
|
|
84
93
|
end
|
|
85
94
|
end
|
|
86
95
|
end
|
|
@@ -17,53 +17,30 @@ module Elkrb
|
|
|
17
17
|
# database.constraints = NodeConstraints.new(layer: 2) # Bottom
|
|
18
18
|
# # Enforces tier structure
|
|
19
19
|
class LayerConstraint < BaseConstraint
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# Marks nodes with layer assignment that layered algorithm
|
|
23
|
-
# must respect.
|
|
20
|
+
# Nothing to do before layout: the layered algorithm reads
|
|
21
|
+
# `constraints.layer` itself when it assigns layers.
|
|
24
22
|
#
|
|
25
23
|
# @param graph [Graph::Graph] The graph
|
|
26
|
-
# @return [Graph::Graph] The
|
|
24
|
+
# @return [Graph::Graph] The graph, unchanged
|
|
27
25
|
def apply(graph)
|
|
28
|
-
all_nodes(graph).each do |node|
|
|
29
|
-
next unless node.constraints&.layer
|
|
30
|
-
|
|
31
|
-
# Mark node with its required layer
|
|
32
|
-
node.properties ||= {}
|
|
33
|
-
node.properties["_constraint_layer"] = node.constraints.layer
|
|
34
|
-
end
|
|
35
|
-
|
|
36
26
|
graph
|
|
37
27
|
end
|
|
38
28
|
|
|
39
29
|
# Validate layer constraints
|
|
40
30
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# was used and layer information is available.
|
|
31
|
+
# A layer index below 0 names no layer, so the layered algorithm
|
|
32
|
+
# places such a node in layer 0.
|
|
44
33
|
#
|
|
45
34
|
# @param graph [Graph::Graph] The graph to validate
|
|
46
35
|
# @return [Array<String>] List of validation errors
|
|
47
36
|
def validate(graph)
|
|
48
|
-
|
|
37
|
+
all_nodes(graph).filter_map do |node|
|
|
38
|
+
layer = node.constraints&.layer
|
|
39
|
+
next unless layer&.negative?
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
all_nodes(graph).each do |node|
|
|
53
|
-
next unless node.constraints&.layer
|
|
54
|
-
next unless node.properties&.[]("_assigned_layer")
|
|
55
|
-
|
|
56
|
-
expected_layer = node.constraints.layer
|
|
57
|
-
actual_layer = node.properties["_assigned_layer"]
|
|
58
|
-
|
|
59
|
-
if expected_layer != actual_layer
|
|
60
|
-
errors << "Node '#{node.id}' constrained to layer " \
|
|
61
|
-
"#{expected_layer} but assigned to layer " \
|
|
62
|
-
"#{actual_layer}"
|
|
63
|
-
end
|
|
41
|
+
"Node '#{node.id}' constrained to layer #{layer}, " \
|
|
42
|
+
"which does not exist; layers start at 0"
|
|
64
43
|
end
|
|
65
|
-
|
|
66
|
-
errors
|
|
67
44
|
end
|
|
68
45
|
end
|
|
69
46
|
end
|