ea 0.6.29 → 0.6.31

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: 521d4fdcc042ad06bd18a829f39810ccef7df8f750bbe229c6d7c38ea3e45c2e
4
- data.tar.gz: 49f8e753ae47eda15409252023e894e66949e590c579c9da7fb4140acc74e550
3
+ metadata.gz: da25cfb5a4f0690b2b73df2d3c12fe3cd042c96fe6aab67d07e2c8871868feaf
4
+ data.tar.gz: 8be40d4bb501b904f1ba2609446d332ea260f3d04a831528d213d6747e352dfa
5
5
  SHA512:
6
- metadata.gz: 151b418a45c19c998c41957ddb471127f6e110689372a01e046152024c271b10ccb069af527c877ee6404cfe22c0323f55232e9140ba5a4882b382534a26b4b6
7
- data.tar.gz: d2b597bfebed6432a78285f8898d73ef6cfdbbf65362132eed375b387bdc0aa1cfb40ceddabea22695060b8467e70c94768618b7a102bca556f0ab5351882955
6
+ metadata.gz: 271180e45139ae7e67a63a66a9fd78c8311edd379ee73f3a5ab9241f7b43d1be737bffca276e0585473f0294f1f557d3ff46fb254cdd105b8b6526ffb052fd36
7
+ data.tar.gz: 63856efa026109bf92d89468c10e6325f9a2f411e4f49217335667b590842d1b5f8f0f860f17c3527bb57236a3d096729ae411a9c3166c4f4efbb169db30eea0
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ # EA AUTO-GENERATED "Context Diagram" support.
7
+ #
8
+ # Context diagrams are regenerated by EA at DRAW time from the
9
+ # model, so the stored t_diagramlinks rows are stale:
10
+ #
11
+ # - Hidden rows are NEVER drawn (B6847577: 42 Hidden=1
12
+ # dependencies, zero rendered).
13
+ # - Every drawn connector is a DIRECT line: the straight
14
+ # segment between the two box-outline crossings of the
15
+ # center-to-center ray (verified: B6847577 focal left-edge
16
+ # dock predicted 171.8 vs EA 171; E58034A3's 22 visible
17
+ # links all render straight despite stale EDGE geometry).
18
+ # - Package contexts ("Context Diagram: Package
19
+ # Clients|Suppliers of X") synthesize ONE extra line per
20
+ # placed object carrying a model Dependency with the focal
21
+ # package X, unless a visible link already connects the
22
+ # pair (B6847577: 15 synthesized + 2 visible = 17 drawn).
23
+ # When the focal is not placed exactly once, nothing
24
+ # synthesizes (E02FA330: 7 placed suppliers, 0 lines).
25
+ module ContextDiagramSupport
26
+ NAME_PREFIX = "context diagram"
27
+ PACKAGE_PATTERN = /\AContext Diagram: Package (Clients|Suppliers) of (.+)\z/i
28
+ FOCAL_CONNECTOR_TYPES = %w[Dependency].freeze
29
+
30
+ module_function
31
+
32
+ def context?(diagram_row)
33
+ diagram_row.name.to_s.downcase.start_with?(NAME_PREFIX)
34
+ end
35
+
36
+ # {"variant" => "Clients"|"Suppliers", "focal_name" => X}
37
+ # for package contexts, nil otherwise.
38
+ def package_context(diagram_row)
39
+ match = PACKAGE_PATTERN.match(diagram_row.name.to_s)
40
+ return nil unless match
41
+
42
+ # The name after "of " is matched VERBATIM: EA stores
43
+ # object names with their own whitespace ("of Topology
44
+ # Simple" pairs the object " Topology Simple").
45
+ { "variant" => match[1].capitalize, "focal_name" => match[2] }
46
+ end
47
+
48
+ # Placed-object rows for the focal package (exact name match),
49
+ # or [] — callers synthesize only when exactly one exists.
50
+ def focal_placements(diagram_row, database)
51
+ pkg = package_context(diagram_row)
52
+ return [] unless pkg
53
+
54
+ placements = database.diagram_objects_for(diagram_row.diagram_id) || []
55
+ placements.select do |placement|
56
+ obj = database.find_object(placement.ea_object_id.to_i)
57
+ obj && obj.name.to_s == pkg["focal_name"]
58
+ end
59
+ end
60
+
61
+ # EA object IDs of placed elements that carry a model
62
+ # Dependency with the focal package, minus pairs already
63
+ # connected by a visible link row.
64
+ def autoline_candidates(diagram_row, database)
65
+ pkg = package_context(diagram_row)
66
+ return {} unless pkg
67
+
68
+ focal_rows = focal_placements(diagram_row, database)
69
+ return {} unless focal_rows.size == 1
70
+
71
+ focal_id = focal_rows.first.ea_object_id.to_i
72
+ placed_ids = placed_object_ids_for(diagram_row, database)
73
+ visible = visible_focal_partners(diagram_row, database, focal_id)
74
+
75
+ into_focal = pkg["variant"] == "Clients"
76
+ candidates = {}
77
+ (database.collections[:connectors] || []).each do |conn|
78
+ next unless FOCAL_CONNECTOR_TYPES.include?(conn.connector_type)
79
+
80
+ start_id = conn.start_object_id.to_i
81
+ end_id = conn.end_object_id.to_i
82
+ other = if into_focal && end_id == focal_id
83
+ start_id
84
+ elsif !into_focal && start_id == focal_id
85
+ end_id
86
+ end
87
+ next unless other
88
+ next unless placed_ids.include?(other)
89
+ next if visible.include?(other)
90
+
91
+ candidates[other] ||= conn
92
+ end
93
+ candidates
94
+ end
95
+
96
+ # SuppressFOC=1 in t_diagram.StyleEx freezes an element
97
+ # context diagram to its stored visible links; absent or 0
98
+ # makes EA regenerate ALL model connectors among the placed
99
+ # elements (568CDFC9 draws all 10 incl. hidden rows'
100
+ # connectors; 3072804C/801EEB12 with =1 draw visible rows
101
+ # only).
102
+ def element_regenerate?(diagram_row)
103
+ return false if package_context(diagram_row)
104
+
105
+ diagram_row.styleex.to_s.match(/SuppressFOC=(\d)/)&.public_send(:[], 1) != "1"
106
+ end
107
+
108
+ # t_connector rows EA regenerates for a SuppressFOC=0 element
109
+ # context: connectors among the placed elements that touch
110
+ # the focal, plus "star" links joining a focal neighbour to a
111
+ # non-neighbour (E0790EFC draws the 8 focal links and skips
112
+ # the ring1-ring1 web; 85039DAE draws ring2-ring1 and skips
113
+ # ring2-ring2). Ring1-ring1 links never draw. Self-loops of
114
+ # the focal included. Hidden rows do not block regeneration;
115
+ # visible rows keep their stored rendering instead.
116
+ def regenerated_element_connectors(diagram_row, database)
117
+ return [] unless element_regenerate?(diagram_row)
118
+
119
+ placed_ids = placed_object_ids_for(diagram_row, database)
120
+ visible_conn_ids = (database.diagram_links_for(diagram_row.diagram_id) || [])
121
+ .reject { |link| link.hidden.to_i.nonzero? }
122
+ .map(&:connectorid).compact.to_set
123
+ focal_id = focal_element_id(diagram_row, database, placed_ids)
124
+ neighbours = focal_neighbours(database, focal_id)
125
+ (database.collections[:connectors] || []).filter_map do |conn|
126
+ next nil if conn.connector_type == "Nesting"
127
+ next nil unless placed_ids.include?(conn.start_object_id.to_i)
128
+ next nil unless placed_ids.include?(conn.end_object_id.to_i)
129
+ next nil if visible_conn_ids.include?(conn.connector_id)
130
+ next nil unless regenerated_pair?(conn, focal_id, neighbours)
131
+
132
+ conn
133
+ end
134
+ end
135
+
136
+ def focal_element_id(diagram_row, database, placed_ids)
137
+ focal_name = diagram_row.name.to_s
138
+ .sub(/\AContext [Dd]iagram:\s*/, "")
139
+ .strip
140
+ focal = (database.diagram_objects_for(diagram_row.diagram_id) || [])
141
+ .select do |placement|
142
+ obj = database.find_object(placement.ea_object_id.to_i)
143
+ obj && obj.name.to_s == focal_name
144
+ end
145
+ return focal.first.ea_object_id.to_i if focal.size == 1
146
+
147
+ # Renamed focus: EA keeps drawing the original element's
148
+ # context ("Context Diagram: TS_Feature" still renders the
149
+ # TS_FeatureComponent star after the rename). Fall back to
150
+ # the placed element most connected WITHIN the placed set
151
+ # (global model degree is misleading — GM_Composite links
152
+ # to hundreds of off-diagram elements).
153
+ degree = Hash.new(0)
154
+ (database.collections[:connectors] || []).each do |conn|
155
+ start_id = conn.start_object_id.to_i
156
+ end_id = conn.end_object_id.to_i
157
+ next unless placed_ids.include?(start_id) &&
158
+ placed_ids.include?(end_id)
159
+
160
+ degree[start_id] += 1
161
+ degree[end_id] += 1
162
+ end
163
+ placed_ids.max_by { |id| [degree[id], -id] }
164
+ end
165
+
166
+ def focal_neighbours(database, focal_id)
167
+ return Set.new unless focal_id
168
+
169
+ neighbours = Set.new
170
+ (database.collections[:connectors] || []).each do |conn|
171
+ next if conn.connector_type == "Nesting"
172
+
173
+ neighbours << conn.end_object_id.to_i if conn.start_object_id.to_i == focal_id
174
+ neighbours << conn.start_object_id.to_i if conn.end_object_id.to_i == focal_id
175
+ end
176
+ neighbours
177
+ end
178
+
179
+ def regenerated_pair?(conn, focal_id, neighbours)
180
+ return false unless focal_id
181
+
182
+ start_id = conn.start_object_id.to_i
183
+ end_id = conn.end_object_id.to_i
184
+ return true if start_id == focal_id || end_id == focal_id
185
+
186
+ (neighbours.include?(start_id) ? 1 : 0) +
187
+ (neighbours.include?(end_id) ? 1 : 0) == 1
188
+ end
189
+
190
+ def placed_object_ids_for(diagram_row, database)
191
+ (database.diagram_objects_for(diagram_row.diagram_id) || [])
192
+ .map { |p| p.ea_object_id.to_i }
193
+ end
194
+
195
+ # Object IDs whose pair with the focal is already drawn by a
196
+ # visible link row (connector exists, both ends placed).
197
+ def visible_focal_partners(diagram_row, database, focal_id)
198
+ placed_ids = placed_object_ids_for(diagram_row, database)
199
+ partners = []
200
+ (database.diagram_links_for(diagram_row.diagram_id) || []).each do |link|
201
+ next if link.hidden.to_i.nonzero?
202
+
203
+ conn = database.find_connector(link.connectorid)
204
+ next unless conn
205
+
206
+ start_id = conn.start_object_id.to_i
207
+ end_id = conn.end_object_id.to_i
208
+ next unless placed_ids.include?(start_id) && placed_ids.include?(end_id)
209
+ next unless start_id == focal_id || end_id == focal_id
210
+
211
+ partners << (start_id == focal_id ? end_id : start_id)
212
+ end
213
+ partners
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -130,11 +130,116 @@ module Ea
130
130
 
