ea 0.6.107 → 0.6.109
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/config/themes/119.yml +3 -2
- data/lib/ea/diagram/display_config.rb +5 -0
- data/lib/ea/fonts/cambria_7_styled_widths.json +293 -0
- data/lib/ea/fonts/metrics.rb +50 -7
- data/lib/ea/model/diagram_element.rb +4 -0
- data/lib/ea/sources/qea/diagram_builder/auto_routing.rb +124 -11
- data/lib/ea/sources/qea/diagram_builder/font_settings.rb +50 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +82 -82
- data/lib/ea/sources/qea/diagram_builder.rb +8 -30
- data/lib/ea/sources/qea/instance_builder.rb +1 -0
- data/lib/ea/svg/ea_emitter/color_resolver.rb +1 -2
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +2 -1
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +19 -42
- data/lib/ea/svg/ea_emitter/compartment/header.rb +2 -1
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +6 -4
- data/lib/ea/svg/ea_emitter/compartment/note_html.rb +66 -0
- data/lib/ea/svg/ea_emitter/compartment/operations.rb +3 -1
- data/lib/ea/svg/ea_emitter/compartment.rb +1 -0
- data/lib/ea/svg/ea_emitter/connector_redock.rb +30 -14
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +11 -9
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +20 -10
- data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +9 -5
- data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +8 -5
- data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +3 -2
- data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +10 -5
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +11 -15
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +14 -12
- data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +26 -13
- data/lib/ea/svg/ea_emitter/elements.rb +45 -37
- data/lib/ea/svg/ea_emitter/font_resolver.rb +14 -4
- data/lib/ea/svg/ea_emitter/ghost_labels.rb +3 -0
- data/lib/ea/svg/ea_emitter/label/end_label.rb +37 -4
- data/lib/ea/svg/ea_emitter/labels.rb +12 -4
- data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
- data/lib/ea/svg/ea_emitter/text_renderer.rb +6 -5
- data/lib/ea/theme/definition.rb +5 -2
- data/lib/ea/theme/loader.rb +3 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -1
|
@@ -53,10 +53,10 @@ module Ea
|
|
|
53
53
|
return stored_top_pairs(connector, source_bounds, pairs)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
refs = [source_ref, target_ref]
|
|
57
|
+
duids = connector.style.to_h.values_at(:soid, :eoid)
|
|
58
|
+
outline = bounds.each_with_index.map do |box, index|
|
|
59
|
+
outline_bounds(box, diagram, refs[index], duids[index])
|
|
60
60
|
end
|
|
61
61
|
candidate = clipped_pairs(outline, anchors)
|
|
62
62
|
candidate[0] = external_source_point(connector, outline.first, anchors, candidate.first)
|
|
@@ -231,19 +231,34 @@ module Ea
|
|
|
231
231
|
return false unless bounds.all? { |box| box.width.positive? && box.height.positive? }
|
|
232
232
|
|
|
233
233
|
ends = [[refs.first, style[:soid]], [refs.last, style[:eoid]]]
|
|
234
|
-
ends.all? { |ref, duid|
|
|
234
|
+
ends.all? { |ref, duid| border_width?(diagram, ref, duid, 2) }
|
|
235
235
|
end
|
|
236
236
|
private_class_method :bordered_outline?
|
|
237
237
|
|
|
238
238
|
# Every placement of the element (the DUID-matched one when the
|
|
239
|
-
# connector names it) must declare
|
|
240
|
-
def
|
|
239
|
+
# connector names it) must declare the same border width.
|
|
240
|
+
def border_width?(diagram, ref, duid, width)
|
|
241
|
+
placed = end_placements(diagram, ref, duid)
|
|
242
|
+
placed.any? && placed.all? { |el| el.line_width == width || el.style.to_h[:lwth] == width.to_s }
|
|
243
|
+
end
|
|
244
|
+
private_class_method :border_width?
|
|
245
|
+
|
|
246
|
+
# Explicit zero-width borders keep that end on the geometric edge;
|
|
247
|
+
# absent or inherited widths retain the default outer-stroke docking.
|
|
248
|
+
def outline_bounds(box, diagram, ref, duid)
|
|
249
|
+
return box if border_width?(diagram, ref, duid, 0)
|
|
250
|
+
|
|
251
|
+
Ea::Model::Bounds.new(x: box.x - 1, y: box.y - 1,
|
|
252
|
+
width: box.width + 2, height: box.height + 2)
|
|
253
|
+
end
|
|
254
|
+
private_class_method :outline_bounds
|
|
255
|
+
|
|
256
|
+
def end_placements(diagram, ref, duid)
|
|
241
257
|
placed = Array(diagram&.elements).select { |el| el.model_element_ref == ref }
|
|
242
258
|
named = placed.select { |el| duid && el.duid.to_s.casecmp?(duid.to_s) }
|
|
243
|
-
|
|
244
|
-
placed.any? && placed.all? { |el| el.line_width == 2 || el.style.to_h[:lwth] == "2" }
|
|
259
|
+
named.empty? ? placed : named
|
|
245
260
|
end
|
|
246
|
-
private_class_method :
|
|
261
|
+
private_class_method :end_placements
|
|
247
262
|
|
|
248
263
|
def outline_connector?(connector, bounds)
|
|
249
264
|
return true if default_model_connector?(connector)
|
|
@@ -397,12 +412,13 @@ module Ea
|
|
|
397
412
|
end
|
|
398
413
|
private_class_method :explicit_horizontal_source_dock?
|
|
399
414
|
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
#
|
|
415
|
+
# The QEA producer certifies stored Association rays from their
|
|
416
|
+
# endpoint origins. Keep the clipped ray even when a diagonal end
|
|
417
|
+
# coincides with a header divider. Other callers need a certified
|
|
418
|
+
# horizontal port before bypassing compartment row seating.
|
|
403
419
|
def explicit_association_ray?(pairs, connector)
|
|
404
420
|
produced = connector.style.to_h[:stored_association_ray] == true
|
|
405
|
-
return true if produced
|
|
421
|
+
return true if produced
|
|
406
422
|
|
|
407
423
|
certified_association_ray?(pairs, connector)
|
|
408
424
|
end
|
|
@@ -48,6 +48,7 @@ module Ea
|
|
|
48
48
|
parts << ": #{type_str}" unless type_str.empty?
|
|
49
49
|
mult = multiplicity
|
|
50
50
|
parts << " #{mult}" unless mult.empty?
|
|
51
|
+
parts << " {ordered}" if property.is_ordered
|
|
51
52
|
parts << " = #{property.default_value}" if property.default_value && !property.default_value.to_s.empty?
|
|
52
53
|
parts.join.rstrip
|
|
53
54
|
end
|
|
@@ -100,20 +101,21 @@ module Ea
|
|
|
100
101
|
def multiplicity
|
|
101
102
|
lower = property.multiplicity_lower
|
|
102
103
|
upper = property.multiplicity_upper
|
|
103
|
-
if lower == 1 && upper == 1
|
|
104
|
-
raw = property.multiplicity_lower_raw
|
|
105
|
-
# Literal text bounds EA displays verbatim ('elements:
|
|
106
|
-
# Character [size]' - stored bounds 'size'/'size' parse
|
|
107
|
-
# to the 1..1 default).
|
|
108
|
-
return "[#{raw}]" if raw && raw == property.multiplicity_upper_raw
|
|
109
|
-
|
|
110
|
-
return ""
|
|
111
|
-
end
|
|
104
|
+
return default_multiplicity if lower == 1 && upper == 1
|
|
112
105
|
return "" if lower.nil? && upper.nil?
|
|
106
|
+
return "[#{lower}]" if lower == upper && upper != -1
|
|
113
107
|
|
|
114
108
|
"[#{lower || 0}..#{bound_text(upper)}]"
|
|
115
109
|
end
|
|
116
110
|
|
|
111
|
+
# Literal text bounds EA displays verbatim ('elements:
|
|
112
|
+
# Character [size]' - stored bounds 'size'/'size' parse
|
|
113
|
+
# to the 1..1 default).
|
|
114
|
+
def default_multiplicity
|
|
115
|
+
raw = property.multiplicity_lower_raw
|
|
116
|
+
raw && raw == property.multiplicity_upper_raw ? "[#{raw}]" : ""
|
|
117
|
+
end
|
|
118
|
+
|
|
117
119
|
def bound_text(value)
|
|
118
120
|
# EA renders a stored literal bound ("n") verbatim, and a
|
|
119
121
|
# computed-unbounded upper as "*" (verified against
|
|
@@ -47,12 +47,14 @@ module Ea
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def self.render(lines, bounds:, first_y:, family:,
|
|
50
|
-
size:, size_unit: DEFAULT_FONT_UNIT,
|
|
50
|
+
size:, size_unit: DEFAULT_FONT_UNIT, logical_height: nil,
|
|
51
51
|
fill: "#000000",
|
|
52
52
|
visibility_x_offset: DEFAULT_VISIBILITY_X_OFFSET,
|
|
53
53
|
content_x_offset: nil)
|
|
54
54
|
|
|
55
|
-
line_h =
|
|
55
|
+
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
56
|
+
size, family: family, unit: size_unit, offset: ROW_LINE_OFFSET, logical_height: logical_height
|
|
57
|
+
)
|
|
56
58
|
offsets = group_lead_offsets(lines)
|
|
57
59
|
text_blocks = []
|
|
58
60
|
lines.each_with_index do |line, idx|
|
|
@@ -60,15 +62,27 @@ module Ea
|
|
|
60
62
|
visibility, rest = split_visibility(line)
|
|
61
63
|
if visibility
|
|
62
64
|
unless visibility.strip.empty?
|
|
63
|
-
text_blocks << build_text(
|
|
65
|
+
text_blocks << build_text(
|
|
66
|
+
bounds.x + visibility_x_offset, y, visibility, family, size, size_unit, fill,
|
|
67
|
+
logical_height: logical_height
|
|
68
|
+
)
|
|
64
69
|
end
|
|
65
70
|
offset = (content_x_offset || DEFAULT_CONTENT_X_OFFSET) +
|
|
66
71
|
2 * (size - 7)
|
|
67
|
-
|
|
72
|
+
offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
|
|
73
|
+
text_blocks << build_text(
|
|
74
|
+
bounds.x + offset, y, rest, family, size, size_unit, fill, logical_height: logical_height
|
|
75
|
+
)
|
|
68
76
|
elsif line.strip.start_with?("«")
|
|
69
|
-
text_blocks << build_text(
|
|
77
|
+
text_blocks << build_text(
|
|
78
|
+
bounds.x + STEREOTYPE_HEADER_X_OFFSET, y, line.strip, family, size, size_unit, fill,
|
|
79
|
+
logical_height: logical_height
|
|
80
|
+
)
|
|
70
81
|
else
|
|
71
|
-
text_blocks << build_text(
|
|
82
|
+
text_blocks << build_text(
|
|
83
|
+
bounds.x + visibility_x_offset, y, line.strip, family, size, size_unit, fill,
|
|
84
|
+
logical_height: logical_height
|
|
85
|
+
)
|
|
72
86
|
end
|
|
73
87
|
end
|
|
74
88
|
group_style = "stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; fill:#{fill};fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00"
|
|
@@ -149,9 +163,11 @@ module Ea
|
|
|
149
163
|
end
|
|
150
164
|
private_class_method :split_visibility
|
|
151
165
|
|
|
152
|
-
def self.build_text(x, y, content, family, size, size_unit = DEFAULT_FONT_UNIT,
|
|
166
|
+
def self.build_text(x, y, content, family, size, size_unit = DEFAULT_FONT_UNIT,
|
|
167
|
+
fill = "#000000", logical_height: nil)
|
|
153
168
|
TextRenderer.new(content: content, x: x, y: y,
|
|
154
|
-
|
|
169
|
+
family: family, size: size, size_unit: size_unit,
|
|
170
|
+
logical_height: logical_height, fill: fill).to_svg
|
|
155
171
|
end
|
|
156
172
|
private_class_method :build_text
|
|
157
173
|
|
|
@@ -40,10 +40,14 @@ module Ea
|
|
|
40
40
|
attr_line_offset: 6,
|
|
41
41
|
attr_first_offset: 7,
|
|
42
42
|
attr_group_lead: 0,
|
|
43
|
+
family: nil, size_unit: "pt", logical_height: nil,
|
|
43
44
|
header_ghost: false,
|
|
44
45
|
header_ghost_tight: false)
|
|
45
46
|
@bounds = bounds
|
|
46
47
|
@size = size
|
|
48
|
+
@family = family
|
|
49
|
+
@size_unit = size_unit
|
|
50
|
+
@logical_height = logical_height
|
|
47
51
|
@header_lines_count = header_lines_count
|
|
48
52
|
@attr_lines_count = attr_lines_count
|
|
49
53
|
@op_lines_count = op_lines_count
|
|
@@ -61,6 +65,13 @@ module Ea
|
|
|
61
65
|
@header_ghost_tight = header_ghost_tight
|
|
62
66
|
end
|
|
63
67
|
|
|
68
|
+
def line_height(offset)
|
|
69
|
+
Ea::Fonts::Metrics.compartment_line_height(
|
|
70
|
+
size, family: @family, unit: @size_unit, offset: offset, logical_height: @logical_height
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
private :line_height
|
|
74
|
+
|
|
64
75
|
def header_first_y
|
|
65
76
|
bounds.y + size + (header_top_padding || 12)
|
|
66
77
|
end
|
|
@@ -81,9 +92,9 @@ module Ea
|
|
|
81
92
|
ghost_shift = header_ghost ? (header_ghost_tight ? 3 : 6) : 0
|
|
82
93
|
|
|
83
94
|
header_first_y +
|
|
84
|
-
([header_lines_count, 1].max - 1) * (
|
|
95
|
+
([header_lines_count, 1].max - 1) * line_height(header_line_offset || 6) +
|
|
85
96
|
ghost_shift +
|
|
86
|
-
(marker_count.to_i * (
|
|
97
|
+
(marker_count.to_i * line_height(6)) +
|
|
87
98
|
(divider_offset || 8)
|
|
88
99
|
end
|
|
89
100
|
|
|
@@ -104,13 +115,13 @@ module Ea
|
|
|
104
115
|
def raw_attr_first_y
|
|
105
116
|
return bounds.y + size + (header_top_padding || 12) + 12 unless divider_y
|
|
106
117
|
|
|
107
|
-
divider_y +
|
|
118
|
+
divider_y + line_height((attr_first_offset || 7) - 1) + 1
|
|
108
119
|
end
|
|
109
120
|
|
|
110
121
|
def attr_bottom_y
|
|
111
122
|
return attr_first_y unless attr_lines_count&.positive?
|
|
112
123
|
|
|
113
|
-
attr_first_y + (attr_lines_count - 1) * (
|
|
124
|
+
attr_first_y + (attr_lines_count - 1) * line_height(attr_line_offset || 6) +
|
|
114
125
|
@attr_group_lead.to_i
|
|
115
126
|
end
|
|
116
127
|
|
|
@@ -137,17 +148,16 @@ module Ea
|
|
|
137
148
|
def op_bottom_y
|
|
138
149
|
return op_divider_y unless op_lines_count&.positive?
|
|
139
150
|
|
|
140
|
-
op_first_y + (op_lines_count - 1) * (
|
|
151
|
+
op_first_y + (op_lines_count - 1) * line_height(6)
|
|
141
152
|
end
|
|
142
153
|
|
|
143
154
|
# EA draws a single divider under the header and seats
|
|
144
|
-
# enumeration
|
|
145
|
-
#
|
|
146
|
-
# "literals" header. Verified against EA-published SVGs.
|
|
155
|
+
# the enumeration caption directly after it (+14 at 7pt),
|
|
156
|
+
# followed by the literal rows without a second divider.
|
|
147
157
|
def enum_literal_first_y
|
|
148
158
|
return nil unless divider_y
|
|
149
159
|
|
|
150
|
-
divider_y +
|
|
160
|
+
divider_y + line_height((attr_first_offset || 7) - 1) + 1
|
|
151
161
|
end
|
|
152
162
|
|
|
153
163
|
# OCL constraints follow the regular content with a divider and caption.
|
|
@@ -199,7 +209,7 @@ module Ea
|
|
|
199
209
|
def enum_literals_bottom_y
|
|
200
210
|
return nil unless enum_literal_first_y
|
|
201
211
|
|
|
202
|
-
enum_literal_first_y + ([enum_literals_count, 1].max - 1) * (
|
|
212
|
+
enum_literal_first_y + ([enum_literals_count, 1].max - 1) * line_height(6)
|
|
203
213
|
end
|
|
204
214
|
end
|
|
205
215
|
end
|
|
@@ -39,27 +39,31 @@ module Ea
|
|
|
39
39
|
def self.compartment_row(constraint, context, index)
|
|
40
40
|
build_text(context.bounds.x + 5,
|
|
41
41
|
context.geometry.constraints_first_y + (index + 1) * (context.size + 5),
|
|
42
|
-
"{#{constraint.name}}", context.family, context.size, context.text_fill
|
|
42
|
+
"{#{constraint.name}}", context.family, context.size, context.text_fill,
|
|
43
|
+
size_unit: context.size_unit, logical_height: context.logical_height)
|
|
43
44
|
end
|
|
44
45
|
private_class_method :compartment_row
|
|
45
46
|
|
|
46
47
|
def self.compartment_caption(context)
|
|
47
48
|
TextRenderer.new(content: "constraints", x: caption_x(context),
|
|
48
49
|
y: context.geometry.constraints_first_y, family: context.family,
|
|
49
|
-
size: context.size,
|
|
50
|
+
size: context.size, size_unit: context.size_unit, logical_height: context.logical_height,
|
|
51
|
+
fill: context.text_fill, style: "italic").to_svg
|
|
50
52
|
end
|
|
51
53
|
private_class_method :compartment_caption
|
|
52
54
|
|
|
53
55
|
def self.caption_x(context)
|
|
54
56
|
width = TextRenderer.estimate_width("constraints", context.size, nil,
|
|
55
|
-
family: context.family, style: "italic"
|
|
57
|
+
family: context.family, style: "italic",
|
|
58
|
+
logical_height: context.logical_height).round
|
|
56
59
|
context.bounds.x + ((context.bounds.width - width) / 2).floor
|
|
57
60
|
end
|
|
58
61
|
private_class_method :caption_x
|
|
59
62
|
|
|
60
|
-
def self.build_text(x, y, content, family, size, fill)
|
|
63
|
+
def self.build_text(x, y, content, family, size, fill, size_unit: "px", logical_height: nil)
|
|
61
64
|
TextRenderer.new(content: content, x: x, y: y,
|
|
62
|
-
|
|
65
|
+
family: family, size: size, size_unit: size_unit,
|
|
66
|
+
logical_height: logical_height, fill: fill).to_svg
|
|
63
67
|
end
|
|
64
68
|
private_class_method :build_text
|
|
65
69
|
end
|
|
@@ -16,19 +16,22 @@ module Ea
|
|
|
16
16
|
# EA lists literals alphabetically (case-insensitive) and
|
|
17
17
|
# spaces rows at the same 13px pitch as attributes at 7pt
|
|
18
18
|
# (size + 6) — corpus-verified against EA-published SVGs.
|
|
19
|
-
def self.render(literals, bounds:, first_y:, family:, size:)
|
|
20
|
-
line_h =
|
|
19
|
+
def self.render(literals, bounds:, first_y:, family:, size:, size_unit: "px", logical_height: nil)
|
|
20
|
+
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
21
|
+
size, family: family, unit: size_unit, logical_height: logical_height
|
|
22
|
+
)
|
|
21
23
|
sorted = literals.sort_by { |l| [l.name.to_s.downcase, l.name.to_s] }
|
|
22
24
|
text_blocks = sorted.each_with_index.map do |literal, idx|
|
|
23
25
|
y = first_y + (idx * line_h)
|
|
24
|
-
build_text(bounds.x + 5, y, literal.name.to_s, family, size)
|
|
26
|
+
build_text(bounds.x + 5, y, literal.name.to_s, family, size, size_unit, logical_height: logical_height)
|
|
25
27
|
end
|
|
26
28
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def self.build_text(x, y, content, family, size)
|
|
31
|
+
def self.build_text(x, y, content, family, size, size_unit, logical_height: nil)
|
|
30
32
|
TextRenderer.new(content: content, x: x, y: y,
|
|
31
|
-
|
|
33
|
+
family: family, size: size, size_unit: size_unit,
|
|
34
|
+
logical_height: logical_height).to_svg
|
|
32
35
|
end
|
|
33
36
|
private_class_method :build_text
|
|
34
37
|
end
|
|
@@ -19,7 +19,7 @@ module Ea
|
|
|
19
19
|
module HeaderLinePipeline
|
|
20
20
|
Context = Struct.new(:classifier, :diagram_package_id,
|
|
21
21
|
:visually_nested, :umldi_keyword,
|
|
22
|
-
:bounds_width, :font_size, :family,
|
|
22
|
+
:bounds_width, :font_size, :family, :logical_height,
|
|
23
23
|
:off_canvas_parent_name,
|
|
24
24
|
:foreign_package_name,
|
|
25
25
|
:suppress_stereotypes,
|
|
@@ -57,7 +57,8 @@ module Ea
|
|
|
57
57
|
width = lambda do |s|
|
|
58
58
|
TextRenderer.estimate_width(
|
|
59
59
|
s, context.font_size || 9, nil,
|
|
60
|
-
family: context.family, weight: "700", style: font_style
|
|
60
|
+
family: context.family, weight: "700", style: font_style,
|
|
61
|
+
logical_height: context.logical_height
|
|
61
62
|
).round
|
|
62
63
|
end
|
|
63
64
|
return [text] if context.bounds_width.nil?
|
|
@@ -20,15 +20,20 @@ module Ea
|
|
|
20
20
|
return [] unless inst.is_a?(Ea::Model::InstanceSpecification)
|
|
21
21
|
|
|
22
22
|
lines = stereotype_lines(inst, context)
|
|
23
|
-
|
|
24
|
-
if inst.classifier_name && !inst.classifier_name.empty?
|
|
25
|
-
label = "#{label}: #{inst.classifier_name}"
|
|
26
|
-
end
|
|
27
|
-
lines + HeaderLinePipeline.wrap_words(label, context, :bold).map do |line|
|
|
23
|
+
lines + HeaderLinePipeline.wrap_words(label_for(inst), context, :bold).map do |line|
|
|
28
24
|
[line, :bold]
|
|
29
25
|
end
|
|
30
26
|
end
|
|
31
27
|
|
|
28
|
+
# "Name / alias: Classifier"; t_object.Alias is the instance role.
|
|
29
|
+
def self.label_for(inst)
|
|
30
|
+
label = inst.name.to_s
|
|
31
|
+
label = "#{label} / #{inst.role_name}" unless inst.role_name.to_s.empty?
|
|
32
|
+
return label if inst.classifier_name.to_s.empty?
|
|
33
|
+
|
|
34
|
+
"#{label}: #{inst.classifier_name}"
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def self.stereotype_lines(inst, context)
|
|
33
38
|
return [] if context.suppress_stereotypes
|
|
34
39
|
|
|
@@ -11,12 +11,9 @@ module Ea
|
|
|
11
11
|
# - Foreign elements (owning package ≠ the diagram's package,
|
|
12
12
|
# gated on t_diagram.ShowForeign) render the qualified
|
|
13
13
|
# name "OwningPackage::Name".
|
|
14
|
-
# - Qualified
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# each centered independently. Corpus-verified: every
|
|
18
|
-
# wrapped header overflows its box and every single-line
|
|
19
|
-
# header fits (tightest observed slack 6px).
|
|
14
|
+
# - Qualified names wrap after their colon separator when
|
|
15
|
+
# the measured name reaches the box width minus 6px.
|
|
16
|
+
# Each resulting line retains its separator and is centered.
|
|
20
17
|
#
|
|
21
18
|
# InstanceSpecification returns [] (InstanceSpec provider
|
|
22
19
|
# already emitted the full label).
|
|
@@ -42,9 +39,8 @@ module Ea
|
|
|
42
39
|
return [[name, weight]]
|
|
43
40
|
end
|
|
44
41
|
|
|
45
|
-
if name.
|
|
46
|
-
|
|
47
|
-
[["#{qualifier}::", weight], [base, weight]]
|
|
42
|
+
if (qualified = name.match(/\A(.+?:{1,2})([^:\s].*)\z/))
|
|
43
|
+
[[qualified[1], weight], [qualified[2], weight]]
|
|
48
44
|
else
|
|
49
45
|
HeaderLinePipeline.wrap_words(name, context, weight).map do |line|
|
|
50
46
|
[line, weight]
|
|
@@ -56,13 +52,13 @@ module Ea
|
|
|
56
52
|
font_style = weight == :bold_italic ? "italic" : "normal"
|
|
57
53
|
len = TextRenderer.estimate_width(
|
|
58
54
|
name, context.font_size || 9, nil,
|
|
59
|
-
family: context.family, weight: "700", style: font_style
|
|
55
|
+
family: context.family, weight: "700", style: font_style,
|
|
56
|
+
logical_height: context.logical_height
|
|
60
57
|
).round
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
len > context.bounds_width.to_i - 6
|
|
58
|
+
# Qualified classifier names also wrap at the exact boundary;
|
|
59
|
+
# ordinary word wrapping keeps the shared pipeline threshold.
|
|
60
|
+
usable = context.bounds_width.to_i - 6
|
|
61
|
+
name.match?(/:{1,2}[^:\s]/) ? len >= usable : len > usable
|
|
66
62
|
end
|
|
67
63
|
|
|
68
64
|
# EA renders the plain classifier name in element headers;
|
|
@@ -12,17 +12,20 @@ module Ea
|
|
|
12
12
|
# x= is the start position, not the center).
|
|
13
13
|
class HeaderRenderer
|
|
14
14
|
def self.render(lines, bounds:, first_y:, family:,
|
|
15
|
-
size:, size_unit: "pt",
|
|
15
|
+
size:, size_unit: "pt", logical_height: nil,
|
|
16
16
|
fill: "#000000", weight_normal: 400, weight_bold: 700,
|
|
17
17
|
stroke_in_text: "#000000", width_factor: 0.612,
|
|
18
18
|
line_offset: 6)
|
|
19
|
-
|
|
19
|
+
logical_height = nil unless size_unit == "pt"
|
|
20
|
+
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
21
|
+
size, family: family, unit: size_unit, offset: line_offset, logical_height: logical_height
|
|
22
|
+
)
|
|
20
23
|
text_blocks = lines.each_with_index.map do |(text, style), idx|
|
|
21
24
|
weight = case style
|
|
22
25
|
when :bold, :bold_italic then weight_bold
|
|
23
26
|
else weight_normal
|
|
24
27
|
end
|
|
25
|
-
font_style =
|
|
28
|
+
font_style = %i[italic italic_tight bold_italic].include?(style) ? "italic" : "normal"
|
|
26
29
|
# EA spaces the line AFTER a right-aligned ghost 3px
|
|
27
30
|
# wider than the pitch. Interfaces use a tighter +3
|
|
28
31
|
# (936AA434/Surface: ghost 100, «interface» 116).
|
|
@@ -33,23 +36,22 @@ module Ea
|
|
|
33
36
|
end
|
|
34
37
|
y = first_y + (idx * line_h) +
|
|
35
38
|
(idx >= 1 && ghost_shift.positive? ? ghost_shift : 0)
|
|
36
|
-
x = if style
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
bounds.x + bounds.width -
|
|
39
|
+
x = if %i[italic italic_tight].include?(style)
|
|
40
|
+
# Parent ghosts end 3px inside the box's right edge.
|
|
41
|
+
bounds.x + bounds.width - 3 -
|
|
40
42
|
Ea::Svg::EaEmitter::TextRenderer.estimate_width(
|
|
41
43
|
text, size, width_factor,
|
|
42
|
-
family: family, weight: weight, style: font_style
|
|
44
|
+
family: family, weight: weight, style: font_style, logical_height: logical_height
|
|
43
45
|
).round
|
|
44
46
|
else
|
|
45
47
|
center_x_for(text, bounds, size, width_factor,
|
|
46
48
|
family: family, weight: weight,
|
|
47
|
-
style: font_style)
|
|
49
|
+
style: font_style, logical_height: logical_height)
|
|
48
50
|
end
|
|
49
51
|
TextRenderer.new(
|
|
50
52
|
content: text,
|
|
51
53
|
x: x, y: y,
|
|
52
|
-
family: family, size: size, size_unit: size_unit,
|
|
54
|
+
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
53
55
|
weight: weight, style: font_style, fill: fill,
|
|
54
56
|
stroke_in_text: stroke_in_text, width_factor: width_factor
|
|
55
57
|
).to_svg
|
|
@@ -60,9 +62,9 @@ module Ea
|
|
|
60
62
|
# EA centers text by its INTEGER textLength and floors the
|
|
61
63
|
# resulting x (verified exact against EA-published SVGs).
|
|
62
64
|
def self.center_x_for(text, bounds, size, width_factor,
|
|
63
|
-
family: nil, weight: nil, style: nil)
|
|
65
|
+
family: nil, weight: nil, style: nil, logical_height: nil)
|
|
64
66
|
len = Ea::Svg::EaEmitter::TextRenderer.estimate_width(
|
|
65
|
-
text, size, width_factor, family: family, weight: weight, style: style
|
|
67
|
+
text, size, width_factor, family: family, weight: weight, style: style, logical_height: logical_height
|
|
66
68
|
).round
|
|
67
69
|
(bounds.x + (bounds.width - len) / 2.0).floor
|
|
68
70
|
end
|
|
@@ -16,10 +16,13 @@ module Ea
|
|
|
16
16
|
VISIBILITY_X_OFFSET = 5
|
|
17
17
|
CONTENT_X_OFFSET = 22
|
|
18
18
|
|
|
19
|
-
def self.render(operations, bounds:, first_y:, family:, size:
|
|
19
|
+
def self.render(operations, bounds:, first_y:, family:, size:, size_unit: "px",
|
|
20
|
+
logical_height: nil, parameter_detail: nil)
|
|
20
21
|
# EA's op rows pitch 13 (size + 6), same as attribute
|
|
21
22
|
# rows (6D4C665D: ops 541..658 step 13).
|
|
22
|
-
line_h =
|
|
23
|
+
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
24
|
+
size, family: family, unit: size_unit, logical_height: logical_height
|
|
25
|
+
)
|
|
23
26
|
text_blocks = []
|
|
24
27
|
has_receptions = operations.any?(&:is_reception)
|
|
25
28
|
if has_receptions
|
|
@@ -27,7 +30,7 @@ module Ea
|
|
|
27
30
|
content: "receptions",
|
|
28
31
|
x: bounds.x + (bounds.width / 2.0) - (8 * size * 0.65) / 2,
|
|
29
32
|
y: first_y,
|
|
30
|
-
family: family, size: size,
|
|
33
|
+
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
31
34
|
style: "italic", fill: "#000000"
|
|
32
35
|
).to_svg
|
|
33
36
|
end
|
|
@@ -36,15 +39,16 @@ module Ea
|
|
|
36
39
|
ops.each_with_index do |op, idx|
|
|
37
40
|
y = first_y + ((idx + offset) * line_h)
|
|
38
41
|
text_blocks << build_text(bounds.x + VISIBILITY_X_OFFSET, y,
|
|
39
|
-
|
|
42
|
+
visibility_prefix(op), family, size, size_unit, logical_height: logical_height)
|
|
43
|
+
text = operation_text(op, parameter_detail: parameter_detail)
|
|
40
44
|
text_blocks << build_text(bounds.x + AttributeRenderer.content_x_offset(size), y,
|
|
41
|
-
|
|
45
|
+
text, family, size, size_unit, logical_height: logical_height)
|
|
42
46
|
end
|
|
43
47
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# Builds display lines for a classifier's operations.
|
|
47
|
-
def self.lines_for(classifier)
|
|
51
|
+
def self.lines_for(classifier, parameter_detail: nil)
|
|
48
52
|
ops = classifier.operations
|
|
49
53
|
return [] unless ops&.any?
|
|
50
54
|
|
|
@@ -52,7 +56,7 @@ module Ea
|
|
|
52
56
|
visibility = visibility_prefix(op).strip
|
|
53
57
|
params = (op.parameters || []).map do |p|
|
|
54
58
|
type = namespace_double_colon(p.type_name)
|
|
55
|
-
p
|
|
59
|
+
parameter_text(p, type, parameter_detail)
|
|
56
60
|
end.join(", ")
|
|
57
61
|
return_type = namespace_double_colon(op.return_type_name)
|
|
58
62
|
"#{visibility} #{op.name}(#{params}): #{return_type}".strip
|
|
@@ -64,9 +68,9 @@ module Ea
|
|
|
64
68
|
end
|
|
65
69
|
private_class_method :visibility_prefix
|
|
66
70
|
|
|
67
|
-
def self.operation_text(operation)
|
|
71
|
+
def self.operation_text(operation, parameter_detail: nil)
|
|
68
72
|
prefix = operation.is_reception ? "«signal» " : ""
|
|
69
|
-
text = "#{prefix}#{operation.name}(#{reception_params(operation)})"
|
|
73
|
+
text = "#{prefix}#{operation.name}(#{reception_params(operation, parameter_detail)})"
|
|
70
74
|
return text unless operation.return_type_name && !operation.is_reception
|
|
71
75
|
|
|
72
76
|
"#{text}: #{operation.return_type_name}"
|
|
@@ -80,19 +84,28 @@ module Ea
|
|
|
80
84
|
end
|
|
81
85
|
private_class_method :namespace_double_colon
|
|
82
86
|
|
|
83
|
-
def self.
|
|
87
|
+
def self.parameter_text(param, type, parameter_detail)
|
|
88
|
+
return "#{type}*" if param.direction == "inout"
|
|
89
|
+
return type if parameter_detail == :type_only
|
|
90
|
+
|
|
91
|
+
"#{param.name}: #{type}"
|
|
92
|
+
end
|
|
93
|
+
private_class_method :parameter_text
|
|
94
|
+
|
|
95
|
+
def self.reception_params(operation, parameter_detail)
|
|
84
96
|
params = (operation.parameters || []).filter_map { |p|
|
|
85
97
|
next nil if p.type_name.to_s.empty?
|
|
86
98
|
|
|
87
|
-
p
|
|
99
|
+
parameter_text(p, p.type_name, parameter_detail)
|
|
88
100
|
}.join(", ")
|
|
89
101
|
params
|
|
90
102
|
end
|
|
91
103
|
private_class_method :reception_params
|
|
92
104
|
|
|
93
|
-
def self.build_text(x, y, content, family, size)
|
|
105
|
+
def self.build_text(x, y, content, family, size, size_unit, logical_height: nil)
|
|
94
106
|
TextRenderer.new(content: content, x: x, y: y,
|
|
95
|
-
|
|
107
|
+
family: family, size: size, size_unit: size_unit,
|
|
108
|
+
logical_height: logical_height).to_svg
|
|
96
109
|
end
|
|
97
110
|
private_class_method :build_text
|
|
98
111
|
end
|