ea 0.6.3 → 0.6.5
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.md +72 -0
- data/config/themes/119.yml +4 -4
- data/frontend/dist/app.iife.js +8 -8
- data/frontend/src/App.vue +6 -2
- data/frontend/src/components/AppHeader.vue +35 -8
- data/frontend/src/components/AppSidebar.vue +2 -2
- data/frontend/src/components/ClassDetails.vue +103 -120
- data/frontend/src/components/DiagramView.vue +19 -6
- data/frontend/src/components/PackageDetails.vue +67 -26
- data/frontend/src/components/PackageTreeNode.vue +25 -20
- data/frontend/src/components/SearchResults.vue +21 -8
- data/frontend/src/components/WelcomeView.vue +2 -2
- data/frontend/src/stores/dataStore.ts +142 -48
- data/frontend/src/stores/uiStore.ts +3 -4
- data/frontend/src/types.ts +160 -163
- data/lib/ea/model/diagram_connector.rb +9 -0
- data/lib/ea/model/property.rb +6 -1
- data/lib/ea/sources/qea/classifier_builder.rb +3 -1
- data/lib/ea/sources/qea/diagram_builder.rb +91 -9
- data/lib/ea/sources/qea/property_builder.rb +12 -2
- data/lib/ea/spa/output/single_file_strategy.rb +1 -1
- data/lib/ea/spa/projector.rb +108 -6
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
- data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
- data/lib/ea/svg/ea_emitter/elements.rb +70 -20
- data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
- data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
- data/lib/ea/svg/ea_emitter/labels.rb +38 -2
- data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
- data/lib/ea/svg/parity/checker.rb +19 -9
- data/lib/ea/svg/parity/corpus_report.rb +83 -0
- data/lib/ea/svg/parity/differ.rb +231 -0
- data/lib/ea/svg/parity.rb +2 -0
- data/lib/ea/theme/definition.rb +3 -3
- data/lib/ea/theme/loader.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
|
@@ -71,18 +71,29 @@ module Ea
|
|
|
71
71
|
font_size: size,
|
|
72
72
|
off_canvas_parent_name: parent_name) : []
|
|
73
73
|
is_classifier = classifier.is_a?(Ea::Model::Classifier)
|
|
74
|
-
attr_lines =
|
|
74
|
+
attr_lines = if is_classifier && show_attributes?
|
|
75
|
+
Element::AttributeRenderer.lines_for(
|
|
76
|
+
classifier, lookup: attribute_lookup,
|
|
77
|
+
exclude_association_ids: drawn_association_ids
|
|
78
|
+
)
|
|
79
|
+
else
|
|
80
|
+
[]
|
|
81
|
+
end
|
|
75
82
|
op_lines = (is_classifier && show_operations?) ? Element::OperationRenderer.lines_for(classifier) : []
|
|
76
|
-
geometry =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
84
|
+
tagged_values_for(classifier).size,
|
|
85
|
+
header_lines)
|
|
86
|
+
# EA suppresses the whole attribute compartment when it does
|
|
87
|
+
# not fit the stored box height (never truncates rows) - the
|
|
88
|
+
# AOS box on CityFurniture hides its association property
|
|
89
|
+
# this way. Verified against EA-published reference SVGs.
|
|
90
|
+
if attr_lines.any? &&
|
|
91
|
+
geometry.attr_bottom_y.to_i + size > bounds.y + bounds.height
|
|
92
|
+
attr_lines = []
|
|
93
|
+
geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
94
|
+
tagged_values_for(classifier).size,
|
|
95
|
+
header_lines)
|
|
96
|
+
end
|
|
86
97
|
RenderContext.new(
|
|
87
98
|
element: element,
|
|
88
99
|
bounds: bounds,
|
|
@@ -109,6 +120,47 @@ module Ea
|
|
|
109
120
|
)
|
|
110
121
|
end
|
|
111
122
|
|
|
123
|
+
def compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
124
|
+
tagged_count, header_lines)
|
|
125
|
+
CompartmentGeometry.new(bounds: bounds, size: size,
|
|
126
|
+
header_lines_count: header_lines.size,
|
|
127
|
+
attr_lines_count: attr_lines.size,
|
|
128
|
+
op_lines_count: op_lines.size,
|
|
129
|
+
tagged_values_count: tagged_count,
|
|
130
|
+
header_top_padding: header_padding_for(header_lines),
|
|
131
|
+
header_line_offset: theme.compartments.header_line_offset,
|
|
132
|
+
divider_offset: theme.compartments.divider_offset,
|
|
133
|
+
attr_line_offset: theme.compartments.attr_line_offset,
|
|
134
|
+
attr_first_offset: attr_first_offset_for(attr_lines))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# EA seats a stereotype line 3px higher than a bare class name
|
|
138
|
+
# (first baseline +16 vs +19 at 7pt) - verified against the
|
|
139
|
+
# EA-published reference SVGs.
|
|
140
|
+
def header_padding_for(header_lines)
|
|
141
|
+
first = header_lines.first
|
|
142
|
+
return theme.compartments.header_top_padding unless first
|
|
143
|
+
|
|
144
|
+
if first.last == :italic
|
|
145
|
+
6 # ghost-led: EA seats the right-aligned ghost at +13
|
|
146
|
+
elsif first.first.to_s.start_with?("«")
|
|
147
|
+
9
|
|
148
|
+
else
|
|
149
|
+
theme.compartments.header_top_padding
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A stereotype group header («Property») sits 4px below where
|
|
154
|
+
# plain rows start (divider+18 vs divider+14 at 7pt).
|
|
155
|
+
def attr_first_offset_for(attr_lines)
|
|
156
|
+
attr_lines.first.to_s.start_with?("«") ? 11 : 7
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def drawn_association_ids
|
|
160
|
+
(diagram.connectors || []).select(&:renderable?)
|
|
161
|
+
.map(&:relationship_ref).compact.to_set
|
|
162
|
+
end
|
|
163
|
+
|
|
112
164
|
def enum_literals_for(classifier)
|
|
113
165
|
return [] unless classifier.is_a?(Ea::Model::Enumeration)
|
|
114
166
|
|
|
@@ -166,19 +218,17 @@ module Ea
|
|
|
166
218
|
(diagram.elements || []).any? { |e| e.model_element_ref == model_element_ref }
|
|
167
219
|
end
|
|
168
220
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
221
|
+
# EA suppresses the parent-class ghost line only when the
|
|
222
|
+
# parent's package is itself placed on the diagram as a
|
|
223
|
+
# package element. A mere same-package classifier does NOT
|
|
224
|
+
# suppress it — verified against EA-published reference SVGs
|
|
225
|
+
# (CityFurniture renders the ghost for AbstractOccupiedSpace
|
|
226
|
+
# although the class shares its parent's package).
|
|
174
227
|
def parent_package_represented_on_diagram?(parent)
|
|
175
228
|
parent_pkg = parent.is_a?(Ea::Model::Classifier) ? parent.package_id : nil
|
|
176
229
|
return false unless parent_pkg
|
|
177
230
|
|
|
178
|
-
(diagram.elements || []).any?
|
|
179
|
-
sibling = model_index[e.model_element_ref]
|
|
180
|
-
sibling.is_a?(Ea::Model::Classifier) && sibling.package_id == parent_pkg
|
|
181
|
-
end
|
|
231
|
+
(diagram.elements || []).any? { |e| e.model_element_ref == parent_pkg }
|
|
182
232
|
end
|
|
183
233
|
|
|
184
234
|
# EA renders a package's child classifiers and sub-packages as
|
|
@@ -17,12 +17,11 @@ module Ea
|
|
|
17
17
|
# text-emitting layer.
|
|
18
18
|
class FontResolver
|
|
19
19
|
DEFAULT_FONT_FAMILY = "Yu Gothic UI"
|
|
20
|
-
# EA's default element compartment font size
|
|
21
|
-
# reference SVGs
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
DEFAULT_ELEMENT_FONT_SIZE = 9
|
|
20
|
+
# EA's default element compartment font size. EA-published
|
|
21
|
+
# reference SVGs render body text at 7pt (the current EA
|
|
22
|
+
# theme size) for every element whose objectstyle fontsz is
|
|
23
|
+
# a use-default marker (see QEA DiagramBuilder.font_size_from).
|
|
24
|
+
DEFAULT_ELEMENT_FONT_SIZE = 7
|
|
26
25
|
|
|
27
26
|
attr_reader :diagram, :theme
|
|
28
27
|
|
|
@@ -202,7 +202,9 @@ module Ea
|
|
|
202
202
|
offset_y = box["oy"] || box[:oy]
|
|
203
203
|
return nil if offset_x.nil? && offset_y.nil?
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
# Waypoint anchors are mirrored to screen-down space; the
|
|
206
|
+
# stored OY offset counts the other way, so negate it.
|
|
207
|
+
[anchor[0] + offset_x.to_i, anchor[1] - offset_y.to_i]
|
|
206
208
|
end
|
|
207
209
|
|
|
208
210
|
def property_y_offset
|
|
@@ -73,16 +73,52 @@ module Ea
|
|
|
73
73
|
target_pt = points.last
|
|
74
74
|
boxes = connector.label_boxes || {}
|
|
75
75
|
|
|
76
|
+
src_tb, src_mb = end_boxes(boxes, :llt, :llb) ||
|
|
77
|
+
default_label_boxes(points, :source)
|
|
78
|
+
tgt_tb, tgt_mb = end_boxes(boxes, :lrt, :lrb) ||
|
|
79
|
+
default_label_boxes(points, :target)
|
|
80
|
+
|
|
76
81
|
texts = []
|
|
77
|
-
end_renderer.texts(text_box:
|
|
82
|
+
end_renderer.texts(text_box: src_tb, mult_box: src_mb,
|
|
78
83
|
anchor: source_pt, connector: connector,
|
|
79
84
|
end_kind: :source).each { |t| texts << t }
|
|
80
|
-
end_renderer.texts(text_box:
|
|
85
|
+
end_renderer.texts(text_box: tgt_tb, mult_box: tgt_mb,
|
|
81
86
|
anchor: target_pt, connector: connector,
|
|
82
87
|
end_kind: :target).each { |t| texts << t }
|
|
83
88
|
texts
|
|
84
89
|
end
|
|
85
90
|
|
|
91
|
+
def end_boxes(boxes, text_key, mult_key)
|
|
92
|
+
return nil if boxes[text_key].nil? && boxes[mult_key].nil?
|
|
93
|
+
|
|
94
|
+
[boxes[text_key], boxes[mult_key]]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# EA's default label placement for connectors without stored
|
|
98
|
+
# label boxes: role name 5px along the line and 31px below it,
|
|
99
|
+
# multiplicity 18px along and 21px above (screen-down coords).
|
|
100
|
+
# Derived from 219 labels across EA-published reference SVGs.
|
|
101
|
+
def default_label_boxes(points, end_kind)
|
|
102
|
+
e = end_kind == :source ? points.first : points.last
|
|
103
|
+
other = end_kind == :source ? points[1] : points[-2]
|
|
104
|
+
return [nil, nil] unless e && other
|
|
105
|
+
|
|
106
|
+
ux = other[0] - e[0]
|
|
107
|
+
uy = other[1] - e[1]
|
|
108
|
+
len = Math.sqrt(ux**2 + uy**2)
|
|
109
|
+
return [nil, nil] if len < 0.5
|
|
110
|
+
|
|
111
|
+
ux /= len
|
|
112
|
+
uy /= len
|
|
113
|
+
vx = uy
|
|
114
|
+
vy = -ux
|
|
115
|
+
role_box = { "ox" => (5 * ux - 31 * vx).round,
|
|
116
|
+
"oy" => (5 * uy - 31 * vy).round }
|
|
117
|
+
mult_box = { "ox" => (18 * ux + 21 * vx).round,
|
|
118
|
+
"oy" => (18 * uy + 21 * vy).round }
|
|
119
|
+
[role_box, mult_box]
|
|
120
|
+
end
|
|
121
|
+
|
|
86
122
|
def registry
|
|
87
123
|
@registry ||= Label::Registry.new(model_index: model_index)
|
|
88
124
|
end
|
|
@@ -20,7 +20,7 @@ module Ea
|
|
|
20
20
|
DEFAULT_FILL = "#000000"
|
|
21
21
|
DEFAULT_ROTATION = -0.00
|
|
22
22
|
DEFAULT_STROKE_IN_TEXT = "#000000"
|
|
23
|
-
DEFAULT_WIDTH_FACTOR = 0.
|
|
23
|
+
DEFAULT_WIDTH_FACTOR = 0.612
|
|
24
24
|
|
|
25
25
|
attr_reader :content, :x, :y, :family, :size, :weight, :style,
|
|
26
26
|
:fill, :text_length, :rotation, :size_unit,
|
|
@@ -55,19 +55,108 @@ module Ea
|
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
+
# nil coordinates occur on degenerate placements (e.g. label
|
|
59
|
+
# boxes without offsets); EA always has a position, so render
|
|
60
|
+
# at the origin rather than crashing the whole diagram.
|
|
58
61
|
def format_attrs
|
|
59
|
-
%(x="#{format('%.2f', x)}" y="#{format('%.2f', y)}")
|
|
62
|
+
%(x="#{format('%.2f', x.to_f)}" y="#{format('%.2f', y.to_f)}")
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def format_style
|
|
63
66
|
"font-family:#{family}; font-weight:#{weight}; font-style:#{style}; font-size:#{size}#{size_unit}; fill:#{fill};fill-opacity:1.00; stroke:#{stroke_in_text}; stroke-opacity:0.00 stroke-width:0; white-space: pre;"
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# Per-codepoint advance widths (em), least-squares fitted from
|
|
70
|
+
# EA's own textLength attributes across ~8k published reference
|
|
71
|
+
# texts (mean residual 0.097em - real-metric extraction).
|
|
72
|
+
GLYPH_WIDTHS = {
|
|
73
|
+
"e" => 0.731,
|
|
74
|
+
"t" => 0.438,
|
|
75
|
+
"a" => 0.72,
|
|
76
|
+
"r" => 0.446,
|
|
77
|
+
"i" => 0.302,
|
|
78
|
+
"o" => 0.717,
|
|
79
|
+
"n" => 0.693,
|
|
80
|
+
" " => 0.311,
|
|
81
|
+
"s" => 0.568,
|
|
82
|
+
"c" => 0.567,
|
|
83
|
+
"l" => 0.287,
|
|
84
|
+
"u" => 0.732,
|
|
85
|
+
":" => 0.433,
|
|
86
|
+
"m" => 1.125,
|
|
87
|
+
"+" => 0.951,
|
|
88
|
+
"d" => 0.707,
|
|
89
|
+
"p" => 0.703,
|
|
90
|
+
"C" => 0.725,
|
|
91
|
+
"g" => 0.726,
|
|
92
|
+
"y" => 0.733,
|
|
93
|
+
"." => 0.458,
|
|
94
|
+
"S" => 0.706,
|
|
95
|
+
"f" => 0.426,
|
|
96
|
+
"_" => 0.755,
|
|
97
|
+
"T" => 0.702,
|
|
98
|
+
"L" => 0.604,
|
|
99
|
+
"D" => 0.871,
|
|
100
|
+
"1" => 0.708,
|
|
101
|
+
"0" => 0.703,
|
|
102
|
+
"P" => 0.698,
|
|
103
|
+
"h" => 0.699,
|
|
104
|
+
"\u00bb" => 0.802,
|
|
105
|
+
"\u00ab" => 0.566,
|
|
106
|
+
"b" => 0.72,
|
|
107
|
+
"I" => 0.422,
|
|
108
|
+
"A" => 0.907,
|
|
109
|
+
"v" => 0.685,
|
|
110
|
+
"M" => 1.294,
|
|
111
|
+
"R" => 0.764,
|
|
112
|
+
")" => 0.405,
|
|
113
|
+
"(" => 0.289,
|
|
114
|
+
"*" => 0.646,
|
|
115
|
+
"[" => -0.029,
|
|
116
|
+
"]" => 0.669,
|
|
117
|
+
"O" => 0.968,
|
|
118
|
+
"E" => 0.683,
|
|
119
|
+
"B" => 0.801,
|
|
120
|
+
"V" => 0.898,
|
|
121
|
+
"x" => 0.628,
|
|
122
|
+
"G" => 0.875,
|
|
123
|
+
"=" => 0.629,
|
|
124
|
+
"F" => 0.74,
|
|
125
|
+
"N" => 0.893,
|
|
126
|
+
"U" => 0.907,
|
|
127
|
+
"2" => 0.743,
|
|
128
|
+
"w" => 0.983,
|
|
129
|
+
"}" => 0.345,
|
|
130
|
+
"{" => 0.461,
|
|
131
|
+
"k" => 0.792,
|
|
132
|
+
"9" => 0.839,
|
|
133
|
+
"j" => 0.366,
|
|
134
|
+
"q" => 0.657,
|
|
135
|
+
"3" => 0.694,
|
|
136
|
+
"5" => 0.72,
|
|
137
|
+
">" => 0.682,
|
|
138
|
+
"," => 0.549,
|
|
139
|
+
"-" => 0.485,
|
|
140
|
+
"<" => 0.696,
|
|
141
|
+
"Q" => 0.967,
|
|
142
|
+
"6" => 0.776,
|
|
143
|
+
"4" => 0.658,
|
|
144
|
+
"/" => 0.496,
|
|
145
|
+
"z" => 0.59,
|
|
146
|
+
"H" => 0.854,
|
|
147
|
+
"W" => 1.292,
|
|
148
|
+
"\"" => 0.508,
|
|
149
|
+
"J" => 0.555,
|
|
150
|
+
"X" => 0.711,
|
|
151
|
+
"7" => 0.671,
|
|
152
|
+
"8" => 0.74
|
|
153
|
+
}.freeze
|
|
154
|
+
GLYPH_FALLBACK = 0.61
|
|
155
|
+
|
|
156
|
+
def self.estimate_width(text, size, _width_factor = nil)
|
|
157
|
+
text.to_s.each_char.sum do |ch|
|
|
158
|
+
(GLYPH_WIDTHS[ch] || GLYPH_FALLBACK) * size.to_f
|
|
159
|
+
end
|
|
71
160
|
end
|
|
72
161
|
|
|
73
162
|
def formatted_text_length
|
|
@@ -77,7 +166,7 @@ module Ea
|
|
|
77
166
|
end
|
|
78
167
|
|
|
79
168
|
def formatted_transform
|
|
80
|
-
"rotate(#{format('%<r>.2f', r: rotation)} #{format('%.2f', x)} #{format('%.2f', y)})"
|
|
169
|
+
"rotate(#{format('%<r>.2f', r: rotation)} #{format('%.2f', x.to_f)} #{format('%.2f', y.to_f)})"
|
|
81
170
|
end
|
|
82
171
|
|
|
83
172
|
def escaped_content
|
|
@@ -25,6 +25,7 @@ module Ea
|
|
|
25
25
|
text: count_diff("text"),
|
|
26
26
|
group: top_level_group_diff,
|
|
27
27
|
font_family: font_family_match,
|
|
28
|
+
font_size: font_size_match,
|
|
28
29
|
view_box: view_box_match,
|
|
29
30
|
text_overlap: text_overlap_ratio
|
|
30
31
|
)
|
|
@@ -51,9 +52,23 @@ module Ea
|
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def font_family_match
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
font_style_histogram(our_doc, "font-family") ==
|
|
56
|
+
font_style_histogram(ref_doc, "font-family")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Exact histogram of every emitted font-size (e.g.
|
|
60
|
+
# {"9.00"=>42, "13.00"=>21}) — a first-text comparison would
|
|
61
|
+
# pass diagrams whose body text renders at the wrong size.
|
|
62
|
+
def font_size_match
|
|
63
|
+
font_style_histogram(our_doc, "font-size") ==
|
|
64
|
+
font_style_histogram(ref_doc, "font-size")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def font_style_histogram(doc, property)
|
|
68
|
+
doc.css("text").each_with_object(Hash.new(0)) do |node, acc|
|
|
69
|
+
value = (node["style"] || "")[/#{Regexp.quote(property)}:([^;]+)/, 1]
|
|
70
|
+
acc[value] += 1 if value
|
|
71
|
+
end
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
def view_box_match
|
|
@@ -69,11 +84,6 @@ module Ea
|
|
|
69
84
|
(our_set & ref_set).size.to_f / (our_set | ref_set).size.to_f
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
def first_text_style(doc)
|
|
73
|
-
first = doc.css("text").first
|
|
74
|
-
first ? (first["style"] || "") : ""
|
|
75
|
-
end
|
|
76
|
-
|
|
77
87
|
Diff = Struct.new(:ours, :reference, keyword_init: true) do
|
|
78
88
|
def delta
|
|
79
89
|
ours - reference
|
|
@@ -85,7 +95,7 @@ module Ea
|
|
|
85
95
|
end
|
|
86
96
|
|
|
87
97
|
Report = Struct.new(:rect, :path, :polygon, :text, :group,
|
|
88
|
-
:font_family, :view_box, :text_overlap,
|
|
98
|
+
:font_family, :font_size, :view_box, :text_overlap,
|
|
89
99
|
keyword_init: true) do
|
|
90
100
|
def shape_delta_total
|
|
91
101
|
[rect, path, polygon].sum(&:delta).abs
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Svg
|
|
7
|
+
module Parity
|
|
8
|
+
# Renders every diagram of a Document and diffs it against the
|
|
9
|
+
# EA-authored reference export for that diagram (EAID_-named
|
|
10
|
+
# files), aggregating itemized deltas into a corpus report.
|
|
11
|
+
#
|
|
12
|
+
# References stay wherever EA published them (e.g. an EA "Save
|
|
13
|
+
# as image" directory); nothing is copied into the gem.
|
|
14
|
+
class CorpusReport
|
|
15
|
+
DELTA_KEYS = %i[extra_text missing_text moved_text wrong_font
|
|
16
|
+
wrong_size wrong_weight].freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :document, :ref_dir
|
|
19
|
+
|
|
20
|
+
def initialize(document:, ref_dir:)
|
|
21
|
+
@document = document
|
|
22
|
+
@ref_dir = ref_dir
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns { diagrams: [...], totals: {...} } and writes JSON
|
|
26
|
+
# when output_path is given.
|
|
27
|
+
def run(output_path: nil)
|
|
28
|
+
index = document.index_by_id
|
|
29
|
+
rows = diagrams_with_references.map do |diagram|
|
|
30
|
+
measure_one(diagram, index)
|
|
31
|
+
end.compact
|
|
32
|
+
|
|
33
|
+
result = { totals: totals(rows), diagrams: rows }
|
|
34
|
+
File.write(output_path, JSON.pretty_generate(result)) if output_path
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def diagrams_with_references
|
|
41
|
+
document.diagrams.select { |d| reference_path_for(d) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# EA publishes images as EAID_<guid-with-underscores>.svg;
|
|
45
|
+
# fall back to the raw diagram id for non-QEA sources.
|
|
46
|
+
def reference_path_for(diagram)
|
|
47
|
+
[
|
|
48
|
+
File.join(ref_dir, "EAID_#{diagram.id.tr('-', '_')}.svg"),
|
|
49
|
+
File.join(ref_dir, "#{Ea::Sources::Qea::IdNormalizer.to_eaid(diagram.id)}.svg"),
|
|
50
|
+
File.join(ref_dir, "#{diagram.id}.svg")
|
|
51
|
+
].find { |p| File.exist?(p) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def measure_one(diagram, index)
|
|
55
|
+
ref_path = reference_path_for(diagram)
|
|
56
|
+
return nil unless ref_path
|
|
57
|
+
|
|
58
|
+
ours = Ea::Svg::EaEmitter::Document.new(
|
|
59
|
+
diagram, model_index: index, document: document
|
|
60
|
+
).render
|
|
61
|
+
differ = Differ.new(ours: ours, reference: File.read(ref_path))
|
|
62
|
+
deltas = differ.text_deltas
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
id: diagram.id, name: diagram.name,
|
|
66
|
+
deltas: DELTA_KEYS.to_h { |k| [k, deltas[k].size] },
|
|
67
|
+
shapes: differ.shape_summary.slice(:matched, :missing, :extra)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def totals(rows)
|
|
72
|
+
sum = DELTA_KEYS.to_h { |k| [k, 0] }
|
|
73
|
+
shapes = { matched: 0, missing: 0, extra: 0 }
|
|
74
|
+
rows.each do |row|
|
|
75
|
+
DELTA_KEYS.each { |k| sum[k] += row[:deltas][k] }
|
|
76
|
+
shapes.each_key { |k| shapes[k] += row[:shapes][k] }
|
|
77
|
+
end
|
|
78
|
+
sum.merge(shapes: shapes, diagrams: rows.size)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|