131
131
  def build_connectors(diagram_row)
132
132
  links = database.diagram_links_for(diagram_row.diagram_id) || []
133
+ context = ContextDiagramSupport.context?(diagram_row)
134
+ # EA regenerates context diagrams at draw time; their
135
+ # Hidden link rows are stale and never render.
136
+ links = links.reject { |link_row| hidden?(link_row) } if context
133
137
  explicit = links.map { |link_row| build_connector(link_row, diagram_row) }
134
- phantom = phantom_connectors(diagram_row, explicit)
138
+ explicit += if ContextDiagramSupport.package_context(diagram_row)
139
+ context_autolines(diagram_row)
140
+ else
141
+ regenerated_element_connectors(diagram_row)
142
+ end
143
+ # EA does not phantom-synthesize on context diagrams — it
144
+ # regenerates instead (3072804C: phantoms doubled the
145
+ # drawn arrows until disabled).
146
+ phantom = context ? [] : phantom_connectors(diagram_row, explicit)
135
147
  explicit + phantom
136
148
  end
137
149
 
150
+ # SuppressFOC=0 element contexts regenerate every model
151
+ # connector among the placed elements; only the visible rows
152
+ # carry stored geometry, the rest synthesize direct routes.
153
+ def regenerated_element_connectors(diagram_row)
154
+ ContextDiagramSupport
155
+ .regenerated_element_connectors(diagram_row, database)
156
+ .each_with_index.map do |conn, idx|
157
+ build_regenerated_connector(conn, diagram_row, idx)
158
+ end.compact
159
+ end
160
+
161
+ def build_regenerated_connector(conn, diagram_row, idx)
162
+ source_id = conn.start_object_id.to_i
163
+ target_id = conn.end_object_id.to_i
164
+ source = diagram_object_placement(diagram_row.diagram_id, source_id)
165
+ target = diagram_object_placement(diagram_row.diagram_id, target_id)
166
+ return nil unless source && target
167
+
168
+ source_bounds = bounds_from_rect(source)
169
+ target_bounds = bounds_from_rect(target)
170
+ waypoints = if source_id == target_id
171
+ self_loop_waypoints(source_bounds)
172
+ else
173
+ direct_waypoints(source_bounds, target_bounds)
174
+ end
175
+ Ea::Model::DiagramConnector.new(
176
+ id: IdNormalizer.synthetic("ec", diagram_row.diagram_id, idx),
177
+ diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
178
+ relationship_ref: ref_for_raw_connector(conn),
179
+ connector_type: conn.connector_type,
180
+ direction: conn.direction,
181
+ source_stereotype: conn.sourcestereotype,
182
+ target_stereotype: conn.deststereotype,
183
+ waypoints: waypoints,
184
+ label_boxes: {},
185
+ style: { direct: true },
186
+ hidden: false,
187
+ has_geometry_offsets: false
188
+ )
189
+ end
190
+
191
+ # EA's regenerated self-loop: a small elbow exiting and
192
+ # re-entering the box's right edge (568CDFC9: 30px out,
193
+ # 15px tall, centered on the box's vertical middle).
194
+ SELF_LOOP_OUT = 30
195
+ SELF_LOOP_TALL = 15
196
+
197
+ def self_loop_waypoints(bounds)
198
+ right = bounds.x + bounds.width
199
+ cy = bounds.y + (bounds.height / 2)
200
+ pts = [[right, cy + SELF_LOOP_TALL / 2.0],
201
+ [right + SELF_LOOP_OUT, cy + SELF_LOOP_TALL / 2.0],
202
+ [right + SELF_LOOP_OUT, cy - SELF_LOOP_TALL / 2.0],
203
+ [right, cy - SELF_LOOP_TALL / 2.0]]
204
+ pts.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
205
+ end
206
+
207
+ # Package context diagrams synthesize one Dependency line per
208
+ # placed client/supplier of the focal package that has no
209
+ # visible link row connecting it (see ContextDiagramSupport).
210
+ def context_autolines(diagram_row)
211
+ ContextDiagramSupport.autoline_candidates(diagram_row, database)
212
+ .map { |object_id, conn| context_autoline(conn, diagram_row, object_id) }
213
+ .compact
214
+ end
215
+
216
+ def context_autoline(conn, diagram_row, object_id)
217
+ focal = ContextDiagramSupport.focal_placements(diagram_row, database).first
218
+ return nil unless focal
219
+
220
+ into_focal = ContextDiagramSupport.package_context(diagram_row)["variant"] == "Clients"
221
+ source_id = into_focal ? object_id : focal.ea_object_id.to_i
222
+ target_id = into_focal ? focal.ea_object_id.to_i : object_id
223
+ source_placement = diagram_object_placement(diagram_row.diagram_id, source_id)
224
+ target_placement = diagram_object_placement(diagram_row.diagram_id, target_id)
225
+ return nil unless source_placement && target_placement
226
+
227
+ source_bounds = bounds_from_rect(source_placement)
228
+ target_bounds = bounds_from_rect(target_placement)
229
+ waypoints = direct_waypoints(source_bounds, target_bounds)
230
+ Ea::Model::DiagramConnector.new(
231
+ id: IdNormalizer.synthetic("xc", diagram_row.diagram_id, object_id),
232
+ diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
233
+ relationship_ref: ref_for_raw_connector(conn),
234
+ connector_type: conn.connector_type,
235
+ waypoints: waypoints,
236
+ label_boxes: {},
237
+ style: { direct: true },
238
+ hidden: false,
239
+ has_geometry_offsets: false
240
+ )
241
+ end
242
+
138
243
  # EA renders PHANTOM CONNECTORS for associations that exist
