ea 0.6.107 → 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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/config/themes/119.yml +3 -2
  3. data/lib/ea/fonts/cambria_7_styled_widths.json +293 -0
  4. data/lib/ea/fonts/metrics.rb +50 -7
  5. data/lib/ea/model/diagram_element.rb +4 -0
  6. data/lib/ea/sources/qea/diagram_builder/auto_routing.rb +124 -11
  7. data/lib/ea/sources/qea/diagram_builder/font_settings.rb +50 -0
  8. data/lib/ea/sources/qea/diagram_builder/routing.rb +82 -82
  9. data/lib/ea/sources/qea/diagram_builder.rb +8 -30
  10. data/lib/ea/sources/qea/instance_builder.rb +1 -0
  11. data/lib/ea/svg/ea_emitter/color_resolver.rb +1 -2
  12. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +2 -1
  13. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +11 -5
  14. data/lib/ea/svg/ea_emitter/compartment/header.rb +2 -1
  15. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +6 -4
  16. data/lib/ea/svg/ea_emitter/compartment/note_html.rb +66 -0
  17. data/lib/ea/svg/ea_emitter/compartment/operations.rb +2 -1
  18. data/lib/ea/svg/ea_emitter/compartment.rb +1 -0
  19. data/lib/ea/svg/ea_emitter/connector_redock.rb +30 -14
  20. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +11 -9
  21. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +24 -8
  22. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +18 -7
  23. data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +9 -5
  24. data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +8 -5
  25. data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +3 -2
  26. data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +10 -5
  27. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +2 -1
  28. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +14 -12
  29. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +10 -7
  30. data/lib/ea/svg/ea_emitter/elements.rb +39 -32
  31. data/lib/ea/svg/ea_emitter/font_resolver.rb +14 -4
  32. data/lib/ea/svg/ea_emitter/ghost_labels.rb +3 -0
  33. data/lib/ea/svg/ea_emitter/label/end_label.rb +37 -4
  34. data/lib/ea/svg/ea_emitter/labels.rb +12 -4
  35. data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
  36. data/lib/ea/svg/ea_emitter/text_renderer.rb +6 -5
  37. data/lib/ea/theme/definition.rb +5 -2
  38. data/lib/ea/theme/loader.rb +3 -2
  39. data/lib/ea/version.rb +1 -1
  40. metadata +4 -1
@@ -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,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) * (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
@@ -147,7 +158,7 @@ module Ea
147
158
  def enum_literal_first_y
148
159
  return nil unless divider_y
149
160
 
150
- divider_y + size + (attr_first_offset || 7)
161
+ divider_y + line_height((attr_first_offset || 7) - 1) + 1
151
162
  end
152
163
 
153
164
  # OCL constraints follow the regular content with a divider and caption.
@@ -199,7 +210,7 @@ module Ea
199
210
  def enum_literals_bottom_y
200
211
  return nil unless enum_literal_first_y
201
212
 
202
- enum_literal_first_y + ([enum_literals_count, 1].max - 1) * (size + 6)
213
+ enum_literal_first_y + ([enum_literals_count, 1].max - 1) * line_height(6)
203
214
  end
204
215
  end
205
216
  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
 
@@ -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
- 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,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 = size + 6
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
- visibility_prefix(op), family, size)
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
- operation_text(op), family, size)
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
- family: family, size: size).to_svg
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
@@ -116,6 +116,7 @@ module Ea
116
116
  size = font_resolver.size_for(element)
117
117
  family = font_resolver.family_for(element)
118
118
  size_unit = font_resolver.size_unit_for(element)
119
+ logical_height = font_resolver.logical_height_for(element)
119
120
  parent_name = off_canvas_parent_name_for(classifier)
120
121
 
121
122
 
@@ -152,8 +153,8 @@ module Ea
152
153
  # 22 + 3 (CharacterString 197+27=224, Integer 92+25=117).
153
154
  # Titles never drive growth - they wrap instead.
154
155
  if attr_lines.any? || op_lines.any?
155
- row_w = [row_content_width(attr_lines, 5, size, family),
156
- row_content_width(op_lines, 3, size, family)].compact.max
156
+ row_w = [row_content_width(attr_lines, 5, size, family, logical_height),
157
+ row_content_width(op_lines, 3, size, family, logical_height)].compact.max
157
158
  if row_w && row_w > bounds.width
