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
data/lib/ea/spa/projector.rb
CHANGED
|
@@ -76,7 +76,22 @@ module Ea
|
|
|
76
76
|
def metadata_hash
|
|
77
77
|
base = document.metadata
|
|
78
78
|
merged = configuration ? configuration.apply_to_metadata(base) : base
|
|
79
|
-
JSON.parse(merged.to_json)
|
|
79
|
+
hash = JSON.parse(merged.to_json)
|
|
80
|
+
hash["statistics"] = statistics_hash
|
|
81
|
+
hash
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def statistics_hash
|
|
85
|
+
{
|
|
86
|
+
"packages" => document.packages.size,
|
|
87
|
+
"classes" => document.classifiers.size,
|
|
88
|
+
"attributes" => document.classifiers.sum { |c| c.properties.size },
|
|
89
|
+
"operations" => document.classifiers.sum { |c| c.operations.size },
|
|
90
|
+
"associations" => document.relationships.count do |r|
|
|
91
|
+
r.is_a?(Ea::Model::Association)
|
|
92
|
+
end,
|
|
93
|
+
"diagrams" => render_diagrams? ? document.diagrams.size : 0
|
|
94
|
+
}
|
|
80
95
|
end
|
|
81
96
|
|
|
82
97
|
def view_extras
|
|
@@ -84,14 +99,16 @@ module Ea
|
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
def build_package_tree
|
|
102
|
+
sub_ids_by_parent = document.packages.group_by(&:parent_id)
|
|
87
103
|
nodes = document.packages.map do |pkg|
|
|
88
104
|
PackageTreeNode.new(
|
|
89
105
|
id: pkg.id,
|
|
90
106
|
name: pkg.name,
|
|
91
107
|
parent_id: pkg.parent_id,
|
|
92
|
-
child_ids: pkg.sub_package_ids
|
|
108
|
+
child_ids: (pkg.sub_package_ids +
|
|
109
|
+
(sub_ids_by_parent[pkg.id] || []).map(&:id)).uniq,
|
|
93
110
|
classifier_ids: classifiers_in_package_ids(pkg.id),
|
|
94
|
-
diagram_ids: pkg.
|
|
111
|
+
diagram_ids: diagram_ids_for_package(pkg.id)
|
|
95
112
|
)
|
|
96
113
|
end
|
|
97
114
|
PackageTree.new(
|
|
@@ -105,7 +122,7 @@ module Ea
|
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
def build_entries
|
|
108
|
-
document.classifiers.map do |c|
|
|
125
|
+
classifier_entries = document.classifiers.map do |c|
|
|
109
126
|
SkeletonEntry.new(
|
|
110
127
|
id: c.id,
|
|
111
128
|
name: c.name,
|
|
@@ -115,6 +132,27 @@ module Ea
|
|
|
115
132
|
shard_url: shard_url_for.call(c)
|
|
116
133
|
)
|
|
117
134
|
end
|
|
135
|
+
package_entries = document.packages.map do |p|
|
|
136
|
+
SkeletonEntry.new(
|
|
137
|
+
id: p.id,
|
|
138
|
+
name: p.name,
|
|
139
|
+
kind: "package",
|
|
140
|
+
package_id: p.parent_id,
|
|
141
|
+
qualified_name: p.qualified_name,
|
|
142
|
+
shard_url: shard_url_for.call(p)
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
diagram_entries = render_diagrams? ? document.diagrams.map do |d|
|
|
146
|
+
SkeletonEntry.new(
|
|
147
|
+
id: d.id,
|
|
148
|
+
name: d.name,
|
|
149
|
+
kind: "diagram",
|
|
150
|
+
package_id: d.package_id,
|
|
151
|
+
qualified_name: d.name,
|
|
152
|
+
shard_url: shard_url_for.call(d)
|
|
153
|
+
)
|
|
154
|
+
end : []
|
|
155
|
+
classifier_entries + package_entries + diagram_entries
|
|
118
156
|
end
|
|
119
157
|
|
|
120
158
|
def build_search_entries
|
|
@@ -191,8 +229,72 @@ module Ea
|
|
|
191
229
|
end
|
|
192
230
|
end
|
|
193
231
|
|
|
194
|
-
def payload_for(
|
|
195
|
-
JSON.parse(
|
|
232
|
+
def payload_for(model_element)
|
|
233
|
+
payload = JSON.parse(model_element.to_json)
|
|
234
|
+
case model_element
|
|
235
|
+
when Ea::Model::Classifier
|
|
236
|
+
augment_classifier_payload!(payload, model_element)
|
|
237
|
+
when Ea::Model::Package
|
|
238
|
+
payload["classifierIds"] = classifiers_in_package_ids(model_element.id)
|
|
239
|
+
payload["diagramIds"] = diagram_ids_for_package(model_element.id)
|
|
240
|
+
when Ea::Model::Diagram
|
|
241
|
+
augment_diagram_payload!(payload, model_element)
|
|
242
|
+
end
|
|
243
|
+
payload
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def diagram_ids_for_package(package_id)
|
|
247
|
+
(document.packages.find { |p| p.id == package_id }&.diagram_ids ||
|
|
248
|
+
[]) + document.diagrams.select { |d| d.package_id == package_id }.map(&:id)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def augment_classifier_payload!(payload, classifier)
|
|
252
|
+
rels = document.relationships_for(classifier.id)
|
|
253
|
+
payload["generalizations"] = rels
|
|
254
|
+
.select { |r| r.is_a?(Ea::Model::Generalization) && r.specific_id == classifier.id }
|
|
255
|
+
.map { |r| { "id" => r.id, "targetId" => r.general_id } }
|
|
256
|
+
payload["specializations"] = rels
|
|
257
|
+
.select { |r| r.is_a?(Ea::Model::Generalization) && r.general_id == classifier.id }
|
|
258
|
+
.map { |r| { "id" => r.id, "targetId" => r.specific_id } }
|
|
259
|
+
payload["associations"] = rels
|
|
260
|
+
.select { |r| r.is_a?(Ea::Model::Association) }
|
|
261
|
+
.map { |r| association_stub(r, classifier.id) }
|
|
262
|
+
payload
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def association_stub(assoc, classifier_id)
|
|
266
|
+
{
|
|
267
|
+
"id" => assoc.id,
|
|
268
|
+
"name" => assoc.name,
|
|
269
|
+
"sourceId" => assoc.source_id,
|
|
270
|
+
"targetId" => assoc.target_id,
|
|
271
|
+
"thisEndRoleName" => assoc.source_id == classifier_id ? assoc.source_role_name : assoc.target_role_name,
|
|
272
|
+
"otherEndRoleName" => assoc.source_id == classifier_id ? assoc.target_role_name : assoc.source_role_name,
|
|
273
|
+
"otherEndId" => assoc.source_id == classifier_id ? assoc.target_id : assoc.source_id,
|
|
274
|
+
"otherEndMultiplicity" => cardinality(assoc, classifier_id),
|
|
275
|
+
"otherEndAggregation" => assoc.source_id == classifier_id ? assoc.target_aggregation : assoc.source_aggregation
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def cardinality(assoc, classifier_id)
|
|
280
|
+
if assoc.source_id == classifier_id
|
|
281
|
+
[assoc.target_multiplicity_lower, assoc.target_multiplicity_upper]
|
|
282
|
+
else
|
|
283
|
+
[assoc.source_multiplicity_lower, assoc.source_multiplicity_upper]
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def augment_diagram_payload!(payload, diagram)
|
|
288
|
+
payload["svg"] = Ea::Svg::EaEmitter::Document.new(
|
|
289
|
+
diagram,
|
|
290
|
+
model_index: document.index_by_id,
|
|
291
|
+
document: document
|
|
292
|
+
).render
|
|
293
|
+
payload
|
|
294
|
+
rescue StandardError => e
|
|
295
|
+
warn "ea: skipping SVG render for diagram #{diagram.id} " \
|
|
296
|
+
"(#{diagram.name.inspect}): #{e.class}: #{e.message}"
|
|
297
|
+
payload
|
|
196
298
|
end
|
|
197
299
|
end
|
|
198
300
|
end
|
|
@@ -10,45 +10,29 @@ module Ea
|
|
|
10
10
|
module EnumLiterals
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
+
# EA: literal rows sit directly under the header divider,
|
|
14
|
+
# 13px apart at 7pt (attr rows use 14), first row at
|
|
15
|
+
# divider + 14. No compartment divider, no header label.
|
|
16
|
+
LITERAL_LINE_OFFSET = 6
|
|
17
|
+
|
|
13
18
|
def render(context)
|
|
14
19
|
return nil unless context.enum_literals.any?
|
|
15
20
|
return nil unless context.geometry.enum_literal_first_y
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
Element::DividerRenderer.render(
|
|
19
|
-
context.bounds,
|
|
20
|
-
y: context.geometry.enum_divider_y,
|
|
21
|
-
stroke: context.stroke,
|
|
22
|
-
stroke_width: context.stroke_width
|
|
23
|
-
),
|
|
24
|
-
render_literals_block(context)
|
|
25
|
-
].join("\n")
|
|
22
|
+
render_literals_block(context)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def render_literals_block(context)
|
|
29
|
-
line_h = context.size +
|
|
30
|
-
text_blocks = [
|
|
26
|
+
line_h = context.size + LITERAL_LINE_OFFSET
|
|
27
|
+
text_blocks = []
|
|
31
28
|
context.enum_literals.each_with_index do |literal, idx|
|
|
32
|
-
y = context.geometry.enum_literal_first_y + (
|
|
29
|
+
y = context.geometry.enum_literal_first_y + (idx * line_h)
|
|
33
30
|
text_blocks << visibility_placeholder(context, y)
|
|
34
31
|
text_blocks << literal_name_text(context, literal, y)
|
|
35
32
|
end
|
|
36
33
|
wrap_group(text_blocks, context.theme.text_color)
|
|
37
34
|
end
|
|
38
35
|
|
|
39
|
-
def literal_header_text(context)
|
|
40
|
-
TextRenderer.new(
|
|
41
|
-
content: "literals",
|
|
42
|
-
x: context.bounds.x + (context.bounds.width / 2.0) -
|
|
43
|
-
(8 * context.size * context.theme.text_width_factor) / 2,
|
|
44
|
-
y: context.geometry.enum_literal_first_y,
|
|
45
|
-
family: context.family, size: context.size, size_unit: context.size_unit,
|
|
46
|
-
style: "italic", fill: context.theme.text_color,
|
|
47
|
-
stroke_in_text: context.theme.stroke_in_text_color,
|
|
48
|
-
width_factor: context.theme.text_width_factor
|
|
49
|
-
).to_svg
|
|
50
|
-
end
|
|
51
|
-
|
|
52
36
|
def visibility_placeholder(context, y)
|
|
53
37
|
TextRenderer.new(
|
|
54
38
|
content: " ",
|
|
@@ -103,6 +103,11 @@ module Ea
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def bound_text(value)
|
|
106
|
+
# EA renders a stored literal bound ("n") verbatim, and a
|
|
107
|
+
# computed-unbounded upper as "*" (verified against
|
|
108
|
+
# EA-published reference SVGs).
|
|
109
|
+
return property.multiplicity_upper_raw if value == -1 && property.multiplicity_upper_raw
|
|
110
|
+
|
|
106
111
|
value == -1 ? "*" : value.to_s
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -9,15 +9,19 @@ module Ea
|
|
|
9
9
|
# content) matching EA's encoding.
|
|
10
10
|
class AttributeRenderer
|
|
11
11
|
DEFAULT_VISIBILITY_X_OFFSET = 5
|
|
12
|
-
DEFAULT_CONTENT_X_OFFSET =
|
|
12
|
+
DEFAULT_CONTENT_X_OFFSET = 22
|
|
13
13
|
DEFAULT_FONT_UNIT = "pt"
|
|
14
|
+
# EA seats compartment stereotype headers («Property») 12px
|
|
15
|
+
# from the box edge and spaces rows 13px at 7pt.
|
|
16
|
+
STEREOTYPE_HEADER_X_OFFSET = 12
|
|
17
|
+
ROW_LINE_OFFSET = 6
|
|
14
18
|
|
|
15
19
|
def self.render(lines, bounds:, first_y:, family:,
|
|
16
20
|
size:, size_unit: DEFAULT_FONT_UNIT,
|
|
17
21
|
fill: "#000000",
|
|
18
22
|
visibility_x_offset: DEFAULT_VISIBILITY_X_OFFSET,
|
|
19
23
|
content_x_offset: DEFAULT_CONTENT_X_OFFSET)
|
|
20
|
-
line_h = size +
|
|
24
|
+
line_h = size + ROW_LINE_OFFSET
|
|
21
25
|
text_blocks = []
|
|
22
26
|
lines.each_with_index do |line, idx|
|
|
23
27
|
y = first_y + (idx * line_h)
|
|
@@ -25,6 +29,8 @@ module Ea
|
|
|
25
29
|
if visibility
|
|
26
30
|
text_blocks << build_text(bounds.x + visibility_x_offset, y, visibility, family, size, size_unit, fill)
|
|
27
31
|
text_blocks << build_text(bounds.x + content_x_offset, y, rest, family, size, size_unit, fill)
|
|
32
|
+
elsif line.strip.start_with?("«")
|
|
33
|
+
text_blocks << build_text(bounds.x + STEREOTYPE_HEADER_X_OFFSET, y, line.strip, family, size, size_unit, fill)
|
|
28
34
|
else
|
|
29
35
|
text_blocks << build_text(bounds.x + visibility_x_offset, y, line.strip, family, size, size_unit, fill)
|
|
30
36
|
end
|
|
@@ -44,19 +50,44 @@ module Ea
|
|
|
44
50
|
#
|
|
45
51
|
# Pass a `lookup` (any callable returning a Classifier for
|
|
46
52
|
# an id) to enable inherited-property namespace prefixing.
|
|
47
|
-
def self.lines_for(classifier, lookup: nil
|
|
48
|
-
|
|
53
|
+
def self.lines_for(classifier, lookup: nil,
|
|
54
|
+
exclude_association_ids: nil)
|
|
55
|
+
props = displayable_properties(classifier, exclude_association_ids)
|
|
49
56
|
return [] unless props
|
|
50
57
|
|
|
51
|
-
props.
|
|
52
|
-
|
|
53
|
-
stereotype
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
stereotypes = props.map { |p| property_stereotype(p) }.compact.uniq
|
|
59
|
+
if stereotypes.size == 1 && props.all? { |p| property_stereotype(p) }
|
|
60
|
+
# EA renders ONE compartment stereotype header for a
|
|
61
|
+
# uniformly-stereotyped attribute set (e.g. «Property»
|
|
62
|
+
# above GML property rows) and lists rows
|
|
63
|
+
# alphabetically - verified against EA-published SVGs.
|
|
64
|
+
header = "«#{canonical_stereotype(stereotypes.first)}»"
|
|
65
|
+
[header] + props.sort_by { |p| p.name.to_s }.map do |prop|
|
|
66
|
+
AttributeLineBuilder.new(prop, host: classifier,
|
|
67
|
+
lookup: lookup).to_s
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
props.flat_map do |prop|
|
|
71
|
+
lines = []
|
|
72
|
+
stereotype = property_stereotype(prop)
|
|
73
|
+
lines << "«#{stereotype}»" if stereotype
|
|
74
|
+
lines << AttributeLineBuilder.new(prop, host: classifier,
|
|
56
75
|
lookup: lookup).to_s
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
end
|
|
59
79
|
|
|
80
|
+
STEREOTYPE_DISPLAY = {
|
|
81
|
+
"property" => "Property",
|
|
82
|
+
"featuretype" => "FeatureType",
|
|
83
|
+
"objecttype" => "ObjectType",
|
|
84
|
+
"codelist" => "CodeList"
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
def self.canonical_stereotype(ref)
|
|
88
|
+
STEREOTYPE_DISPLAY.fetch(ref.to_s.downcase, ref.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
60
91
|
# Internal helpers
|
|
61
92
|
|
|
62
93
|
def self.split_visibility(line)
|
|
@@ -75,10 +106,19 @@ module Ea
|
|
|
75
106
|
end
|
|
76
107
|
private_class_method :build_text
|
|
77
108
|
|
|
78
|
-
|
|
109
|
+
# Association-end properties render in the box only when
|
|
110
|
+
# their association is NOT drawn on the diagram (drawn ones
|
|
111
|
+
# render as connector role labels). exclude_association_ids
|
|
112
|
+
# carries the diagram's drawn association ids; nil preserves
|
|
113
|
+
# the legacy hide-all behavior.
|
|
114
|
+
def self.displayable_properties(classifier, exclude_association_ids)
|
|
79
115
|
return nil unless classifier.properties
|
|
80
116
|
|
|
81
|
-
classifier.properties.reject
|
|
117
|
+
classifier.properties.reject do |prop|
|
|
118
|
+
prop.association_id &&
|
|
119
|
+
(exclude_association_ids.nil? ||
|
|
120
|
+
exclude_association_ids.include?(prop.association_id))
|
|
121
|
+
end
|
|
82
122
|
end
|
|
83
123
|
private_class_method :displayable_properties
|
|
84
124
|
|
|
@@ -27,10 +27,10 @@ module Ea
|
|
|
27
27
|
def initialize(bounds:, size:, header_lines_count:,
|
|
28
28
|
attr_lines_count:, op_lines_count:,
|
|
29
29
|
tagged_values_count:,
|
|
30
|
-
header_top_padding:
|
|
30
|
+
header_top_padding: 12,
|
|
31
31
|
header_line_offset: 6,
|
|
32
32
|
divider_offset: 8,
|
|
33
|
-
attr_line_offset:
|
|
33
|
+
attr_line_offset: 6,
|
|
34
34
|
attr_first_offset: 7)
|
|
35
35
|
@bounds = bounds
|
|
36
36
|
@size = size
|
|
@@ -46,7 +46,7 @@ module Ea
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def header_first_y
|
|
49
|
-
bounds.y + size + (header_top_padding ||
|
|
49
|
+
bounds.y + size + (header_top_padding || 12)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Y of the divider line between header and the rest.
|
|
@@ -61,7 +61,7 @@ module Ea
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def attr_first_y
|
|
64
|
-
return bounds.y + size + (header_top_padding ||
|
|
64
|
+
return bounds.y + size + (header_top_padding || 12) + 12 unless divider_y
|
|
65
65
|
|
|
66
66
|
divider_y + size + (attr_first_offset || 7)
|
|
67
67
|
end
|
|
@@ -69,7 +69,7 @@ module Ea
|
|
|
69
69
|
def attr_bottom_y
|
|
70
70
|
return attr_first_y unless attr_lines_count&.positive?
|
|
71
71
|
|
|
72
|
-
attr_first_y + (attr_lines_count - 1) * (size + (attr_line_offset ||
|
|
72
|
+
attr_first_y + (attr_lines_count - 1) * (size + (attr_line_offset || 6))
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def op_divider_y
|
|
@@ -88,12 +88,14 @@ module Ea
|
|
|
88
88
|
op_first_y + (op_lines_count - 1) * (size + 4)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
# EA draws a single divider under the header and seats
|
|
92
|
+
# enumeration literals directly after it (+14 at 7pt),
|
|
93
|
+
# exactly like attribute rows - no second divider and no
|
|
94
|
+
# "literals" header. Verified against EA-published SVGs.
|
|
95
95
|
def enum_literal_first_y
|
|
96
|
-
|
|
96
|
+
return nil unless divider_y
|
|
97
|
+
|
|
98
|
+
divider_y + size + (attr_first_offset || 7)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
# Tagged values appear after attributes (or after ops if
|
|
@@ -51,22 +51,13 @@ module Ea
|
|
|
51
51
|
(bounds_width - QUALIFIED_WRAP_PADDING)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# EA renders the plain classifier name in element headers
|
|
55
|
+
# (element ownership qualification is a display option the
|
|
56
|
+
# corpus diagrams leave off - verified against EA-published
|
|
57
|
+
# reference SVGs).
|
|
58
|
+
def self.display_name(classifier, _diagram_package_id = nil,
|
|
55
59
|
_visually_nested = false)
|
|
56
|
-
|
|
57
|
-
(classifier.qualified_name || classifier.name).to_s
|
|
58
|
-
else
|
|
59
|
-
classifier.name.to_s
|
|
60
|
-
end
|
|
61
|
-
return name if diagram_package_id.nil?
|
|
62
|
-
return name unless name.include?("::")
|
|
63
|
-
|
|
64
|
-
if classifier.is_a?(Ea::Model::Classifier) &&
|
|
65
|
-
classifier.package_id == diagram_package_id
|
|
66
|
-
name.split("::").last
|
|
67
|
-
else
|
|
68
|
-
name
|
|
69
|
-
end
|
|
60
|
+
classifier.name.to_s
|
|
70
61
|
end
|
|
71
62
|
end
|
|
72
63
|
end
|
|
@@ -18,25 +18,26 @@ module Ea
|
|
|
18
18
|
classifier = context.classifier
|
|
19
19
|
return [] if classifier.is_a?(Ea::Model::InstanceSpecification)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
return []
|
|
21
|
+
labels = label_for(classifier, context.umldi_keyword)
|
|
22
|
+
return [] if labels.empty?
|
|
23
23
|
|
|
24
|
-
[
|
|
24
|
+
labels.map { |l| [l, :normal] }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# @return [String
|
|
27
|
+
# @return [Array<String>] «name» labels (EA renders one
|
|
28
|
+
# line per applied stereotype)
|
|
28
29
|
def self.label_for(classifier, umldi_keyword)
|
|
29
|
-
return "«#{umldi_keyword}»" if umldi_keyword && !umldi_keyword.empty?
|
|
30
|
+
return ["«#{umldi_keyword}»"] if umldi_keyword && !umldi_keyword.empty?
|
|
30
31
|
|
|
31
32
|
explicit = explicit_stereotype(classifier)
|
|
32
33
|
return explicit if explicit
|
|
33
34
|
|
|
34
35
|
case classifier
|
|
35
36
|
when Ea::Model::Klass, Ea::Model::Package, Ea::Model::Note
|
|
36
|
-
|
|
37
|
+
[]
|
|
37
38
|
else
|
|
38
39
|
fallback = fallback_name(classifier)
|
|
39
|
-
fallback ? "«#{fallback}»" :
|
|
40
|
+
fallback ? ["«#{fallback}»"] : []
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -47,7 +48,23 @@ module Ea
|
|
|
47
48
|
refs = classifier.stereotype_refs
|
|
48
49
|
return nil unless refs&.any?
|
|
49
50
|
|
|
50
|
-
"«#{
|
|
51
|
+
refs.map { |ref| "«#{canonical(ref)}»" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# EA displays canonical stereotype spellings regardless of
|
|
55
|
+
# how the QEA stored them (property → Property). Prefer the
|
|
56
|
+
# MDG alias registry when its definition is available; the
|
|
57
|
+
# overrides map covers the spellings verified directly
|
|
58
|
+
# against EA-published reference SVGs.
|
|
59
|
+
DISPLAY_OVERRIDES = {
|
|
60
|
+
"property" => "Property",
|
|
61
|
+
"featuretype" => "FeatureType",
|
|
62
|
+
"objecttype" => "ObjectType",
|
|
63
|
+
"codelist" => "CodeList"
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
def self.canonical(ref)
|
|
67
|
+
DISPLAY_OVERRIDES.fetch(ref.downcase, ref)
|
|
51
68
|
end
|
|
52
69
|
|
|
53
70
|
def self.fallback_name(classifier)
|
|
@@ -14,7 +14,7 @@ module Ea
|
|
|
14
14
|
def self.render(lines, bounds:, first_y:, family:,
|
|
15
15
|
size:, size_unit: "pt",
|
|
16
16
|
fill: "#000000", weight_normal: 400, weight_bold: 700,
|
|
17
|
-
stroke_in_text: "#000000", width_factor: 0.
|
|
17
|
+
stroke_in_text: "#000000", width_factor: 0.612,
|
|
18
18
|
line_offset: 6)
|
|
19
19
|
line_h = size + line_offset
|
|
20
20
|
text_blocks = lines.each_with_index.map do |(text, style), idx|
|
|
@@ -23,11 +23,23 @@ module Ea
|
|
|
23
23
|
else weight_normal
|
|
24
24
|
end
|
|
25
25
|
font_style = (style == :italic || style == :bold_italic) ? "italic" : "normal"
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# EA spaces the line AFTER a right-aligned ghost 6px
|
|
27
|
+
# wider (ghost +13, next +32 at 7pt) - verified against
|
|
28
|
+
# EA-published reference SVGs.
|
|
29
|
+
y = first_y + (idx * line_h) +
|
|
30
|
+
(idx >= 1 && lines.first.last == :italic ? 6 : 0)
|
|
31
|
+
x = if style == :italic
|
|
32
|
+
# EA right-aligns the off-canvas parent ghost at
|
|
33
|
+
# the box's right edge (verified across EA-published
|
|
34
|
+
# reference SVGs).
|
|
35
|
+
bounds.x + bounds.width -
|
|
36
|
+
Ea::Svg::EaEmitter::TextRenderer.estimate_width(text, size, width_factor)
|
|
37
|
+
else
|
|
38
|
+
center_x_for(text, bounds, size, width_factor)
|
|
39
|
+
end
|
|
28
40
|
TextRenderer.new(
|
|
29
41
|
content: text,
|
|
30
|
-
x:
|
|
42
|
+
x: x, y: y,
|
|
31
43
|
family: family, size: size, size_unit: size_unit,
|
|
32
44
|
weight: weight, style: font_style, fill: fill,
|
|
33
45
|
stroke_in_text: stroke_in_text, width_factor: width_factor
|
|
@@ -24,8 +24,8 @@ module Ea
|
|
|
24
24
|
class LegendRenderer
|
|
25
25
|
CONTAINER_RX = 3
|
|
26
26
|
ICON_SIZE = 17
|
|
27
|
-
TITLE_Y_OFFSET =
|
|
28
|
-
FIRST_ITEM_Y_OFFSET =
|
|
27
|
+
TITLE_Y_OFFSET = 22
|
|
28
|
+
FIRST_ITEM_Y_OFFSET = 33
|
|
29
29
|
ITEM_SPACING = 19
|
|
30
30
|
ICON_X_OFFSET = 10
|
|
31
31
|
ICON_LABEL_GAP = 22
|
|
@@ -130,11 +130,36 @@ module Ea
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def title_text
|
|
133
|
-
x =
|
|
133
|
+
x = title_x
|
|
134
134
|
y = bounds.y + TITLE_Y_OFFSET
|
|
135
135
|
build_text(legend.title, x: x, y: y, size: title_font_size, weight: 700)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# EA centers the title over the ITEM BLOCK (icon left edge
|
|
139
|
+
# to the right end of the widest label), not the container
|
|
140
|
+
# box - verified against EA-published reference SVGs.
|
|
141
|
+
def title_x
|
|
142
|
+
icon_left = bounds.x + ICON_X_OFFSET
|
|
143
|
+
block_right = icon_left + ICON_LABEL_GAP +
|
|
144
|
+
widest_item_label_width
|
|
145
|
+
block_left = icon_left
|
|
146
|
+
block_left + (block_right - block_left - title_text_width) / 2.0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
CJK_RANGE = /[\u3000-\u9fff\uff00-\uffef]/
|
|
150
|
+
|
|
151
|
+
# CJK glyphs run ~0.82em, others ~0.612em (regressed from
|
|
152
|
+
# EA's own textLength values on mixed-script labels).
|
|
153
|
+
def estimated_width(text, size)
|
|
154
|
+
text.to_s.each_char.sum do |ch|
|
|
155
|
+
ch.match?(CJK_RANGE) ? size * 0.70 : size * 0.612
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def widest_item_label_width
|
|
160
|
+
legend.items.map { |i| estimated_width(i.name, item_font_size) }.max || 0.0
|
|
161
|
+
end
|
|
162
|
+
|
|
138
163
|
def item_top_y(index)
|
|
139
164
|
bounds.y + FIRST_ITEM_Y_OFFSET + (index * ITEM_SPACING)
|
|
140
165
|
end
|
|
@@ -148,7 +173,7 @@ module Ea
|
|
|
148
173
|
end
|
|
149
174
|
|
|
150
175
|
def title_text_width
|
|
151
|
-
legend.title
|
|
176
|
+
estimated_width(legend.title, title_font_size)
|
|
152
177
|
end
|
|
153
178
|
|
|
154
179
|
def build_text(content, x:, y:, size:, weight:)
|