ea 0.6.49 → 0.6.51

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4735ff087541add85998eedbde491751da68513387014dadccd50c84fa8daaf7
4
- data.tar.gz: 5e9a3b35f13099b105791506a67b0e7c2854cce7f54d736e82420aa7cd268a57
3
+ metadata.gz: 3bac390d3be1320d1679f33e3ed0e83b0982c02faea48f6ebbc2acdc31aa345b
4
+ data.tar.gz: c0ffb44d6f3cb775e37edbbe8fc10ddfb10a802c9555846aba5f21188901c3dc
5
5
  SHA512:
6
- metadata.gz: 7d7331be8f61f4c3b704781ca458f3989e8d57d3547a19df796373744f674fad552e2d191bfc637b7ee1d83faf5eadd3d39f18013dbc08040e0eb27f4cc3cea2
7
- data.tar.gz: '0688908443b70ea4ebe4582b78657ef9988aeb81c55f6ec23b9e2ee5e74d9f1005075a0e296697f5f3c41e33a5205ba0ed8044dd03e153ae3d6b8bb22a1912db'
6
+ metadata.gz: b380504257c1e1b3ad40852561d4535e4a2dd4f6894427715eeb2a192b7ca7b9d43b77bed8d9c6c28bfaacf0d62586f4204fac00d3e6642646f1f3f214f2e0e3
7
+ data.tar.gz: 80acb5162983cbe2052c63bbb417adb0a019be316eba63aeb54166680f6c68b4865b2da41378eff7fd9dcc5eab117a46bbc2a577e426451b95bc75a222c556d0
@@ -10,6 +10,7 @@ module Ea
10
10
  attribute :name, :string
11
11
  attribute :classifier_id, :string
12
12
  attribute :classifier_name, :string
13
+ attribute :stereotype, :string
13
14
  attribute :package_id, :string
14
15
  attribute :package_name, :string
15
16
  attribute :qualified_name, :string
@@ -22,6 +23,7 @@ module Ea
22
23
  map "name", to: :name
23
24
  map "classifierId", to: :classifier_id
24
25
  map "classifierName", to: :classifier_name
26
+ map "stereotype", to: :stereotype
25
27
  map "packageId", to: :package_id
26
28
  map "packageName", to: :package_name
27
29
  map "qualifiedName", to: :qualified_name
@@ -36,6 +36,7 @@ module Ea
36
36
  name: row.name.to_s,
37
37
  classifier_id: classifier_id_for(row),
38
38
  classifier_name: classifier_name_for(row),
39
+ stereotype: row.stereotype.to_s,
39
40
  package_id: package_id_for(row),
40
41
  package_name: package_name_for(row),
41
42
  qualified_name: qualified_name_for(row),
@@ -7,9 +7,10 @@ module Ea
7
7
  # Instance slot compartment. Renders each slot on an
8
8
  # InstanceSpecification as "name op value", then appends
9
9
  # "(from PackageName)" in italics below the slots when the
10
- # instance has a classifier. Skipped entirely when the model
11
- # element is not an InstanceSpecification or has neither
12
- # slots nor a classifier.
10
+ # instance is foreign to the diagram's package. Skipped
11
+ # entirely when the model element is not an
12
+ # InstanceSpecification or has neither slots nor a
13
+ # classifier.
13
14
  module InstanceSlots
14
15
  ROW_OFFSET = 13
15
16
  SUBTITLE_EXTRA_OFFSET = 10
@@ -21,7 +22,7 @@ module Ea
21
22
 
22
23
  instance = context.model_element
23
24
  slots = instance.slots || []
24
- subtitle = from_package_subtitle(instance)
25
+ subtitle = from_package_subtitle(instance, context.diagram)
25
26
  return nil if slots.empty? && subtitle.nil?
26
27
 
27
28
  first_y = context.geometry.attr_first_y
@@ -44,7 +45,17 @@ module Ea
44
45
  end
45
46
  module_function :instance?
46
47
 