139
244
  # in t_connector between two elements BOTH placed on the
140
245
  # current diagram, even when no t_diagramlinks entry exists
@@ -142,10 +247,15 @@ module Ea
142
247
  # renders as a straight line between the two element edges,
143
248
  # with SourceRole labels and appropriate markers.
144
249
  #
145
- # Only Association and Aggregation types produce phantom
146
- # connectors. Generalization, Dependency, and Package types
147
- # do NOT get phantom rendering.
148
- PHANTOM_CONNECTOR_TYPES = %w[Association Aggregation].freeze
250
+ # Dependency AND Generalization phantoms draw: Main/
251
+ # 9FCDB717 renders 12 of 13 model dependencies among the
252
+ # placed packages (the 13th suppressed by the diagram's
253
+ # single Hidden link row); All/F209CE4B renders all 11
254
+ # row-less generalizations as white triangles. A Hidden
255
+ # link row suppresses; absence of a row does not.
256
+ PHANTOM_CONNECTOR_TYPES = %w[Association Aggregation Dependency
257
+ Generalization].freeze
258
+ PHANTOM_DOUBLE_BACK_TYPES = %w[Dependency].freeze
149
259
 
150
260
  def phantom_connectors(diagram_row, explicit_connectors)
151
261
  placed_ids = placed_object_ids_on(diagram_row.diagram_id)
