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
data/lib/elkrb/graph/graph.rb
CHANGED
|
@@ -10,17 +10,21 @@ module Elkrb
|
|
|
10
10
|
attribute :y, :float
|
|
11
11
|
attribute :width, :float
|
|
12
12
|
attribute :height, :float
|
|
13
|
+
attribute :labels, Label, collection: true
|
|
14
|
+
attribute :ports, Port, collection: true
|
|
13
15
|
attribute :children, Node, collection: true
|
|
14
16
|
attribute :edges, Edge, collection: true
|
|
15
|
-
attribute :layout_options,
|
|
17
|
+
attribute :layout_options, :hash
|
|
16
18
|
attribute :properties, :hash
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
key_value do
|
|
19
21
|
map "id", to: :id
|
|
20
22
|
map "x", to: :x
|
|
21
23
|
map "y", to: :y
|
|
22
24
|
map "width", to: :width
|
|
23
25
|
map "height", to: :height
|
|
26
|
+
map "labels", to: :labels
|
|
27
|
+
map "ports", to: :ports
|
|
24
28
|
map "children", to: :children
|
|
25
29
|
map "edges", to: :edges
|
|
26
30
|
map "layoutOptions", to: :layout_options
|
|
@@ -33,6 +37,8 @@ module Elkrb
|
|
|
33
37
|
map "y", to: :y
|
|
34
38
|
map "width", to: :width
|
|
35
39
|
map "height", to: :height
|
|
40
|
+
map "labels", to: :labels
|
|
41
|
+
map "ports", to: :ports
|
|
36
42
|
map "children", to: :children
|
|
37
43
|
map "edges", to: :edges
|
|
38
44
|
map "layout_options", to: :layout_options
|
|
@@ -46,10 +52,28 @@ module Elkrb
|
|
|
46
52
|
@y ||= 0.0
|
|
47
53
|
@width ||= 0.0
|
|
48
54
|
@height ||= 0.0
|
|
49
|
-
|
|
50
|
-
@edges ||= []
|
|
55
|
+
set_collection_defaults
|
|
51
56
|
@properties ||= {}
|
|
52
|
-
@layout_options ||=
|
|
57
|
+
@layout_options ||= {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def set_collection_defaults
|
|
61
|
+
@labels = [] if @labels.nil?
|
|
62
|
+
@ports = [] if @ports.nil?
|
|
63
|
+
@children = [] if @children.nil?
|
|
64
|
+
@edges = [] if @edges.nil?
|
|
65
|
+
end
|
|
66
|
+
private :set_collection_defaults
|
|
67
|
+
|
|
68
|
+
# Normalizes a Symbol key however the options arrive — a constructor,
|
|
69
|
+
# a plain setter, or lutaml's own deserialization, which routes through
|
|
70
|
+
# here too. This is the single normalization point: a :hash-typed
|
|
71
|
+
# attribute otherwise stores a raw, unnormalized Hash.
|
|
72
|
+
def layout_options=(value)
|
|
73
|
+
value_set_for(:layout_options)
|
|
74
|
+
attr = self.class.attributes(lutaml_register)[:layout_options]
|
|
75
|
+
cast = attr.cast_value(DeepStringifyKeys.call(value), lutaml_register)
|
|
76
|
+
instance_variable_set(:@layout_options, NormalizeOptionKeys.call(cast))
|
|
53
77
|
end
|
|
54
78
|
|
|
55
79
|
def find_node(node_id)
|
|
@@ -69,21 +93,25 @@ module Elkrb
|
|
|
69
93
|
end
|
|
70
94
|
|
|
71
95
|
def all_edges
|
|
72
|
-
edges = @edges.dup
|
|
73
|
-
@children.each do |child|
|
|
74
|
-
edges.concat(child
|
|
75
|
-
next unless child.respond_to?(:children)
|
|
76
|
-
|
|
77
|
-
child.children.each do |grandchild|
|
|
78
|
-
edges.concat(grandchild.all_edges) if
|
|
79
|
-
grandchild.respond_to?(:all_edges)
|
|
80
|
-
end
|
|
96
|
+
edges = (@edges || []).dup
|
|
97
|
+
(@children || []).each do |child|
|
|
98
|
+
edges.concat(descendant_edges(child))
|
|
81
99
|
end
|
|
82
100
|
edges
|
|
83
101
|
end
|
|
84
102
|
|
|
85
103
|
def hierarchical?
|
|
86
|
-
@children.any?(&:hierarchical?)
|
|
104
|
+
(@children || []).any?(&:hierarchical?)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def descendant_edges(node)
|
|
110
|
+
edges = (node.edges || []).dup
|
|
111
|
+
(node.children || []).each do |child|
|
|
112
|
+
edges.concat(descendant_edges(child))
|
|
113
|
+
end
|
|
114
|
+
edges
|
|
87
115
|
end
|
|
88
116
|
end
|
|
89
117
|
end
|
data/lib/elkrb/graph/label.rb
CHANGED
|
@@ -11,9 +11,10 @@ module Elkrb
|
|
|
11
11
|
attribute :y, :float
|
|
12
12
|
attribute :width, :float
|
|
13
13
|
attribute :height, :float
|
|
14
|
-
attribute :layout_options,
|
|
14
|
+
attribute :layout_options, :hash
|
|
15
|
+
attribute :properties, :hash
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
key_value do
|
|
17
18
|
map "id", to: :id
|
|
18
19
|
map "text", to: :text
|
|
19
20
|
map "x", to: :x
|
|
@@ -21,6 +22,7 @@ module Elkrb
|
|
|
21
22
|
map "width", to: :width
|
|
22
23
|
map "height", to: :height
|
|
23
24
|
map "layoutOptions", to: :layout_options
|
|
25
|
+
map "properties", to: :properties
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
yaml do
|
|
@@ -31,6 +33,7 @@ module Elkrb
|
|
|
31
33
|
map "width", to: :width
|
|
32
34
|
map "height", to: :height
|
|
33
35
|
map "layout_options", to: :layout_options
|
|
36
|
+
map "properties", to: :properties
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def initialize(**attributes)
|
|
@@ -40,6 +43,17 @@ module Elkrb
|
|
|
40
43
|
@width ||= 0.0
|
|
41
44
|
@height ||= 0.0
|
|
42
45
|
end
|
|
46
|
+
|
|
47
|
+
# Normalizes a Symbol key however the options arrive — a constructor,
|
|
48
|
+
# a plain setter, or lutaml's own deserialization, which routes through
|
|
49
|
+
# here too. This is the single normalization point: a :hash-typed
|
|
50
|
+
# attribute otherwise stores a raw, unnormalized Hash.
|
|
51
|
+
def layout_options=(value)
|
|
52
|
+
value_set_for(:layout_options)
|
|
53
|
+
attr = self.class.attributes(lutaml_register)[:layout_options]
|
|
54
|
+
cast = attr.cast_value(DeepStringifyKeys.call(value), lutaml_register)
|
|
55
|
+
instance_variable_set(:@layout_options, NormalizeOptionKeys.call(cast))
|
|
56
|
+
end
|
|
43
57
|
end
|
|
44
58
|
end
|
|
45
59
|
end
|
data/lib/elkrb/graph/node.rb
CHANGED
|
@@ -15,11 +15,11 @@ module Elkrb
|
|
|
15
15
|
attribute :ports, Port, collection: true
|
|
16
16
|
attribute :children, Node, collection: true
|
|
17
17
|
attribute :edges, Edge, collection: true
|
|
18
|
-
attribute :layout_options,
|
|
18
|
+
attribute :layout_options, :hash
|
|
19
19
|
attribute :constraints, NodeConstraints
|
|
20
20
|
attribute :properties, :hash
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
key_value do
|
|
23
23
|
map "id", to: :id
|
|
24
24
|
map "x", to: :x
|
|
25
25
|
map "y", to: :y
|
|
@@ -49,6 +49,17 @@ module Elkrb
|
|
|
49
49
|
map "properties", to: :properties
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Normalizes a Symbol key however the options arrive — a constructor,
|
|
53
|
+
# a plain setter, or lutaml's own deserialization, which routes through
|
|
54
|
+
# here too. This is the single normalization point: a :hash-typed
|
|
55
|
+
# attribute otherwise stores a raw, unnormalized Hash.
|
|
56
|
+
def layout_options=(value)
|
|
57
|
+
value_set_for(:layout_options)
|
|
58
|
+
attr = self.class.attributes(lutaml_register)[:layout_options]
|
|
59
|
+
cast = attr.cast_value(DeepStringifyKeys.call(value), lutaml_register)
|
|
60
|
+
instance_variable_set(:@layout_options, NormalizeOptionKeys.call(cast))
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def hierarchical?
|
|
53
64
|
@children && !@children.empty?
|
|
54
65
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
+
require_relative "read_only_mapping"
|
|
4
5
|
|
|
5
6
|
module Elkrb
|
|
6
7
|
module Graph
|
|
@@ -15,7 +16,7 @@ module Elkrb
|
|
|
15
16
|
attribute :x, :float, default: -> { 0.0 }
|
|
16
17
|
attribute :y, :float, default: -> { 0.0 }
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
key_value do
|
|
19
20
|
map "x", to: :x
|
|
20
21
|
map "y", to: :y
|
|
21
22
|
end
|
|
@@ -60,6 +61,8 @@ module Elkrb
|
|
|
60
61
|
# )
|
|
61
62
|
# # Node positioned 150px right of backend_service
|
|
62
63
|
class NodeConstraints < Lutaml::Model::Serializable
|
|
64
|
+
include ReadOnlyMapping
|
|
65
|
+
|
|
63
66
|
attribute :fixed_position, :boolean, default: -> { false }
|
|
64
67
|
attribute :layer, :integer
|
|
65
68
|
attribute :align_group, :string
|
|
@@ -68,7 +71,29 @@ module Elkrb
|
|
|
68
71
|
attribute :relative_offset, RelativeOffset
|
|
69
72
|
attribute :position_priority, :integer, default: -> { 0 }
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
# Legacy camelCase YAML spellings, still readable so documents written
|
|
75
|
+
# against the old mapping keep working. Output is snake_case only.
|
|
76
|
+
LEGACY_YAML_KEYS = {
|
|
77
|
+
"fixedPosition" => :fixed_position,
|
|
78
|
+
"alignGroup" => :align_group,
|
|
79
|
+
"alignDirection" => :align_direction,
|
|
80
|
+
"relativeTo" => :relative_to,
|
|
81
|
+
"relativeOffset" => :relative_offset,
|
|
82
|
+
"positionPriority" => :position_priority,
|
|
83
|
+
}.freeze
|
|
84
|
+
private_constant :LEGACY_YAML_KEYS
|
|
85
|
+
|
|
86
|
+
# Every snake_case key, applied by the same hook just before the legacy
|
|
87
|
+
# spellings so a legacy spelling still wins.
|
|
88
|
+
CANONICAL_YAML_KEYS =
|
|
89
|
+
LEGACY_YAML_KEYS.values.to_h { |name| [name.to_s, name] }
|
|
90
|
+
.merge("layer" => :layer).freeze
|
|
91
|
+
private_constant :CANONICAL_YAML_KEYS
|
|
92
|
+
|
|
93
|
+
ALL_YAML_KEYS = CANONICAL_YAML_KEYS.merge(LEGACY_YAML_KEYS).freeze
|
|
94
|
+
private_constant :ALL_YAML_KEYS
|
|
95
|
+
|
|
96
|
+
key_value do
|
|
72
97
|
map "fixedPosition", to: :fixed_position
|
|
73
98
|
map "layer", to: :layer
|
|
74
99
|
map "alignGroup", to: :align_group
|
|
@@ -78,14 +103,100 @@ module Elkrb
|
|
|
78
103
|
map "positionPriority", to: :position_priority
|
|
79
104
|
end
|
|
80
105
|
|
|
106
|
+
# YAML is snake_case like every other model here. The camelCase spellings
|
|
107
|
+
# stay readable so existing documents keep working, and a legacy spelling
|
|
108
|
+
# wins when a document carries both -- but only once the canonical rules
|
|
109
|
+
# have run. They run first, so a canonical value that fails to cast or
|
|
110
|
+
# validate raises before the hook is reached: `align_direction: sideways`
|
|
111
|
+
# alongside a valid `alignDirection: horizontal` raises rather than
|
|
112
|
+
# yielding "horizontal". The base had no snake_case key at all, so it
|
|
113
|
+
# could not hit this.
|
|
114
|
+
#
|
|
115
|
+
# The hook also re-applies every snake_case key, just before the legacy
|
|
116
|
+
# ones. Newer lutaml 0.8.x releases no longer run the cardinality check
|
|
117
|
+
# or the public setter for a canonical key (`layer: [1]` parses, and
|
|
118
|
+
# `align_direction: sideways` is stored), so the hook does both for the
|
|
119
|
+
# canonical spelling as well, and the two spellings reject the same input
|
|
120
|
+
# (an array for align_direction fails with a different error class).
|
|
121
|
+
#
|
|
122
|
+
# For every value shape -- scalar, wrong-typed, array and blank -- the
|
|
123
|
+
# hook casts through the attribute the way lutaml's own deserializer
|
|
124
|
+
# does, applies the same cardinality check, and assigns through the
|
|
125
|
+
# public setter, so it produces the same Ruby types and runs
|
|
126
|
+
# align_direction's validation.
|
|
127
|
+
#
|
|
128
|
+
# Two things about the shape of the rule, neither of them free choices:
|
|
129
|
+
#
|
|
130
|
+
# * It is ONE root-document rule, not six per-key ones. A per-key custom
|
|
131
|
+
# rule is dropped before its hook runs when its value is blank
|
|
132
|
+
# (key_value/transform.rb:251), which would turn `alignDirection: ''`
|
|
133
|
+
# from a parse error into a silent nil. A plain `map ... to:` alias is
|
|
134
|
+
# no good either -- it fires even when its key is absent, overwriting a
|
|
135
|
+
# canonical-only read: with nil for an attribute that has no default,
|
|
136
|
+
# and with the declared default otherwise (`fixed_position` back to
|
|
137
|
+
# false, `position_priority` back to 0).
|
|
138
|
+
# * Its `to:` names a sentinel rather than a real attribute. lutaml
|
|
139
|
+
# unwraps a root rule when the document has exactly one key equal to
|
|
140
|
+
# `to:`, handing over that key's VALUE instead of the document
|
|
141
|
+
# (services/rule_value_extractor.rb:36-47). It does that whatever the
|
|
142
|
+
# value's type, whether or not `to:` names an attribute, and for the
|
|
143
|
+
# key spelled as EITHER a String or a Symbol -- line 41 is the
|
|
144
|
+
# size == 1 guard, line 43 tests the two spellings. So
|
|
145
|
+
# the sentinel REDUCES the collision to documents whose sole key is
|
|
146
|
+
# `__legacy_yaml_aliases` in either spelling; it does not eliminate it.
|
|
147
|
+
# Naming a real attribute instead would collide on an ordinary key such
|
|
148
|
+
# as `layer`, which is why the sentinel is still the better target. The
|
|
149
|
+
# guard in the hook makes a NONBLANK NON-HASH collision inert -- a
|
|
150
|
+
# scalar or a nonempty Array, neither of which answers to `key?`; a
|
|
151
|
+
# nonempty Hash is still merged as though it were the document, which
|
|
152
|
+
# is an accepted residual pinned in the spec. A blank unwrapped value
|
|
153
|
+
# never reaches the hook at all -- transform.rb:251 drops the rule via
|
|
154
|
+
# Utils.present? (utils.rb:119) for nil, "", [] and {}.
|
|
155
|
+
#
|
|
156
|
+
# `to:` cannot simply be dropped: without it `nil.to_sym` raises for a
|
|
157
|
+
# single-key document -- except a sole
|
|
158
|
+
# empty-string key, which matches `nil.to_s` and short-circuits first.
|
|
159
|
+
# A custom-method rule does not require the attribute to exist
|
|
160
|
+
# (valid_rule? is `attribute || rule.custom_methods[:from]`).
|
|
81
161
|
yaml do
|
|
82
|
-
map "
|
|
162
|
+
map "fixed_position", to: :fixed_position
|
|
83
163
|
map "layer", to: :layer
|
|
84
|
-
map "
|
|
85
|
-
map "
|
|
86
|
-
map "
|
|
87
|
-
map "
|
|
88
|
-
map "
|
|
164
|
+
map "align_group", to: :align_group
|
|
165
|
+
map "align_direction", to: :align_direction
|
|
166
|
+
map "relative_to", to: :relative_to
|
|
167
|
+
map "relative_offset", to: :relative_offset
|
|
168
|
+
map "position_priority", to: :position_priority
|
|
169
|
+
# Declared last, so a legacy spelling wins. See the note above the
|
|
170
|
+
# block for why this is one rule and why `to:` names no attribute.
|
|
171
|
+
map nil, to: :__legacy_yaml_aliases,
|
|
172
|
+
with: { from: :__elkrb_merge_legacy_aliases,
|
|
173
|
+
to: :__elkrb_omit_from_output }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Serialization hook for every legacy camelCase spelling. Receives the
|
|
177
|
+
# whole YAML document, so a key that is present with a blank value is
|
|
178
|
+
# still seen. Public because lutaml invokes it with `public_send`; the
|
|
179
|
+
# `__elkrb_` prefix keeps a subclass from taking the name by accident.
|
|
180
|
+
# Not part of the supported API.
|
|
181
|
+
#
|
|
182
|
+
# @api private
|
|
183
|
+
def __elkrb_merge_legacy_aliases(model, doc)
|
|
184
|
+
# Only a NONBLANK value reaches here: transform.rb:251 drops the
|
|
185
|
+
# rule for nil, "", [] and {} before the hook runs. So a non-Hash
|
|
186
|
+
# arrives only when lutaml unwrapped a sole `__legacy_yaml_aliases`
|
|
187
|
+
# key holding a nonblank non-Hash value -- a scalar or a nonempty
|
|
188
|
+
# Array -- and ignoring it keeps that case inert. A nonempty Hash is
|
|
189
|
+
# still merged -- see the note above `yaml do`.
|
|
190
|
+
return unless doc.respond_to?(:key?) && doc.respond_to?(:[])
|
|
191
|
+
|
|
192
|
+
ALL_YAML_KEYS.each do |key_name, name|
|
|
193
|
+
# The standard YAML adapter preserves Symbol keys, and lutaml's own
|
|
194
|
+
# mappings resolve either spelling, so both are honoured here.
|
|
195
|
+
key = [key_name, key_name.to_sym].find { |candidate| doc.key?(candidate) }
|
|
196
|
+
next unless key
|
|
197
|
+
|
|
198
|
+
model.public_send(:"#{name}=", cast_legacy(model, name, doc[key]))
|
|
199
|
+
end
|
|
89
200
|
end
|
|
90
201
|
|
|
91
202
|
# Valid alignment directions
|
|
@@ -102,6 +213,26 @@ module Elkrb
|
|
|
102
213
|
end
|
|
103
214
|
@align_direction = value&.to_s&.downcase
|
|
104
215
|
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# Mirrors what lutaml's own deserializer does for a canonical
|
|
220
|
+
# `map ... to:` rule (key_value/transform.rb:261-266): cast through the
|
|
221
|
+
# attribute -- wired to the same parent and root -- then apply the
|
|
222
|
+
# cardinality check. Skipping the cast leaks lutaml wrapper objects into
|
|
223
|
+
# public attributes; skipping valid_collection! lets an array through for
|
|
224
|
+
# a scalar attribute, which the canonical spelling rejects and which
|
|
225
|
+
# reaches the layout engine as an Array.
|
|
226
|
+
def cast_legacy(model, name, value)
|
|
227
|
+
register = model.lutaml_register
|
|
228
|
+
attribute = model.class.attributes(register)[name]
|
|
229
|
+
cast = attribute.cast(
|
|
230
|
+
value, :yaml, register,
|
|
231
|
+
lutaml_parent: model, lutaml_root: model.lutaml_root || model
|
|
232
|
+
)
|
|
233
|
+
attribute.valid_collection!(cast, model.class)
|
|
234
|
+
cast
|
|
235
|
+
end
|
|
105
236
|
end
|
|
106
237
|
end
|
|
107
238
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
# Reopened only to attach NormalizeOptionKeys below. Graph is elkrb's data
|
|
5
|
+
# model namespace, collectively defined across lib/elkrb/graph/*.rb --
|
|
6
|
+
# there is no single file that owns it.
|
|
7
|
+
module Graph
|
|
8
|
+
# Normalizes the result of `attr.cast_value` for an options map, and
|
|
9
|
+
# rejects the one shape that result can take when a caller used a key
|
|
10
|
+
# lutaml-model has reserved.
|
|
11
|
+
#
|
|
12
|
+
# A non-Hash here is an INFERENCE, not a direct observation: lutaml reads
|
|
13
|
+
# a bare `text` or `elements` key as its own wrapper and casts the map
|
|
14
|
+
# down to that key's value, so anything but a Hash means a reserved key
|
|
15
|
+
# was used. Saying so beats letting the next line fail with a bare
|
|
16
|
+
# NoMethodError. The message is pinned by a matcher in
|
|
17
|
+
# spec/elkrb/graph/layout_options_spec.rb and must stay verbatim.
|
|
18
|
+
#
|
|
19
|
+
# Stringifies the TOP LEVEL only, matching the constructor this replaces.
|
|
20
|
+
# Deliberately not DeepStringifyKeys: that one walks nested Hashes and
|
|
21
|
+
# Arrays, this never did, and the models already ran it on the way in.
|
|
22
|
+
module NormalizeOptionKeys
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def call(cast)
|
|
26
|
+
return cast if cast.nil?
|
|
27
|
+
|
|
28
|
+
unless cast.is_a?(::Hash)
|
|
29
|
+
raise Elkrb::ValidationError,
|
|
30
|
+
"layoutOptions cannot use `text` or `elements` as a key; " \
|
|
31
|
+
"both are reserved by lutaml-model"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
cast.transform_keys(&:to_s)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_constant :NormalizeOptionKeys
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/elkrb/graph/port.rb
CHANGED
|
@@ -11,11 +11,11 @@ module Elkrb
|
|
|
11
11
|
attribute :width, :float
|
|
12
12
|
attribute :height, :float
|
|
13
13
|
attribute :labels, Label, collection: true
|
|
14
|
-
attribute :layout_options,
|
|
14
|
+
attribute :layout_options, :hash
|
|
15
15
|
attribute :properties, :hash
|
|
16
|
-
attribute :side, :string
|
|
17
|
-
attribute :index, :integer
|
|
18
|
-
attribute :offset, :float
|
|
16
|
+
attribute :side, :string
|
|
17
|
+
attribute :index, :integer
|
|
18
|
+
attribute :offset, :float
|
|
19
19
|
|
|
20
20
|
# Port sides
|
|
21
21
|
NORTH = "NORTH"
|
|
@@ -29,7 +29,7 @@ module Elkrb
|
|
|
29
29
|
# Node reference (not serialized)
|
|
30
30
|
attr_accessor :node
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
key_value do
|
|
33
33
|
map "id", to: :id
|
|
34
34
|
map "x", to: :x
|
|
35
35
|
map "y", to: :y
|
|
@@ -57,19 +57,21 @@ module Elkrb
|
|
|
57
57
|
map "offset", to: :offset
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
def
|
|
65
|
-
|
|
60
|
+
# Normalizes a Symbol key however the options arrive — a constructor,
|
|
61
|
+
# a plain setter, or lutaml's own deserialization, which routes through
|
|
62
|
+
# here too. This is the single normalization point: a :hash-typed
|
|
63
|
+
# attribute otherwise stores a raw, unnormalized Hash.
|
|
64
|
+
def layout_options=(value)
|
|
65
|
+
value_set_for(:layout_options)
|
|
66
|
+
attr = self.class.attributes(lutaml_register)[:layout_options]
|
|
67
|
+
cast = attr.cast_value(DeepStringifyKeys.call(value), lutaml_register)
|
|
68
|
+
instance_variable_set(:@layout_options, NormalizeOptionKeys.call(cast))
|
|
69
|
+
end
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
end
|
|
72
|
-
@side = normalized
|
|
71
|
+
# @param value [Object] candidate side
|
|
72
|
+
# @return [Boolean] whether the value is a canonical port side
|
|
73
|
+
def valid_side?(value)
|
|
74
|
+
SIDES.include?(value)
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
# Detect port side from position relative to node
|
|
@@ -82,11 +84,17 @@ module Elkrb
|
|
|
82
84
|
# @return [String] The detected side (NORTH, SOUTH, EAST, WEST, UNDEFINED)
|
|
83
85
|
def detect_side(node_width, node_height)
|
|
84
86
|
return UNDEFINED if x.nil? || y.nil? || node_width.nil? || node_height.nil?
|
|
85
|
-
return UNDEFINED
|
|
87
|
+
return UNDEFINED unless node_width.finite? && node_height.finite?
|
|
88
|
+
return UNDEFINED if node_width.zero? || node_height.zero?
|
|
86
89
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
# Relative position (0.0 to 1.0) from the real left/top edge, not
|
|
91
|
+
# from local x=0/y=0 -- for a negative dimension, local 0 is the
|
|
92
|
+
# real RIGHT/BOTTOM edge, so dividing by the raw signed value alone
|
|
93
|
+
# (as this used to) mislabels EAST ports as WEST and vice versa.
|
|
94
|
+
left = [0, node_width].min
|
|
95
|
+
top = [0, node_height].min
|
|
96
|
+
rel_x = (x - left) / node_width.abs.to_f
|
|
97
|
+
rel_y = (y - top) / node_height.abs.to_f
|
|
90
98
|
|
|
91
99
|
# Calculate distance to each side
|
|
92
100
|
distances = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
# Reopened only to attach ReadOnlyMapping below. Graph is elkrb's data
|
|
5
|
+
# model namespace, collectively defined across lib/elkrb/graph/*.rb --
|
|
6
|
+
# there is no single file that owns it.
|
|
7
|
+
module Graph
|
|
8
|
+
# The write half of a read-only `with:` mapping. lutaml requires both
|
|
9
|
+
# halves of a `with:` pair, and a legacy alias must contribute nothing to
|
|
10
|
+
# output -- one output vocabulary -- so this writes nothing on purpose.
|
|
11
|
+
module ReadOnlyMapping
|
|
12
|
+
# Public because lutaml invokes it with `public_send`; the `__elkrb_`
|
|
13
|
+
# prefix keeps a subclass from taking the name by accident. Not part of
|
|
14
|
+
# the supported API.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
def __elkrb_omit_from_output(model, doc); end
|
|
18
|
+
end
|
|
19
|
+
private_constant :ReadOnlyMapping
|
|
20
|
+
end
|
|
21
|
+
end
|