ea 0.6.130 → 0.6.132
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/metrics.rb +13 -4
- data/lib/ea/fonts/source_sans_pro_advances.json +1 -0
- data/lib/ea/model/diagram_connector.rb +7 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +8 -3
- data/lib/ea/sources/qea/diagram_builder.rb +26 -0
- data/lib/ea/sources/xmi/adapter.rb +33 -10
- data/lib/ea/sources/xmi/annotation_builder.rb +8 -0
- data/lib/ea/sources/xmi/classifier_builder.rb +11 -1
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges.rb +48 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +63 -8
- data/lib/ea/sources/xmi/extension_geometry_parser.rb +7 -5
- data/lib/ea/sources/xmi/extension_style_parser.rb +6 -5
- data/lib/ea/sources/xmi/extension_types.rb +59 -0
- data/lib/ea/sources/xmi/instance_builder.rb +98 -0
- data/lib/ea/sources/xmi/operation_builder.rb +5 -5
- data/lib/ea/sources/xmi/reception_builder.rb +59 -0
- data/lib/ea/sources/xmi/relationship_builder.rb +61 -1
- data/lib/ea/sources/xmi/stored_route.rb +146 -0
- data/lib/ea/sources/xmi/type_classifier_map.rb +1 -0
- data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +10 -22
- data/lib/ea/sources/xmi.rb +4 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +140 -0
- data/lib/ea/svg/ea_emitter/connector_redock.rb +36 -2
- data/lib/ea/svg/ea_emitter/connectors.rb +17 -36
- data/lib/ea/svg/ea_emitter/label/end_label.rb +58 -6
- data/lib/ea/svg/ea_emitter/labels.rb +24 -0
- data/lib/ea/svg/ea_emitter.rb +2 -0
- data/lib/ea/version.rb +1 -1
- metadata +9 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Routes an XMI connector the way the QEA source does: the same
|
|
7
|
+
# center-ray ports and the same QEA route helpers, over the same
|
|
8
|
+
# inputs (link style, bends in `x:y;` form, connector type, endpoint
|
|
9
|
+
# boxes). Returns nil where QEA would fall back to bare SX/SY deltas,
|
|
10
|
+
# which the generic router keeps handling.
|
|
11
|
+
class StoredRoute < Qea::DiagramBuilder
|
|
12
|
+
Link = Struct.new(:style, :path, :geometry)
|
|
13
|
+
Connector = Struct.new(:connector_type)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super(nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def waypoints(link:, connector_type:, source:, target:, bends:, offsets:, edge: nil)
|
|
20
|
+
connector = Connector.new(connector_type)
|
|
21
|
+
ports = { source: port_for(source, offsets[:source]), target: port_for(target, offsets[:target]) }
|
|
22
|
+
edge ||= 0
|
|
23
|
+
points = initial_points(link, connector, source, target, bends, ports, edge)
|
|
24
|
+
return unless points
|
|
25
|
+
|
|
26
|
+
finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Placements here are already Ea::Model::Bounds.
|
|
32
|
+
def bounds_from_rect(bounds)
|
|
33
|
+
bounds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ported?(offset)
|
|
37
|
+
offset.none?(&:nil?) && offset.any?(&:nonzero?)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def port_for(box, offset)
|
|
41
|
+
ray_port(box, offset) if ported?(offset)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initial_points(link, connector, source, target, bends, ports, edge)
|
|
45
|
+
src = ports[:source] || element_edge_point(source, :source, edge)
|
|
46
|
+
tgt = ports[:target] || element_edge_point(target, :target, edge)
|
|
47
|
+
if Qea::DiagramStyleParser.parse(link.style)[:mode] == "1"
|
|
48
|
+
[src, tgt]
|
|
49
|
+
elsif edge == 2 && ports.values.all? && !valid_tree_path?(bends, source)
|
|
50
|
+
[src, [tgt[0], src[1]], tgt]
|
|
51
|
+
elsif edge == 2 && bends.size == 1
|
|
52
|
+
tree_points(source, target, bends.first)
|
|
53
|
+
elsif bends.any?
|
|
54
|
+
bent_points(src, bends, tgt, link, connector, source, target)
|
|
55
|
+
elsif ports.values.any?
|
|
56
|
+
[src, tgt]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def tree_points(src, tgt, bend)
|
|
61
|
+
target_y = tgt.y + tgt.height <= src.y ? tgt.y + tgt.height : tgt.y
|
|
62
|
+
[[src.x + src.width, bend[1]], [bend[0], bend[1]], [bend[0], target_y]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bent_points(src, bends, tgt, link, connector, source, target)
|
|
66
|
+
points = [src, *bends, tgt]
|
|
67
|
+
redock_parent_above(points, link, source, target) if connector.connector_type == "Generalization"
|
|
68
|
+
points
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def finish(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
72
|
+
tree_route = stored_tree_route(points, link, connector, source, target, ports)
|
|
73
|
+
if tree_route
|
|
74
|
+
points = tree_route
|
|
75
|
+
elsif %w[Association Aggregation].include?(connector.connector_type) && edge != 2
|
|
76
|
+
points = facing(points, link, connector, source, target, bends)
|
|
77
|
+
end
|
|
78
|
+
points = level_entry_route(points, link, connector, source, target) if bends.any? && !tree_route
|
|
79
|
+
points = drop_zero_length_tree_segments(points, link)
|
|
80
|
+
empty_path_ray(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An empty Path has no authored corner to keep: a computed EDGE2 jog
|
|
84
|
+
# between ports that face across a gap becomes the straight port line.
|
|
85
|
+
def empty_path_ray(points, link, connector, source, target, bends, ports, offsets, edge)
|
|
86
|
+
style = Qea::DiagramStyleParser.parse(link.style)
|
|
87
|
+
return points unless %w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
88
|
+
points.size == 3 && bends.empty? && edge == 2 && ports.values.all? &&
|
|
89
|
+
style[:mode] == "3" && style[:lwidth] == "2" && !style[:tree]
|
|
90
|
+
return points unless ray_gap?(source, target, offsets)
|
|
91
|
+
|
|
92
|
+
ports.values_at(:source, :target)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ray_gap?(source, target, offsets)
|
|
96
|
+
return false unless source.x + source.width < target.x
|
|
97
|
+
|
|
98
|
+
[[source, offsets[:source]], [target, offsets[:target]]].all? do |box, (x, y)|
|
|
99
|
+
within_height?(stored_ray_origin(box, x, y).y, source) &&
|
|
100
|
+
within_height?(stored_ray_origin(box, x, y).y, target)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def stored_tree_route(points, link, connector, source, target, ports)
|
|
105
|
+
stored_square_route(link, connector, source, target) ||
|
|
106
|
+
stored_stair_route(link, connector, source, target) ||
|
|
107
|
+
stored_lateral_route(link, connector, source, target, ported: ports.values.all?) ||
|
|
108
|
+
derived_lateral_route(link, connector, source, target) ||
|
|
109
|
+
horizontal_tree_route(points, link, connector, source, target)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def facing(points, link, connector, source, target, bends)
|
|
113
|
+
stored_tree = preserve_stored_tree?(points, link, connector, source, target)
|
|
114
|
+
unless stored_tree || level_corner_route?(points, link, connector, source, target)
|
|
115
|
+
points = facing_edge_route(points, source, target)
|
|
116
|
+
end
|
|
117
|
+
if connector.connector_type == "Aggregation" && bends.any? && points.size >= 4 &&
|
|
118
|
+
(generalization_outline_offset(link, points.size) == 1 ||
|
|
119
|
+
(stored_tree && flush_width_zero_tree?(link)))
|
|
120
|
+
redock_parent_above(points, link, source, target, strict: true)
|
|
121
|
+
end
|
|
122
|
+
redock_vertical_tree_trunk(points, link, connector, source, target) if bends.any?
|
|
123
|
+
points
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# EA's SX/SY (EX/EY) point from the box center toward the port,
|
|
127
|
+
# y up; the port is where that ray leaves the box.
|
|
128
|
+
def ray_port(bounds, offset)
|
|
129
|
+
dx = offset[0].to_f
|
|
130
|
+
dy = -offset[1].to_f
|
|
131
|
+
center_x = bounds.x + bounds.width / 2.0
|
|
132
|
+
center_y = bounds.y + bounds.height / 2.0
|
|
133
|
+
t = [ray_scale(dx, bounds.x, bounds.width, center_x),
|
|
134
|
+
ray_scale(dy, bounds.y, bounds.height, center_y)].min
|
|
135
|
+
[(center_x + t * dx).round, (center_y + t * dy).round]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ray_scale(delta, origin, length, center)
|
|
139
|
+
return Float::INFINITY if delta.zero?
|
|
140
|
+
|
|
141
|
+
((delta.positive? ? origin + length : origin) - center) / delta
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -40,6 +40,14 @@ module Ea
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
# Text of the shape's UMLKeywordLabel child; name labels and
|
|
44
|
+
# other labelled children never contribute a keyword.
|
|
45
|
+
def keyword_text(shape)
|
|
46
|
+
label = shape.xpath("./ownedElement[@*[local-name()=\"type\"]=\"umldi:UMLKeywordLabel\"]").first
|
|
47
|
+
text = label && label["text"]
|
|
48
|
+
text unless text.nil? || text.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
43
51
|
def doc
|
|
44
52
|
@doc ||= Nokogiri::XML(File.read(xmi_path))
|
|
45
53
|
end
|
|
@@ -47,34 +55,14 @@ module Ea
|
|
|
47
55
|
def keywords_by_diagram
|
|
48
56
|
@keywords_by_diagram ||= begin
|
|
49
57
|
result = {}
|
|
50
|
-
# The Sparx XMI export declares the umldi: namespace
|
|
51
|
-
# but the elements inside Diagram have lost their
|
|
52
|
-
# prefix binding once parsed by Nokogiri — the
|
|
53
|
-
# element name resolves to local-name "ownedElement"
|
|
54
|
-
# and the umldi:UMLClassifierShape type information
|
|
55
|
-
# lives in the literal `type` attribute. We therefore
|
|
56
|
-
# match shape children by the presence of `modelElement`
|
|
57
|
-
# and the keyword label by its `text` attribute.
|
|
58
58
|
doc.xpath("//*[local-name()=\"Diagram\"]").each do |diagram|
|
|
59
59
|
diagram_id = diagram["id"] || diagram["xmi:id"]
|
|
60
60
|
next unless diagram_id
|
|
61
61
|
|
|
62
62
|
keywords = {}
|
|
63
63
|
diagram.xpath("./ownedElement[@modelElement]").each do |shape|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Keyword label is the first ownedElement child
|
|
68
|
-
# with a non-empty `text` attribute. The name
|
|
69
|
-
# label follows it.
|
|
70
|
-
shape.xpath("./ownedElement[@text]").each do |child|
|
|
71
|
-
text = child["text"]
|
|
72
|
-
next if text.nil? || text.empty?
|
|
73
|
-
next if text.start_with?("«") # safety
|
|
74
|
-
|
|
75
|
-
keywords[model_id] = text
|
|
76
|
-
break
|
|
77
|
-
end
|
|
64
|
+
text = keyword_text(shape)
|
|
65
|
+
keywords[shape["modelElement"]] = text if text
|
|
78
66
|
end
|
|
79
67
|
result[diagram_id] = keywords unless keywords.empty?
|
|
80
68
|
end
|
data/lib/ea/sources/xmi.rb
CHANGED
|
@@ -18,14 +18,18 @@ module Ea
|
|
|
18
18
|
autoload :MetadataBuilder, "ea/sources/xmi/metadata_builder"
|
|
19
19
|
autoload :PackageBuilder, "ea/sources/xmi/package_builder"
|
|
20
20
|
autoload :ClassifierBuilder, "ea/sources/xmi/classifier_builder"
|
|
21
|
+
autoload :InstanceBuilder, "ea/sources/xmi/instance_builder"
|
|
21
22
|
autoload :PropertyBuilder, "ea/sources/xmi/property_builder"
|
|
22
23
|
autoload :OperationBuilder, "ea/sources/xmi/operation_builder"
|
|
24
|
+
autoload :ReceptionBuilder, "ea/sources/xmi/reception_builder"
|
|
23
25
|
autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
|
|
24
26
|
autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
|
|
25
27
|
autoload :TagBuilder, "ea/sources/xmi/tag_builder"
|
|
26
28
|
autoload :NoteBuilder, "ea/sources/xmi/note_builder"
|
|
27
29
|
autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
|
|
28
30
|
autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
|
|
31
|
+
autoload :StoredRoute, "ea/sources/xmi/stored_route"
|
|
32
|
+
autoload :ExtensionTypes, "ea/sources/xmi/extension_types"
|
|
29
33
|
autoload :ExtensionElements, "ea/sources/xmi/extension_elements"
|
|
30
34
|
autoload :ExtensionGeometryParser, "ea/sources/xmi/extension_geometry_parser"
|
|
31
35
|
autoload :ExtensionStyleParser, "ea/sources/xmi/extension_style_parser"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Bridges decorate resolved render paths; stored routes and marker seats
|
|
7
|
+
# remain centerlines. The connector's stored order key (reproducing EA's
|
|
8
|
+
# exported link order), rather than style group order, determines which
|
|
9
|
+
# connector passes over a crossing.
|
|
10
|
+
class ConnectorBridges
|
|
11
|
+
RADIUS = 6
|
|
12
|
+
|
|
13
|
+
def initialize(_diagram, routes)
|
|
14
|
+
@routes = routes.map { |connector, points| [connector, points, priority_for(connector)] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def path_for(connector, points)
|
|
18
|
+
priority = priority_for(connector)
|
|
19
|
+
return centerline(connector, points) unless priority
|
|
20
|
+
|
|
21
|
+
segments = points.each_cons(2).map do |first, last|
|
|
22
|
+
crossings_for(connector, priority, first, last)
|
|
23
|
+
end
|
|
24
|
+
return centerline(connector, points) if segments.all?(&:empty?)
|
|
25
|
+
|
|
26
|
+
serialize(points, segments)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def centerline(connector, points)
|
|
32
|
+
seats = points.map { |point| coordinates(point) }
|
|
33
|
+
# Direct lines retain EA's target-first repeat and source-first model.
|
|
34
|
+
if points.size == 2 && direct?(connector)
|
|
35
|
+
"M #{seats.last} L #{seats.first} M #{seats.last}"
|
|
36
|
+
else
|
|
37
|
+
"M #{seats.join(" L ")}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def direct?(connector)
|
|
42
|
+
style = connector.style || {}
|
|
43
|
+
style.key?(:direct) || style.key?("direct")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def serialize(points, segments)
|
|
47
|
+
commands = ["M #{coordinates(points.first)}"]
|
|
48
|
+
points.each_cons(2).with_index do |(first, last), index|
|
|
49
|
+
segments[index].each do |crossing|
|
|
50
|
+
commands.concat(arc_commands(first, last, crossing))
|
|
51
|
+
end
|
|
52
|
+
commands << "L #{coordinates(last)}"
|
|
53
|
+
end
|
|
54
|
+
commands.join(" ")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# nil or empty: no bridge decision for this connector.
|
|
58
|
+
def priority_for(connector)
|
|
59
|
+
connector.paint_order if connector.paint_order&.any?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def crossings_for(connector, priority, first, last)
|
|
63
|
+
axis = axis_for(first, last)
|
|
64
|
+
return [] unless axis
|
|
65
|
+
|
|
66
|
+
crossings = lower_routes(connector, priority).flat_map do |_partner, points, _order|
|
|
67
|
+
points.each_cons(2).filter_map do |start, finish|
|
|
68
|
+
crossing(first, last, start, finish, axis)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
separate_crossings(crossings, axis).sort_by { |point| (point[axis] - first[axis]).abs }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The order key is only proven across different start placements, so
|
|
75
|
+
# connectors leaving the same placement make no carrier decision.
|
|
76
|
+
def lower_routes(connector, priority)
|
|
77
|
+
@routes.select do |partner, _points, order|
|
|
78
|
+
!partner.equal?(connector) && order && order.size == priority.size &&
|
|
79
|
+
!same_source?(connector, partner) && (order <=> priority).negative?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def same_source?(connector, partner)
|
|
84
|
+
connector.source_element_ref && connector.source_element_ref == partner.source_element_ref
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Suppress both overlapping spans, including coincident crossings,
|
|
88
|
+
# so route order cannot select one of several ambiguous arcs.
|
|
89
|
+
def separate_crossings(crossings, axis)
|
|
90
|
+
crossings.reject.with_index do |point, index|
|
|
91
|
+
crossings.each_with_index.any? do |other, other_index|
|
|
92
|
+
index != other_index && (point[axis] - other[axis]).abs < 2 * RADIUS
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def crossing(first, last, start, finish, axis)
|
|
98
|
+
return unless axis_for(start, finish) == 1 - axis
|
|
99
|
+
|
|
100
|
+
point = axis.zero? ? [start[0], first[1]] : [first[0], start[1]]
|
|
101
|
+
return unless segment_interior?(point, first, last, axis) &&
|
|
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])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def interior?(value, first, last)
|
|
112
|
+
minimum, maximum = [first, last].minmax
|
|
113
|
+
value > minimum && value < maximum &&
|
|
114
|
+
value - minimum >= RADIUS && maximum - value >= RADIUS
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def axis_for(first, last)
|
|
118
|
+
return 0 if first[1] == last[1] && first[0] != last[0]
|
|
119
|
+
return 1 if first[0] == last[0] && first[1] != last[1]
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def arc_commands(first, last, crossing)
|
|
125
|
+
axis = axis_for(first, last)
|
|
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
|
|
131
|
+
["L #{coordinates(start)}", "A #{RADIUS} #{RADIUS} 180 0 0 #{coordinates(finish)}"]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def coordinates(point)
|
|
135
|
+
point.map { |value| Canvas.coord(value) }.join(" ")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -16,9 +16,10 @@ module Ea
|
|
|
16
16
|
# Returns replacement waypoint pairs for direct or lateral tree routes,
|
|
17
17
|
# for the connector, or nil when the connector must keep its
|
|
18
18
|
# parsed waypoints (not direct, not 2-point, unresolved ends,
|
|
19
|
-
# self-loop, or no drawn bounds for either end).
|
|
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
|
|
22
23
|
|
|
23
24
|
tether = association_class_pairs(connector, bounds_map)
|
|
24
25
|
return tether if tether
|
|
@@ -53,8 +54,8 @@ module Ea
|
|
|
53
54
|
|
|
54
55
|
return nil if flush_lateral_line?(connector, points, source_bounds, target_bounds)
|
|
55
56
|
|
|
56
|
-
anchors = [connector.source_anchor, connector.target_anchor]
|
|
57
57
|
bounds = [source_bounds, target_bounds]
|
|
58
|
+
anchors = drawn_center_anchors(connector, diagram, [source_ref, target_ref], bounds)
|
|
58
59
|
original = clipped_pairs(bounds, anchors)
|
|
59
60
|
unless stroke_outline?(connector, bounds) || overlap_ray?(connector) ||
|
|
60
61
|
bordered_outline?(connector, diagram, [source_ref, target_ref], bounds)
|
|
@@ -78,6 +79,38 @@ module Ea
|
|
|
78
79
|
auto_port_elbow_pairs(connector, source, target, pairs)
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
def drawn_center_anchors(connector, diagram, refs, bounds)
|
|
83
|
+
anchors = [connector.source_anchor, connector.target_anchor]
|
|
84
|
+
return anchors unless connector.style.to_h[:centered_origin_ray] == true
|
|
85
|
+
|
|
86
|
+
duids = connector.style.to_h.values_at(:soid, :eoid)
|
|
87
|
+
anchors.each_with_index.map do |anchor, index|
|
|
88
|
+
saved = unique_logical_bounds(diagram, refs[index], duids[index])
|
|
89
|
+
centered_anchor(anchor, saved, bounds[index])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
private_class_method :drawn_center_anchors
|
|
93
|
+
|
|
94
|
+
def unique_logical_bounds(diagram, ref, duid)
|
|
95
|
+
placements = end_placements(diagram, ref, duid)
|
|
96
|
+
placements.first.bounds if placements.size == 1
|
|
97
|
+
end
|
|
98
|
+
private_class_method :unique_logical_bounds
|
|
99
|
+
|
|
100
|
+
def centered_anchor(anchor, saved, drawn)
|
|
101
|
+
return anchor unless anchor && saved
|
|
102
|
+
|
|
103
|
+
Ea::Model::Point.new(x: anchor.x + center_shift(saved, drawn, :x, :width),
|
|
104
|
+
y: anchor.y + center_shift(saved, drawn, :y, :height))
|
|
105
|
+
end
|
|
106
|
+
private_class_method :centered_anchor
|
|
107
|
+
|
|
108
|
+
def center_shift(saved, drawn, coordinate, size)
|
|
109
|
+
drawn.public_send(coordinate) + drawn.public_send(size) / 2 -
|
|
110
|
+
saved.public_send(coordinate) - saved.public_send(size) / 2
|
|
111
|
+
end
|
|
112
|
+
private_class_method :center_shift
|
|
113
|
+
|
|
81
114
|
# A tether joins its saved association anchor to the facing
|
|
82
115
|
# side-center of the attached class, rather than to the other proxy.
|
|
83
116
|
def association_class_pairs(connector, bounds_map)
|
|
@@ -475,6 +508,7 @@ module Ea
|
|
|
475
508
|
def row_slot_adjust(pairs, connector, diagram, model_index,
|
|
476
509
|
bounds_map, divider_y_by_ref)
|
|
477
510
|
return nil unless bounds_map && divider_y_by_ref
|
|
511
|
+
return nil if connector.style.to_h[:umldi_route] == true
|
|
478
512
|
return nil unless pairs.size == 2
|
|
479
513
|
return nil if flush_overlap_ray?(connector)
|
|
480
514
|
return nil if explicit_association_ray?(pairs, connector)
|
|
@@ -74,11 +74,14 @@ module Ea
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def paths_with_styles
|
|
77
|
-
visible_connectors.filter_map do |connector|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
routes = visible_connectors.filter_map do |connector|
|
|
78
|
+
points = resolved_points(connector)
|
|
79
|
+
[connector, points.map { |point| translate_point(point) }] if points.size >= 2
|
|
80
|
+
end
|
|
81
|
+
bridges = ConnectorBridges.new(diagram, routes)
|
|
82
|
+
routes.map do |connector, points|
|
|
83
|
+
[[self.class.stroke_width_for(connector, stroke_width), dashed?(connector)],
|
|
84
|
+
path_for(connector, points, bridges)]
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
|
|
@@ -103,39 +106,17 @@ module Ea
|
|
|
103
106
|
(diagram.connectors || []).select(&:renderable?)
|
|
104
107
|
end
|
|
105
108
|
|
|
106
|
-
def
|
|
109
|
+
def resolved_points(connector)
|
|
107
110
|
pts = ConnectorRedock.pairs_for(connector, diagram, model_index,
|
|
108
111
|
drawn_bounds) ||
|
|
109
112
|
waypoints_for(connector)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return nil if pts.size < 2
|
|
114
|
-
|
|
115
|
-
# DIRECT (auto-routed straight) connectors: EA writes the
|
|
116
|
-
# segment TARGET-FIRST with a trailing zero-length repeat —
|
|
117
|
-
# "M target L source M target" (B6847577, E58034A3,
|
|
118
|
-
# 8CF27BE8). Same pixels as source-first; only the byte
|
|
119
|
-
# order differs, so reversal lives here at the emitter and
|
|
120
|
-
# the marker anchoring (source = first waypoint) is
|
|
121
|
-
# untouched.
|
|
122
|
-
if pts.size == 2 && direct?(connector)
|
|
123
|
-
x1, y1 = translate_point(pts.first)
|
|
124
|
-
x2, y2 = translate_point(pts.last)
|
|
125
|
-
d = "M #{Canvas.coord(x2)} #{Canvas.coord(y2)} L #{Canvas.coord(x1)} #{Canvas.coord(y1)} " \
|
|
126
|
-
"M #{Canvas.coord(x2)} #{Canvas.coord(y2)}"
|
|
127
|
-
else
|
|
128
|
-
d = pts.each_with_index.map do |p, idx|
|
|
129
|
-
x, y = translate_point(p)
|
|
130
|
-
"#{idx.zero? ? 'M' : 'L'} #{Canvas.coord(x)} #{Canvas.coord(y)}"
|
|
131
|
-
end.join(" ")
|
|
132
|
-
end
|
|
133
|
-
%(<path d="#{d}" shape-rendering="auto"/>)
|
|
113
|
+
ConnectorRedock.row_slot_adjust(pts, connector, diagram,
|
|
114
|
+
model_index, drawn_bounds,
|
|
115
|
+
divider_y_by_ref) || pts
|
|
134
116
|
end
|
|
135
117
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
style.key?(:direct) || style.key?("direct")
|
|
118
|
+
def path_for(connector, pts, bridges)
|
|
119
|
+
%(<path d="#{bridges.path_for(connector, pts)}" shape-rendering="auto"/>)
|
|
139
120
|
end
|
|
140
121
|
|
|
141
122
|
def waypoints_for(connector)
|
|
@@ -146,10 +127,10 @@ module Ea
|
|
|
146
127
|
end
|
|
147
128
|
end
|
|
148
129
|
|
|
149
|
-
def translate_point(
|
|
150
|
-
return
|
|
130
|
+
def translate_point(point)
|
|
131
|
+
return point unless @canvas
|
|
151
132
|
|
|
152
|
-
[@canvas.translate_x(
|
|
133
|
+
[@canvas.translate_x(point[0]), @canvas.translate_y(point[1])]
|
|
153
134
|
end
|
|
154
135
|
end
|
|
155
136
|
end
|
|
@@ -54,12 +54,14 @@ module Ea
|
|
|
54
54
|
# end_kind - :source or :target
|
|
55
55
|
# heading - [dx, dy] of the first leg leaving the anchor
|
|
56
56
|
# source_bounds - effective drawn source outline, in anchor coordinates
|
|
57
|
+
# target_face - exact drawn target dock face when it leads the emitted leg
|
|
57
58
|
# bend - first bend of a saved descending vertical stair
|
|
58
59
|
# stored_labels - the connector has stored label boxes at
|
|
59
60
|
# either end; false keeps the opposite-end
|
|
60
61
|
# fallback
|
|
61
62
|
def texts(text_box:, mult_box:, anchor:, connector:, end_kind:,
|
|
62
|
-
tree: false, heading: nil, stored_labels: false, bend: nil, source_bounds: nil
|
|
63
|
+
tree: false, heading: nil, stored_labels: false, bend: nil, source_bounds: nil,
|
|
64
|
+
target_face: nil)
|
|
63
65
|
from_property, role, mult = role_and_multiplicity(connector, end_kind)
|
|
64
66
|
if role_empty?(role) && mult_empty?(mult)
|
|
65
67
|
other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
|
|
@@ -114,13 +116,23 @@ module Ea
|
|
|
114
116
|
target_diagonal_property_role(connector, heading, anchor, text_box, role) ||
|
|
115
117
|
target_vertical_role(connector, heading, anchor, text_box, role) ||
|
|
116
118
|
target_rightward_role(heading, anchor, text_box, role) || text_pos
|
|
119
|
+
if horizontal_aligned_target_baseline?(connector, heading, text_box, role)
|
|
120
|
+
text_pos = [text_pos[0], seat_corner(anchor, text_box)[1] - 7]
|
|
121
|
+
end
|
|
122
|
+
face_multiplicity = if target_left_face_multiplicity?(connector, heading, mult_box, mult,
|
|
123
|
+
target_face)
|
|
124
|
+
directional_multiplicity(:left, anchor, mult_box, mult)
|
|
125
|
+
end
|
|
117
126
|
mult_pos = target_multiplicity_seat(connector, heading, anchor, mult_box) ||
|
|
118
|
-
target_horizontal_multiplicity(heading, anchor, mult_box, mult) ||
|
|
127
|
+
face_multiplicity || target_horizontal_multiplicity(heading, anchor, mult_box, mult) ||
|
|
128
|
+
mult_pos
|
|
119
129
|
end
|
|
120
130
|
|
|
121
131
|
if end_kind == :source
|
|
122
132
|
text_pos = source_top_property_role(connector, heading, anchor, text_box, role) ||
|
|
123
133
|
source_bottom_property_role(connector, heading, anchor, text_box, role) || text_pos
|
|
134
|
+
mult_pos = source_top_property_multiplicity(connector, heading, anchor,
|
|
135
|
+
[text_box, mult_box], mult) || mult_pos
|
|
124
136
|
if external_upward_property_source?(connector, heading, anchor, text_box, source_bounds)
|
|
125
137
|
text_pos = external_upward_property_role(anchor, role) || text_pos
|
|
126
138
|
end
|
|
@@ -143,6 +155,23 @@ module Ea
|
|
|
143
155
|
|
|
144
156
|
private
|
|
145
157
|
|
|
158
|
+
# ALN2 single-line target roles retain their horizontal placement
|
|
159
|
+
# and use the saved corner's baseline on a straight stored ray.
|
|
160
|
+
def horizontal_aligned_target_baseline?(connector, heading, box, role)
|
|
161
|
+
return false unless heading && heading[1].zero? && !heading[0].zero?
|
|
162
|
+
return false unless saved_target_ray?(connector) && !role_empty?(role)
|
|
163
|
+
return false unless connector_source_dest_stereotype(connector, :target).to_s.empty?
|
|
164
|
+
|
|
165
|
+
aligned_target_baseline_box?(box)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def aligned_target_baseline_box?(box)
|
|
169
|
+
return false unless seat_font? && font_weight == 400 && font_style == "normal"
|
|
170
|
+
return false unless box && box["alignment"] == 2 && [13, 14].include?(box["cy"])
|
|
171
|
+
|
|
172
|
+
cambria_box?(box.merge("alignment" => 0))
|
|
173
|
+
end
|
|
174
|
+
|
|
146
175
|
def plain_leftward_target_role(connector, heading, anchor, box, role)
|
|
147
176
|
return nil unless heading && heading[0].negative? && heading[1].zero?
|
|
148
177
|
return nil unless plain_target_role?(connector, box, role)
|
|
@@ -339,14 +368,27 @@ module Ea
|
|
|
339
368
|
# EDGE=1 certifies the top dock independently of the clipped ray's
|
|
340
369
|
# heading. An absent source box uses the upward Property seat.
|
|
341
370
|
def source_top_property_role(connector, heading, anchor, box, role)
|
|
342
|
-
return nil unless
|
|
343
|
-
return nil unless zero_source_property_box?(box)
|
|
344
|
-
|
|
345
|
-
return nil unless source_top_property_route?(connector.style.to_h)
|
|
371
|
+
return nil unless source_top_property_box?(connector, heading, box)
|
|
346
372
|
|
|
347
373
|
source_property_seat(connector, :up, anchor, box, role)
|
|
348
374
|
end
|
|
349
375
|
|
|
376
|
+
def source_top_property_multiplicity(connector, heading, anchor, boxes, mult)
|
|
377
|
+
return nil if mult_empty?(mult)
|
|
378
|
+
|
|
379
|
+
role_box, mult_box = boxes
|
|
380
|
+
return nil unless source_top_property_box?(connector, heading, role_box)
|
|
381
|
+
return nil unless source_top_property_box?(connector, heading, mult_box)
|
|
382
|
+
|
|
383
|
+
mult_seat(:up, anchor, mult_box)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def source_top_property_box?(connector, heading, box)
|
|
387
|
+
source_seat?(:source, heading) && heading[0].negative? && heading[1].positive? &&
|
|
388
|
+
zero_source_property_box?(box) && source_top_property_route?(connector.style.to_h) &&
|
|
389
|
+
source_property_seat?(connector, :up, box)
|
|
390
|
+
end
|
|
391
|
+
|
|
350
392
|
# A persisted origin above the drawn source outline keeps the
|
|
351
393
|
# upward seat's inset and the shared upper baseline, including
|
|
352
394
|
# steep diagonals. Origins clipped onto the face retain -6.
|
|
@@ -697,11 +739,21 @@ module Ea
|
|
|
697
739
|
def target_horizontal_multiplicity(heading, anchor, box, mult)
|
|
698
740
|
return nil unless seat_font? && heading && heading[1].zero? && !heading[0].zero?
|
|
699
741
|
return nil if mult_empty?(mult)
|
|
742
|
+
|
|
700
743
|
return directional_multiplicity(:left, anchor, box, mult) if heading[0].negative?
|
|
701
744
|
|
|
702
745
|
mult_seat(:right, anchor, box)
|
|
703
746
|
end
|
|
704
747
|
|
|
748
|
+
def target_left_face_multiplicity?(connector, heading, box, mult, target_face)
|
|
749
|
+
seat_font? && !mult_empty?(mult) && property_label?(connector, :target) && cambria_box?(box) &&
|
|
750
|
+
stored_offsets_zero?(box) && target_left_face_heading?(heading, target_face)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def target_left_face_heading?(heading, target_face)
|
|
754
|
+
heading && !heading[1].zero? && heading[0].negative? && target_face == [-1, 0]
|
|
755
|
+
end
|
|
756
|
+
|
|
705
757
|
def target_multiplicity_seat(connector, heading, anchor, box)
|
|
706
758
|
return nil unless seat_font?
|
|
707
759
|
return nil unless heading && heading[0].zero? && heading[1].negative?
|
|
@@ -100,6 +100,7 @@ module Ea
|
|
|
100
100
|
end_kind: :target, tree: tree && tree[:target],
|
|
101
101
|
bend: Label::TargetBendSeat.reference(points),
|
|
102
102
|
heading: (stored_tgt || stored_labels) && target_heading(connector, points),
|
|
103
|
+
target_face: target_dock_heading(connector, target_pt, target_heading(connector, points)),
|
|
103
104
|
stored_labels: stored_labels).each { |t| texts << t }
|
|
104
105
|
texts
|
|
105
106
|
end
|
|
@@ -202,6 +203,29 @@ module Ea
|
|
|
202
203
|
drawn_bounds[source]
|
|
203
204
|
end
|
|
204
205
|
|
|
206
|
+
def target_dock_bounds(connector)
|
|
207
|
+
return nil unless model_index && drawn_bounds
|
|
208
|
+
|
|
209
|
+
relationship = model_index[connector.relationship_ref]
|
|
210
|
+
return nil unless relationship
|
|
211
|
+
|
|
212
|
+
source, target = ConnectorRedock.end_refs(relationship)
|
|
213
|
+
return nil if source == target
|
|
214
|
+
return nil unless drawn_bounds[source]
|
|
215
|
+
|
|
216
|
+
drawn_bounds[target]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def target_dock_heading(connector, point, heading)
|
|
220
|
+
return nil unless heading
|
|
221
|
+
|
|
222
|
+
box = target_dock_bounds(connector)
|
|
223
|
+
face = box && dock_face_heading(point, box)
|
|
224
|
+
return nil unless face&.zip(heading)&.any? { |axis, delta| (axis * delta).positive? }
|
|
225
|
+
|
|
226
|
+
face
|
|
227
|
+
end
|
|
228
|
+
|
|
205
229
|
# Saved rays can start outside the box. Project onto its boundary;
|
|
206
230
|
# corners stay ambiguous and the face must still lead the emitted leg.
|
|
207
231
|
def source_dock_heading(connector, point, box)
|