@@ -203,7 +313,7 @@ module Ea
203
313
  target_stereotype: conn.deststereotype,
204
314
  waypoints: waypoints,
205
315
  label_boxes: DEFAULT_LABEL_BOXES,
206
- style: {},
316
+ style: PHANTOM_DOUBLE_BACK_TYPES.include?(conn.connector_type) ? { direct: true } : {},
207
317
  hidden: false,
208
318
  has_geometry_offsets: false
209
319
  )
@@ -235,6 +345,8 @@ module Ea
235
345
  def build_connector(link_row, diagram_row)
236
346
  connector = database.find_connector(link_row.connectorid)
237
347
  geom = parse_geometry_fields(link_row.geometry)
348
+ context = ContextDiagramSupport.package_context(diagram_row) ||
349
+ ContextDiagramSupport.element_regenerate?(diagram_row)
238
350
  Ea::Model::DiagramConnector.new(
239
351
  id: IdNormalizer.synthetic("dc", diagram_row.diagram_id,
240
352
  link_row.instance_id),
@@ -244,17 +356,45 @@ module Ea
244
356
  direction: connector&.direction,
245
357
  source_stereotype: connector&.sourcestereotype,
246
358
  target_stereotype: connector&.deststereotype,
247
- waypoints: waypoints_for_link(link_row, diagram_row),
359
+ waypoints: context ? context_waypoints_for(connector, diagram_row) :
360
+ waypoints_for_link(link_row, diagram_row),
248
361
  source_port: connection_port(diagram_row, connector, :source, geom),
249
362
  target_port: connection_port(diagram_row, connector, :target, geom),
250
- label_boxes: geom[:label_boxes] || {},
251
- style: DiagramStyleParser.parse(link_row.style),
363
+ label_boxes: context ? {} : (geom[:label_boxes] || {}),
364
+ style: context ? { direct: true } :
365
+ DiagramStyleParser.parse(link_row.style),
252
366
  hidden: hidden?(link_row),
253
367
  ghost_labels: ghost_labels_for(connector, diagram_row, link_row),
254
368
  has_geometry_offsets: geometry_has_offsets?(link_row.geometry)
255
369
  )