47
- def from_package_subtitle(instance)
48
+ # EA draws "(from PackageName)" only for instances FOREIGN
49
+ # to the diagram's own package: every instance diagram in
50
+ # the corpus has local instances and EA renders the
51
+ # subtitle nowhere (FC590D99: 23 local FIG2010 instances,
52
+ # zero "(from" lines in the published SVG).
53
+ def from_package_subtitle(instance, diagram)
54
+ if diagram && instance.package_id && diagram.package_id &&
55
+ instance.package_id == diagram.package_id
56
+ return nil
57
+ end
58
+
48
59
  pkg = instance.package_name
49
60
  return nil if pkg.nil? || pkg.empty?
50
61
 
@@ -18,6 +18,7 @@ module Ea
18
18
  # coordinate math changes don't ripple into rendering.
19
19
  class CompartmentGeometry
20
20
  attr_reader :bounds, :size, :header_lines_count, :header_ghost,
21
+ :header_ghost_tight,
21
22
  :attr_lines_count, :op_lines_count,
22
23
  :tagged_values_count,
23
24
  :constraints_count,
@@ -38,7 +39,8 @@ module Ea
38
39
  divider_offset: 8,
39
40
  attr_line_offset: 6,
40
41
  attr_first_offset: 7,
41
- header_ghost: false)
42
+ header_ghost: false,
43
+ header_ghost_tight: false)
42
44
  @bounds = bounds
43
45
  @size = size
44
46
  @header_lines_count = header_lines_count
@@ -54,6 +56,7 @@ module Ea
54
56
  @attr_line_offset = attr_line_offset
55
57
  @attr_first_offset = attr_first_offset
56
58
  @header_ghost = header_ghost
59
+ @header_ghost_tight = header_ghost_tight
57
60
  end
58
61
 
59
62
  def header_first_y
@@ -71,9 +74,9 @@ module Ea
71
74
  return nil if header_lines_count.zero?
72
75
 
73
76
  # The off-canvas parent ghost shifts every following
74
- # header line +6, and the divider tracks the LAST header
75
- # baseline + 8 (DD2AC61B: name baseline 85, divider 93).
76
- ghost_shift = header_ghost ? 6 : 0
77
+ # header line +3, and the divider tracks the LAST header
78
+ # baseline + 8 (936AA434: ghost 100, stereo 116 = +16).
79
+ ghost_shift = header_ghost ? (header_ghost_tight ? 3 : 6) : 0
77
80
 
78
81
  header_first_y +
79
82
  ([header_lines_count, 1].max - 1) * (size + (header_line_offset || 6)) +
@@ -22,6 +22,7 @@ module Ea
22
22
  :bounds_width, :font_size, :family,
23
23
  :off_canvas_parent_name,
24
24
  :foreign_package_name,
25
+ :suppress_stereotypes,
25
26
  keyword_init: true)
26
27
 
27
28
  PROVIDERS = [
@@ -39,9 +40,18 @@ module Ea
39
40
  end
40
41
 
41
42
  # Greedy word wrap measured with the header's own font.
42
- # EA wraps overflowing headers at word boundaries
43
- # (81F92FC7: instance header "new ownership: LA_Right"
43
+ # EA wraps overflowing headers at word boundaries and after
44
+ # "::" qualifiers (F851A657: class "Content
45
+ # information::MD_FeatureCatalogueDescription" renders as
46
+ # "Content information::" / "MD_FeatureCatalogueDescription";
47
+ # 81F92FC7: instance header "new ownership: LA_Right"
44
48
  # renders as "new ownership:" / "LA_Right").
49
+ # Headers wrap against a USABLE width of box width - 6:
50
+ # EA wrapped "SU_PB2: LA_SpatialUnit" (parts 36 + 59 +
51
+ # space 4 = 99) inside a 100px box, while
52
+ # "FuelStation: LA_BAUnit" (94) stayed single in a 100px
53
+ # box - the corpus boundary is margin 5 wrapped vs 6
54
+ # unwrapped (FC590D99 vs CBC03448).
45
55
  def self.wrap_words(text, context, weight)
46
56
  font_style = weight == :bold_italic ? "italic" : "normal"
47
57
  width = lambda do |s|
@@ -51,15 +61,17 @@ module Ea
51
61
  ).round
