ea 0.6.29 → 0.6.30

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: d0b7f42f300df411de328df0ab9c076927ef919fbe723cf262310107f1c324ba
4
+ data.tar.gz: 27d5559ae59009bb83a9fcf3d806354fe4300c52e7dc736820d798823fb578f4
5
5
  SHA512:
6
- metadata.gz: 151b418a45c19c998c41957ddb471127f6e110689372a01e046152024c271b10ccb069af527c877ee6404cfe22c0323f55232e9140ba5a4882b382534a26b4b6
7
- data.tar.gz: d2b597bfebed6432a78285f8898d73ef6cfdbbf65362132eed375b387bdc0aa1cfb40ceddabea22695060b8467e70c94768618b7a102bca556f0ab5351882955
6
+ metadata.gz: '09b19a0e6f50428e7840bd962f741753b8512cc9d41cd00954a86467cf6e4965e2016e903a9f496162d63bc98415ab7c3b74b9c8e81c371e027a4074c48c7222'
7
+ data.tar.gz: eaa09bf8b99d624cb785581fa5c674c9cd2f37cbfac9a7554d4cfd21859f84edfe9643a6cc8ad1863b358b74c46bf656cf7ec4be5549796ed4f1f275c36d1f1d
@@ -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
@@ -235,6 +340,8 @@ module Ea
235
340
  def build_connector(link_row, diagram_row)
236
341
  connector = database.find_connector(link_row.connectorid)
237
342
  geom = parse_geometry_fields(link_row.geometry)
343
+ context = ContextDiagramSupport.package_context(diagram_row) ||
344
+ ContextDiagramSupport.element_regenerate?(diagram_row)
238
345
  Ea::Model::DiagramConnector.new(
239
346
  id: IdNormalizer.synthetic("dc", diagram_row.diagram_id,
240
347
  link_row.instance_id),
@@ -244,17 +351,45 @@ module Ea
244
351
  direction: connector&.direction,
245
352
  source_stereotype: connector&.sourcestereotype,
246
353
  target_stereotype: connector&.deststereotype,
247
- waypoints: waypoints_for_link(link_row, diagram_row),
354
+ waypoints: context ? context_waypoints_for(connector, diagram_row) :
355
+ waypoints_for_link(link_row, diagram_row),
248
356
  source_port: connection_port(diagram_row, connector, :source, geom),
249
357
  target_port: connection_port(diagram_row, connector, :target, geom),
250
- label_boxes: geom[:label_boxes] || {},
251
- style: DiagramStyleParser.parse(link_row.style),
358
+ label_boxes: context ? {} : (geom[:label_boxes] || {}),
359
+ style: context ? { direct: true } :
360
+ DiagramStyleParser.parse(link_row.style),
252
361
  hidden: hidden?(link_row),
253
362
  ghost_labels: ghost_labels_for(connector, diagram_row, link_row),
254
363
  has_geometry_offsets: geometry_has_offsets?(link_row.geometry)
255
364
  )
256
365
  end
257
366
 
367
+ # Context diagrams re-route every drawn connector as a
368
+ # direct line at draw time: the center-to-center ray clipped
369
+ # at both box outlines. The stored SX/SY/EDGE geometry is
370
+ # stale on these diagrams (corpus-verified E58034A3: 22
371
+ # visible links render straight despite EDGE= geometry).
372
+ def context_waypoints_for(connector, diagram_row)
373
+ return [] unless connector
374
+
375
+ source = diagram_object_placement(diagram_row.diagram_id,
376
+ connector.start_object_id)
377
+ target = diagram_object_placement(diagram_row.diagram_id,
378
+ connector.end_object_id)
379
+ return [] unless source && target
380
+
381
+ direct_waypoints(bounds_from_rect(source), bounds_from_rect(target))
382
+ end
383
+
384
+ # Straight-line waypoints between the two box-outline
385
+ # crossings of the center-to-center ray.
386
+ def direct_waypoints(source_bounds, target_bounds)
387
+ s = bounds_edge_point(source_bounds, target_bounds)
388
+ t = bounds_edge_point(target_bounds, source_bounds)
389
+ [Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: s[0], y: s[1])),
390
+ Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: t[0], y: t[1]))]
391
+ end
392
+
258
393
  # EA's t_diagramlinks.Geometry may carry explicit SX/SY/EX/EY
259
394
  # offset fields (even zeros) OR omit them entirely. The
260
395
  # presence of SX= gates the «import» package_anchor marker:
@@ -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
@@ -21,7 +21,7 @@ module Ea
21
21
  whole_end_at_source = whole_end_at_source?(connector)
22
22
  anchor = whole_end_at_source ? target : source
23
23
  base = whole_end_at_source ? before_target : after_source
24
- shape = effective_type?(connector) == "Dependency" ? :arrow : :triangle
24
+ shape = effective_type?(connector) == "Dependency" ? :dependency_arrow : :triangle
25
25
  [Registry::Spec.new(shape: shape, anchor: anchor, base: base)]
26
26
  end
27
27
 
@@ -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.30"
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.30
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