ea 0.6.136 → 0.6.138
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/fonts/cambria_7_regular_widths.json +1 -0
- data/lib/ea/model/diagram_connector.rb +4 -0
- data/lib/ea/sources/xmi/diagram_builder/association_class_metadata.rb +127 -0
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges/property_roles.rb +71 -0
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges.rb +56 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +203 -14
- data/lib/ea/sources/xmi/extension_elements.rb +5 -4
- data/lib/ea/sources/xmi/relationship_builder.rb +20 -2
- data/lib/ea/sources/xmi/stored_route.rb +194 -1
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +3 -2
- data/lib/ea/svg/ea_emitter/connector_redock.rb +2 -1
- data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +10 -5
- data/lib/ea/svg/ea_emitter/label/end_label.rb +119 -20
- data/lib/ea/svg/ea_emitter/labels.rb +15 -2
- data/lib/ea/svg/ea_emitter/render_context.rb +7 -2
- data/lib/ea/theme/definition.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
|
@@ -45,12 +45,30 @@ module Ea
|
|
|
45
45
|
def package_import_relationships(built)
|
|
46
46
|
built_ids = built.to_set(&:id)
|
|
47
47
|
connector_index.filter_map do |id, connector|
|
|
48
|
-
next if built_ids.include?(id)
|
|
48
|
+
next if built_ids.include?(id)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
extension_import_relationship(id, connector)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def extension_import_relationship(id, connector)
|
|
55
|
+
case connector.properties&.ea_type
|
|
56
|
+
when "Package" then package_import_relationship(id, connector)
|
|
57
|
+
when "Dependency"
|
|
58
|
+
dependency_import_relationship(id, connector) if connector_stereotype(connector) == "import"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# EA exports elementImport outside packagedElement, so the model walk
|
|
63
|
+
# misses it; the extension retains the importing/imported endpoints.
|
|
64
|
+
def dependency_import_relationship(id, connector)
|
|
65
|
+
Ea::Model::Dependency.new(
|
|
66
|
+
id: id, stereotype: "import",
|
|
67
|
+
client_id: connector.source&.idref,
|
|
68
|
+
supplier_id: connector.target&.idref
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
54
72
|
def connector_stereotype(connector)
|
|
55
73
|
connector&.xrefs&.value.to_s[CONNECTOR_STEREOTYPE]
|
|
56
74
|
end
|
|
@@ -26,8 +26,174 @@ module Ea
|
|
|
26
26
|
finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Direct custom-line associations and aggregations retain ray origins as
|
|
30
|
+
# well as their computed boundary ports, just as the QEA builder does.
|
|
31
|
+
def ray_metadata(model, link:, bounds:, offsets:, edge:)
|
|
32
|
+
return {} unless direct_association_ray?(model, bounds, offsets)
|
|
33
|
+
|
|
34
|
+
source, target = bounds.values_at(:source, :target)
|
|
35
|
+
geom = ray_geometry(offsets, edge)
|
|
36
|
+
return {} unless saved_association_ray?(model, link, source, target, geom)
|
|
37
|
+
|
|
38
|
+
ray_points(source, target, offsets, geom)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def phantom_waypoints(source, target)
|
|
42
|
+
[bounds_edge_point(source, target), bounds_edge_point(target, source)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Apply QEA's saved Association stages after the initial route, including
|
|
46
|
+
# UMLDI fallback. A single stored corner and an empty-path stored ray
|
|
47
|
+
# carry mathematical origins that the generic facing-edge route loses.
|
|
48
|
+
def association_route(model, link:, bounds:, offsets:, edge:)
|
|
49
|
+
overlap = umldi_association_metadata(model, link, bounds, offsets, edge)
|
|
50
|
+
return overlap unless overlap.empty?
|
|
51
|
+
|
|
52
|
+
partial = partial_port_metadata(model, link, bounds, offsets, edge)
|
|
53
|
+
return partial unless partial.empty?
|
|
54
|
+
|
|
55
|
+
return {} unless stored_association_candidate?(model, bounds)
|
|
56
|
+
|
|
57
|
+
candidate = association_model(model, bounds, offsets)
|
|
58
|
+
initial = candidate.waypoints
|
|
59
|
+
association_stored_ray(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
60
|
+
return {} if candidate.waypoints.equal?(initial)
|
|
61
|
+
|
|
62
|
+
association_metadata(candidate)
|
|
63
|
+
end
|
|
64
|
+
|
|
29
65
|
private
|
|
30
66
|
|
|
67
|
+
def umldi_association_metadata(model, link, bounds, offsets, edge)
|
|
68
|
+
overlap = umldi_overlap_metadata(model, link, bounds, offsets, edge)
|
|
69
|
+
return overlap unless overlap.empty?
|
|
70
|
+
|
|
71
|
+
umldi_stored_ray_metadata(model, link, bounds, offsets, edge)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# UMLDI owns the route and ports. Classify a thin saved ray only when
|
|
75
|
+
# its origins pass the shared unstroked-box checks; XMI has no QEA
|
|
76
|
+
# database from which to infer a wider element outline.
|
|
77
|
+
def umldi_stored_ray_metadata(model, link, bounds, offsets, edge)
|
|
78
|
+
return {} unless umldi_thin_ray_candidate?(model, bounds, offsets)
|
|
79
|
+
|
|
80
|
+
candidate = association_model(model, bounds, offsets)
|
|
81
|
+
return {} unless association_ray_candidate?(candidate, link)
|
|
82
|
+
return {} unless association_ray_origins(*bounds.values_at(:source, :target),
|
|
83
|
+
ray_geometry(offsets, edge), "0")
|
|
84
|
+
|
|
85
|
+
{ style: model.style.to_h.merge(stored_association_ray: true, association_ray_outline: true) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def umldi_thin_ray_candidate?(model, bounds, offsets)
|
|
89
|
+
model.style[:umldi_route] && model.style[:lwidth] == "0" &&
|
|
90
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# UMLDI owns the drawn route; retain QEA's overlap origins only for
|
|
94
|
+
# interpreting the stored relative end labels against that route.
|
|
95
|
+
def umldi_overlap_metadata(model, link, bounds, offsets, edge)
|
|
96
|
+
return {} unless umldi_overlap_candidate?(model, bounds, offsets)
|
|
97
|
+
|
|
98
|
+
candidate = association_model(model, bounds, offsets)
|
|
99
|
+
overlap_ray_origins(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
100
|
+
return {} unless candidate.style[:overlap_ray]
|
|
101
|
+
|
|
102
|
+
{ source_anchor: candidate.source_anchor, target_anchor: candidate.target_anchor,
|
|
103
|
+
has_geometry_offsets: true, style: candidate.style.to_h }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def umldi_overlap_candidate?(model, bounds, offsets)
|
|
107
|
+
model.style.to_h.values_at(:umldi_route, :mode, :lwidth) == [true, "2", "0"] &&
|
|
108
|
+
model.waypoints.size == 2 && bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A zero-offset end has no explicit port, but QEA still carries both
|
|
112
|
+
# mathematical origins for the emitter's final boundary redocking.
|
|
113
|
+
def partial_port_metadata(model, link, bounds, offsets, edge)
|
|
114
|
+
return {} unless partial_port_candidate?(model, link, bounds, offsets)
|
|
115
|
+
|
|
116
|
+
source, target = bounds.values_at(:source, :target)
|
|
117
|
+
return {} unless saved_association_ray?(model, link, source, target, ray_geometry(offsets, edge))
|
|
118
|
+
|
|
119
|
+
association_ends(bounds, offsets).merge(has_geometry_offsets: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def partial_port_candidate?(model, link, bounds, offsets)
|
|
123
|
+
model.connector_type == "Aggregation" && !model.style[:umldi_route] && !link.path.to_s.empty? &&
|
|
124
|
+
direct_association_ray?(model, bounds, offsets) &&
|
|
125
|
+
offsets.values.count { |offset| offset.all?(&:zero?) } == 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def stored_association_candidate?(model, bounds)
|
|
129
|
+
model.connector_type == "Association" && bounds.values.all? &&
|
|
130
|
+
association_ray_style?(model.style.to_h) && model.style[:lwidth] == "2"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def association_metadata(model)
|
|
134
|
+
fields = %i[waypoints source_anchor target_anchor source_port target_port has_geometry_offsets]
|
|
135
|
+
fields.to_h { |name| [name, model.public_send(name)] }.merge(
|
|
136
|
+
style: model.style.to_h.reject { |key, _| key == :umldi_route }
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def association_model(model, bounds, offsets)
|
|
141
|
+
Ea::Model::DiagramConnector.new(
|
|
142
|
+
{ connector_type: model.connector_type, style: model.style.to_h.dup, waypoints: model.waypoints,
|
|
143
|
+
has_geometry_offsets: !offsets[:source][0].nil? }.merge(association_ends(bounds, offsets))
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def association_ends(bounds, offsets)
|
|
148
|
+
%i[source target].each_with_object({}) do |kind, result|
|
|
149
|
+
box = bounds.fetch(kind)
|
|
150
|
+
offset = offsets.fetch(kind)
|
|
151
|
+
port = port_for(box, offset)
|
|
152
|
+
result["#{kind}_port".to_sym] = point_for(port) if port
|
|
153
|
+
result["#{kind}_anchor".to_sym] = association_anchor(box, offset)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def association_anchor(box, offset)
|
|
158
|
+
Ea::Model::Point.new(x: box.x + box.width / 2 + offset[0].to_i,
|
|
159
|
+
y: box.y + box.height / 2 + offset[1].to_i)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def stored_ray_boxes(_link, bounds)
|
|
163
|
+
bounds.values_at(:source, :target)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def direct_association_ray?(model, bounds, offsets)
|
|
167
|
+
%w[Association Aggregation].include?(model.connector_type) && model.waypoints.size == 2 &&
|
|
168
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["1", "2", nil] &&
|
|
169
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ray_geometry(offsets, edge)
|
|
173
|
+
{ sx: offsets[:source][0], sy: offsets[:source][1],
|
|
174
|
+
ex: offsets[:target][0], ey: offsets[:target][1], edge: edge }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def saved_association_ray?(model, link, source, target, geom)
|
|
178
|
+
(!geom[:sy].zero? || !geom[:ey].zero?) &&
|
|
179
|
+
stored_ray_gap?(source, target, "1", geom[:edge]) &&
|
|
180
|
+
stored_association_path?(model, link, source, target, geom)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def ray_points(source, target, offsets, geom)
|
|
184
|
+
ports = [port_for(source, offsets[:source]), port_for(target, offsets[:target])]
|
|
185
|
+
return {} unless ports.all?
|
|
186
|
+
|
|
187
|
+
{ source_port: point_for(ports.first), target_port: point_for(ports.last),
|
|
188
|
+
source_anchor: stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
189
|
+
target_anchor: stored_ray_origin(target, geom[:ex], geom[:ey]),
|
|
190
|
+
has_geometry_offsets: true }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def point_for(pair)
|
|
194
|
+
Ea::Model::Point.new(x: pair[0], y: pair[1])
|
|
195
|
+
end
|
|
196
|
+
|
|
31
197
|
# Placements here are already Ea::Model::Bounds.
|
|
32
198
|
def bounds_from_rect(bounds)
|
|
33
199
|
bounds
|
|
@@ -64,10 +230,37 @@ module Ea
|
|
|
64
230
|
|
|
65
231
|
def bent_points(src, bends, tgt, link, connector, source, target)
|
|
66
232
|
points = [src, *bends, tgt]
|
|
67
|
-
|
|
233
|
+
if connector.connector_type == "Generalization"
|
|
234
|
+
preserve_side_entry = authored_lateral_side_entry?(points, bends, link, source, target)
|
|
235
|
+
redock_parent_above(points, link, source, target)
|
|
236
|
+
points[-1] = [points[-1][0], points[1][1]] if preserve_side_entry
|
|
237
|
+
end
|
|
68
238
|
points
|
|
69
239
|
end
|
|
70
240
|
|
|
241
|
+
# A single LH tree bend can be the authored side-entry level itself.
|
|
242
|
+
# Parent-above redocking normally moves a Generalization to the target
|
|
243
|
+
# bottom, which destroys that horizontal leg and disagrees with QEA.
|
|
244
|
+
def authored_lateral_side_entry?(points, bends, link, source, target)
|
|
245
|
+
return false unless bends.one? && points.size == 3 && lateral_side_entry_style?(link) &&
|
|
246
|
+
target.y + target.height <= source.y
|
|
247
|
+
|
|
248
|
+
level_on_target_side?(points[1], target)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def lateral_side_entry_style?(link)
|
|
252
|
+
style = Qea::DiagramStyleParser.parse(link.style)
|
|
253
|
+
style[:mode] == "3" && style[:tree] == "LH" && style[:lwidth] == "2"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def within_width?(x, bounds)
|
|
257
|
+
x.between?(bounds.x, bounds.x + bounds.width)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def level_on_target_side?(bend, target)
|
|
261
|
+
bend[1].between?(target.y, target.y + target.height) && !within_width?(bend[0], target)
|
|
262
|
+
end
|
|
263
|
+
|
|
71
264
|
def finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
72
265
|
tree_route = stored_tree_route(points, link, connector, source, target, ports)
|
|
73
266
|
if tree_route
|
|
@@ -94,8 +94,9 @@ module Ea
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def content_x_offset(theme, size, size_unit, family)
|
|
97
|
-
# EA
|
|
98
|
-
|
|
97
|
+
# EA seats literal names at the attribute-name content x, with the
|
|
98
|
+
# attribute row point-size increment.
|
|
99
|
+
offset = theme.attribute_content_x_offset || 26
|
|
99
100
|
offset += 2 * (size - 7) if size_unit == "pt"
|
|
100
101
|
offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
|
|
101
102
|
offset
|
|
@@ -19,11 +19,12 @@ module Ea
|
|
|
19
19
|
# self-loop, an authoritative UMLDI route, or no drawn bounds for either end).
|
|
20
20
|
def pairs_for(connector, diagram, model_index, bounds_map)
|
|
21
21
|
return nil unless bounds_map
|
|
22
|
-
return nil if connector.style.to_h[:umldi_route] == true
|
|
23
22
|
|
|
24
23
|
tether = association_class_pairs(connector, bounds_map)
|
|
25
24
|
return tether if tether
|
|
26
25
|
|
|
26
|
+
return nil if connector.style.to_h[:umldi_route] == true
|
|
27
|
+
|
|
27
28
|
points = waypoint_pairs(connector)
|
|
28
29
|
lateral = lateral_tree?(connector, points)
|
|
29
30
|
trunk = trunk_tree?(connector, points)
|
|
@@ -19,10 +19,10 @@ module Ea
|
|
|
19
19
|
def self.render(tagged_values, bounds:, first_y:, family:, size:, fill: "#000000",
|
|
20
20
|
size_unit: "px", logical_height: nil)
|
|
21
21
|
line_h = line_height(size, family, size_unit, logical_height)
|
|
22
|
-
text_blocks = [build_text(
|
|
22
|
+
text_blocks = [build_text(header_x(bounds, family, size, size_unit, logical_height),
|
|
23
23
|
first_y, HEADER_TEXT, family, size, fill,
|
|
24
24
|
style: "italic", size_unit: size_unit, logical_height: logical_height)]
|
|
25
|
-
tagged_values.each_with_index do |tv, idx|
|
|
25
|
+
ordered_values(tagged_values).each_with_index do |tv, idx|
|
|
26
26
|
text_blocks << build_text(bounds.x + LINE_X_OFFSET, first_y + ((idx + 1) * line_h),
|
|
27
27
|
"#{tv.key} = #{tv.value}", family, size, fill,
|
|
28
28
|
size_unit: size_unit, logical_height: logical_height)
|
|
@@ -30,6 +30,11 @@ module Ea
|
|
|
30
30
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def self.ordered_values(tagged_values)
|
|
34
|
+
tagged_values.sort_by { |tv| tv.key.to_s.downcase }
|
|
35
|
+
end
|
|
36
|
+
private_class_method :ordered_values
|
|
37
|
+
|
|
33
38
|
def self.build_text(x, y, content, family, size, fill, style: "normal",
|
|
34
39
|
size_unit: "px", logical_height: nil)
|
|
35
40
|
TextRenderer.new(content: content, x: x, y: y,
|
|
@@ -48,7 +53,7 @@ module Ea
|
|
|
48
53
|
private_class_method :line_height
|
|
49
54
|
|
|
50
55
|
# Center the "tags" header text within the bounds width.
|
|
51
|
-
def self.
|
|
56
|
+
def self.header_x(bounds, family, size, size_unit, logical_height)
|
|
52
57
|
# QEA point captions center on the same integer width emitted in SVG.
|
|
53
58
|
# Retain the legacy pixel API's placement.
|
|
54
59
|
text_width = if size_unit == "pt"
|
|
@@ -60,9 +65,9 @@ module Ea
|
|
|
60
65
|
HEADER_TEXT.length * (size / 2)
|
|
61
66
|
end
|
|
62
67
|
centered = ((bounds.width - text_width) / 2.0).floor
|
|
63
|
-
[HEADER_X_OFFSET, centered].max
|
|
68
|
+
bounds.x + [HEADER_X_OFFSET, centered].max
|
|
64
69
|
end
|
|
65
|
-
private_class_method :
|
|
70
|
+
private_class_method :header_x
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
|
@@ -22,9 +22,11 @@ module Ea
|
|
|
22
22
|
SEAT_FONT_SIZE = 7
|
|
23
23
|
SEAT_X_INSET = 3
|
|
24
24
|
SEAT_STEREOTYPE_GAP = 12
|
|
25
|
-
# EA keeps an
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# EA keeps an end-label role's left edge at diagram x >= 3; the
|
|
26
|
+
# stereotype line moves with the role. Measured on the Plateau
|
|
27
|
+
# v5.1 export: of 776 saved end labels, the only three whose
|
|
28
|
+
# UMLDI bounds start left of 3 (47E4600F, CB34B578, 159E9DA0) are
|
|
29
|
+
# drawn at x = 3, and no label is drawn left of it.
|
|
28
30
|
MIN_ROLE_X = 3
|
|
29
31
|
|
|
30
32
|
attr_reader :canvas, :model_index, :document, :theme,
|
|
@@ -61,12 +63,13 @@ module Ea
|
|
|
61
63
|
# fallback
|
|
62
64
|
def texts(text_box:, mult_box:, anchor:, connector:, end_kind:,
|
|
63
65
|
tree: false, heading: nil, stored_labels: false, bend: nil, source_bounds: nil,
|
|
64
|
-
target_face: nil)
|
|
66
|
+
target_face: nil, umldi_bounds: {})
|
|
65
67
|
from_property, role, mult = role_and_multiplicity(connector, end_kind)
|
|
66
68
|
if role_empty?(role) && mult_empty?(mult)
|
|
67
69
|
other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
|
|
68
70
|
opposite(end_kind))
|
|
69
|
-
if stored_labels
|
|
71
|
+
if stored_labels || connector.style.to_h[:explicit_end_roles] ||
|
|
72
|
+
connector.style.to_h[:phantom_outline] == true
|
|
70
73
|
return [] if role_empty?(other_role) && mult_empty?(other_mult)
|
|
71
74
|
|
|
72
75
|
return own_stereotype_only(text_box, anchor, connector,
|
|
@@ -104,7 +107,7 @@ module Ea
|
|
|
104
107
|
plain_vertical_role(connector, direction, anchor, text_box, role) ||
|
|
105
108
|
source_property_seat(connector, direction, anchor, text_box, role) ||
|
|
106
109
|
plain_upward_seat(connector, direction, anchor, text_box, role) ||
|
|
107
|
-
role_seat(direction, anchor, text_box, role) || text_pos
|
|
110
|
+
role_seat(connector, direction, anchor, text_box, role) || text_pos
|
|
108
111
|
mult_pos = directional_multiplicity(direction, anchor, mult_box, mult) ||
|
|
109
112
|
horizontal_tree_multiplicity(connector, direction, anchor, mult_box, mult) ||
|
|
110
113
|
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
@@ -149,12 +152,53 @@ module Ea
|
|
|
149
152
|
text_pos, mult_pos = stair_positions if stair_positions
|
|
150
153
|
end
|
|
151
154
|
|
|
155
|
+
role_bounds = umldi_bounds[end_kind == :source ? :source_role : :target_role]
|
|
156
|
+
mult_bounds = umldi_bounds[end_kind == :source ? :source_multiplicity : :target_multiplicity]
|
|
157
|
+
text_pos = umldi_label_position(role_bounds) if role_bounds
|
|
158
|
+
centering = role_bounds ? bounds_role_centering(connector, end_kind, role, stereo, text_box) : 0
|
|
159
|
+
text_pos = [text_pos[0] + centering, text_pos[1]] if centering.positive?
|
|
160
|
+
mult_pos = umldi_label_position(mult_bounds) if mult_bounds
|
|
161
|
+
if stereo && role_bounds
|
|
162
|
+
stereo_pos = umldi_stereotype_position(role_bounds, role, stereo, text_box)
|
|
163
|
+
stereo_pos = [role_bounds[:x], stereo_pos[1]] if centering.positive?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if text_pos && !role_empty?(role) && min_role_x_cluster?(connector, end_kind, heading, text_box)
|
|
167
|
+
text_pos, stereo_pos = clamp_role_x(text_pos, stereo_pos)
|
|
168
|
+
end
|
|
169
|
+
|
|
152
170
|
build_texts(role:, mult:, standalone:,
|
|
153
|
-
text_pos:, mult_pos:, stereo:, text_box:)
|
|
171
|
+
text_pos:, mult_pos:, stereo:, text_box:, stereo_pos:)
|
|
154
172
|
end
|
|
155
173
|
|
|
156
174
|
private
|
|
157
175
|
|
|
176
|
+
# Measured only for default Cambria 7pt Property roles on a vertical
|
|
177
|
+
# leg rising from the end; other fonts, boxes and legs keep their
|
|
178
|
+
# unclamped seats.
|
|
179
|
+
def min_role_x_cluster?(connector, end_kind, heading, text_box)
|
|
180
|
+
rising_vertical?(heading) && plain_seat_font? &&
|
|
181
|
+
cambria_box?(text_box) && property_label?(connector, end_kind)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def rising_vertical?(heading)
|
|
185
|
+
heading && heading[0].zero? && heading[1].negative?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def plain_seat_font?
|
|
189
|
+
seat_font? && font_weight == 400 && font_style == "normal"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Shift the role and its stereotype line together when the role
|
|
193
|
+
# would start left of EA's minimum label x.
|
|
194
|
+
def clamp_role_x(text_pos, stereo_pos)
|
|
195
|
+
shift = MIN_ROLE_X - text_pos[0]
|
|
196
|
+
return [text_pos, stereo_pos] unless shift.positive?
|
|
197
|
+
|
|
198
|
+
[[text_pos[0] + shift, text_pos[1]],
|
|
199
|
+
stereo_pos && [stereo_pos[0] + shift, stereo_pos[1]]]
|
|
200
|
+
end
|
|
201
|
+
|
|
158
202
|
# ALN2 single-line target roles retain their horizontal placement
|
|
159
203
|
# and use the saved corner's baseline on a straight stored ray.
|
|
160
204
|
def horizontal_aligned_target_baseline?(connector, heading, box, role)
|
|
@@ -190,8 +234,11 @@ module Ea
|
|
|
190
234
|
[x - width - SEAT_X_INSET, y - 7] if width
|
|
191
235
|
end
|
|
192
236
|
|
|
237
|
+
# UMLDI routes carry the inferred saved-ray flag; their stored LRT
|
|
238
|
+
# box keeps the plain target seat.
|
|
193
239
|
def moved_plain_target_ray?(connector, box)
|
|
194
|
-
saved_target_ray?(connector) &&
|
|
240
|
+
saved_target_ray?(connector) && connector.style.to_h[:umldi_route] != true &&
|
|
241
|
+
box.values_at("alignment", "cy") == [1, 16] &&
|
|
195
242
|
!stored_offsets_zero?(box)
|
|
196
243
|
end
|
|
197
244
|
|
|
@@ -318,15 +365,33 @@ module Ea
|
|
|
318
365
|
nil
|
|
319
366
|
end
|
|
320
367
|
|
|
368
|
+
# Saved bounds seat a source Property role at the stereotype's left
|
|
369
|
+
# edge. A centred stored box (ALN=1), or a phantom's synthesized box
|
|
370
|
+
# without alignment, centres the shorter role over it; a
|
|
371
|
+
# left-aligned box keeps both lines on that edge.
|
|
372
|
+
def bounds_role_centering(connector, end_kind, role, stereo, text_box)
|
|
373
|
+
return 0 unless end_kind == :source && stereo && seat_font?
|
|
374
|
+
return 0 unless centred_bounds_box?(connector, text_box)
|
|
375
|
+
return 0 unless property_label?(connector, :source)
|
|
376
|
+
|
|
377
|
+
-[stereotype_offset(role, stereo, { "alignment" => 1 }), 0].min
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Phantom outlines have no saved diagram link, so their boxes are
|
|
381
|
+
# synthesized and EA draws its centred default label.
|
|
382
|
+
def centred_bounds_box?(connector, text_box)
|
|
383
|
+
connector.style.to_h[:phantom_outline] == true || text_box.to_h["alignment"] == 1
|
|
384
|
+
end
|
|
385
|
+
|
|
321
386
|
# Role text on a rightward leg is left-aligned at the corner,
|
|
322
387
|
# 7px above it.
|
|
323
|
-
def role_seat(direction, anchor, box, role)
|
|
388
|
+
def role_seat(connector, direction, anchor, box, role)
|
|
324
389
|
return nil unless direction == :right
|
|
325
390
|
return nil if box.nil? || role_empty?(role)
|
|
326
391
|
return nil unless default_presentation?(box)
|
|
327
392
|
|
|
328
393
|
x, y = seat_corner(anchor, box)
|
|
329
|
-
[x + SEAT_X_INSET, y - 7]
|
|
394
|
+
[x + SEAT_X_INSET + source_property_centering(connector, direction, box, role), y - 7]
|
|
330
395
|
end
|
|
331
396
|
|
|
332
397
|
# Property-role clusters use a direction-specific horizontal seat;
|
|
@@ -348,12 +413,26 @@ module Ea
|
|
|
348
413
|
# Center a short role above the wider stereotype without moving
|
|
349
414
|
# the directional cluster's left edge. Other boxes keep their seat.
|
|
350
415
|
def source_property_centering(connector, direction, box, role)
|
|
351
|
-
return 0 unless
|
|
416
|
+
return 0 unless centered_property_box?(connector, direction, box)
|
|
417
|
+
|
|
418
|
+
property_role_centering(connector, :source, box, role)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def property_role_centering(connector, end_kind, box, role)
|
|
352
422
|
return 0 unless seat_font? && font_weight == 400 && font_style == "normal"
|
|
353
|
-
return 0 unless
|
|
423
|
+
return 0 unless property_label?(connector, end_kind)
|
|
424
|
+
|
|
425
|
+
stereo = "«#{connector_source_dest_stereotype(connector, end_kind)}»"
|
|
426
|
+
-[stereotype_offset(role, stereo, box.merge("alignment" => 1)), 0].min
|
|
427
|
+
end
|
|
354
428
|
|
|
355
|
-
|
|
356
|
-
|
|
429
|
+
# Measured box heights: left/down Property clusters store CY 35,
|
|
430
|
+
# right-seat clusters CY 31. Phantom outlines leaving down carry a
|
|
431
|
+
# synthesized zero box that centers the same way.
|
|
432
|
+
def centered_property_box?(connector, direction, box)
|
|
433
|
+
cy = { left: 35, down: 35, right: 31 }[direction]
|
|
434
|
+
(cy && box.values_at("cy", "alignment") == [cy, 1]) ||
|
|
435
|
+
(direction == :down && phantom_property_box?(connector, direction, box))
|
|
357
436
|
end
|
|
358
437
|
|
|
359
438
|
def source_property_seat?(connector, direction, box)
|
|
@@ -367,7 +446,7 @@ module Ea
|
|
|
367
446
|
# Their default Property cluster uses the directional seat with a
|
|
368
447
|
# centered stereotype; persisted or moved boxes keep their own seat.
|
|
369
448
|
def phantom_property_box?(connector, direction, box)
|
|
370
|
-
%i[up left].include?(direction) && seat_font? && cambria_box?(box) &&
|
|
449
|
+
%i[up left down].include?(direction) && seat_font? && cambria_box?(box) &&
|
|
371
450
|
box["alignment"].nil? && stored_offsets_zero?(box) &&
|
|
372
451
|
connector.style.to_h[:phantom_outline] == true &&
|
|
373
452
|
property_label?(connector, :source)
|
|
@@ -522,7 +601,7 @@ module Ea
|
|
|
522
601
|
|
|
523
602
|
x, y = seat_corner(anchor, box)
|
|
524
603
|
inset, = property_role_offsets(:down)
|
|
525
|
-
[x - inset - width, y - 7]
|
|
604
|
+
[x - inset - width + property_role_centering(connector, :target, box, role), y - 7]
|
|
526
605
|
end
|
|
527
606
|
|
|
528
607
|
def upward_target_property?(connector, heading)
|
|
@@ -665,7 +744,8 @@ module Ea
|
|
|
665
744
|
|
|
666
745
|
def downward_source_route?(style, box)
|
|
667
746
|
[orthogonal_stub_route?(style), lateral_horizontal_route?(style), mode_two_route?(style),
|
|
668
|
-
downward_property_route?(style), persisted_width_zero_down_box?(box, style)
|
|
747
|
+
downward_property_route?(style), persisted_width_zero_down_box?(box, style),
|
|
748
|
+
centered_width_zero_down_box?(box, style)].any?
|
|
669
749
|
end
|
|
670
750
|
|
|
671
751
|
def persisted_width_zero_down_box?(box, style)
|
|
@@ -673,6 +753,11 @@ module Ea
|
|
|
673
753
|
style.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
|
|
674
754
|
end
|
|
675
755
|
|
|
756
|
+
def centered_width_zero_down_box?(box, style)
|
|
757
|
+
box["alignment"] == 1 && stored_offsets_zero?(box) &&
|
|
758
|
+
style.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
|
|
759
|
+
end
|
|
760
|
+
|
|
676
761
|
# Mode 3, width 2 on the horizontal lateral tree.
|
|
677
762
|
def lateral_horizontal_route?(style)
|
|
678
763
|
style.values_at(:mode, :lwidth, :tree) == %w[3 2 LH]
|
|
@@ -820,13 +905,14 @@ module Ea
|
|
|
820
905
|
end
|
|
821
906
|
|
|
822
907
|
def build_texts(role:, mult:, standalone:, text_pos:, mult_pos:,
|
|
823
|
-
stereo:, text_box:)
|
|
908
|
+
stereo:, text_box:, stereo_pos: nil)
|
|
824
909
|
if role && !role.empty? && text_pos
|
|
825
910
|
x, y = text_pos
|
|
826
911
|
texts = [text_at(x, y, role)]
|
|
827
912
|
if stereo
|
|
828
|
-
stereo_x = x + stereotype_offset(role, stereo, text_box)
|
|
829
|
-
|
|
913
|
+
stereo_x, stereo_y = stereo_pos || [x + stereotype_offset(role, stereo, text_box),
|
|
914
|
+
y + property_y_offset(text_box)]
|
|
915
|
+
texts << text_at(stereo_x, stereo_y, stereo)
|
|
830
916
|
end
|
|
831
917
|
if mult && !mult.empty? && mult_pos
|
|
832
918
|
texts << text_at(*mult_pos, mult)
|
|
@@ -839,6 +925,19 @@ module Ea
|
|
|
839
925
|
end
|
|
840
926
|
end
|
|
841
927
|
|
|
928
|
+
def umldi_label_position(bounds)
|
|
929
|
+
[bounds[:x], bounds[:y] + font_pixel_size.ceil]
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def umldi_stereotype_position(bounds, role, stereo, text_box)
|
|
933
|
+
[bounds[:x] + stereotype_offset(role, stereo, text_box),
|
|
934
|
+
bounds[:y] + font_pixel_size.ceil + (font_pixel_size.ceil + 2)]
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def font_pixel_size
|
|
938
|
+
font_unit == "pt" ? font_size * 4.0 / 3 : font_size
|
|
939
|
+
end
|
|
940
|
+
|
|
842
941
|
def stereotype_offset(role, stereo, text_box)
|
|
843
942
|
return 0 unless text_box && text_box["alignment"] == 1
|
|
844
943
|
|
|
@@ -83,7 +83,8 @@ module Ea
|
|
|
83
83
|
|
|
84
84
|
stored_src = end_boxes(boxes, :llt, :llb)
|
|
85
85
|
stored_tgt = end_boxes(boxes, :lrt, :lrb)
|
|
86
|
-
|
|
86
|
+
umldi_bounds = connector.umldi_label_bounds || {}
|
|
87
|
+
stored_labels = !(stored_src.nil? && stored_tgt.nil?) || !umldi_bounds.empty?
|
|
87
88
|
src_tb, src_mb = stored_src || unstored_label_boxes(points, :source, stored_labels)
|
|
88
89
|
tgt_tb, tgt_mb = stored_tgt || unstored_label_boxes(points, :target, stored_labels)
|
|
89
90
|
|
|
@@ -93,6 +94,7 @@ module Ea
|
|
|
93
94
|
end_kind: :source, tree: tree && tree[:source],
|
|
94
95
|
heading: (stored_src || stored_labels) && source_heading(connector, points),
|
|
95
96
|
stored_labels: stored_labels,
|
|
97
|
+
umldi_bounds: available_umldi_bounds(umldi_bounds, boxes, :source),
|
|
96
98
|
source_bounds: source_dock_bounds(connector)).each { |t| texts << t }
|
|
97
99
|
end_renderer.texts(text_box: tgt_tb,
|
|
98
100
|
mult_box: tgt_mb,
|
|
@@ -101,10 +103,21 @@ module Ea
|
|
|
101
103
|
bend: Label::TargetBendSeat.reference(points),
|
|
102
104
|
heading: (stored_tgt || stored_labels) && target_heading(connector, points),
|
|
103
105
|
target_face: target_dock_heading(connector, target_pt, target_heading(connector, points)),
|
|
104
|
-
stored_labels: stored_labels
|
|
106
|
+
stored_labels: stored_labels,
|
|
107
|
+
umldi_bounds: available_umldi_bounds(umldi_bounds, boxes, :target)).each { |t| texts << t }
|
|
105
108
|
texts
|
|
106
109
|
end
|
|
107
110
|
|
|
111
|
+
# Role bounds the XMI builder keeps belong to this connector's own
|
|
112
|
+
# memberEnd and win over a stored role box; a stored multiplicity box
|
|
113
|
+
# wins over multiplicity bounds.
|
|
114
|
+
def available_umldi_bounds(bounds, label_boxes, end_kind)
|
|
115
|
+
mult_key = end_kind == :source ? :llb : :lrb
|
|
116
|
+
result = bounds.dup
|
|
117
|
+
result.delete(end_kind == :source ? :source_multiplicity : :target_multiplicity) if label_boxes.key?(mult_key)
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
|
|
108
121
|
# A connector that stores label boxes for only one end seats the
|
|
109
122
|
# other end's labels at its anchor, like a stored zero offset.
|
|
110
123
|
def unstored_label_boxes(points, end_kind, stored_labels)
|
|
@@ -52,11 +52,16 @@ module Ea
|
|
|
52
52
|
# feature compartment — even one whose rows are all hidden
|
|
53
53
|
# by visibility flags (10E3E08A: CodeList with 3 attributes
|
|
54
54
|
# toggled off still renders the divider over an empty body;
|
|
55
|
-
# only feature-less elements omit it).
|
|
55
|
+
# only feature-less elements omit it). Association-end
|
|
56
|
+
# properties are not EA attributes (QEA keeps them as connector
|
|
57
|
+
# roles; the XMI reads them as ownedAttribute), so a class whose
|
|
58
|
+
# only properties are association ends has no feature compartment
|
|
59
|
+
# and no divider (A59065AF urf::Administration).
|
|
56
60
|
def hidden_features_present?
|
|
57
61
|
return false unless classifier.respond_to?(:properties)
|
|
58
62
|
|
|
59
|
-
classifier.properties.any?
|
|
63
|
+
classifier.properties.any? { |property| !property.association_id } ||
|
|
64
|
+
classifier.operations.any?
|
|
60
65
|
end
|
|
61
66
|
end
|
|
62
67
|
end
|
data/lib/ea/theme/definition.rb
CHANGED
|
@@ -138,8 +138,8 @@ module Ea
|
|
|
138
138
|
@label_font_size_unit || font_size_unit
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
141
|
+
# Content x of attribute and enum-literal names; a theme's own
|
|
142
|
+
# attribute_spec overrides the inherited seat.
|
|
143
143
|
def attribute_content_x_offset
|
|
144
144
|
@attribute_spec_override ? attribute_spec.content_x_offset : 22
|
|
145
145
|
end
|
data/lib/ea/version.rb
CHANGED