256
370
  end
257
371
 
372
+ # Context diagrams re-route every drawn connector as a
373
+ # direct line at draw time: the center-to-center ray clipped
374
+ # at both box outlines. The stored SX/SY/EDGE geometry is
375
+ # stale on these diagrams (corpus-verified E58034A3: 22
376
+ # visible links render straight despite EDGE= geometry).
377
+ def context_waypoints_for(connector, diagram_row)
378
+ return [] unless connector
379
+
380
+ source = diagram_object_placement(diagram_row.diagram_id,
381
+ connector.start_object_id)
382
+ target = diagram_object_placement(diagram_row.diagram_id,
383
+ connector.end_object_id)
384
+ return [] unless source && target
385
+
386
+ direct_waypoints(bounds_from_rect(source), bounds_from_rect(target))
387
+ end
388
+
389
+ # Straight-line waypoints between the two box-outline
390
+ # crossings of the center-to-center ray.
391
+ def direct_waypoints(source_bounds, target_bounds)
392
+ s = bounds_edge_point(source_bounds, target_bounds)
393
+ t = bounds_edge_point(target_bounds, source_bounds)
394
+ [Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: s[0], y: s[1])),
395
+ Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: t[0], y: t[1]))]
396
+ end
397
+
258
398
  # EA's t_diagramlinks.Geometry may carry explicit SX/SY/EX/EY
259
399
  # offset fields (even zeros) OR omit them entirely. The
260
400
  # presence of SX= gates the «import» package_anchor marker:
@@ -62,9 +62,8 @@ module Ea
62
62
  target_multiplicity_upper: parse_upper(row.destcard),
63
63
  source_multiplicity_raw: raw_card(row.sourcecard),
64
64
  target_multiplicity_raw: raw_card(row.destcard),
65
- source_aggregation: ConnectorRelationshipMap
66
- .source_aggregation_for(row.connector_type),
67
- target_aggregation: aggregation_from(row.destaccess),
65
+ source_aggregation: source_aggregation_for(row),
66
+ target_aggregation: aggregation_from(row.destisaggregate),
68
67
  source_navigable: navigable?(row.sourceaccess, row.direction,
69
68
  :source),