52
62
  end
53
63
  return [text] if context.bounds_width.nil?
54
- return [text] if width.call(text) <= context.bounds_width.to_i
64
+ usable = context.bounds_width.to_i - 6
65
+ return [text] if width.call(text) <= usable
55
66
 
67
+ segments = wrap_segments(text)
56
68
  lines = []
57
69
  current = +""
58
- text.split(" ").each do |word|
59
- candidate = current.empty? ? word : "#{current} #{word}"
60
- if !current.empty? && width.call(candidate) > context.bounds_width.to_i
70
+ segments.each do |segment|
71
+ candidate = current + segment
72
+ if !current.empty? && width.call(candidate) > usable
61
73
  lines << current
62
- current = word
74
+ current = +segment.lstrip
63
75
  else
64
76
  current = candidate
65
77
  end
@@ -67,6 +79,32 @@ module Ea
67
79
  lines << current unless current.empty?
68
80
  lines
69
81
  end
82
+
83
+ # Break opportunities: after each space and after each
84
+ # "::". Each segment carries the separator that PRECEDES
85
+ # it ("" after "::", " " after a word), so joining all
86
+ # segments reproduces the original text exactly.
87
+ def self.wrap_segments(text)
88
+ segments = []
89
+ pending = ""
90
+ text.split(" ").each do |word|
91
+ chunk = +""
92
+ word.split(/(::)/).each_slice(2) do |part, sep|
93
+ chunk << pending << part
94
+ pending = ""
95
+ if sep
96
+ chunk << sep
97
+ segments << chunk.dup
98
+ chunk.clear
99
+ end
100
+ end
101
+ unless chunk.empty?
102
+ segments << chunk.dup
103
+ pending = " "
104
+ end
105
+ end
106
+ segments
107
+ end
70
108
  end
71
109
  end
72
110
  end
@@ -6,22 +6,37 @@ module Ea
6
6
  module Element
7
7
  module HeaderLineProvider
8
8
  # InstanceSpecification: short-circuits the pipeline. The
9
- # header for an instance is "name: Classifier" on one bold
10
- # line, word-wrapped when it overflows the box (81F92FC7:
11
- # "new ownership:" / "LA_Right").
9
+ # header for an instance is its own «stereotype» line (the
10
+ # t_object.Stereotype value, verbatim - «featureType»,
11
+ # «FeatureType», «type» …) followed by "name: Classifier"
12
+ # on one bold line, word-wrapped when it overflows the box
13
+ # (81F92FC7: "new ownership:" / "LA_Right"). The stereotype
14
+ # line is suppressed by the diagram pdata flag
15
+ # HideEStereo=1 (FC590D99 shows it; the Annex C "Case"
16
+ # diagrams with HideEStereo=1 show none).
12
17
  class InstanceSpec
13
18
  def self.call(context)
14
19
  inst = context.classifier
15
20
  return [] unless inst.is_a?(Ea::Model::InstanceSpecification)
16
21
 
22
+ lines = stereotype_lines(inst, context)
17
23
  label = inst.name.to_s
18
24
  if inst.classifier_name && !inst.classifier_name.empty?
19
25
  label = "#{label}: #{inst.classifier_name}"
20
26
  end
21
- HeaderLinePipeline.wrap_words(label, context, :bold).map do |line|
27
+ lines + HeaderLinePipeline.wrap_words(label, context, :bold).map do |line|
22
28
  [line, :bold]
23
29
  end
24
30
  end
31
+
32
+ def self.stereotype_lines(inst, context)
33
+ return [] if context.suppress_stereotypes
34
+
35
+ st = inst.stereotype.to_s
36
+ return [] if st.empty?
37
+
38
+ [["«#{st}»", :normal]]
39
+ end
25
40
  end
26
41
  end
27
42
  end
@@ -58,7 +58,11 @@ module Ea
58
58
  name, context.font_size || 9, nil,
59
59
  family: context.family, weight: "700", style: font_style
60
60
  ).round
61
- len > context.bounds_width
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
62
66
  end
63
67
 
64
68
  # EA renders the plain classifier name in element headers;
