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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/config/themes/119.yml +3 -2
  3. data/lib/ea/diagram/display_config.rb +5 -0
  4. data/lib/ea/fonts/cambria_7_styled_widths.json +293 -0
  5. data/lib/ea/fonts/metrics.rb +50 -7
  6. data/lib/ea/model/diagram_element.rb +4 -0
  7. data/lib/ea/sources/qea/diagram_builder/auto_routing.rb +124 -11
  8. data/lib/ea/sources/qea/diagram_builder/font_settings.rb +50 -0
  9. data/lib/ea/sources/qea/diagram_builder/routing.rb +82 -82
  10. data/lib/ea/sources/qea/diagram_builder.rb +8 -30
  11. data/lib/ea/sources/qea/instance_builder.rb +1 -0
  12. data/lib/ea/svg/ea_emitter/color_resolver.rb +1 -2
  13. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +2 -1
  14. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +19 -42
  15. data/lib/ea/svg/ea_emitter/compartment/header.rb +2 -1
  16. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +6 -4
  17. data/lib/ea/svg/ea_emitter/compartment/note_html.rb +66 -0
  18. data/lib/ea/svg/ea_emitter/compartment/operations.rb +3 -1
  19. data/lib/ea/svg/ea_emitter/compartment.rb +1 -0
  20. data/lib/ea/svg/ea_emitter/connector_redock.rb +30 -14
  21. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +11 -9
  22. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +24 -8
  23. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +20 -10
  24. data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +9 -5
  25. data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +8 -5
  26. data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +3 -2
  27. data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +10 -5
  28. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +11 -15
  29. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +14 -12
  30. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +26 -13
  31. data/lib/ea/svg/ea_emitter/elements.rb +45 -37
  32. data/lib/ea/svg/ea_emitter/font_resolver.rb +14 -4
  33. data/lib/ea/svg/ea_emitter/ghost_labels.rb +3 -0
  34. data/lib/ea/svg/ea_emitter/label/end_label.rb +37 -4
  35. data/lib/ea/svg/ea_emitter/labels.rb +12 -4
  36. data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
  37. data/lib/ea/svg/ea_emitter/text_renderer.rb +6 -5
  38. data/lib/ea/theme/definition.rb +5 -2
  39. data/lib/ea/theme/loader.rb +3 -2
  40. data/lib/ea/version.rb +1 -1
  41. 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
- # EA docks these markers on the outer edge of the 2px box stroke.
57
- outline = bounds.map do |box|
58
- Ea::Model::Bounds.new(x: box.x - 1, y: box.y - 1,
59
- width: box.width + 2, height: box.height + 2)
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| border_two?(diagram, 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 a border width of 2.
240
- def border_two?(diagram, ref, duid)
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
- placed = named unless named.empty?
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 :border_two?
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
- # A stored Association ray whose source port certifies the shared
401
- # anchor row is one explicit horizontal line: EA seats neither end
402
- # in a compartment row, unlike Generalization/Aggregation targets.
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 && pairs.first[1] == pairs.last[1]
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 = size + ROW_LINE_OFFSET
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(bounds.x + visibility_x_offset, y, visibility, family, size, size_unit, fill)
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
- text_blocks << build_text(bounds.x + offset, y, rest, family, size, size_unit, fill)
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(bounds.x + STEREOTYPE_HEADER_X_OFFSET, y, line.strip, family, size, size_unit, fill)
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(bounds.x + visibility_x_offset, y, line.strip, family, size, size_unit, fill)
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, fill = "#000000")
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
- family: family, size: size, size_unit: size_unit, fill: fill).to_svg
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) * (size + (header_line_offset || 6)) +
95
+ ([header_lines_count, 1].max - 1) * line_height(header_line_offset || 6) +
85
96
  ghost_shift +
86
- (marker_count.to_i * (size + 6)) +
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 + size + (attr_first_offset || 7)
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) * (size + (attr_line_offset || 6)) +
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) * (size + 6)
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 literals directly after it (+14 at 7pt),
145
- # exactly like attribute rows - no second divider and no
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 + size + (attr_first_offset || 7)
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) * (size + 6)
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, fill: context.text_fill, style: "italic").to_svg
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").round
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
- family: family, size: size, fill: fill).to_svg
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 = size + 6
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
- family: family, size: size).to_svg
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
- label = inst.name.to_s
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-name wrap: when the combined integer
15
- # textLength exceeds the element bounds width, EA splits
16
- # "pkg::Class" across two lines ("pkg::" then "Class"),
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.include?("::")
46
- qualifier, base = name.split("::", 2)
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
- # Same usable width as wrap_words: box width - 6.
62
- # EA wraps "Content information::MD_FeatureCatalogue-
63
- # Description" (parts 86 + 137 + space 4 = 227) inside
64
- # a 230px box (F851A657).
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
- line_h = size + line_offset
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 = (style == :italic || style == :bold_italic) ? "italic" : "normal"
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 == :italic
37
- # EA right-aligns the off-canvas parent ghost at
38
- # the box's right edge by its integer textLength.
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 = size + 6
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
- visibility_prefix(op), family, size)
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
- operation_text(op), family, size)
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.direction == "inout" ? "#{type}*" : "#{p.name}: #{type}"
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.reception_params(operation)
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.direction == "inout" ? "#{p.type_name}*" : "#{p.name}: #{p.type_name}"
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
- family: family, size: size).to_svg
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