158
159
  bounds = Ea::Model::Bounds.new(
159
160
  x: bounds.x, y: bounds.y,
@@ -167,18 +168,19 @@ module Ea
167
168
  visually_nested: visually_nested?(element),
168
169
  umldi_keyword: element.umldi_keyword,
169
170
  bounds_width: bounds.width,
170
- font_size: size,
171
+ font_size: size, logical_height: logical_height,
171
172
  family: family,
172
173
  off_canvas_parent_name: parent_name,
173
174
  foreign_package_name: foreign_package_name_for(classifier),
174
175
  suppress_stereotypes: suppress_stereotypes?) : []
175
176
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
176
- tagged_values_for(classifier).size,
177
- header_lines, classifier: classifier,
178
- constraints_count: constraints_for(classifier).size,
179
- marker_count: marker_lines_for(classifier, element).size,
180
- enum_literals_count: enum_row_count_for(classifier),
181
- attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
177
+ tagged_values_for(classifier).size,
178
+ header_lines, classifier: classifier, family: family,
179
+ size_unit: size_unit, logical_height: logical_height,
180
+ constraints_count: constraints_for(classifier).size,
181
+ marker_count: marker_lines_for(classifier, element).size,
182
+ enum_literals_count: enum_row_count_for(classifier),
183
+ attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
182
184
  # EA suppresses the whole attribute compartment when its
183
185
  # rows do not fit the stored box height (never truncates
184
186
  # rows) - the AOS box on CityFurniture hides its
@@ -196,12 +198,13 @@ module Ea
196
198
  geometry.attr_bottom_y.to_i > bounds.y + bounds.height
197
199
  attr_lines = []
198
200
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
199
- tagged_values_for(classifier).size,
200
- header_lines, classifier: classifier,
201
- constraints_count: constraints_for(classifier).size,
202
- marker_count: marker_lines_for(classifier, element).size,
203
- enum_literals_count: enum_row_count_for(classifier),
204
- attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
201
+ tagged_values_for(classifier).size,
202
+ header_lines, classifier: classifier, family: family,
203
+ size_unit: size_unit, logical_height: logical_height,
204
+ constraints_count: constraints_for(classifier).size,
205
+ marker_count: marker_lines_for(classifier, element).size,
206
+ enum_literals_count: enum_row_count_for(classifier),
207
+ attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
205
208
  end
206
209
  # EA suppresses the operations compartment when its rows
207
210
  # overflow the stored box height (same rule as attributes)
@@ -216,22 +219,24 @@ module Ea
216
219
  height: geometry.op_bottom_y.to_i + 8 - bounds.y
217
220
  )
218
221
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
219
- tagged_values_for(classifier).size,
220
- header_lines, classifier: classifier,
221
- constraints_count: constraints_for(classifier).size,
222
- marker_count: marker_lines_for(classifier, element).size,
223
- enum_literals_count: enum_row_count_for(classifier),
224
- attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
222
+ tagged_values_for(classifier).size,
223
+ header_lines, classifier: classifier, family: family,
224
+ size_unit: size_unit, logical_height: logical_height,
225
+ constraints_count: constraints_for(classifier).size,
226
+ marker_count: marker_lines_for(classifier, element).size,
227
+ enum_literals_count: enum_row_count_for(classifier),
228
+ attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
225
229
  elsif op_lines.any? && geometry.op_first_y &&
226
230
  geometry.op_bottom_y.to_i > bounds.y + bounds.height
227
231
  op_lines = []
228
232
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
229
- tagged_values_for(classifier).size,
230
- header_lines, classifier: classifier,
231
- constraints_count: constraints_for(classifier).size,
232
- marker_count: marker_lines_for(classifier, element).size,
233
- enum_literals_count: enum_row_count_for(classifier),
234
- attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
233
+ tagged_values_for(classifier).size,
234
+ header_lines, classifier: classifier, family: family,
235
+ size_unit: size_unit, logical_height: logical_height,
236
+ constraints_count: constraints_for(classifier).size,
237
+ marker_count: marker_lines_for(classifier, element).size,
238
+ enum_literals_count: enum_row_count_for(classifier),
239
+ attr_group_lead: Element::AttributeRenderer.group_lead_total(attr_lines))
235
240
  end
236
241
  # Real attributes grow the DRAWN box past the stored
237
242
  # height instead of being dropped (0CABA7D7: stored h44
