ea 0.6.112 → 0.6.114

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: 6014a00e5fa8c483688ab06cc70027d3b3a8adcd65afbe98ef6782ea6a5473a3
4
- data.tar.gz: bd75f6912efc6bb4bc1a963c21ef53056ac421de33af7960bc8e94ee00d8e1c1
3
+ metadata.gz: 251161fffac3b07a12851b0abd26a046998a61b19a9418da8c6d6ceea1cbb09f
4
+ data.tar.gz: 549486eec86fb19c6275db6ab7dafdcb7987c1a22b12df2740140e60c7bb2690
5
5
  SHA512:
6
- metadata.gz: ca1099b7bbc4e22f6e874953b49c29afdc4d8bfc11aef61d7f6f55b3d4c6a2062d064f65e7812fb060c0946ebc47edd04d9a86a45e872584f0a40587f6454750
7
- data.tar.gz: 5b56489efbaaf470115c56e180e0e78d8f9f8e46d0c4b21c030ab0443a7bf07bc41317125b16331ec76fef1aa3fae29b2731e35fb38b110743a60a18d0e88183
6
+ metadata.gz: c5cccbc615740b54619be7e36e4c6edb6382768d97b8a120841aca69018b73e693d9dadb8480e78c9e1e0a18ccfc09122668caf9680571496d99b44372c593cd
7
+ data.tar.gz: 87a9b79c59a7102d10aaf4968c052f5a63c1e707ef4daf2376d1278ca79b1317e2fc7d47988d57cb09c5abe88d57aeb12b611ba7066e421a1820ce07ac7ff581
@@ -126,6 +126,8 @@ module Ea
126
126
  @current_obj_row = obj_row
127
127
  style_hash = DiagramStyleParser.parse(obj_row.objectstyle)
128
128
  style_hash[:stored_attribute_order] = true
129
+ fqname = stereotype_fqname_for(obj_row)
130
+ style_hash[:stereotype_fqname] = fqname if fqname
129
131
  font_style = effective_font_style(obj_row, style_hash)
