ea 0.6.112 → 0.6.114
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/lib/ea/sources/qea/diagram_builder.rb +15 -0
- data/lib/ea/svg/ea_emitter/connector_redock.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +10 -10
- data/lib/ea/svg/ea_emitter/elements.rb +3 -1
- data/lib/ea/svg/ea_emitter/font_resolver.rb +4 -10
- data/lib/ea/svg/ea_emitter/label/end_label.rb +95 -8
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +18 -4
- data/lib/ea/svg/ea_emitter/labels.rb +13 -1
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 251161fffac3b07a12851b0abd26a046998a61b19a9418da8c6d6ceea1cbb09f
|
|
4
|
+
data.tar.gz: 549486eec86fb19c6275db6ab7dafdcb7987c1a22b12df2740140e60c7bb2690
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5cccbc615740b54619be7e36e4c6edb6382768d97b8a120841aca69018b73e693d9dadb8480e78c9e1e0a18ccfc09122668caf9680571496d99b44372c593cd
|
|
7
|
+
data.tar.gz: 87a9b79c59a7102d10aaf4968c052f5a63c1e707ef4daf2376d1278ca79b1317e2fc7d47988d57cb09c5abe88d57aeb12b611ba7066e421a1820ce07ac7ff581
|
|
@@ -126,6 +126,8 @@ module Ea
|
|
|
126
126
|
@current_obj_row = obj_row
|
|
127
127
|
style_hash = DiagramStyleParser.parse(obj_row.objectstyle)
|
|
128
128
|
style_hash[:stored_attribute_order] = true
|
|
129
|
+
fqname = stereotype_fqname_for(obj_row)
|
|
130
|
+
style_hash[:stereotype_fqname] = fqname if fqname
|
|
129
131
|
font_style = effective_font_style(obj_row, style_hash)
|
|
130
132
|
Ea::Model::DiagramElement.new(
|
|
131
133
|
id: IdNormalizer.synthetic("de", diagram_row.diagram_id,
|
|
@@ -150,6 +152,19 @@ module Ea
|
|
|
150
152
|
raw == "1" || raw == "-1"
|
|
151
153
|
end
|
|
152
154
|
|
|
155
|
+
def stereotype_fqname_for(placement)
|
|
156
|
+
object = database.find_object(placement.ea_object_id)
|
|
157
|
+
return nil unless object
|
|
158
|
+
|
|
159
|
+
database.xrefs_for_client(object.ea_guid).each do |xref|
|
|
160
|
+
application = Xref::Parser.parse(xref.description).stereotype
|
|
161
|
+
next unless application && application.name == object.stereotype
|
|
162
|
+
|
|
163
|
+
return application.fqname
|
|
164
|
+
end
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
153
168
|
def build_connectors(diagram_row)
|
|
154
169
|
links = database.diagram_links_for(diagram_row.diagram_id) || []
|
|
155
170
|
context = ContextDiagramSupport.context?(diagram_row)
|
|
@@ -389,6 +389,7 @@ module Ea
|
|
|
389
389
|
dividers = [divider_y_by_ref[source_ref],
|
|
390
390
|
divider_y_by_ref[target_ref]]
|
|
391
391
|
return nil unless bounds.all? && dividers.all?
|
|
392
|
+
return nil if flush_lateral_line?(connector, pairs, *bounds)
|
|
392
393
|
|
|
393
394
|
preserve_source = explicit_horizontal_source_dock?(pairs, connector)
|
|
394
395
|
adjusted = row_slot_pairs(pairs, bounds, dividers, preserve_source: preserve_source)
|
|
@@ -22,7 +22,7 @@ module Ea
|
|
|
22
22
|
# render zero «...» lines, classes included).
|
|
23
23
|
return [] if context.suppress_stereotypes
|
|
24
24
|
|
|
25
|
-
labels = label_for(classifier, context.umldi_keyword)
|
|
25
|
+
labels = label_for(classifier, context.umldi_keyword, context.stereotype_fqname)
|
|
26
26
|
return [] if labels.empty?
|
|
27
27
|
|
|
28
28
|
labels.map { |l| [l, :normal] }
|
|
@@ -30,10 +30,10 @@ module Ea
|
|
|
30
30
|
|
|
31
31
|
# @return [Array<String>] «name» labels (EA renders one
|
|
32
32
|
# line per applied stereotype)
|
|
33
|
-
def self.label_for(classifier, umldi_keyword)
|
|
33
|
+
def self.label_for(classifier, umldi_keyword, stereotype_fqname = nil)
|
|
34
34
|
return ["«#{umldi_keyword}»"] if umldi_keyword && !umldi_keyword.empty?
|
|
35
35
|
|
|
36
|
-
explicit = explicit_stereotype(classifier)
|
|
36
|
+
explicit = explicit_stereotype(classifier, stereotype_fqname)
|
|
37
37
|
return explicit if explicit
|
|
38
38
|
|
|
39
39
|
case classifier
|
|
@@ -45,21 +45,21 @@ module Ea
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def self.explicit_stereotype(classifier)
|
|
48
|
+
def self.explicit_stereotype(classifier, stereotype_fqname = nil)
|
|
49
49
|
return nil unless classifier.is_a?(Ea::Model::Classifier) ||
|
|
50
50
|
classifier.is_a?(Ea::Model::Package)
|
|
51
51
|
|
|
52
52
|
refs = classifier.stereotype_refs
|
|
53
53
|
return nil unless refs&.any?
|
|
54
54
|
|
|
55
|
-
refs.map { |ref| "«#{canonical(ref)}»" }
|
|
55
|
+
refs.map { |ref| "«#{canonical(ref, stereotype_fqname)}»" }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# EA
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
# EA's standard Class stereotype displays as «type» while
|
|
59
|
+
# its semantic name remains Type. Custom profiles retain case.
|
|
60
|
+
def self.canonical(ref, stereotype_fqname = nil)
|
|
61
|
+
return "type" if ref == "Type" && stereotype_fqname == "StandardProfileL2::Type"
|
|
62
|
+
|
|
63
63
|
ref
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -166,10 +166,12 @@ module Ea
|
|
|
166
166
|
)
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
|
+
stereotype_fqname = element.style&.fetch(:stereotype_fqname, nil)
|
|
169
170
|
header_lines = classifier ? Element::HeaderLines.for(classifier,
|
|
170
171
|
diagram_package_id: diagram.package_id,
|
|
171
172
|
visually_nested: visually_nested?(element),
|
|
172
173
|
umldi_keyword: element.umldi_keyword,
|
|
174
|
+
stereotype_fqname: stereotype_fqname,
|
|
173
175
|
bounds_width: bounds.width,
|
|
174
176
|
font_size: size, logical_height: logical_height,
|
|
175
177
|
family: family,
|
|
@@ -452,7 +454,7 @@ module Ea
|
|
|
452
454
|
def tagged_values_for(classifier)
|
|
453
455
|
return [] unless classifier.is_a?(Ea::Model::Classifier)
|
|
454
456
|
|
|
455
|
-
classifier.tagged_values || []
|
|
457
|
+
(classifier.tagged_values || []).reject { |tag| tag.value.nil? || tag.value.empty? }
|
|
456
458
|
end
|
|
457
459
|
|
|
458
460
|
# Real OCL constraints render trailing, after the content
|
|
@@ -10,7 +10,7 @@ module Ea
|
|
|
10
10
|
# 2. theme.font_family / theme.font_size (when theme is themed)
|
|
11
11
|
# 3. diagram-level default (most common non-nil value across
|
|
12
12
|
# the diagram's elements)
|
|
13
|
-
# 4.
|
|
13
|
+
# 4. unthemed fallback (Cambria)
|
|
14
14
|
#
|
|
15
15
|
# Lives outside the Elements emitter so the resolution rule is
|
|
16
16
|
# defined once (MECE) and reusable by Labels and any future
|
|
@@ -83,14 +83,8 @@ module Ea
|
|
|
83
83
|
theme.element_font_family || theme.font_family
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# "Calibri" 10. We can't query EA's runtime, so we infer
|
|
89
|
-
# from the diagram:
|
|
90
|
-
#
|
|
91
|
-
# - If ANY element specifies font=, use the most common.
|
|
92
|
-
# - If NO element specifies font= (all are "use default"),
|
|
93
|
-
# assume the EA English-locale default: Calibri 10.
|
|
86
|
+
# Unthemed diagrams without a saved family use Cambria.
|
|
87
|
+
# Saved families still supply the diagram default.
|
|
94
88
|
def diagram_default_family
|
|
95
89
|
families = element_families
|
|
96
90
|
return FALLBACK_DEFAULT_FAMILY if families.empty?
|
|
@@ -98,7 +92,7 @@ module Ea
|
|
|
98
92
|
most_common(families)
|
|
99
93
|
end
|
|
100
94
|
|
|
101
|
-
FALLBACK_DEFAULT_FAMILY = "
|
|
95
|
+
FALLBACK_DEFAULT_FAMILY = "Cambria"
|
|
102
96
|
|
|
103
97
|
def most_common(values)
|
|
104
98
|
return nil if values.empty?
|
|
@@ -100,12 +100,18 @@ module Ea
|
|
|
100
100
|
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
101
101
|
elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
|
|
102
102
|
text_pos = target_property_role(connector, heading, anchor, text_box, role) ||
|
|
103
|
+
target_diagonal_property_role(connector, heading, anchor, text_box, role) ||
|
|
103
104
|
target_vertical_role(connector, heading, anchor, text_box, role) ||
|
|
104
105
|
target_rightward_role(heading, anchor, text_box, role) || text_pos
|
|
105
106
|
mult_pos = target_multiplicity_seat(connector, heading, anchor, mult_box) ||
|
|
106
107
|
target_horizontal_multiplicity(heading, anchor, mult_box, mult) || mult_pos
|
|
107
108
|
end
|
|
108
109
|
|
|
110
|
+
if end_kind == :source
|
|
111
|
+
text_pos = source_top_property_role(connector, heading, anchor, text_box, role) ||
|
|
112
|
+
source_bottom_property_role(connector, heading, anchor, text_box, role) || text_pos
|
|
113
|
+
end
|
|
114
|
+
|
|
109
115
|
build_texts(role:, mult:, standalone:,
|
|
110
116
|
text_pos:, mult_pos:, stereo:, text_box:)
|
|
111
117
|
end
|
|
@@ -185,10 +191,10 @@ module Ea
|
|
|
185
191
|
[x + SEAT_X_INSET, y - 7]
|
|
186
192
|
end
|
|
187
193
|
|
|
188
|
-
# Property-role clusters use a direction-specific
|
|
194
|
+
# Property-role clusters use a direction-specific horizontal seat;
|
|
189
195
|
# alignment controls the stereotype line within that cluster.
|
|
190
196
|
def source_property_seat(connector, direction, anchor, box, role)
|
|
191
|
-
return nil unless %i[up left down].include?(direction) && property_role_box?(box, direction)
|
|
197
|
+
return nil unless %i[up left right down].include?(direction) && property_role_box?(box, direction)
|
|
192
198
|
return nil unless source_property_font?(connector, direction)
|
|
193
199
|
|
|
194
200
|
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
@@ -196,11 +202,62 @@ module Ea
|
|
|
196
202
|
|
|
197
203
|
x, y = seat_corner(anchor, box)
|
|
198
204
|
inset, baseline = property_role_offsets(direction)
|
|
199
|
-
left = x
|
|
205
|
+
left = property_role_x(direction, x, inset + width)
|
|
200
206
|
left = [left, MIN_ROLE_X].max if direction == :up
|
|
201
207
|
[left, y + baseline]
|
|
202
208
|
end
|
|
203
209
|
|
|
210
|
+
def property_role_x(direction, origin, offset)
|
|
211
|
+
direction == :right ? origin + offset : origin - offset
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# EDGE=1 certifies the top dock independently of the clipped ray's
|
|
215
|
+
# heading. An absent source box uses the upward Property seat.
|
|
216
|
+
def source_top_property_role(connector, heading, anchor, box, role)
|
|
217
|
+
return nil unless source_seat?(:source, heading) && heading[0].negative? && heading[1].positive?
|
|
218
|
+
return nil unless zero_source_property_box?(box)
|
|
219
|
+
|
|
220
|
+
return nil unless source_top_property_route?(connector.style.to_h)
|
|
221
|
+
|
|
222
|
+
source_property_seat(connector, :up, anchor, box, role)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def zero_source_property_box?(box)
|
|
226
|
+
box && box["alignment"]&.zero? && stored_offsets_zero?(box)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def source_top_property_route?(style)
|
|
230
|
+
style[:stored_top_dock] == true && style.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# A saved external ray that first meets the source's bottom face
|
|
234
|
+
# seats an absent source Property role like the downward Property
|
|
235
|
+
# seat. The caller reports that face as a downward heading.
|
|
236
|
+
def source_bottom_property_role(connector, heading, anchor, box, role)
|
|
237
|
+
return nil unless source_bottom_dock?(connector, heading, box)
|
|
238
|
+
|
|
239
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
240
|
+
return nil unless width
|
|
241
|
+
|
|
242
|
+
x, y = seat_corner(anchor, box)
|
|
243
|
+
inset, baseline = property_role_offsets(:down)
|
|
244
|
+
[x - inset - width, y + baseline]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def source_bottom_dock?(connector, heading, box)
|
|
248
|
+
source_seat?(:source, heading) && heading[0].zero? && heading[1].positive? &&
|
|
249
|
+
zero_source_property_box?(box) && default_presentation?(box) &&
|
|
250
|
+
source_bottom_property_route?(connector.style.to_h) && source_property_cambria?(connector)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# A downward leg on the horizontal lateral tree seats an absent
|
|
254
|
+
# box like the up leg already does on the same route.
|
|
255
|
+
def source_bottom_property_route?(style)
|
|
256
|
+
return false unless style.values_at(:mode, :lwidth) == %w[3 2]
|
|
257
|
+
|
|
258
|
+
style[:tree] == "LH" || (style[:stored_association_ray] == true && style[:tree].nil?)
|
|
259
|
+
end
|
|
260
|
+
|
|
204
261
|
def plain_upward_seat(connector, direction, anchor, box, role)
|
|
205
262
|
return nil unless direction == :up && cambria_box?(box)
|
|
206
263
|
return nil unless [0, 1].include?(box["alignment"])
|
|
@@ -233,6 +290,31 @@ module Ea
|
|
|
233
290
|
property_label?(connector, :target) && downward_property_route?(connector.style.to_h)
|
|
234
291
|
end
|
|
235
292
|
|
|
293
|
+
# Zero-offset diagonal labels use the dominant leg's Property
|
|
294
|
+
# right edge. Moved boxes keep their explicit placement.
|
|
295
|
+
def target_diagonal_property_role(connector, heading, anchor, box, role)
|
|
296
|
+
return nil unless diagonal_property_box?(connector, box)
|
|
297
|
+
|
|
298
|
+
inset = diagonal_property_inset(heading)
|
|
299
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family) if inset
|
|
300
|
+
[anchor[0] - inset - width, anchor[1] - 7] if width
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def diagonal_property_box?(connector, box)
|
|
304
|
+
plain_vertical_box?(box) && box["alignment"] == 1 && stored_offsets_zero?(box) &&
|
|
305
|
+
property_label?(connector, :target) &&
|
|
306
|
+
connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def diagonal_property_inset(heading)
|
|
310
|
+
return nil unless heading && !heading.include?(0)
|
|
311
|
+
|
|
312
|
+
dx, dy = heading
|
|
313
|
+
return property_role_offsets(:left).first if dx.negative? && dx.abs > dy.abs
|
|
314
|
+
|
|
315
|
+
property_role_offsets(:down).first if dy.negative? && dy.abs > dx.abs
|
|
316
|
+
end
|
|
317
|
+
|
|
236
318
|
def target_vertical_role(connector, heading, anchor, box, role)
|
|
237
319
|
return nil unless heading && heading[0].zero? && !heading[1].zero?
|
|
238
320
|
|
|
@@ -301,28 +383,33 @@ module Ea
|
|
|
301
383
|
def property_role_offsets(direction)
|
|
302
384
|
case direction
|
|
303
385
|
when :up then [60, -6]
|
|
304
|
-
when :left then [60, -7]
|
|
386
|
+
when :left, :right then [60, -7]
|
|
305
387
|
else [65, 15]
|
|
306
388
|
end
|
|
307
389
|
end
|
|
308
390
|
|
|
309
391
|
def source_property_font?(connector, direction)
|
|
310
|
-
|
|
311
|
-
property_label?(connector, :source) &&
|
|
392
|
+
source_property_cambria?(connector) &&
|
|
312
393
|
source_property_route?(connector.style.to_h, direction)
|
|
313
394
|
end
|
|
314
395
|
|
|
396
|
+
def source_property_cambria?(connector)
|
|
397
|
+
font_family.to_s.casecmp("Cambria").zero? && property_label?(connector, :source)
|
|
398
|
+
end
|
|
399
|
+
|
|
315
400
|
def source_property_route?(style, direction)
|
|
316
401
|
case direction
|
|
317
402
|
when :left then left_property_route?(style)
|
|
403
|
+
when :right then style.values_at(:mode, :lwidth, :tree) == %w[3 2 H]
|
|
318
404
|
when :down then downward_property_route?(style)
|
|
319
405
|
else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
|
|
320
406
|
end
|
|
321
407
|
end
|
|
322
408
|
|
|
323
409
|
def left_property_route?(style)
|
|
324
|
-
|
|
325
|
-
[
|
|
410
|
+
downward_property_route?(style) ||
|
|
411
|
+
(style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
|
|
412
|
+
[nil, "H", "LV"].include?(style[:tree]))
|
|
326
413
|
end
|
|
327
414
|
|
|
328
415
|
def downward_property_route?(style)
|
|
@@ -96,8 +96,8 @@ module Ea
|
|
|
96
96
|
LINE_PITCH = 13
|
|
97
97
|
|
|
98
98
|
def label_position(connector, points)
|
|
99
|
-
|
|
100
|
-
return
|
|
99
|
+
stored_seat = stored_stereotype_position(connector, points) || stored_name_position(connector, points)
|
|
100
|
+
return stored_seat if stored_seat
|
|
101
101
|
|
|
102
102
|
return midpoint(points) unless default_substitutable_seat?(connector) &&
|
|
103
103
|
upward_four_point_route?(points)
|
|
@@ -106,9 +106,23 @@ module Ea
|
|
|
106
106
|
[source[0] + 3, ((source[1] + bend[1]) / 2).floor - 1]
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# LMB is the row below the midpoint; LMT carries the name above it.
|
|
110
|
+
def stored_stereotype_position(connector, points)
|
|
111
|
+
return nil unless connector.is_a?(Ea::Model::DiagramConnector) && measured_name_font?
|
|
112
|
+
return nil unless stereotype_label(connector)
|
|
113
|
+
|
|
114
|
+
box = connector.label_boxes.to_h[:lmb]
|
|
115
|
+
return nil unless default_name_box?(box)
|
|
116
|
+
|
|
117
|
+
stored_position(connector, points, box, LINE_PITCH)
|
|
118
|
+
end
|
|
119
|
+
|
|
109
120
|
# At 7pt, a centered name's stored corner is 3px below its baseline.
|
|
110
121
|
def stored_name_position(connector, points)
|
|
111
|
-
|
|
122
|
+
stored_position(connector, points, stored_name_box(connector), -3)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def stored_position(connector, points, box, baseline)
|
|
112
126
|
return nil unless box
|
|
113
127
|
|
|
114
128
|
width = Ea::Fonts::Metrics.text_length(midpoint_content(connector), font_size, family: font_family)
|
|
@@ -116,7 +130,7 @@ module Ea
|
|
|
116
130
|
|
|
117
131
|
x, y = midpoint(points)
|
|
118
132
|
offset_x, offset_y = box.values_at("ox", "oy").map(&:to_i)
|
|
119
|
-
[x + offset_x - (width / 2.0).floor, y + offset_y
|
|
133
|
+
[x + offset_x - (width / 2.0).floor, y + offset_y + baseline]
|
|
120
134
|
end
|
|
121
135
|
|
|
122
136
|
def stored_name_box(connector)
|
|
@@ -129,7 +129,7 @@ module Ea
|
|
|
129
129
|
|
|
130
130
|
box = source_dock_bounds(connector)
|
|
131
131
|
face = box && dock_face_heading(points.first, box)
|
|
132
|
-
return face if face &&
|
|
132
|
+
return face if face && face_leads_leg?(connector, face, heading, box)
|
|
133
133
|
|
|
134
134
|
stored_upward_heading(connector, heading)
|
|
135
135
|
end
|
|
@@ -147,6 +147,18 @@ module Ea
|
|
|
147
147
|
original.any?(&:zero?) ? original : heading
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# A saved external ray enters the source through the face it first
|
|
151
|
+
# meets, even when the clipped leg then runs back across the box.
|
|
152
|
+
def face_leads_leg?(connector, face, heading, box)
|
|
153
|
+
anchor = connector.source_anchor
|
|
154
|
+
if face == [0, 1] && anchor && anchor.y > box.y + box.height &&
|
|
155
|
+
connector.style.to_h[:stored_association_ray] == true
|
|
156
|
+
return true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
|
|
160
|
+
end
|
|
161
|
+
|
|
150
162
|
def stored_upward_heading(connector, heading)
|
|
151
163
|
return heading unless heading[1].negative?
|
|
152
164
|
|
data/lib/ea/version.rb
CHANGED