70
69
  target_navigable: navigable?(row.destaccess, row.direction, :target)
@@ -112,15 +111,27 @@ module Ea
112
111
  end
113
112
  end
114
113
 
115
- def aggregation_from(access_value)
116
- case access_value.to_s
117
- when "0" then "none"
118
- when "1" then "shared"
119
- when "2" then "composite"
114
+ # EA stores the aggregate end in t_connector.SourceIsAggregate/
115
+ # DestIsAggregate (0 none, 1 shared, 2 composite) — NOT in
116
+ # DestAccess, which carries the accessibility string
117
+ # ("Public"). The connector TYPE only implies a source-side
118
+ # aggregation when the flags are silent (legacy
119
+ # Connector_Type encoding).
120
+ def aggregation_from(flag_value)
121
+ case flag_value.to_i
122
+ when 1 then "shared"
123
+ when 2 then "composite"
120
124
  else "none"
121
125
  end
122
126
  end
123
127
 
128
+ def source_aggregation_for(row)
129
+ flagged = aggregation_from(row.sourceisaggregate)
130
+ return flagged if flagged != "none"
131
+
132
+ ConnectorRelationshipMap.source_aggregation_for(row.connector_type)
133
+ end
134
+
124
135
  def navigable?(_access, direction, end_label)
125
136
  # EA's navigability is messy. Treat unspecified direction
126
137
  # as bidirectional, "Source -> Destination" as target-only
@@ -27,6 +27,7 @@ module Ea
27
27
  autoload :InstanceBuilder, "ea/sources/qea/instance_builder"
28
28
  autoload :LegendBuilder, "ea/sources/qea/legend_builder"
29
29
  autoload :DiagramBuilder, "ea/sources/qea/diagram_builder"
30
+ autoload :ContextDiagramSupport, "ea/sources/qea/context_diagram_support"
30
31
  autoload :DiagramStyleParser, "ea/sources/qea/diagram_style_parser"
31
32
  autoload :Xref, "ea/sources/qea/xref"
32
33
  autoload :Adapter, "ea/sources/qea/adapter"
@@ -62,9 +62,22 @@ module Ea
62
62
  x, y = translate_point(p)
63
63
  "#{idx.zero? ? 'M' : 'L'} #{Canvas.coord(x)} #{Canvas.coord(y)}"
64
64
  end.join(" ")
65
+ # EA writes a trailing zero-length "M <start>" repeat for
66
+ # DIRECT (auto-routed straight) connectors — verified on
67
+ # context diagrams where every line carries it (B6847577,
68
+ # E58034A3, 8CF27BE8).
69
+ if pts.size == 2 && direct?(connector)
70
+ x, y = translate_point(pts.first)
71
+ d = "#{d} M #{Canvas.coord(x)} #{Canvas.coord(y)}"
72
+ end
65
73
  %(<path d="#{d}" shape-rendering="auto"/>)
66
74
  end
67
75
 
76
+ def direct?(connector)
77
+ style = connector.style || {}
78
+ style.key?(:direct) || style.key?("direct")
79
+ end
80
+
68
81
  def waypoints_for(connector)
69
82
  (connector.waypoints || []).filter_map do |wp|
70
83
  next unless wp.position
@@ -16,6 +16,15 @@ module Ea
16
16
 
17
17
  def self.specs_for(connector, source, target, before_target,
18
18
  after_source, relationship: nil)
19
+ # EA draws the navigability arrow on directed
20
+ # associations AND aggregations (333EFCCF: 6 aggregations
21
+ # render diamond + path arrow); Direction
22
+ # "Unspecified"/empty draws a plain line (EC670ED5: the
23
+ # single Unspecified association is arrowless;
24
+ # C852CC42/TransferNode: 9 Unspecified all arrowless).
25
+ direction = connector.direction.to_s
26
+ return [] if direction.empty? || direction == "Unspecified"
27
+
19
28
  return bidirectional_specs(source, target, before_target,
20
29
  after_source) if bidirectional?(connector)
21
30
 
@@ -4,49 +4,59 @@ module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
6
  module Marker
