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 +4 -4
- data/lib/ea/sources/qea/context_diagram_support.rb +218 -0
- data/lib/ea/sources/qea/diagram_builder.rb +139 -4
- data/lib/ea/sources/qea.rb +1 -0
- data/lib/ea/svg/ea_emitter/connectors.rb +13 -0
- data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +1 -1
- data/lib/ea/svg/ea_emitter/markers.rb +35 -2
- data/lib/ea/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0b7f42f300df411de328df0ab9c076927ef919fbe723cf262310107f1c324ba
|
|
4
|
+
data.tar.gz: 27d5559ae59009bb83a9fcf3d806354fe4300c52e7dc736820d798823fb578f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
data/lib/ea/sources/qea.rb
CHANGED
|
@@ -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" ? :
|
|
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 =
|
|
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
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.
|
|
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-
|
|
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
|