ea 0.6.106 → 0.6.108
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/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 +9 -30
- data/lib/ea/sources/qea/instance_builder.rb +1 -0
- data/lib/ea/sources/qea/property_builder.rb +11 -1
- 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/constraints.rb +5 -14
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +11 -5
- 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 +2 -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 +42 -17
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +46 -15
- data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +34 -2
- 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 +2 -1
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +14 -12
- data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +10 -7
- data/lib/ea/svg/ea_emitter/elements.rb +41 -33
- 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/marker/diamond.rb +17 -23
- 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
- data/memory/deviations.md +3 -0
- metadata +5 -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"
|
|
@@ -87,7 +101,7 @@ module Ea
|
|
|
87
101
|
# Pass a `lookup` (any callable returning a Classifier for
|
|
88
102
|
# an id) to enable inherited-property namespace prefixing.
|
|
89
103
|
def self.lines_for(classifier, lookup: nil,
|
|
90
|
-
|
|
104
|
+
exclude_association_ids: nil, stored_order: false)
|
|
91
105
|
props = displayable_properties(classifier, exclude_association_ids)
|
|
92
106
|
return [] unless props
|
|
93
107
|
|
|
@@ -95,19 +109,14 @@ module Ea
|
|
|
95
109
|
if stereotypes.size == 1 && props.all? { |p| property_stereotype(p) }
|
|
96
110
|
# EA renders ONE compartment stereotype header for a
|
|
97
111
|
# uniformly-stereotyped attribute set (e.g. «Property»
|
|
98
|
-
# above GML property rows)
|
|
99
|
-
# alphabetically - verified against EA-published SVGs.
|
|
112
|
+
# above GML property rows).
|
|
100
113
|
header = "«#{canonical_stereotype(stereotypes.first)}»"
|
|
101
|
-
[header] + props
|
|
114
|
+
[header] + ordered_properties(props, stored_order, case_sensitive: true).map do |prop|
|
|
102
115
|
AttributeLineBuilder.new(prop, host: classifier,
|
|
103
116
|
lookup: lookup).to_s
|
|
104
117
|
end
|
|
105
118
|
else
|
|
106
|
-
|
|
107
|
-
# insensitive) — corpus-verified: 991 boxes strictly
|
|
108
|
-
# alphabetical vs 24 strictly stored-Pos order.
|
|
109
|
-
props.sort_by { |p| [p.name.to_s.downcase, p.name.to_s] }
|
|
110
|
-
.flat_map do |prop|
|
|
119
|
+
ordered_properties(props, stored_order).flat_map do |prop|
|
|
111
120
|
lines = []
|
|
112
121
|
stereotype = property_stereotype(prop)
|
|
113
122
|
lines << "«#{stereotype}»" if stereotype
|
|
@@ -117,6 +126,20 @@ module Ea
|
|
|
117
126
|
end
|
|
118
127
|
end
|
|
119
128
|
|
|
129
|
+
# QEA properties arrive in stored Pos order. Association ends retain
|
|
130
|
+
# their alphabetical fallback rather than using connector Pos values.
|
|
131
|
+
def self.ordered_properties(props, stored_order, case_sensitive: false)
|
|
132
|
+
alphabetical = lambda do |prop|
|
|
133
|
+
name = prop.name.to_s
|
|
134
|
+
case_sensitive ? [name] : [name.downcase, name]
|
|
135
|
+
end
|
|
136
|
+
return props.sort_by(&alphabetical) unless stored_order
|
|
137
|
+
|
|
138
|
+
own, ends = props.partition { |prop| !prop.association_id }
|
|
139
|
+
own + ends.sort_by(&alphabetical)
|
|
140
|
+
end
|
|
141
|
+
private_class_method :ordered_properties
|
|
142
|
+
|
|
120
143
|
STEREOTYPE_DISPLAY = {
|
|
121
144
|
"property" => "Property",
|
|
122
145
|
"featuretype" => "FeatureType",
|
|
@@ -140,9 +163,11 @@ module Ea
|
|
|
140
163
|
end
|
|
141
164
|
private_class_method :split_visibility
|
|
142
165
|
|
|
143
|
-
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)
|
|
144
168
|
TextRenderer.new(content: content, x: x, y: y,
|
|
145
|
-
|
|
169
|
+
family: family, size: size, size_unit: size_unit,
|
|
170
|
+
logical_height: logical_height, fill: fill).to_svg
|
|
146
171
|
end
|
|
147
172
|
private_class_method :build_text
|
|
148
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,7 +148,7 @@ 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
|
|
@@ -147,17 +158,33 @@ module Ea
|
|
|
147
158
|
def enum_literal_first_y
|
|
148
159
|
return nil unless divider_y
|
|
149
160
|
|
|
150
|
-
divider_y +
|
|
161
|
+
divider_y + line_height((attr_first_offset || 7) - 1) + 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# OCL constraints follow the regular content with a divider and caption.
|
|
165
|
+
# Suppress the whole compartment when its last row cannot fit.
|
|
166
|
+
def constraints_first_y
|
|
167
|
+
return nil unless constraints_count.to_i.positive?
|
|
168
|
+
|
|
169
|
+
first_y = constraints_caption_y
|
|
170
|
+
first_y if first_y + constraints_count * (size + 5) + 4 <= bounds.y + bounds.height
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def constraints_caption_y
|
|
174
|
+
(regular_content_bottom_y || raw_attr_first_y) + size + 13
|
|
151
175
|
end
|
|
152
176
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
def constraints_divider_y
|
|
178
|
+
constraints_first_y - size - 7 if constraints_first_y
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def constraints_bottom_y
|
|
182
|
+
constraints_first_y + constraints_count * (size + 5) if constraints_first_y
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Tagged values follow the last visible content compartment.
|
|
158
186
|
def tagged_value_first_y
|
|
159
|
-
return nil unless tagged_values_count.to_i.positive?
|
|
160
|
-
constraints_count.to_i.positive?
|
|
187
|
+
return nil unless tagged_values_count.to_i.positive?
|
|
161
188
|
|
|
162
189
|
(content_bottom_y || raw_attr_first_y) + size + 5
|
|
163
190
|
end
|
|
@@ -169,6 +196,10 @@ module Ea
|
|
|
169
196
|
# the autosize gate must not treat the empty-compartment
|
|
170
197
|
# anchor as content.
|
|
171
198
|
def content_bottom_y
|
|
199
|
+
[regular_content_bottom_y, constraints_bottom_y].compact.max
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def regular_content_bottom_y
|
|
172
203
|
bottoms = []
|
|
173
204
|
bottoms << attr_bottom_y if attr_lines_count.to_i.positive?
|
|
174
205
|
bottoms << enum_literals_bottom_y if enum_literals_count.to_i.positive?
|
|
@@ -179,10 +210,10 @@ module Ea
|
|
|
179
210
|
def enum_literals_bottom_y
|
|
180
211
|
return nil unless enum_literal_first_y
|
|
181
212
|
|
|
182
|
-
enum_literal_first_y + ([enum_literals_count, 1].max - 1) * (
|
|
213
|
+
enum_literal_first_y + ([enum_literals_count, 1].max - 1) * line_height(6)
|
|
183
214
|
end
|
|
184
215
|
end
|
|
185
216
|
end
|
|
186
217
|
end
|
|
187
218
|
end
|
|
188
|
-
end
|
|
219
|
+
end
|
|
@@ -29,9 +29,41 @@ module Ea
|
|
|
29
29
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def self.
|
|
32
|
+
def self.render_compartment(context)
|
|
33
|
+
rows = context.constraints.each_with_index.map do |constraint, idx|
|
|
34
|
+
compartment_row(constraint, context, idx)
|
|
35
|
+
end
|
|
36
|
+
%(<g style="#{Style::TEXT_GROUP}">\n#{([compartment_caption(context)] + rows).join("\n")}\n</g>)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.compartment_row(constraint, context, index)
|
|
40
|
+
build_text(context.bounds.x + 5,
|
|
41
|
+
context.geometry.constraints_first_y + (index + 1) * (context.size + 5),
|
|
42
|
+
"{#{constraint.name}}", context.family, context.size, context.text_fill,
|
|
43
|
+
size_unit: context.size_unit, logical_height: context.logical_height)
|
|
44
|
+
end
|
|
45
|
+
private_class_method :compartment_row
|
|
46
|
+
|
|
47
|
+
def self.compartment_caption(context)
|
|
48
|
+
TextRenderer.new(content: "constraints", x: caption_x(context),
|
|
49
|
+
y: context.geometry.constraints_first_y, family: context.family,
|
|
50
|
+
size: context.size, size_unit: context.size_unit, logical_height: context.logical_height,
|
|
51
|
+
fill: context.text_fill, style: "italic").to_svg
|
|
52
|
+
end
|
|
53
|
+
private_class_method :compartment_caption
|
|
54
|
+
|
|
55
|
+
def self.caption_x(context)
|
|
56
|
+
width = TextRenderer.estimate_width("constraints", context.size, nil,
|
|
57
|
+
family: context.family, style: "italic",
|
|
58
|
+
logical_height: context.logical_height).round
|
|
59
|
+
context.bounds.x + ((context.bounds.width - width) / 2).floor
|
|
60
|
+
end
|
|
61
|
+
private_class_method :caption_x
|
|
62
|
+
|
|
63
|
+
def self.build_text(x, y, content, family, size, fill, size_unit: "px", logical_height: nil)
|
|
33
64
|
TextRenderer.new(content: content, x: x, y: y,
|
|
34
|
-
|
|
65
|
+
family: family, size: size, size_unit: size_unit,
|
|
66
|
+
logical_height: logical_height, fill: fill).to_svg
|
|
35
67
|
end
|
|
36
68
|
private_class_method :build_text
|
|
37
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
|
|
|
@@ -56,7 +56,8 @@ module Ea
|
|
|
56
56
|
font_style = weight == :bold_italic ? "italic" : "normal"
|
|
57
57
|
len = TextRenderer.estimate_width(
|
|
58
58
|
name, context.font_size || 9, nil,
|
|
59
|
-
family: context.family, weight: "700", style: font_style
|
|
59
|
+
family: context.family, weight: "700", style: font_style,
|
|
60
|
+
logical_height: context.logical_height
|
|
60
61
|
).round
|
|
61
62
|
# Same usable width as wrap_words: box width - 6.
|
|
62
63
|
# EA wraps "Content information::MD_FeatureCatalogue-
|
|
@@ -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,12 @@ 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", logical_height: nil)
|
|
20
20
|
# EA's op rows pitch 13 (size + 6), same as attribute
|
|
21
21
|
# rows (6D4C665D: ops 541..658 step 13).
|
|
22
|
-
line_h =
|
|
22
|
+
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
23
|
+
size, family: family, unit: size_unit, logical_height: logical_height
|
|
24
|
+
)
|
|
23
25
|
text_blocks = []
|
|
24
26
|
has_receptions = operations.any?(&:is_reception)
|
|
25
27
|
if has_receptions
|
|
@@ -27,7 +29,7 @@ module Ea
|
|
|
27
29
|
content: "receptions",
|
|
28
30
|
x: bounds.x + (bounds.width / 2.0) - (8 * size * 0.65) / 2,
|
|
29
31
|
y: first_y,
|
|
30
|
-
family: family, size: size,
|
|
32
|
+
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
31
33
|
style: "italic", fill: "#000000"
|
|
32
34
|
).to_svg
|
|
33
35
|
end
|
|
@@ -36,9 +38,9 @@ module Ea
|
|
|
36
38
|
ops.each_with_index do |op, idx|
|
|
37
39
|
y = first_y + ((idx + offset) * line_h)
|
|
38
40
|
text_blocks << build_text(bounds.x + VISIBILITY_X_OFFSET, y,
|
|
39
|
-
|
|
41
|
+
visibility_prefix(op), family, size, size_unit, logical_height: logical_height)
|
|
40
42
|
text_blocks << build_text(bounds.x + AttributeRenderer.content_x_offset(size), y,
|
|
41
|
-
|
|
43
|
+
operation_text(op), family, size, size_unit, logical_height: logical_height)
|
|
42
44
|
end
|
|
43
45
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
44
46
|
end
|
|
@@ -90,9 +92,10 @@ module Ea
|
|
|
90
92
|
end
|
|
91
93
|
private_class_method :reception_params
|
|
92
94
|
|
|
93
|
-
def self.build_text(x, y, content, family, size)
|
|
95
|
+
def self.build_text(x, y, content, family, size, size_unit, logical_height: nil)
|
|
94
96
|
TextRenderer.new(content: content, x: x, y: y,
|
|
95
|
-
|
|
97
|
+
family: family, size: size, size_unit: size_unit,
|
|
98
|
+
logical_height: logical_height).to_svg
|
|
96
99
|
end
|
|
97
100
|
private_class_method :build_text
|
|
98
101
|
end
|