7
- # Diamond marker at the whole end of an aggregation or
8
- # composition, PLUS a navigability arrow at the part end
9
- # when the connector's direction is "Destination -> Source".
10
- # Reference SVGs confirm EA emits BOTH markers for
11
- # reverse-direction aggregations on the plateau: the diamond
12
- # distinguishes aggregation from association, the arrow shows
13
- # navigability direction. Forward-direction ("Source ->
14
- # Destination") aggregations get the diamond only — verified
15
- # against basic.qea's "Two Level Class Composition Hierarchy"
16
- # diagram, where the reference SVG has 17 diamonds and 0
17
- # arrows.
7
+ # Diamond marker at the whole end of an aggregation, plus a
8
+ # navigability arrow apexing at the diamond's line-side tip.
9
+ #
10
+ # WHOLE-END SELECTION: EA flags the whole end either through
11
+ # t_connector.DestAccess (1 shared / 2 composite -> whole at
12
+ # the TARGET; corpus-verified 333EFCCF: PersonalRight_E is
13
+ # the DestAccess=2 end and every diamond sits there) or
14
+ # through the Connector_Type itself ("Aggregation"/
15
+ # "Composition" -> whole at the SOURCE; basic.qea's
16
+ # composition diagram). DestAccess wins when both appear.
17
+ # Without relationship data the direction heuristic applies
18
+ # ("Destination -> Source" -> whole at target).
19
+ #
20
+ # ARROW: dest-flagged aggregations draw a base-12 height-15
21
+ # open arrow stacked on the diamond tip pointing into the
22
+ # whole (333EFCCF: 6 diamonds + 6 path arrows). Type-inferred
23
+ # source-side aggregations draw the diamond only (basic.qea:
24
+ # 17 diamonds, 0 arrows).
18
25
  class Diamond < Kind
19
26
  def self.handles?(effective_type)
20
27
  effective_type == "Aggregation" || effective_type == "Composition"
21
28
  end
22
29
 
23
30
  def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
24
- whole_end_at_source = whole_end_at_source?(connector)
25
- whole_anchor = whole_end_at_source ? source : target
26
- whole_base = whole_end_at_source ? after_source : before_target
31
+ whole_at_target = whole_at_target?(connector, relationship)
32
+ whole_anchor = whole_at_target ? target : source
33
+ whole_base = whole_at_target ? before_target : after_source
27
34
  specs = [Registry::Spec.new(shape: :diamond,
28
35
  anchor: whole_anchor,
29
36
  base: whole_base)]
30
- return specs unless navigable_part_end?(connector)
37
+ return specs unless whole_at_target
31
38
 
32
- part_anchor = whole_end_at_source ? target : source
33
- part_base = whole_end_at_source ? before_target : after_source
34
39
  specs << Registry::Spec.new(shape: :arrow,
35
- anchor: part_anchor,
36
- base: part_base)
40
+ anchor: whole_anchor,
41
+ base: whole_base)
37
42
  specs
38
43
  end
39
44
 
40
- # EA renders the navigability arrow at the part end of an
41
- # aggregation only when the connector's direction is
42
- # "Destination -> Source" — the conventional EA encoding
43
- # for "part -> whole" with navigability back to the whole.
44
- # Forward-direction aggregations ("Source -> Destination")
45
- # omit the arrow: the diamond already communicates the
46
- # aggregation semantic and the destination is not
47
- # explicitly navigable.
48
- def self.navigable_part_end?(connector)
49
- connector.direction == "Destination -> Source"
45
+ def self.whole_at_target?(connector, relationship)
46
+ return true if dest_flagged?(relationship)
47
+ return false if source_flagged?(relationship)
48
+
49
+ !whole_end_at_source?(connector)
50
+ end
51
+
52
+ def self.dest_flagged?(relationship)
53
+ relationship &&
54
+ %w[shared composite].include?(relationship.target_aggregation.to_s)
55
+ end
56
+
57
+ def self.source_flagged?(relationship)
58
+ relationship &&
59
+ %w[shared composite].include?(relationship.source_aggregation.to_s)
50
60
  end
51
61
  end
52
62
  end
@@ -14,14 +14,19 @@ module Ea
14
14
  # the diagram's generalizations use polygons.
15
15
  class OpenTriangle < Kind
16
16
  def self.handles?(effective_type)
17
- %w[Generalization Realization Dependency].include?(effective_type)
17
+ %w[Generalization Realization Realisation Dependency
18
+ InformationFlow NoteLink].include?(effective_type)
18
19
  end
19
20
 
20
21
  def self.specs_for(connector, source, target, before_target, after_source, relationship: nil)
21
22
  whole_end_at_source = whole_end_at_source?(connector)
22
23
  anchor = whole_end_at_source ? target : source
23
24
  base = whole_end_at_source ? before_target : after_source
