ea 0.6.131 → 0.6.133
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 +45 -3
- data/lib/ea/sources/qea/diagram_builder.rb +26 -0
- 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/relationship_builder.rb +1 -1
- data/lib/ea/sources/xmi/stored_route.rb +146 -0
- data/lib/ea/sources/xmi.rb +1 -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 +5 -1
|
@@ -9,6 +9,8 @@ module Ea
|
|
|
9
9
|
# SOID/EOID, EDGE, label boxes) when present. Falls back to
|
|
10
10
|
# the UML-DI owned_element bounds otherwise.
|
|
11
11
|
class DiagramBuilder
|
|
12
|
+
autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
13
|
+
|
|
12
14
|
attr_reader :root, :xmi_path
|
|
13
15
|
|
|
14
16
|
def initialize(root, xmi_path = nil)
|
|
@@ -53,6 +55,7 @@ module Ea
|
|
|
53
55
|
bounds: canvas_bounds(placed_elements),
|
|
54
56
|
style: style_for(ext_diagram),
|
|
55
57
|
style_ex: style_ex_for(ext_diagram),
|
|
58
|
+
show_notes: style_ex_for(ext_diagram).to_s.include?("ShowNotes=1"),
|
|
56
59
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
57
60
|
connectors: build_connectors(placed_connectors,
|
|
58
61
|
placed_elements, id),
|
|
@@ -144,20 +147,23 @@ module Ea
|
|
|
144
147
|
acc[pe.style.duid] = pe if pe.style.duid
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
placed_connectors.map
|
|
150
|
+
placed_connectors.each_with_index.map do |placed, index|
|
|
151
|
+
build_connector(placed, by_duid, owner_id, index)
|
|
152
|
+
end
|
|
148
153
|
end
|
|
149
154
|
|
|
150
|
-
def build_connector(placed, by_duid, owner_id)
|
|
155
|
+
def build_connector(placed, by_duid, owner_id, rank)
|
|
151
156
|
geom = placed.geometry
|
|
152
157
|
style = placed.style
|
|
153
158
|
source = by_duid[style.soid]
|
|
154
159
|
target = by_duid[style.eoid]
|
|
155
|
-
waypoints = compute_waypoints(geom, source, target)
|
|
156
160
|
ext_meta = connector_meta(placed.subject)
|
|
161
|
+
route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
|
|
157
162
|
Ea::Model::DiagramConnector.new(
|
|
158
163
|
id: IdNormalizer.synthetic_id(owner_id, "conn", placed.subject),
|
|
159
164
|
diagram_id: owner_id,
|
|
160
165
|
relationship_ref: placed.subject,
|
|
166
|
+
paint_order: [rank],
|
|
161
167
|
source_duid: style.soid,
|
|
162
168
|
target_duid: style.eoid,
|
|
163
169
|
source_element_ref: source && synthetic_element_id(owner_id, source),
|
|
@@ -166,8 +172,8 @@ module Ea
|
|
|
166
172
|
target_edge: geom.edge,
|
|
167
173
|
connector_type: ext_meta[:ea_type],
|
|
168
174
|
direction: ext_meta[:direction],
|
|
169
|
-
waypoints: waypoints,
|
|
170
|
-
style:
|
|
175
|
+
waypoints: route[:waypoints],
|
|
176
|
+
style: route[:style],
|
|
171
177
|
line_color: style.color,
|
|
172
178
|
line_width: style.line_width,
|
|
173
179
|
hidden: style.hidden,
|
|
@@ -211,11 +217,35 @@ module Ea
|
|
|
211
217
|
IdNormalizer.synthetic_id(owner_id, "elem", placed.subject)
|
|
212
218
|
end
|
|
213
219
|
|
|
214
|
-
def
|
|
220
|
+
def connector_route(placed, source, target, owner_id, type)
|
|
221
|
+
native = umldi_waypoints(owner_id, placed) if placed.geometry.bend_points.to_a.empty?
|
|
222
|
+
points = native || compute_waypoints(placed.geometry, source, target,
|
|
223
|
+
style: placed.style, connector_type: type)
|
|
224
|
+
native ||= umldi_waypoints(owner_id, placed) if points.empty?
|
|
225
|
+
{ waypoints: native || points, style: native ? { umldi_route: true } : {} }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# EA's unique exported edge supplies bendless routes and recovers
|
|
229
|
+
# routes whose placement endpoints cannot be resolved.
|
|
230
|
+
def umldi_waypoints(diagram_id, placed)
|
|
231
|
+
return unless xmi_path
|
|
232
|
+
|
|
233
|
+
route = umldi_edges.route_for(diagram_id, placed.subject)
|
|
234
|
+
route&.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def umldi_edges
|
|
238
|
+
@umldi_edges ||= UmldiEdges.new(xmi_path)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def compute_waypoints(geom, source, target, style:, connector_type:)
|
|
215
242
|
src_bounds = bounds_from_placed(source)
|
|
216
243
|
tgt_bounds = bounds_from_placed(target)
|
|
217
244
|
return [] unless src_bounds && tgt_bounds
|
|
218
245
|
|
|
246
|
+
stored = stored_route(geom, style, connector_type, src_bounds, tgt_bounds)
|
|
247
|
+
return stored if stored
|
|
248
|
+
|
|
219
249
|
router = Ea::Svg::ConnectorRouter.new(
|
|
220
250
|
source_bounds: src_bounds,
|
|
221
251
|
target_bounds: tgt_bounds,
|
|
@@ -229,6 +259,27 @@ module Ea
|
|
|
229
259
|
end
|
|
230
260
|
end
|
|
231
261
|
|
|
262
|
+
# Stored bends are routed by StoredRoute where a QEA rule claims
|
|
263
|
+
# them; anything else keeps the generic router.
|
|
264
|
+
def stored_route(geom, style, connector_type, src_bounds, tgt_bounds)
|
|
265
|
+
offsets = { source: [geom.sx, geom.sy], target: [geom.ex, geom.ey] }
|
|
266
|
+
points = StoredRoute.new.waypoints(
|
|
267
|
+
link: StoredRoute::Link.new(style.raw, geom.path.to_s.tr("$", ";"), offset_geometry(geom)),
|
|
268
|
+
connector_type: connector_type, source: src_bounds, target: tgt_bounds,
|
|
269
|
+
bends: geom.bend_points.to_a, offsets: offsets, edge: geom.edge
|
|
270
|
+
)
|
|
271
|
+
return unless points
|
|
272
|
+
|
|
273
|
+
points.map do |x, y|
|
|
274
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def offset_geometry(geom)
|
|
279
|
+
{ "SX" => geom.sx, "SY" => geom.sy, "EX" => geom.ex, "EY" => geom.ey }
|
|
280
|
+
.filter_map { |key, value| "#{key}=#{value};" if value }.join
|
|
281
|
+
end
|
|
282
|
+
|
|
232
283
|
def bounds_from_placed(placed)
|
|
233
284
|
return nil unless placed
|
|
234
285
|
|
|
@@ -264,12 +315,16 @@ module Ea
|
|
|
264
315
|
|
|
265
316
|
label_boxes.transform_values do |box|
|
|
266
317
|
{
|
|
267
|
-
"cx" => box.cx, "cy" => box.cy,
|
|
268
318
|
"ox" => box.ox, "oy" => box.oy,
|
|
319
|
+
"cx" => box.cx, "cy" => box.cy,
|
|
320
|
+
"hidden" => box.hidden,
|
|
269
321
|
"bold" => box.bold,
|
|
270
322
|
"italic" => box.italic,
|
|
271
323
|
"underline" => box.underline,
|
|
272
|
-
"
|
|
324
|
+
"color" => box.color,
|
|
325
|
+
"alignment" => box.align,
|
|
326
|
+
"direction" => box.direction,
|
|
327
|
+
"rotation" => box.rotation
|
|
273
328
|
}
|
|
274
329
|
end
|
|
275
330
|
end
|
|
@@ -38,8 +38,9 @@ module Ea
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# EA's Path= field uses the format `x1:y1$x2:y2$...` where
|
|
41
|
-
# each pair is a bend point.
|
|
42
|
-
#
|
|
41
|
+
# each pair is a bend point. EA stores Y negated (as in QEA
|
|
42
|
+
# t_diagramlinks.Path); the result uses screen-positive Y,
|
|
43
|
+
# like element bounds.
|
|
43
44
|
def parse_bend_path(raw)
|
|
44
45
|
return [] if raw.nil? || raw.empty?
|
|
45
46
|
|
|
@@ -49,7 +50,7 @@ module Ea
|
|
|
49
50
|
x_str, y_str = pair.split(":", 2)
|
|
50
51
|
x = int(x_str)
|
|
51
52
|
y = int(y_str)
|
|
52
|
-
[x, y] if x && y
|
|
53
|
+
[x, -y] if x && y
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -72,7 +73,7 @@ module Ea
|
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def extract_label_boxes(geometry_string)
|
|
75
|
-
geometry_string.to_s.scan(/\$?([A-Z]{3})=((?:[A-Za-z]{1,4}=-?\d
|
|
76
|
+
geometry_string.to_s.scan(/\$?([A-Z]{3})=((?:[A-Za-z]{1,4}=-?\d+:?)+)/).each_with_object({}) do |(key, body), acc|
|
|
76
77
|
sym = key.downcase.to_sym
|
|
77
78
|
next unless LABEL_BOX_KEYS.include?(sym)
|
|
78
79
|
next if body.nil? || body.empty?
|
|
@@ -98,6 +99,7 @@ module Ea
|
|
|
98
99
|
italic: hash["ITA"] == 1,
|
|
99
100
|
underline: hash["UND"] == 1,
|
|
100
101
|
hidden: hash["HDN"] == 1,
|
|
102
|
+
color: hash["CLR"],
|
|
101
103
|
align: hash["ALN"],
|
|
102
104
|
direction: hash["DIR"],
|
|
103
105
|
rotation: hash["ROT"]
|
|
@@ -114,7 +116,7 @@ module Ea
|
|
|
114
116
|
|
|
115
117
|
LabelBox = Struct.new(
|
|
116
118
|
:cx, :cy, :ox, :oy,
|
|
117
|
-
:bold, :italic, :underline, :hidden,
|
|
119
|
+
:bold, :italic, :underline, :hidden, :color,
|
|
118
120
|
:align, :direction, :rotation,
|
|
119
121
|
keyword_init: true
|
|
120
122
|
)
|
|
@@ -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
|
data/lib/ea/sources/xmi.rb
CHANGED
|
@@ -28,6 +28,7 @@ module Ea
|
|
|
28
28
|
autoload :NoteBuilder, "ea/sources/xmi/note_builder"
|
|
29
29
|
autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
|
|
30
30
|
autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
|
|
31
|
+
autoload :StoredRoute, "ea/sources/xmi/stored_route"
|
|
31
32
|
autoload :ExtensionTypes, "ea/sources/xmi/extension_types"
|
|
32
33
|
autoload :ExtensionElements, "ea/sources/xmi/extension_elements"
|
|
33
34
|
autoload :ExtensionGeometryParser, "ea/sources/xmi/extension_geometry_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
|