@@ -265,7 +270,7 @@ module Ea
265
270
  stroke: resolve_stroke,
266
271
  stroke_width: resolve_stroke_width,
267
272
  text_fill: theme.text_color,
268
- family: family, size: size, size_unit: size_unit,
273
+ family: family, size: size, size_unit: size_unit, logical_height: logical_height,
269
274
  header_lines: header_lines,
270
275
  attr_lines: attr_lines,
271
276
  op_lines: op_lines,
@@ -283,14 +288,14 @@ module Ea
283
288
  )
284
289
  end
285
290
 
286
- def row_content_width(lines, right_pad, size, family)
291
+ def row_content_width(lines, right_pad, size, family, logical_height)
287
292
  return nil if lines.to_a.empty?
288
293
 
289
294
  tl = lines.filter_map do |line|
290
295
  text = line.sub(/\A\S /, "")
291
296
  Ea::Fonts::Metrics.text_length(text, size,
292
297
  family: family,
293
- weight: "400")&.round
298
+ weight: "400", logical_height: logical_height)&.round
294
299
  end.max
295
300
  return nil unless tl
296
301
 
@@ -301,8 +306,9 @@ module Ea
301
306
  tagged_count, header_lines, classifier: nil,
302
307
  constraints_count: 0, marker_count: 0,
303
308
  enum_literals_count: 0,
304
- attr_group_lead: 0)
305
- CompartmentGeometry.new(bounds: bounds, size: size,
309
+ attr_group_lead: 0, family: nil, size_unit: "pt", logical_height: nil)
310
+ CompartmentGeometry.new(bounds: bounds, size: size, family: family,
311
+ size_unit: size_unit, logical_height: logical_height,
306
312
  header_ghost: header_lines.any? &&
307
313
  %i[italic italic_tight].include?(header_lines.first.last),
308
314
  header_ghost_tight: header_lines.any? &&
@@ -496,6 +502,7 @@ module Ea
496
502
 
497
503
  def foreign_package_name_for(classifier)
498
504
  return nil unless diagram.show_foreign
505
+ return nil if diagram.style_ex.to_s.split(";").include?("NoFullScope=1")
499
506
  return nil unless classifier.is_a?(Ea::Model::Classifier)
500
507
 
501
508
  # Nested classifiers (t_object.ParentID -> a Class) qualify
@@ -41,9 +41,8 @@ module Ea
41
41
  DEFAULT_MARKER_FAMILIES.include?(explicit)
42
42
  return explicit
43
43
  end
44
- return theme.font_family if theme.themed? && theme.font_family
45
44
 
46
- diagram_default_family || DEFAULT_FONT_FAMILY
45
+ theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
47
46
  end
48
47
 
49
48
  def size_for(element)
@@ -58,8 +57,12 @@ module Ea
58
57
  DEFAULT_ELEMENT_FONT_SIZE
59
58
  end
60
59
 
61
- def size_unit_for(_element)
62
- theme.font_size_unit || "pt"
60
+ def size_unit_for(element)
61
+ element&.font_size_unit || theme.font_size_unit || "pt"
62
+ end
63
+
64
+ def logical_height_for(element)
65
+ element&.font_logical_height if size_unit_for(element) == "pt"
63
66
  end
64
67
 
65
68
  def weight_for(element)
@@ -73,6 +76,13 @@ module Ea
73
76
 
74
77
  private
75
78
 
79
+ def theme_family_for(explicit)
80
+ return unless theme.themed?
81
+ return theme.font_family if DEFAULT_MARKER_FAMILIES.include?(explicit)
82
+
83
+ theme.element_font_family || theme.font_family
84
+ end
85
+
76
86
  # EA's app-default font depends on the system locale:
77
87
  # Japanese Windows → "Yu Gothic UI" 13, English Windows →
78
88
  # "Calibri" 10. We can't query EA's runtime, so we infer
@@ -42,8 +42,11 @@ module Ea
42
42
 
43
43
  private
44
44
 
45
+ # EA does not draw a connector whose other end is missing from
46
+ # the diagram, so it has no waypoints and no ghost label either.
45
47
  def visible_connectors
46
48
  (diagram.connectors || []).select(&:renderable?)
49
+ .reject { |connector| connector.waypoints.empty? }
47
50
  end
48
51
 
49
52
  def text_for(ghost)