ea 0.6.137 → 0.6.139
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/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 +127 -14
- data/lib/ea/sources/xmi/stored_route.rb +202 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +38 -40
- 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/svg/ea_emitter/segment_geometry.rb +123 -0
- data/lib/ea/svg/ea_emitter.rb +2 -0
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34c3f680cb51a88a683e57db2c059fcee8979e5b6b31cfaae4c3cbc3ac91ac86
|
|
4
|
+
data.tar.gz: a4cbd25958c54c24d6d1801bf4e4cc83f76f42d652f371cf9d491865160d7725
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c87682bc64b73abb6502c0dfd71ca7b9209ab8bcd71b79762032d0c4008f80999e3785257b72f39d73a816247d1dbcc5580a70e64f526a8ee98ee1aab32b645a
|
|
7
|
+
data.tar.gz: ed3c4dd24e1ed8b1bd9c371545d56392cda07b2e0f3223f27d84761b728963c5df7df67fab8da905bdd586150fb94750a5cbe3f2bd1bec9fd11adfd3ec18b9c6
|
|
@@ -38,6 +38,9 @@ module Ea
|
|
|
38
38
|
attribute :hidden, :boolean
|
|
39
39
|
attribute :is_substitutable, :boolean, default: false
|
|
40
40
|
attribute :label_boxes, :hash, default: -> { {} }
|
|
41
|
+
# Absolute labels saved by UML-DI when the EA extension omits the
|
|
42
|
+
# corresponding relative label boxes.
|
|
43
|
+
attribute :umldi_label_bounds, :hash, default: -> { {} }
|
|
41
44
|
attribute :ghost_labels, GhostLabel, collection: true, initialize_empty: true
|
|
42
45
|
# True when the t_diagramlinks.Geometry string carries explicit
|
|
43
46
|
# SX/SY/EX/EY offset fields. EA renders the «import» package_anchor
|
|
@@ -86,6 +89,7 @@ module Ea
|
|
|
86
89
|
map "lineWidth", to: :line_width
|
|
87
90
|
map "hidden", to: :hidden, render_default: true
|
|
88
91
|
map "labelBoxes", to: :label_boxes
|
|
92
|
+
map "umldiLabelBounds", to: :umldi_label_bounds
|
|
89
93
|
map "ghostLabels", to: :ghost_labels, render_empty: true
|
|
90
94
|
map "hasGeometryOffsets", to: :has_geometry_offsets, render_default: true
|
|
91
95
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class DiagramBuilder
|
|
7
|
+
class UmldiEdges
|
|
8
|
+
# Association membership proves which saved role bounds an edge owns.
|
|
9
|
+
class PropertyRoles
|
|
10
|
+
def initialize(doc)
|
|
11
|
+
@doc = doc
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def keys_for(diagram_id, connector_id)
|
|
15
|
+
property_roles.dig(diagram_id, connector_id) || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def property_roles
|
|
21
|
+
@property_roles ||= begin
|
|
22
|
+
document = @doc
|
|
23
|
+
declared = declared_properties(document)
|
|
24
|
+
document.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
25
|
+
[diagram["id"] || diagram["xmi:id"], diagram_property_roles(diagram, declared)]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def declared_properties(document)
|
|
31
|
+
members = document.xpath("//*[local-name()='memberEnd']").to_set do |node|
|
|
32
|
+
[node["xmi:idref"], node.parent["xmi:id"]]
|
|
33
|
+
end
|
|
34
|
+
document.xpath("//*[@association][@xmi:id]", "xmi" => "http://www.omg.org/spec/XMI/20131001")
|
|
35
|
+
.filter_map do |node|
|
|
36
|
+
pair = [node["xmi:id"], node["association"]]
|
|
37
|
+
pair if node.attributes["type"]&.value == "uml:Property" && members.include?(pair)
|
|
38
|
+
end.to_set
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def diagram_property_roles(diagram, declared)
|
|
42
|
+
unique_edges(diagram).transform_values do |edge|
|
|
43
|
+
edge.xpath("./ownedElement[@modelElement]").filter_map do |label|
|
|
44
|
+
key = role_key(label)
|
|
45
|
+
key if key &&
|
|
46
|
+
declared.include?([label["modelElement"], edge["modelElement"]])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unique_edges(diagram)
|
|
52
|
+
diagram.xpath("./ownedElement[@modelElement]")
|
|
53
|
+
.select { |node| node.attributes["type"]&.value.to_s.end_with?("UMLEdge") }
|
|
54
|
+
.group_by { |edge| edge["modelElement"] }
|
|
55
|
+
.select { |_, edges| edges.one? }.transform_values(&:first)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def role_key(label)
|
|
59
|
+
return unless label.attributes["type"]&.value.to_s.end_with?("UMLAssociationEndLabel")
|
|
60
|
+
|
|
61
|
+
case label["xmi:id"] || label["id"]
|
|
62
|
+
when /\AEAID_SEL/ then :source_role
|
|
63
|
+
when /\AEAID_TEL/ then :target_role
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -11,6 +11,8 @@ module Ea
|
|
|
11
11
|
# drawn by more than one edge in the same diagram has neither; an edge
|
|
12
12
|
# with fewer than two waypoints has no route.
|
|
13
13
|
class UmldiEdges
|
|
14
|
+
autoload :PropertyRoles, "ea/sources/xmi/diagram_builder/umldi_edges/property_roles"
|
|
15
|
+
|
|
14
16
|
def initialize(xmi_path)
|
|
15
17
|
@xmi_path = xmi_path
|
|
16
18
|
end
|
|
@@ -23,6 +25,20 @@ module Ea
|
|
|
23
25
|
ends.dig(diagram_id, connector_id)
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
# Absolute saved UMLDI bounds for connector label boxes. These are
|
|
29
|
+
# kept separate from extension label offsets: callers may use them
|
|
30
|
+
# when extension geometry is absent or the association owns the role.
|
|
31
|
+
def label_bounds_for(diagram_id, connector_id)
|
|
32
|
+
label_bounds.dig(diagram_id, connector_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Role label keys whose UMLDI label is bound to the property that
|
|
36
|
+
# the connector's association declares, so the bounds describe that
|
|
37
|
+
# end's own role label rather than an unrelated label.
|
|
38
|
+
def property_role_keys_for(diagram_id, connector_id)
|
|
39
|
+
(@property_roles ||= PropertyRoles.new(doc)).keys_for(diagram_id, connector_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
26
42
|
private
|
|
27
43
|
|
|
28
44
|
def ends
|
|
@@ -31,6 +47,46 @@ module Ea
|
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
|
|
50
|
+
def label_bounds
|
|
51
|
+
@label_bounds ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
52
|
+
[diagram["id"] || diagram["xmi:id"], diagram_label_bounds(diagram)]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def diagram_label_bounds(diagram)
|
|
57
|
+
boxes_by_connector = unique_edges(diagram).to_h do |connector_id, edge|
|
|
58
|
+
[connector_id, edge_label_bounds(edge)]
|
|
59
|
+
end
|
|
60
|
+
boxes_by_connector.reject { |_, boxes| boxes.empty? }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def edge_label_bounds(edge)
|
|
64
|
+
edge.xpath("./ownedElement[@modelElement]").each_with_object({}) do |label, found|
|
|
65
|
+
bounds = label.at_xpath("./*[local-name()=\"bounds\"]")
|
|
66
|
+
key = label_key(label)
|
|
67
|
+
next unless key && bounds
|
|
68
|
+
|
|
69
|
+
found[key] = bounds_values(bounds)
|
|
70
|
+
rescue ArgumentError, TypeError
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def bounds_values(bounds)
|
|
76
|
+
%w[x y width height].to_h { |attribute| [attribute.to_sym, Integer(bounds[attribute])] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def label_key(label)
|
|
80
|
+
type = label.attributes["type"]&.value.to_s
|
|
81
|
+
role = (label["id"] || label["xmi:id"]).to_s
|
|
82
|
+
end_name = { "EAID_SEL" => "source", "EAID_SML" => "source",
|
|
83
|
+
"EAID_TEL" => "target", "EAID_TML" => "target" }
|
|
84
|
+
side = end_name.find { |prefix, _| role.start_with?(prefix) }&.last
|
|
85
|
+
suffix = { "UMLAssociationEndLabel" => "role", "UMLMultiplicityLabel" => "multiplicity" }
|
|
86
|
+
label_kind = suffix[type.split(":").last]
|
|
87
|
+
"#{side}_#{label_kind}".to_sym if side && label_kind
|
|
88
|
+
end
|
|
89
|
+
|
|
34
90
|
def diagram_ends(diagram)
|
|
35
91
|
unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
|
|
36
92
|
acc[connector_id] = [edge["source"], edge["target"]] if edge["source"] && edge["target"]
|
|
@@ -50,6 +50,7 @@ module Ea
|
|
|
50
50
|
placed_connectors = ext_elements.placed_connectors
|
|
51
51
|
props = ext_diagram.properties
|
|
52
52
|
umldi_keywords = keywords_for(id)
|
|
53
|
+
context = context_diagram?(ext_diagram)
|
|
53
54
|
|
|
54
55
|
Ea::Model::Diagram.new(
|
|
55
56
|
id: id,
|
|
@@ -65,8 +66,7 @@ module Ea
|
|
|
65
66
|
show_parents: !style_flag?(ext_diagram, "HideParents"),
|
|
66
67
|
hand_draw: style_flag?(ext_diagram, "HandDraw"),
|
|
67
68
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
68
|
-
connectors: build_connectors(placed_connectors,
|
|
69
|
-
placed_elements, id),
|
|
69
|
+
connectors: build_connectors(placed_connectors, placed_elements, id, context),
|
|
70
70
|
annotations: []
|
|
71
71
|
)
|
|
72
72
|
end
|
|
@@ -102,6 +102,13 @@ module Ea
|
|
|
102
102
|
value.to_i.nonzero? ? true : false
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def context_diagram?(ext_diagram)
|
|
106
|
+
name = ext_diagram.properties&.name.to_s
|
|
107
|
+
return true if name.match?(/\AContext Diagram: Package (Clients|Suppliers) of /i)
|
|
108
|
+
|
|
109
|
+
style_ex_for(ext_diagram).to_s.match(/(?:\A|;)SuppressFOC=(\d)/)&.public_send(:[], 1) != "1"
|
|
110
|
+
end
|
|
111
|
+
|
|
105
112
|
def page_bounds(ext_diagram)
|
|
106
113
|
style = style_for(ext_diagram).to_s
|
|
107
114
|
width = style[/(?:\A|;)DocSize\.cx=(\d+)/, 1]
|
|
@@ -176,7 +183,7 @@ module Ea
|
|
|
176
183
|
height: bottom - top)
|
|
177
184
|
end
|
|
178
185
|
|
|
179
|
-
def build_connectors(placed_connectors, placed_elements, owner_id)
|
|
186
|
+
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
180
187
|
# Index elements by DUID so we can resolve SOID/EOID.
|
|
181
188
|
by_duid = placed_elements.each_with_object({}) do |pe, acc|
|
|
182
189
|
acc[pe.style.duid] = pe if pe.style.duid
|
|
@@ -184,40 +191,46 @@ module Ea
|
|
|
184
191
|
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
185
192
|
|
|
186
193
|
placed_connectors.each_with_index.map do |placed, index|
|
|
187
|
-
connector = build_connector(placed, by_duid, by_subject, owner_id, index)
|
|
188
|
-
mark_association_class(connector, placed, placed_elements)
|
|
194
|
+
connector = build_connector(placed, by_duid, by_subject, owner_id, rank: index, context: context)
|
|
195
|
+
mark_association_class(connector, placed, placed_elements, context)
|
|
189
196
|
end
|
|
190
197
|
end
|
|
191
198
|
|
|
192
|
-
def mark_association_class(connector, placed, placed_elements)
|
|
199
|
+
def mark_association_class(connector, placed, placed_elements, context)
|
|
200
|
+
mark_plain_end_seats(connector, placed, context)
|
|
193
201
|
reference = association_class_metadata.for(
|
|
194
202
|
connector_index[placed.subject],
|
|
195
203
|
placed_subjects: placed_elements.map(&:subject),
|
|
196
204
|
waypoints: connector.waypoints
|
|
197
205
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
return connector unless reference
|
|
207
|
+
|
|
208
|
+
connector.style[:association_class_ref] = reference
|
|
209
|
+
connector.source_anchor = connector.waypoints.first.position
|
|
202
210
|
connector
|
|
203
211
|
end
|
|
204
212
|
|
|
213
|
+
def mark_plain_end_seats(connector, placed, context)
|
|
214
|
+
mark_plain_source_seat(connector, placed, context)
|
|
215
|
+
mark_plain_target_seat(connector, placed, context)
|
|
216
|
+
end
|
|
217
|
+
|
|
205
218
|
def association_class_metadata
|
|
206
219
|
@association_class_metadata ||= AssociationClassMetadata.new(xmi_path)
|
|
207
220
|
end
|
|
208
221
|
|
|
209
|
-
def build_connector(placed, by_duid, by_subject, owner_id,
|
|
222
|
+
def build_connector(placed, by_duid, by_subject, owner_id, options)
|
|
210
223
|
geom = placed.geometry
|
|
211
224
|
style = placed.style
|
|
212
225
|
edge_source, edge_target = connector_ends(owner_id, placed)
|
|
213
226
|
source, target = placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
214
227
|
ext_meta = connector_meta(placed.subject)
|
|
215
228
|
route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
|
|
216
|
-
Ea::Model::DiagramConnector.new(
|
|
229
|
+
connector = Ea::Model::DiagramConnector.new(
|
|
217
230
|
id: IdNormalizer.synthetic_id(owner_id, "conn", placed.subject),
|
|
218
231
|
diagram_id: owner_id,
|
|
219
232
|
relationship_ref: placed.subject,
|
|
220
|
-
paint_order: [rank],
|
|
233
|
+
paint_order: [options.fetch(:rank)],
|
|
221
234
|
source_duid: style.soid,
|
|
222
235
|
target_duid: style.eoid,
|
|
223
236
|
source_element_ref: source && synthetic_element_id(owner_id, source),
|
|
@@ -234,8 +247,96 @@ module Ea
|
|
|
234
247
|
line_color: style.color,
|
|
235
248
|
line_width: style.line_width,
|
|
236
249
|
hidden: style.hidden,
|
|
237
|
-
label_boxes:
|
|
250
|
+
label_boxes: connector_label_boxes(placed, ext_meta[:ea_type],
|
|
251
|
+
options.fetch(:context) ? nil : owner_id)
|
|
238
252
|
)
|
|
253
|
+
connector.umldi_label_bounds = if options.fetch(:context)
|
|
254
|
+
{}
|
|
255
|
+
else
|
|
256
|
+
umldi_label_bounds_for(owner_id, placed.subject,
|
|
257
|
+
connector.label_boxes)
|
|
258
|
+
end
|
|
259
|
+
mark_stored_ray(connector, placed, source, target) unless options.fetch(:context)
|
|
260
|
+
mark_association_route(connector, placed, source, target) unless options.fetch(:context)
|
|
261
|
+
connector
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def umldi_label_bounds_for(owner_id, connector_id, label_boxes)
|
|
265
|
+
return {} unless xmi_path
|
|
266
|
+
|
|
267
|
+
bounds = umldi_edges.label_bounds_for(owner_id, connector_id)
|
|
268
|
+
return {} unless bounds
|
|
269
|
+
|
|
270
|
+
property_roles = umldi_edges.property_role_keys_for(owner_id, connector_id)
|
|
271
|
+
bounds.reject do |key, _|
|
|
272
|
+
extension_key = { source_role: :llt, source_multiplicity: :llb,
|
|
273
|
+
target_role: :lrt, target_multiplicity: :lrb }.fetch(key)
|
|
274
|
+
label_boxes.key?(extension_key) && !property_roles.include?(key)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def mark_stored_ray(connector, placed, source, target)
|
|
279
|
+
return if connector.style[:umldi_route]
|
|
280
|
+
|
|
281
|
+
metadata = stored_ray_metadata(connector, placed, source, target)
|
|
282
|
+
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
283
|
+
return if metadata.empty? || connector.connector_type == "Generalization"
|
|
284
|
+
|
|
285
|
+
connector.style[:association_ray_outline] = true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def stored_ray_metadata(connector, placed, source, target, stage: :ray_metadata)
|
|
289
|
+
geom = placed.geometry
|
|
290
|
+
link = StoredRoute::Link.new(placed.style.raw, geom.path.to_s.tr("$", ";"))
|
|
291
|
+
bounds = { source: bounds_from_placed(source), target: bounds_from_placed(target) }
|
|
292
|
+
offsets = { source: [geom.sx, geom.sy], target: [geom.ex, geom.ey] }
|
|
293
|
+
StoredRoute.new.public_send(stage, connector, link: link, bounds: bounds, offsets: offsets, edge: geom.edge)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def mark_association_route(connector, placed, source, target)
|
|
297
|
+
metadata = stored_ray_metadata(connector, placed, source, target, stage: :association_route)
|
|
298
|
+
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def mark_plain_source_seat(connector, placed, context)
|
|
302
|
+
return if context || !placed.geometry.path.to_s.empty? || connector.connector_type != "Aggregation"
|
|
303
|
+
return unless connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
|
|
304
|
+
return unless connector.source_stereotype.to_s.empty?
|
|
305
|
+
|
|
306
|
+
connector.style[:plain_source_seat] = true
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def mark_plain_target_seat(connector, placed, context)
|
|
310
|
+
return if context || !placed.geometry.path.to_s.empty? || connector.connector_type != "Association"
|
|
311
|
+
return unless connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
|
|
312
|
+
return unless connector.target_stereotype.to_s.empty?
|
|
313
|
+
|
|
314
|
+
connector.style[:plain_target_seat] = true
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
PHANTOM_TYPES = %w[Association Aggregation].freeze
|
|
318
|
+
|
|
319
|
+
# EA exports a model relationship drawn without a diagram link row as
|
|
320
|
+
# an Association or Aggregation link with the default style and an
|
|
321
|
+
# all-zero, EDGE-less, bendless geometry; the QEA source builds it as
|
|
322
|
+
# a phantom connector.
|
|
323
|
+
def phantom_link?(placed, type)
|
|
324
|
+
PHANTOM_TYPES.include?(type) && placed.style.raw == ";Hidden=0;" && default_geometry?(placed.geometry)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def default_geometry?(geom)
|
|
328
|
+
geom.edge.nil? && [geom.sx, geom.sy, geom.ex, geom.ey].all?(&:zero?) &&
|
|
329
|
+
geom.path.to_s.empty? && geom.label_boxes.to_h.empty?
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def connector_label_boxes(placed, type, owner_id)
|
|
333
|
+
return serialize_label_boxes(placed.geometry.label_boxes) unless phantom_link?(placed, type)
|
|
334
|
+
|
|
335
|
+
# An empty extension placement can still have saved UMLDI labels.
|
|
336
|
+
# Synthetic phantom offsets must not suppress those authored bounds.
|
|
337
|
+
return {} if owner_id && xmi_path && umldi_edges.label_bounds_for(owner_id, placed.subject)
|
|
338
|
+
|
|
339
|
+
Qea::DiagramBuilder::DEFAULT_LABEL_BOXES
|
|
239
340
|
end
|
|
240
341
|
|
|
241
342
|
# The saved SOID/EOID can name the model connector's ends in reverse
|
|
@@ -296,6 +397,10 @@ module Ea
|
|
|
296
397
|
end
|
|
297
398
|
|
|
298
399
|
def connector_route(placed, source, target, owner_id, type)
|
|
400
|
+
phantom_route(placed, source, target, type) || saved_route(placed, source, target, owner_id, type)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def saved_route(placed, source, target, owner_id, type)
|
|
299
404
|
native = umldi_waypoints(owner_id, placed) if placed.geometry.bend_points.to_a.empty?
|
|
300
405
|
points = native || compute_waypoints(placed.geometry, source, target,
|
|
301
406
|
style: placed.style, connector_type: type)
|
|
@@ -303,6 +408,14 @@ module Ea
|
|
|
303
408
|
{ waypoints: native || points, style: connector_route_style(placed, native) }
|
|
304
409
|
end
|
|
305
410
|
|
|
411
|
+
def phantom_route(placed, source, target, type)
|
|
412
|
+
return unless source && target && phantom_link?(placed, type)
|
|
413
|
+
|
|
414
|
+
points = StoredRoute.new.phantom_waypoints(bounds_from_placed(source), bounds_from_placed(target))
|
|
415
|
+
{ waypoints: points.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) },
|
|
416
|
+
style: { lwidth: "1", phantom_outline: true } }
|
|
417
|
+
end
|
|
418
|
+
|
|
306
419
|
def connector_route_style(placed, native)
|
|
307
420
|
route_style = native ? { umldi_route: true } : {}
|
|
308
421
|
# Label seats use EA's saved routing mode on every route source;
|
|
@@ -26,8 +26,210 @@ module Ea
|
|
|
26
26
|
finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Saved rays retain origins and computed boundary ports, matching QEA.
|
|
30
|
+
def ray_metadata(model, link:, bounds:, offsets:, edge:)
|
|
31
|
+
if model.connector_type == "Generalization"
|
|
32
|
+
return generalization_ray_metadata(model, link, bounds, offsets, edge)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return {} unless direct_association_ray?(model, bounds, offsets)
|
|
36
|
+
|
|
37
|
+
source, target = bounds.values_at(:source, :target)
|
|
38
|
+
geom = ray_geometry(offsets, edge)
|
|
39
|
+
return {} unless saved_association_ray?(model, link, source, target, geom)
|
|
40
|
+
|
|
41
|
+
ray_points(source, target, offsets, geom)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def phantom_waypoints(source, target)
|
|
45
|
+
[bounds_edge_point(source, target), bounds_edge_point(target, source)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Apply QEA's saved Association stages after the initial route, including
|
|
49
|
+
# UMLDI fallback. A single stored corner and an empty-path stored ray
|
|
50
|
+
# carry mathematical origins that the generic facing-edge route loses.
|
|
51
|
+
def association_route(model, link:, bounds:, offsets:, edge:)
|
|
52
|
+
overlap = umldi_association_metadata(model, link, bounds, offsets, edge)
|
|
53
|
+
return overlap unless overlap.empty?
|
|
54
|
+
|
|
55
|
+
partial = partial_port_metadata(model, link, bounds, offsets, edge)
|
|
56
|
+
return partial unless partial.empty?
|
|
57
|
+
|
|
58
|
+
return {} unless stored_association_candidate?(model, bounds)
|
|
59
|
+
|
|
60
|
+
candidate = association_model(model, bounds, offsets)
|
|
61
|
+
initial = candidate.waypoints
|
|
62
|
+
association_stored_ray(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
63
|
+
return {} if candidate.waypoints.equal?(initial)
|
|
64
|
+
|
|
65
|
+
association_metadata(candidate)
|
|
66
|
+
end
|
|
67
|
+
|
|
29
68
|
private
|
|
30
69
|
|
|
70
|
+
def umldi_association_metadata(model, link, bounds, offsets, edge)
|
|
71
|
+
overlap = umldi_overlap_metadata(model, link, bounds, offsets, edge)
|
|
72
|
+
return overlap unless overlap.empty?
|
|
73
|
+
|
|
74
|
+
umldi_stored_ray_metadata(model, link, bounds, offsets, edge)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# UMLDI owns the route and ports. Classify a thin saved ray only when
|
|
78
|
+
# its origins pass the shared unstroked-box checks; XMI has no QEA
|
|
79
|
+
# database from which to infer a wider element outline.
|
|
80
|
+
def umldi_stored_ray_metadata(model, link, bounds, offsets, edge)
|
|
81
|
+
return {} unless umldi_thin_ray_candidate?(model, bounds, offsets)
|
|
82
|
+
|
|
83
|
+
candidate = association_model(model, bounds, offsets)
|
|
84
|
+
return {} unless association_ray_candidate?(candidate, link)
|
|
85
|
+
return {} unless association_ray_origins(*bounds.values_at(:source, :target),
|
|
86
|
+
ray_geometry(offsets, edge), "0")
|
|
87
|
+
|
|
88
|
+
{ style: model.style.to_h.merge(stored_association_ray: true, association_ray_outline: true) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def umldi_thin_ray_candidate?(model, bounds, offsets)
|
|
92
|
+
model.style[:umldi_route] && model.style[:lwidth] == "0" &&
|
|
93
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# UMLDI owns the drawn route; retain QEA's overlap origins only for
|
|
97
|
+
# interpreting the stored relative end labels against that route.
|
|
98
|
+
def umldi_overlap_metadata(model, link, bounds, offsets, edge)
|
|
99
|
+
return {} unless umldi_overlap_candidate?(model, bounds, offsets)
|
|
100
|
+
|
|
101
|
+
candidate = association_model(model, bounds, offsets)
|
|
102
|
+
overlap_ray_origins(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
103
|
+
return {} unless candidate.style[:overlap_ray]
|
|
104
|
+
|
|
105
|
+
{ source_anchor: candidate.source_anchor, target_anchor: candidate.target_anchor,
|
|
106
|
+
has_geometry_offsets: true, style: candidate.style.to_h }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def umldi_overlap_candidate?(model, bounds, offsets)
|
|
110
|
+
model.style.to_h.values_at(:umldi_route, :mode, :lwidth) == [true, "2", "0"] &&
|
|
111
|
+
model.waypoints.size == 2 && bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A zero-offset end has no explicit port, but QEA still carries both
|
|
115
|
+
# mathematical origins for the emitter's final boundary redocking.
|
|
116
|
+
def partial_port_metadata(model, link, bounds, offsets, edge)
|
|
117
|
+
return {} unless partial_port_candidate?(model, link, bounds, offsets)
|
|
118
|
+
|
|
119
|
+
source, target = bounds.values_at(:source, :target)
|
|
120
|
+
return {} unless saved_association_ray?(model, link, source, target, ray_geometry(offsets, edge))
|
|
121
|
+
|
|
122
|
+
association_ends(bounds, offsets).merge(has_geometry_offsets: true)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def partial_port_candidate?(model, link, bounds, offsets)
|
|
126
|
+
model.connector_type == "Aggregation" && !model.style[:umldi_route] && !link.path.to_s.empty? &&
|
|
127
|
+
direct_association_ray?(model, bounds, offsets) &&
|
|
128
|
+
offsets.values.count { |offset| offset.all?(&:zero?) } == 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def stored_association_candidate?(model, bounds)
|
|
132
|
+
model.connector_type == "Association" && bounds.values.all? &&
|
|
133
|
+
association_ray_style?(model.style.to_h) && model.style[:lwidth] == "2"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def association_metadata(model)
|
|
137
|
+
fields = %i[waypoints source_anchor target_anchor source_port target_port has_geometry_offsets]
|
|
138
|
+
fields.to_h { |name| [name, model.public_send(name)] }.merge(
|
|
139
|
+
style: model.style.to_h.reject { |key, _| key == :umldi_route }
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def association_model(model, bounds, offsets)
|
|
144
|
+
Ea::Model::DiagramConnector.new(
|
|
145
|
+
{ connector_type: model.connector_type, style: model.style.to_h.dup, waypoints: model.waypoints,
|
|
146
|
+
has_geometry_offsets: !offsets[:source][0].nil? }.merge(association_ends(bounds, offsets))
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def association_ends(bounds, offsets)
|
|
151
|
+
%i[source target].each_with_object({}) do |kind, result|
|
|
152
|
+
box = bounds.fetch(kind)
|
|
153
|
+
offset = offsets.fetch(kind)
|
|
154
|
+
port = port_for(box, offset)
|
|
155
|
+
result["#{kind}_port".to_sym] = point_for(port) if port
|
|
156
|
+
result["#{kind}_anchor".to_sym] = association_anchor(box, offset)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def association_anchor(box, offset)
|
|
161
|
+
Ea::Model::Point.new(x: box.x + box.width / 2 + offset[0].to_i,
|
|
162
|
+
y: box.y + box.height / 2 + offset[1].to_i)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def stored_ray_boxes(_link, bounds)
|
|
166
|
+
bounds.values_at(:source, :target)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def direct_association_ray?(model, bounds, offsets)
|
|
170
|
+
%w[Association Aggregation].include?(model.connector_type) && model.waypoints.size == 2 &&
|
|
171
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["1", "2", nil] &&
|
|
172
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A direct Generalization clips its saved mathematical origins rather
|
|
176
|
+
# than treating boundary ports as the origins of the final drawn ray.
|
|
177
|
+
def generalization_ray_metadata(model, link, bounds, offsets, edge)
|
|
178
|
+
geom = ray_geometry(offsets, edge)
|
|
179
|
+
return {} unless generalization_ray_candidate?(model, link, bounds, offsets, geom)
|
|
180
|
+
|
|
181
|
+
source, target = bounds.values_at(:source, :target)
|
|
182
|
+
return {} unless stored_ray_gap?(source, target, model.style[:mode], edge)
|
|
183
|
+
|
|
184
|
+
points = ray_points(source, target, offsets, geom)
|
|
185
|
+
return {} if points.empty?
|
|
186
|
+
|
|
187
|
+
points.merge(style: generalization_ray_style(model, points, source, target))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def generalization_ray_candidate?(model, link, bounds, offsets, geom)
|
|
191
|
+
model.waypoints.size == 2 && stored_ray_style?(model.style, link) &&
|
|
192
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?) &&
|
|
193
|
+
(!geom[:sy].zero? || !geom[:ey].zero?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def generalization_ray_style(model, points, source, target)
|
|
197
|
+
candidate = Ea::Model::DiagramConnector.new(
|
|
198
|
+
connector_type: model.connector_type, style: model.style.to_h.dup,
|
|
199
|
+
source_anchor: points[:source_anchor], target_anchor: points[:target_anchor]
|
|
200
|
+
)
|
|
201
|
+
return model.style.to_h unless centered_origin_ray?(candidate, source, target)
|
|
202
|
+
|
|
203
|
+
flags = { centered_origin_ray: true }
|
|
204
|
+
flags[:stored_origin_ray] = true if stored_origin_ray?(candidate, source, target)
|
|
205
|
+
model.style.to_h.merge(flags)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def ray_geometry(offsets, edge)
|
|
209
|
+
{ sx: offsets[:source][0], sy: offsets[:source][1],
|
|
210
|
+
ex: offsets[:target][0], ey: offsets[:target][1], edge: edge }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def saved_association_ray?(model, link, source, target, geom)
|
|
214
|
+
(!geom[:sy].zero? || !geom[:ey].zero?) &&
|
|
215
|
+
stored_ray_gap?(source, target, "1", geom[:edge]) &&
|
|
216
|
+
stored_association_path?(model, link, source, target, geom)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def ray_points(source, target, offsets, geom)
|
|
220
|
+
ports = [port_for(source, offsets[:source]), port_for(target, offsets[:target])]
|
|
221
|
+
return {} unless ports.all?
|
|
222
|
+
|
|
223
|
+
{ source_port: point_for(ports.first), target_port: point_for(ports.last),
|
|
224
|
+
source_anchor: stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
225
|
+
target_anchor: stored_ray_origin(target, geom[:ex], geom[:ey]),
|
|
226
|
+
has_geometry_offsets: true }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def point_for(pair)
|
|
230
|
+
Ea::Model::Point.new(x: pair[0], y: pair[1])
|
|
231
|
+
end
|
|
232
|
+
|
|
31
233
|
# Placements here are already Ea::Model::Bounds.
|
|
32
234
|
def bounds_from_rect(bounds)
|
|
33
235
|
bounds
|
|
@@ -9,6 +9,7 @@ module Ea
|
|
|
9
9
|
# connector passes over a crossing.
|
|
10
10
|
class ConnectorBridges
|
|
11
11
|
RADIUS = 6
|
|
12
|
+
PHANTOM = :phantom
|
|
12
13
|
|
|
13
14
|
def initialize(_diagram, routes)
|
|
14
15
|
@routes = routes.map { |connector, points| [connector, points, priority_for(connector)] }
|
|
@@ -56,78 +57,75 @@ module Ea
|
|
|
56
57
|
|
|
57
58
|
# nil or empty: no bridge decision for this connector.
|
|
58
59
|
def priority_for(connector)
|
|
60
|
+
return PHANTOM if phantom?(connector)
|
|
61
|
+
|
|
59
62
|
connector.paint_order if connector.paint_order&.any?
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
# A phantom (relationship without a link row) has no stored order; it
|
|
66
|
+
# is appended after every link in the export, so it passes over them.
|
|
67
|
+
def phantom?(connector)
|
|
68
|
+
connector.paint_order.nil?
|
|
69
|
+
end
|
|
65
70
|
|
|
71
|
+
def crossings_for(connector, priority, first, last)
|
|
66
72
|
crossings = lower_routes(connector, priority).flat_map do |_partner, points, _order|
|
|
67
73
|
points.each_cons(2).filter_map do |start, finish|
|
|
68
|
-
crossing(first, last, start, finish
|
|
74
|
+
crossing(first, last, start, finish)
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
|
-
separate_crossings(crossings,
|
|
77
|
+
separate_crossings(crossings, first, last).sort_by { |point| distance(point, first) }
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# The order key is only proven across different start placements, so
|
|
75
81
|
# connectors leaving the same placement make no carrier decision.
|
|
76
82
|
def lower_routes(connector, priority)
|
|
83
|
+
return keyed_routes(connector) if priority == PHANTOM
|
|
84
|
+
|
|
77
85
|
@routes.select do |partner, _points, order|
|
|
78
|
-
!partner.equal?(connector) && order && order.size == priority.size &&
|
|
86
|
+
!partner.equal?(connector) && order.is_a?(Array) && order.size == priority.size &&
|
|
79
87
|
!same_source?(connector, partner) && (order <=> priority).negative?
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
def keyed_routes(connector)
|
|
92
|
+
@routes.select { |partner, _points, order| !partner.equal?(connector) && order.is_a?(Array) }
|
|
93
|
+
end
|
|
94
|
+
|
|
83
95
|
def same_source?(connector, partner)
|
|
84
96
|
connector.source_element_ref && connector.source_element_ref == partner.source_element_ref
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
# Partners crossing at one point share a single arc (0BE81AC5 draws one
|
|
100
|
+
# arc for three coincident verticals). Distinct crossings whose spans
|
|
101
|
+
# overlap are all suppressed so route order cannot select one of
|
|
102
|
+
# several ambiguous arcs.
|
|
103
|
+
def separate_crossings(crossings, first, last)
|
|
104
|
+
crossings = crossings.uniq { |point| point.map { |value| value.round(6) } }
|
|
105
|
+
axis = SegmentGeometry.dominant_axis(first, last)
|
|
106
|
+
spans = crossings.map do |point|
|
|
107
|
+
SegmentGeometry.chord(first, last, point, RADIUS).map { |seat| seat[axis] }.minmax
|
|
94
108
|
end
|
|
109
|
+
crossings.reject.with_index { |_point, index| overlapping_span?(spans, index) }
|
|
95
110
|
end
|
|
96
111
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
segment_interior?(point, start, finish, 1 - axis)
|
|
103
|
-
|
|
104
|
-
point
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def segment_interior?(point, first, last, axis)
|
|
108
|
-
interior?(point[axis], first[axis], last[axis])
|
|
112
|
+
def overlapping_span?(spans, index)
|
|
113
|
+
minimum, maximum = spans[index]
|
|
114
|
+
spans.each_with_index.any? do |(other_minimum, other_maximum), other_index|
|
|
115
|
+
index != other_index && minimum < other_maximum && other_minimum < maximum
|
|
116
|
+
end
|
|
109
117
|
end
|
|
110
118
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
value > minimum && value < maximum &&
|
|
114
|
-
value - minimum >= RADIUS && maximum - value >= RADIUS
|
|
119
|
+
def crossing(first, last, start, finish)
|
|
120
|
+
SegmentGeometry.crossing(first, last, start, finish, RADIUS)
|
|
115
121
|
end
|
|
116
122
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
return 1 if first[0] == last[0] && first[1] != last[1]
|
|
120
|
-
|
|
121
|
-
nil
|
|
123
|
+
def distance(point, other)
|
|
124
|
+
SegmentGeometry.distance(point, other)
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def arc_commands(first, last, crossing)
|
|
125
|
-
|
|
126
|
-
direction = last[axis] > first[axis] ? 1 : -1
|
|
127
|
-
start = crossing.dup
|
|
128
|
-
finish = crossing.dup
|
|
129
|
-
start[axis] -= direction * RADIUS
|
|
130
|
-
finish[axis] += direction * RADIUS
|
|
128
|
+
start, finish = SegmentGeometry.chord(first, last, crossing, RADIUS)
|
|
131
129
|
["L #{coordinates(start)}", "A #{RADIUS} #{RADIUS} 180 0 0 #{coordinates(finish)}"]
|
|
132
130
|
end
|
|
133
131
|
|
|
@@ -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 || connector.style.to_h[:explicit_end_roles]
|
|
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
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Segment arithmetic for connector bridges: where two segments cross at
|
|
7
|
+
# any slope, and where the arc chord sits on the carrier segment.
|
|
8
|
+
module SegmentGeometry
|
|
9
|
+
EPSILON = 1e-9
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def distance(point, other)
|
|
14
|
+
Math.hypot(point[0] - other[0], point[1] - other[1])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 0 for a horizontal segment, 1 for a vertical one, nil otherwise.
|
|
18
|
+
def axis_for(first, last)
|
|
19
|
+
return 0 if first[1] == last[1] && first[0] != last[0]
|
|
20
|
+
return 1 if first[0] == last[0] && first[1] != last[1]
|
|
21
|
+
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A proper crossing of two segments; parallel and collinear segments
|
|
26
|
+
# never cross. Each span must leave `radius` of room either side.
|
|
27
|
+
def crossing(first, last, start, finish, radius)
|
|
28
|
+
point = axis_crossing(first, last, start, finish) ||
|
|
29
|
+
slanted_crossing(first, last, start, finish)
|
|
30
|
+
return unless point && room?(point, first, last, radius) && room?(point, start, finish, radius)
|
|
31
|
+
return unless chord_fits?(first, last, point, radius)
|
|
32
|
+
|
|
33
|
+
point
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The arc chord, one radius either side of the crossing along the
|
|
37
|
+
# carrier's dominant axis. A horizontal/vertical carrier keeps the
|
|
38
|
+
# exact crossing. On a slanted carrier EA seats the centre on the
|
|
39
|
+
# integer grid at floor(crossing) and offsets it by the half-chord
|
|
40
|
+
# (radius, truncated radius * slope), signed with the line direction.
|
|
41
|
+
def chord(first, last, crossing, radius)
|
|
42
|
+
major = dominant_axis(first, last)
|
|
43
|
+
step = last[major] > first[major] ? radius : -radius
|
|
44
|
+
return axis_chord(first, crossing, major, step) if first[1 - major] == last[1 - major]
|
|
45
|
+
|
|
46
|
+
slanted_chord(first, last, crossing, major, step)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def slanted_chord(first, last, crossing, major, step)
|
|
50
|
+
centre = crossing.map { |value| (value + EPSILON).floor }
|
|
51
|
+
half = half_chord(first, last, major, step)
|
|
52
|
+
[-1, 1].map { |sign| centre.zip(half).map { |c, h| c + sign * h } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def half_chord(first, last, major, step)
|
|
56
|
+
minor = 1 - major
|
|
57
|
+
slope = (last[minor] - first[minor]).fdiv(last[major] - first[major])
|
|
58
|
+
major.zero? ? [step, (step * slope).truncate] : [(step * slope).truncate, step]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def axis_chord(first, crossing, major, step)
|
|
62
|
+
start = crossing.dup
|
|
63
|
+
finish = crossing.dup
|
|
64
|
+
start[1 - major] = finish[1 - major] = first[1 - major]
|
|
65
|
+
start[major] -= step
|
|
66
|
+
finish[major] += step
|
|
67
|
+
[start, finish]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Integer seating must not extend the decorated carrier past an end.
|
|
71
|
+
def chord_fits?(first, last, point, radius)
|
|
72
|
+
chord(first, last, point, radius).all? do |seat|
|
|
73
|
+
[0, 1].all? { |axis| within?(seat[axis], first[axis], last[axis]) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dominant_axis(first, last)
|
|
78
|
+
(last[0] - first[0]).abs >= (last[1] - first[1]).abs ? 0 : 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def axis_crossing(first, last, start, finish)
|
|
82
|
+
axis = axis_for(first, last)
|
|
83
|
+
return unless axis && axis_for(start, finish) == 1 - axis
|
|
84
|
+
|
|
85
|
+
axis.zero? ? [start[0], first[1]] : [first[0], start[1]]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def slanted_crossing(first, last, start, finish)
|
|
89
|
+
return if axis_for(first, last) && axis_for(start, finish)
|
|
90
|
+
|
|
91
|
+
direction = delta(first, last)
|
|
92
|
+
other = delta(start, finish)
|
|
93
|
+
denominator = cross(direction, other)
|
|
94
|
+
return if denominator.zero?
|
|
95
|
+
|
|
96
|
+
advance(first, direction, cross(delta(first, start), other).fdiv(denominator))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def advance(origin, direction, fraction)
|
|
100
|
+
[origin[0] + fraction * direction[0], origin[1] + fraction * direction[1]]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delta(from, to)
|
|
104
|
+
[to[0] - from[0], to[1] - from[1]]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cross(one, other)
|
|
108
|
+
one[0] * other[1] - one[1] * other[0]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def room?(point, first, last, radius)
|
|
112
|
+
distance(point, first) >= radius && distance(point, last) >= radius &&
|
|
113
|
+
[0, 1].all? { |axis| within?(point[axis], first[axis], last[axis]) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def within?(value, from, to)
|
|
117
|
+
minimum, maximum = [from, to].minmax
|
|
118
|
+
value >= minimum - EPSILON && value <= maximum + EPSILON
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/ea/svg/ea_emitter.rb
CHANGED
|
@@ -33,6 +33,8 @@ module Ea
|
|
|
33
33
|
autoload :Connectors, "ea/svg/ea_emitter/connectors"
|
|
34
34
|
autoload :ConnectorBridges, "ea/svg/ea_emitter/connector_bridges"
|
|
35
35
|
private_constant :ConnectorBridges
|
|
36
|
+
autoload :SegmentGeometry, "ea/svg/ea_emitter/segment_geometry"
|
|
37
|
+
private_constant :SegmentGeometry
|
|
36
38
|
autoload :Markers, "ea/svg/ea_emitter/markers"
|
|
37
39
|
autoload :Labels, "ea/svg/ea_emitter/labels"
|
|
38
40
|
autoload :GhostLabels, "ea/svg/ea_emitter/ghost_labels"
|
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.139
|
|
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-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -857,6 +857,7 @@ files:
|
|
|
857
857
|
- lib/ea/sources/xmi/diagram_builder/font_settings.rb
|
|
858
858
|
- lib/ea/sources/xmi/diagram_builder/stereotype_profiles.rb
|
|
859
859
|
- lib/ea/sources/xmi/diagram_builder/umldi_edges.rb
|
|
860
|
+
- lib/ea/sources/xmi/diagram_builder/umldi_edges/property_roles.rb
|
|
860
861
|
- lib/ea/sources/xmi/extension_elements.rb
|
|
861
862
|
- lib/ea/sources/xmi/extension_geometry_parser.rb
|
|
862
863
|
- lib/ea/sources/xmi/extension_style_parser.rb
|
|
@@ -976,6 +977,7 @@ files:
|
|
|
976
977
|
- lib/ea/svg/ea_emitter/markers.rb
|
|
977
978
|
- lib/ea/svg/ea_emitter/orthogonal_ray.rb
|
|
978
979
|
- lib/ea/svg/ea_emitter/render_context.rb
|
|
980
|
+
- lib/ea/svg/ea_emitter/segment_geometry.rb
|
|
979
981
|
- lib/ea/svg/ea_emitter/style.rb
|
|
980
982
|
- lib/ea/svg/ea_emitter/text_renderer.rb
|
|
981
983
|
- lib/ea/svg/ea_emitter/visibility_symbol.rb
|