@@ -15,7 +15,7 @@ module Ea
15
15
  name = context.off_canvas_parent_name
16
16
  return [] unless name
17
17
 
18
- [[name, :italic]]
18
+ [[name, context.classifier.is_a?(Ea::Model::Interface) ? :italic_tight : :italic]]
19
19
  end
20
20
  end
21
21
  end
@@ -17,6 +17,10 @@ module Ea
17
17
  def self.call(context)
18
18
  classifier = context.classifier
19
19
  return [] if classifier.is_a?(Ea::Model::InstanceSpecification)
20
+ # Diagram pdata HideEStereo=1 suppresses classifier
21
+ # stereotype lines too (the Annex C "Case" diagrams
22
+ # render zero «...» lines, classes included).
23
+ return [] if context.suppress_stereotypes
20
24
 
21
25
  labels = label_for(classifier, context.umldi_keyword)
22
26
  return [] if labels.empty?
@@ -23,11 +23,16 @@ module Ea
23
23
  else weight_normal
24
24
  end
25
25
  font_style = (style == :italic || style == :bold_italic) ? "italic" : "normal"
26
- # EA spaces the line AFTER a right-aligned ghost 6px
27
- # wider (ghost +13, next +32 at 7pt) - verified against
28
- # EA-published reference SVGs.
26
+ # EA spaces the line AFTER a right-aligned ghost 3px
27
+ # wider than the pitch. Interfaces use a tighter +3
28
+ # (936AA434/Surface: ghost 100, «interface» 116).
29
+ ghost_shift = case lines.first.last
30
+ when :italic then 6
31
+ when :italic_tight then 3
32
+ else 0
33
+ end
29
34
  y = first_y + (idx * line_h) +
30
- (idx >= 1 && lines.first.last == :italic ? 6 : 0)
35
+ (idx >= 1 && ghost_shift.positive? ? ghost_shift : 0)
31
36
  x = if style == :italic
32
37
  # EA right-aligns the off-canvas parent ghost at
33
38
  # the box's right edge by its integer textLength.
@@ -126,7 +126,8 @@ module Ea
126
126
  font_size: size,
127
127
  family: family,
128
128
  off_canvas_parent_name: parent_name,
129
- foreign_package_name: foreign_package_name_for(classifier)) : []
129
+ foreign_package_name: foreign_package_name_for(classifier),
130
+ suppress_stereotypes: suppress_stereotypes?) : []
130
131
  is_classifier = classifier.is_a?(Ea::Model::Classifier)
131
132
  attr_lines = if is_classifier && show_attributes? &&
132
133
  !classifier.is_a?(Ea::Model::Interface) &&
@@ -235,7 +236,9 @@ module Ea
235
236
  enum_literals_count: 0)
236
237
  CompartmentGeometry.new(bounds: bounds, size: size,
237
238
  header_ghost: header_lines.any? &&
238
- header_lines.first.last == :italic,
239
+ %i[italic italic_tight].include?(header_lines.first.last),
240
+ header_ghost_tight: header_lines.any? &&
241
+ header_lines.first.last == :italic_tight,
239
242
  header_lines_count: header_lines.size,
240
243
  attr_lines_count: attr_lines.size,
241
244
  op_lines_count: op_lines.size,
@@ -274,6 +277,14 @@ module Ea
274
277
  classifier.is_a?(Ea::Model::Interface)
275
278
  end
276
279
 
280
+ # t_diagram pdata HideEStereo=1 suppresses every stereotype
281
+ # line on the diagram - instance boxes ("Case C25" renders
282
+ # no «featureType» despite every instance carrying one) and
283
+ # classifier boxes alike.
284
+ def suppress_stereotypes?
285
+ diagram.style.to_s.match?(/HideEStereo=1/)
286
+ end
287
+
277
288
  # A stereotype group header («Property») sits 4px below where
278
289
  # plain rows start (divider+18 vs divider+14 at 7pt).
279
290
  def attr_first_offset_for(attr_lines)
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.49"
7
+ VERSION = "0.6.51"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.49
4
+ version: 0.6.51
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-05 00:00:00.000000000 Z
11
+ date: 2026-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model