24
- shape = effective_type?(connector) == "Dependency" ? :arrow : :triangle
25
+ shape = if %w[Dependency InformationFlow].include?(effective_type?(connector))
26
+ :dependency_arrow
27
+ else
28
+ :triangle
29
+ end
25
30
  [Registry::Spec.new(shape: shape, anchor: anchor, base: base)]
26
31
  end
27
32
 
@@ -21,8 +21,15 @@ module Ea
21
21
  DIAMOND_HALF_H = 10
22
22
  TRI_HALF_BASE = 6
23
23
  TRI_HEIGHT = 11
24
+ # EA's path arrowheads are height 15 corpus-wide, with
25
+ # distinct bases per connector kind: association
26
+ # navigability arrows base 12, dependency arrows base 10
27
+ # (measured across E58034A3/8CF27BE8 context diagrams and
28
+ # 46217EA4/C2B04372/7A3B5001 dependency figures).
24
29
  ARROW_HALF_BASE = 6
25
- ARROW_HEIGHT = 11
30
+ ARROW_HEIGHT = 15
31
+ DEP_ARROW_HALF_BASE = 5
32
+ DEP_ARROW_HEIGHT = 15
26
33
 
27
34
  attr_reader :diagram, :model_index, :canvas, :grouped, :stroke_width
28
35
 
@@ -109,6 +116,10 @@ module Ea
109
116
  style_key: :connector_line,
110
117
  render: ->(m, anchor, base) { m.arrow_path(anchor, base) }
111
118
  },
119
+ dependency_arrow: {
120
+ style_key: :connector_line,
121
+ render: ->(m, anchor, base) { m.dependency_arrow_path(anchor, base) }
122
+ },
112
123
  package_anchor: {
113
124
  style_key: :connector_line,
114
125
  render: ->(m, anchor, base) { m.package_anchor_path(anchor, base) }
@@ -403,6 +414,28 @@ module Ea
403
414
  %(<path d="#{d}" shape-rendering="auto"/>)
404
415
  end
405
416
 
417
+ # Dependency arrowhead: same open-path form as arrow_path
418
+ # but EA's slimmer base-10 template. Coordinates are whole
419
+ # pixels in EA's output (e.g. "M 2732 183 L 2747 178
420
+ # L 2732 173").
421
+ def dependency_arrow_path(tip, base)
422
+ tx, ty = translate_point(tip)
423
+ bx, by = translate_point(base)
424
+ ux, uy = unit_vector(tx, ty, bx, by)
425
+ return nil unless ux
426
+
427
+ back_x = tx - ux * DEP_ARROW_HEIGHT
428
+ back_y = ty - uy * DEP_ARROW_HEIGHT
429
+ w1_x = back_x + (-uy) * DEP_ARROW_HALF_BASE
430
+ w1_y = back_y + ux * DEP_ARROW_HALF_BASE
431
+ w2_x = back_x - (-uy) * DEP_ARROW_HALF_BASE
432
+ w2_y = back_y - ux * DEP_ARROW_HALF_BASE
433
+ d = "M #{Canvas.coord(w1_x.round)} #{Canvas.coord(w1_y.round)} " \
434
+ "L #{Canvas.coord(tx.round)} #{Canvas.coord(ty.round)} " \
435
+ "L #{Canvas.coord(w2_x.round)} #{Canvas.coord(w2_y.round)}"
436
+ %(<path d="#{d}" shape-rendering="auto"/>)
437
+ end
438
+
406
439
  def unit_vector(tx, ty, bx, by)
407
440
  dx = tx - bx
408
441
  dy = ty - by
@@ -412,7 +445,7 @@ module Ea
412
445
  [dx / len, dy / len]
413
446
  end
414
447
  public :diamond_polygon, :triangle_polygon, :plus_path,
415
- :arrow_path, :package_anchor_path
448
+ :arrow_path, :dependency_arrow_path, :package_anchor_path
416
449
 
417
450
  def translate_point(p)
418
451
  return p unless @canvas
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.29"
7
+ VERSION = "0.6.31"
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.29
4
+ version: 0.6.31
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-03 00:00:00.000000000 Z
11
+ date: 2026-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -813,6 +813,7 @@ files:
813
813
  - lib/ea/sources/qea/annotation_builder.rb
814
814
  - lib/ea/sources/qea/classifier_builder.rb
815
815
  - lib/ea/sources/qea/connector_relationship_map.rb
816
+ - lib/ea/sources/qea/context_diagram_support.rb
816
817
  - lib/ea/sources/qea/diagram_builder.rb
817
818
  - lib/ea/sources/qea/diagram_style_parser.rb
818
819
  - lib/ea/sources/qea/id_normalizer.rb