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 +4 -4
- data/lib/ea/sources/qea/context_diagram_support.rb +218 -0
- data/lib/ea/sources/qea/diagram_builder.rb +149 -9
- data/lib/ea/sources/qea/relationship_builder.rb +19 -8
- 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/arrow_path.rb +9 -0
- data/lib/ea/svg/ea_emitter/marker/diamond.rb +39 -29
- data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +7 -2
- 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: da25cfb5a4f0690b2b73df2d3c12fe3cd042c96fe6aab67d07e2c8871868feaf
|
|
4
|
+
data.tar.gz: 8be40d4bb501b904f1ba2609446d332ea260f3d04a831528d213d6747e352dfa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
66
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
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
|
|
@@ -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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# diagram
|
|
17
|
-
#
|
|
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
|
-
|
|
25
|
-
whole_anchor =
|
|
26
|
-
whole_base =
|
|
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
|
|
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:
|
|
36
|
-
base:
|
|
40
|
+
anchor: whole_anchor,
|
|
41
|
+
base: whole_base)
|
|
37
42
|
specs
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
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.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-
|
|
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
|