130
132
  Ea::Model::DiagramElement.new(
131
133
  id: IdNormalizer.synthetic("de", diagram_row.diagram_id,
@@ -150,6 +152,19 @@ module Ea
150
152
  raw == "1" || raw == "-1"
151
153
  end
152
154
 
155
+ def stereotype_fqname_for(placement)
156
+ object = database.find_object(placement.ea_object_id)
157
+ return nil unless object
158
+
159
+ database.xrefs_for_client(object.ea_guid).each do |xref|
160
+ application = Xref::Parser.parse(xref.description).stereotype
161
+ next unless application && application.name == object.stereotype
162
+
163
+ return application.fqname
164
+ end
165
+ nil
166
+ end
167
+
153
168
  def build_connectors(diagram_row)
154
169
  links = database.diagram_links_for(diagram_row.diagram_id) || []
155
170
  context = ContextDiagramSupport.context?(diagram_row)
@@ -389,6 +389,7 @@ module Ea
389
389
  dividers = [divider_y_by_ref[source_ref],
390
390
  divider_y_by_ref[target_ref]]
391
391
  return nil unless bounds.all? && dividers.all?
392
+ return nil if flush_lateral_line?(connector, pairs, *bounds)
392
393
 
393
394
  preserve_source = explicit_horizontal_source_dock?(pairs, connector)
394
395
  adjusted = row_slot_pairs(pairs, bounds, dividers, preserve_source: preserve_source)
@@ -23,6 +23,7 @@ module Ea
23
23
  :off_canvas_parent_name,
24
24
  :foreign_package_name,
25
25
  :suppress_stereotypes,
26
+ :stereotype_fqname,
26
27
  keyword_init: true)
27
28
 
28
29
  PROVIDERS = [
@@ -22,7 +22,7 @@ module Ea
22
22
  # render zero «...» lines, classes included).
23
23
  return [] if context.suppress_stereotypes
24
24
 
25
- labels = label_for(classifier, context.umldi_keyword)
25
+ labels = label_for(classifier, context.umldi_keyword, context.stereotype_fqname)
26
26
  return [] if labels.empty?
27
27
 
28
28
  labels.map { |l| [l, :normal] }
@@ -30,10 +30,10 @@ module Ea
30
30
 
31
31
  # @return [Array<String>] «name» labels (EA renders one
32
32
  # line per applied stereotype)
33
- def self.label_for(classifier, umldi_keyword)
33
+ def self.label_for(classifier, umldi_keyword, stereotype_fqname = nil)
34
34
  return ["«#{umldi_keyword}»"] if umldi_keyword && !umldi_keyword.empty?
35
35
 
36
- explicit = explicit_stereotype(classifier)
36
+ explicit = explicit_stereotype(classifier, stereotype_fqname)
37
37
  return explicit if explicit
38
38
 
39
39
  case classifier
@@ -45,21 +45,21 @@ module Ea
45
45
  end
46
46
  end
47
47
 
48
- def self.explicit_stereotype(classifier)
48
+ def self.explicit_stereotype(classifier, stereotype_fqname = nil)
49
49
  return nil unless classifier.is_a?(Ea::Model::Classifier) ||
50
50
  classifier.is_a?(Ea::Model::Package)
51
51
 
52
52
  refs = classifier.stereotype_refs
53
53
  return nil unless refs&.any?
54
54
 
55
- refs.map { |ref| "«#{canonical(ref)}»" }
55
+ refs.map { |ref| "«#{canonical(ref, stereotype_fqname)}»" }
56
56
  end
57
57
 
58
- # EA renders the stereotype string exactly as stored in
59
- # t_object.Stereotype — corpus-verified: 3,284 placements
60
- # render verbatim ('CodeList', 'codeList', 'codelist',
61
- # 'featureType' …), zero case-canonicalized.
62
- def self.canonical(ref)
58
+ # EA's standard Class stereotype displays as «type» while
59
+ # its semantic name remains Type. Custom profiles retain case.
60
+ def self.canonical(ref, stereotype_fqname = nil)
61
+ return "type" if ref == "Type" && stereotype_fqname == "StandardProfileL2::Type"
62
+
63
63
  ref
64
64
  end
65
65
 
@@ -166,10 +166,12 @@ module Ea
166
166
  )
167
167
  end
168
168
  end
169
+ stereotype_fqname = element.style&.fetch(:stereotype_fqname, nil)
169
170
  header_lines = classifier ? Element::HeaderLines.for(classifier,
170
171
  diagram_package_id: diagram.package_id,
171
172
  visually_nested: visually_nested?(element),
172
173
  umldi_keyword: element.umldi_keyword,
174
+ stereotype_fqname: stereotype_fqname,
173
175
  bounds_width: bounds.width,
174
176
  font_size: size, logical_height: logical_height,
175
177
  family: family,
@@ -452,7 +454,7 @@ module Ea
452
454
  def tagged_values_for(classifier)
453
455
  return [] unless classifier.is_a?(Ea::Model::Classifier)
454
456
 
455
- classifier.tagged_values || []
457
+ (classifier.tagged_values || []).reject { |tag| tag.value.nil? || tag.value.empty? }
456
458
  end
457
459
 
458
460
  # Real OCL constraints render trailing, after the content
@@ -10,7 +10,7 @@ module Ea
10
10
  # 2. theme.font_family / theme.font_size (when theme is themed)
11
11
  # 3. diagram-level default (most common non-nil value across
12
12
  # the diagram's elements)
13
- # 4. locale fallback (Calibri 10)
13
+ # 4. unthemed fallback (Cambria)
14
14
  #
15
15
  # Lives outside the Elements emitter so the resolution rule is
16
16
  # defined once (MECE) and reusable by Labels and any future
@@ -83,14 +83,8 @@ module Ea
83
83
  theme.element_font_family || theme.font_family
84
84
  end
85
85
 
86
- # EA's app-default font depends on the system locale:
87
- # Japanese Windows → "Yu Gothic UI" 13, English Windows →
88
- # "Calibri" 10. We can't query EA's runtime, so we infer
89
- # from the diagram:
90
- #
91
- # - If ANY element specifies font=, use the most common.
92
- # - If NO element specifies font= (all are "use default"),
93
- # assume the EA English-locale default: Calibri 10.
86
+ # Unthemed diagrams without a saved family use Cambria.
87
+ # Saved families still supply the diagram default.
94
88
  def diagram_default_family
95
89
  families = element_families
96
90
  return FALLBACK_DEFAULT_FAMILY if families.empty?
@@ -98,7 +92,7 @@ module Ea
98
92
  most_common(families)
99
93
  end
100
94
 
101
- FALLBACK_DEFAULT_FAMILY = "Calibri"
95
+ FALLBACK_DEFAULT_FAMILY = "Cambria"
102
96
 
103
97
  def most_common(values)
104
98
  return nil if values.empty?
@@ -100,12 +100,18 @@ module Ea
100
100
  mult_seat(direction, anchor, mult_box) || mult_pos
101
101
  elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
102
102
  text_pos = target_property_role(connector, heading, anchor, text_box, role) ||
103
+ target_diagonal_property_role(connector, heading, anchor, text_box, role) ||
103
104
  target_vertical_role(connector, heading, anchor, text_box, role) ||
104
105
  target_rightward_role(heading, anchor, text_box, role) || text_pos
105
106
  mult_pos = target_multiplicity_seat(connector, heading, anchor, mult_box) ||
106
107
  target_horizontal_multiplicity(heading, anchor, mult_box, mult) || mult_pos
107
108
  end
108
109
 
110
+ if end_kind == :source
111
+ text_pos = source_top_property_role(connector, heading, anchor, text_box, role) ||
112
+ source_bottom_property_role(connector, heading, anchor, text_box, role) || text_pos
113
+ end
114
+
109
115
  build_texts(role:, mult:, standalone:,
110
116
  text_pos:, mult_pos:, stereo:, text_box:)
111
117
  end
@@ -185,10 +191,10 @@ module Ea
185
191
  [x + SEAT_X_INSET, y - 7]
186
192
  end
187
193
 
188
- # Property-role clusters use a direction-specific right edge;
194
+ # Property-role clusters use a direction-specific horizontal seat;
189
195
  # alignment controls the stereotype line within that cluster.
190
196
  def source_property_seat(connector, direction, anchor, box, role)
191
- return nil unless %i[up left down].include?(direction) && property_role_box?(box, direction)
197
+ return nil unless %i[up left right down].include?(direction) && property_role_box?(box, direction)
192
198
  return nil unless source_property_font?(connector, direction)
193
199
 
194
200
  width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
@@ -196,11 +202,62 @@ module Ea
196
202
 
197
203
  x, y = seat_corner(anchor, box)
198
204
  inset, baseline = property_role_offsets(direction)
199
- left = x - inset - width
205
+ left = property_role_x(direction, x, inset + width)
200
206
  left = [left, MIN_ROLE_X].max if direction == :up
201
207
  [left, y + baseline]
202
208
  end
203
209
 
210
+ def property_role_x(direction, origin, offset)
211
+ direction == :right ? origin + offset : origin - offset
212
+ end
213
+
214
+ # EDGE=1 certifies the top dock independently of the clipped ray's
215
+ # heading. An absent source box uses the upward Property seat.
216
+ def source_top_property_role(connector, heading, anchor, box, role)
217
+ return nil unless source_seat?(:source, heading) && heading[0].negative? && heading[1].positive?
218
+ return nil unless zero_source_property_box?(box)
219
+
220
+ return nil unless source_top_property_route?(connector.style.to_h)
221
+
222
+ source_property_seat(connector, :up, anchor, box, role)
223
+ end
224
+
225
+ def zero_source_property_box?(box)
226
+ box && box["alignment"]&.zero? && stored_offsets_zero?(box)
227
+ end
228
+
229
+ def source_top_property_route?(style)
230
+ style[:stored_top_dock] == true && style.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
231
+ end
232
+
233
+ # A saved external ray that first meets the source's bottom face
234
+ # seats an absent source Property role like the downward Property
235
+ # seat. The caller reports that face as a downward heading.
236
+ def source_bottom_property_role(connector, heading, anchor, box, role)
237
+ return nil unless source_bottom_dock?(connector, heading, box)
238
+
239
+ width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
240
+ return nil unless width
241
+
242
+ x, y = seat_corner(anchor, box)
243
+ inset, baseline = property_role_offsets(:down)
244
+ [x - inset - width, y + baseline]
245
+ end
246
+
247
+ def source_bottom_dock?(connector, heading, box)
248
+ source_seat?(:source, heading) && heading[0].zero? && heading[1].positive? &&
249
+ zero_source_property_box?(box) && default_presentation?(box) &&
250
+ source_bottom_property_route?(connector.style.to_h) && source_property_cambria?(connector)
251
+ end
252
+
253
+ # A downward leg on the horizontal lateral tree seats an absent
254
+ # box like the up leg already does on the same route.
255
+ def source_bottom_property_route?(style)
256
+ return false unless style.values_at(:mode, :lwidth) == %w[3 2]
257
+
258
+ style[:tree] == "LH" || (style[:stored_association_ray] == true && style[:tree].nil?)
259
+ end
260
+
204
261
  def plain_upward_seat(connector, direction, anchor, box, role)
205
262
  return nil unless direction == :up && cambria_box?(box)
206
263
  return nil unless [0, 1].include?(box["alignment"])
@@ -233,6 +290,31 @@ module Ea
233
290
  property_label?(connector, :target) && downward_property_route?(connector.style.to_h)
234
291
  end
235
292
 
293
+ # Zero-offset diagonal labels use the dominant leg's Property
294
+ # right edge. Moved boxes keep their explicit placement.
295
+ def target_diagonal_property_role(connector, heading, anchor, box, role)
296
+ return nil unless diagonal_property_box?(connector, box)
297
+
298
+ inset = diagonal_property_inset(heading)
299
+ width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family) if inset
300
+ [anchor[0] - inset - width, anchor[1] - 7] if width
301
+ end
302
+
303
+ def diagonal_property_box?(connector, box)
304
+ plain_vertical_box?(box) && box["alignment"] == 1 && stored_offsets_zero?(box) &&
305
+ property_label?(connector, :target) &&
306
+ connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
307
+ end
308
+
309
+ def diagonal_property_inset(heading)
310
+ return nil unless heading && !heading.include?(0)
311
+
312
+ dx, dy = heading
313
+ return property_role_offsets(:left).first if dx.negative? && dx.abs > dy.abs
314
+
315
+ property_role_offsets(:down).first if dy.negative? && dy.abs > dx.abs
316
+ end
317
+
236
318
  def target_vertical_role(connector, heading, anchor, box, role)
237
319
  return nil unless heading && heading[0].zero? && !heading[1].zero?
238
320
 
@@ -301,28 +383,33 @@ module Ea
301
383
  def property_role_offsets(direction)
302
384
  case direction
303
385
  when :up then [60, -6]
304
- when :left then [60, -7]
386
+ when :left, :right then [60, -7]
305
387
  else [65, 15]
306
388
  end
307
389
  end
308
390
 
309
391
  def source_property_font?(connector, direction)
310
- font_family.to_s.casecmp("Cambria").zero? &&
311
- property_label?(connector, :source) &&
392
+ source_property_cambria?(connector) &&
312
393
  source_property_route?(connector.style.to_h, direction)
313
394
  end
314
395
 
396
+ def source_property_cambria?(connector)
397
+ font_family.to_s.casecmp("Cambria").zero? && property_label?(connector, :source)
398
+ end
399
+
315
400
  def source_property_route?(style, direction)
316
401
  case direction
317
402
  when :left then left_property_route?(style)
403
+ when :right then style.values_at(:mode, :lwidth, :tree) == %w[3 2 H]
318
404
  when :down then downward_property_route?(style)
319
405
  else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
320
406
  end
321
407
  end
322
408
 
323
409
  def left_property_route?(style)
324
- style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
325
- [nil, "H", "LV"].include?(style[:tree])
410
+ downward_property_route?(style) ||
411
+ (style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
412
+ [nil, "H", "LV"].include?(style[:tree]))
326
413
  end
327
414
 
328
415
  def downward_property_route?(style)
@@ -96,8 +96,8 @@ module Ea
96
96
  LINE_PITCH = 13
97
97
 
98
98
  def label_position(connector, points)
99
- name_seat = stored_name_position(connector, points)
100
- return name_seat if name_seat
99
+ stored_seat = stored_stereotype_position(connector, points) || stored_name_position(connector, points)
100
+ return stored_seat if stored_seat
101
101
 
102
102
  return midpoint(points) unless default_substitutable_seat?(connector) &&
103
103
  upward_four_point_route?(points)
@@ -106,9 +106,23 @@ module Ea
106
106
  [source[0] + 3, ((source[1] + bend[1]) / 2).floor - 1]
107
107
  end
108
108
 
109
+ # LMB is the row below the midpoint; LMT carries the name above it.
110
+ def stored_stereotype_position(connector, points)
111
+ return nil unless connector.is_a?(Ea::Model::DiagramConnector) && measured_name_font?
112
+ return nil unless stereotype_label(connector)
113
+
114
+ box = connector.label_boxes.to_h[:lmb]
115
+ return nil unless default_name_box?(box)
116
+
117
+ stored_position(connector, points, box, LINE_PITCH)
118
+ end
119
+
109
120
  # At 7pt, a centered name's stored corner is 3px below its baseline.
110
121
  def stored_name_position(connector, points)
111
- box = stored_name_box(connector)
122
+ stored_position(connector, points, stored_name_box(connector), -3)
123
+ end
124
+
125
+ def stored_position(connector, points, box, baseline)
112
126
  return nil unless box
113
127
 
114
128
  width = Ea::Fonts::Metrics.text_length(midpoint_content(connector), font_size, family: font_family)
@@ -116,7 +130,7 @@ module Ea
116
130
 
117
131
  x, y = midpoint(points)
118
132
  offset_x, offset_y = box.values_at("ox", "oy").map(&:to_i)
119
- [x + offset_x - (width / 2.0).floor, y + offset_y - 3]
133
+ [x + offset_x - (width / 2.0).floor, y + offset_y + baseline]
120
134
  end
121
135
 
122
136
  def stored_name_box(connector)
@@ -129,7 +129,7 @@ module Ea
129
129
 
130
130
  box = source_dock_bounds(connector)
131
131
  face = box && dock_face_heading(points.first, box)
132
- return face if face && face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
132
+ return face if face && face_leads_leg?(connector, face, heading, box)
133
133
 
134
134
  stored_upward_heading(connector, heading)
135
135
  end
@@ -147,6 +147,18 @@ module Ea
147
147
  original.any?(&:zero?) ? original : heading
148
148
  end
149
149
 
150
+ # A saved external ray enters the source through the face it first
151
+ # meets, even when the clipped leg then runs back across the box.
152
+ def face_leads_leg?(connector, face, heading, box)
153
+ anchor = connector.source_anchor
154
+ if face == [0, 1] && anchor && anchor.y > box.y + box.height &&
155
+ connector.style.to_h[:stored_association_ray] == true
156
+ return true
157
+ end
158
+
159
+ face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
160
+ end
161
+
150
162
  def stored_upward_heading(connector, heading)
151
163
  return heading unless heading[1].negative?
152
164
 
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.112"
7
+ VERSION = "0.6.114"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.112
4
+ version: 0.6.114
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.