ea 0.6.97 → 0.6.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ea/sources/qea/diagram_builder/routing.rb +141 -14
- data/lib/ea/sources/qea/diagram_builder.rb +30 -10
- data/lib/ea/svg/ea_emitter/connector_redock.rb +31 -3
- data/lib/ea/svg/ea_emitter/label/end_label.rb +67 -3
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +37 -0
- data/lib/ea/svg/ea_emitter/labels.rb +14 -1
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b9e7581bab2de7d7c5766a5f141c99885bb59cbb7ad974c313c26e86bfb96a2
|
|
4
|
+
data.tar.gz: 8933e36fb2422953eddb1d60069664e39177547beaf2ec17ed17c5b9f3aa4eac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab6882700d57207a5fd2c1b27fa0befe5f287bb61f13387e99dc8f4429dbede75320d9fb6124a126094455d38d4dff7a41dbfc258633609c6695c5dcd216b36d
|
|
7
|
+
data.tar.gz: 82cd636abfe8c20a03dc8f6c5410192c65e674093eaaabc081e8fc84b7ec6544a87f3c87ff255f90fcaa1bc0f7704fdf1b2e46f344cf9288cd51ec4a7e8cfa97
|
|
@@ -8,6 +8,41 @@ module Ea
|
|
|
8
8
|
module Routing
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
# An empty custom-line Path has no authored corner to preserve.
|
|
12
|
+
# Keep normalized port routes; replace only the computed EDGE2 jog.
|
|
13
|
+
def direct_empty_path_ray(model, link_row, diagram_row, geom, context)
|
|
14
|
+
return model if context
|
|
15
|
+
return model unless empty_path_ray_candidate?(model, link_row, geom)
|
|
16
|
+
|
|
17
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
18
|
+
return model unless empty_path_ray_gap?(source, target, geom)
|
|
19
|
+
|
|
20
|
+
model.waypoints = [model.source_port, model.target_port].map do |point|
|
|
21
|
+
Ea::Model::Waypoint.new(position: point)
|
|
22
|
+
end
|
|
23
|
+
set_stored_ray_origins(model, source, target, geom)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def empty_path_ray_gap?(source, target, geom)
|
|
27
|
+
return false unless source.x + source.width < target.x
|
|
28
|
+
|
|
29
|
+
[stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
30
|
+
stored_ray_origin(target, geom[:ex], geom[:ey])].all? do |origin|
|
|
31
|
+
within_height?(origin.y, source) && within_height?(origin.y, target)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def empty_path_ray_candidate?(model, link_row, geom)
|
|
36
|
+
%w[Generalization Aggregation].include?(model.connector_type) &&
|
|
37
|
+
model.waypoints.size == 3 && empty_path_ray_style?(model.style, link_row, geom) &&
|
|
38
|
+
model.has_geometry_offsets && model.source_port && model.target_port
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def empty_path_ray_style?(style, link_row, geom)
|
|
42
|
+
style[:mode] == "3" && style[:lwidth] == "2" && !style[:tree] &&
|
|
43
|
+
geom[:edge] == 2 && link_row.path.to_s.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
11
46
|
# Stored SY/EY use mathematical Y; ray origins use screen Y.
|
|
12
47
|
# Computed-jog origins are already normalized to their derived ports.
|
|
13
48
|
def correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
@@ -80,6 +115,85 @@ module Ea
|
|
|
80
115
|
y: bounds.y + bounds.height / 2 - y_offset.to_i)
|
|
81
116
|
end
|
|
82
117
|
|
|
118
|
+
# A vertical tree stores its trunk bend twice when the parent sits
|
|
119
|
+
# directly above; EA draws that as one bend.
|
|
120
|
+
def drop_zero_length_tree_segments(points, link_row)
|
|
121
|
+
return points unless points.size == 4 && points[1] == points[2] &&
|
|
122
|
+
DiagramStyleParser.parse(link_row.style)[:tree] == "V"
|
|
123
|
+
|
|
124
|
+
points.values_at(0, 1, 3)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A stored vertical tree trunk docks both ends on the box edges that
|
|
128
|
+
# face its bends, at the adjacent bend's x: Aggregations without
|
|
129
|
+
# geometry offsets whose child sits below, and flush Associations
|
|
130
|
+
# whose parent sits above. Other shapes keep their stored docks.
|
|
131
|
+
def redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
|
|
132
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
133
|
+
return unless points.size == 4 && style[:tree] == "V"
|
|
134
|
+
|
|
135
|
+
case connector.connector_type
|
|
136
|
+
when "Aggregation"
|
|
137
|
+
return if geometry_has_offsets?(link_row.geometry)
|
|
138
|
+
|
|
139
|
+
redock_child_below(points, bounds_from_rect(source_placement), bounds_from_rect(target_placement))
|
|
140
|
+
when "Association"
|
|
141
|
+
return unless style[:lwidth] == "0"
|
|
142
|
+
|
|
143
|
+
redock_parent_above(points, link_row, source_placement, target_placement, strict: true)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def redock_child_below(points, src_b, tgt_b)
|
|
148
|
+
first_bend, last_bend = points.values_at(1, -2)
|
|
149
|
+
source_y = src_b.y + src_b.height
|
|
150
|
+
return points unless source_y <= tgt_b.y && first_bend[1] > source_y && last_bend[1] < tgt_b.y
|
|
151
|
+
|
|
152
|
+
points[0] = [clamp_to_span(first_bend[0], src_b), source_y]
|
|
153
|
+
points[-1] = [clamp_to_span(last_bend[0], tgt_b), tgt_b.y]
|
|
154
|
+
points
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def clamp_to_span(position, bounds)
|
|
158
|
+
position.clamp(bounds.x, bounds.x + bounds.width)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A stored-Path Mode 3 width-2 end whose adjacent bend lies level
|
|
162
|
+
# with its box (inside the box's span on one axis, outside on the
|
|
163
|
+
# other) enters the box orthogonally on the outer outline at that
|
|
164
|
+
# bend's coordinate. Bends that are not level keep the port and
|
|
165
|
+
# every authored corner stays.
|
|
166
|
+
def level_entry_route(points, link_row, connector, src, tgt)
|
|
167
|
+
return points unless points.size >= 3 && level_entry_style?(link_row, connector)
|
|
168
|
+
|
|
169
|
+
route = points.dup
|
|
170
|
+
first = level_entry_point(route[1], src)
|
|
171
|
+
last = level_entry_point(route[-2], tgt)
|
|
172
|
+
route[0] = first if first
|
|
173
|
+
route[-1] = last if last
|
|
174
|
+
route
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def level_entry_style?(link_row, connector)
|
|
178
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
179
|
+
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
180
|
+
style[:mode] == "3" && style[:lwidth] == "2" && !style.key?(:tree)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def level_entry_point(bend, box)
|
|
184
|
+
level_y = bend[1] > box.y && bend[1] < box.y + box.height
|
|
185
|
+
level_x = bend[0] > box.x && bend[0] < box.x + box.width
|
|
186
|
+
if level_y && outside_span?(bend[0], box.x, box.width)
|
|
187
|
+
[bend[0] < box.x ? box.x - 1 : box.x + box.width + 1, bend[1]]
|
|
188
|
+
elsif level_x && outside_span?(bend[1], box.y, box.height)
|
|
189
|
+
[bend[0], bend[1] < box.y ? box.y - 1 : box.y + box.height + 1]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def outside_span?(value, start, length)
|
|
194
|
+
value < start || value > start + length
|
|
195
|
+
end
|
|
196
|
+
|
|
83
197
|
# Square routing keeps the stored orthogonal trunk, even when a
|
|
84
198
|
# shorter facing-edge route exists between the same boxes.
|
|
85
199
|
def stored_square_route(link_row, connector, source, target)
|
|
@@ -129,19 +243,29 @@ module Ea
|
|
|
129
243
|
|
|
130
244
|
# A valid lateral corner joins one vertical leg and one horizontal
|
|
131
245
|
# leg. Keep it even when the boxes also admit a shorter direct route.
|
|
132
|
-
|
|
246
|
+
# ported: both ends carry explicit SX/SY and EX/EY offsets. An
|
|
247
|
+
# untyped corner without them may be an authored diagonal.
|
|
248
|
+
def stored_lateral_route(link_row, connector, source, target, ported: false)
|
|
133
249
|
return nil unless %w[Aggregation Association].include?(connector.connector_type)
|
|
134
250
|
|
|
135
251
|
style = DiagramStyleParser.parse(link_row.style)
|
|
136
|
-
return nil unless stored_lateral_style?(style)
|
|
252
|
+
return nil unless stored_lateral_style?(style, connector, ported)
|
|
137
253
|
|
|
138
254
|
corner = stored_lateral_corner(link_row)
|
|
139
|
-
|
|
255
|
+
corner && lateral_corner_route(style, corner, source, target)
|
|
256
|
+
end
|
|
140
257
|
|
|
141
|
-
|
|
142
|
-
|
|
258
|
+
# An untyped Mode 3 Association corner takes whichever leg
|
|
259
|
+
# orientation its position fits; width-2 docks sit on the 1px
|
|
260
|
+
# outer outline.
|
|
261
|
+
def lateral_corner_route(style, corner, source, target)
|
|
262
|
+
case style[:tree]
|
|
263
|
+
when "LH" then horizontal_corner_route(corner, source, target)
|
|
264
|
+
when "LV" then vertical_corner_route(corner, source, target)
|
|
143
265
|
else
|
|
144
|
-
|
|
266
|
+
outline = style[:lwidth] == "2" ? 1 : 0
|
|
267
|
+
horizontal_corner_route(corner, source, target, outline) ||
|
|
268
|
+
vertical_corner_route(corner, source, target, outline)
|
|
145
269
|
end
|
|
146
270
|
end
|
|
147
271
|
|
|
@@ -151,8 +275,11 @@ module Ea
|
|
|
151
275
|
intermediate_waypoints(link_row).first
|
|
152
276
|
end
|
|
153
277
|
|
|
154
|
-
def stored_lateral_style?(style)
|
|
155
|
-
style[:mode] == "3" && %w[0 2].include?(style[:lwidth])
|
|
278
|
+
def stored_lateral_style?(style, connector, ported)
|
|
279
|
+
return false unless style[:mode] == "3" && %w[0 2].include?(style[:lwidth])
|
|
280
|
+
|
|
281
|
+
%w[LH LV].include?(style[:tree]) ||
|
|
282
|
+
(style[:tree].nil? && ported && connector.connector_type == "Association")
|
|
156
283
|
end
|
|
157
284
|
|
|
158
285
|
def horizontal_corner?(corner, source, target)
|
|
@@ -160,13 +287,13 @@ module Ea
|
|
|
160
287
|
!within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
|
|
161
288
|
end
|
|
162
289
|
|
|
163
|
-
def horizontal_corner_route(corner, source, target)
|
|
290
|
+
def horizontal_corner_route(corner, source, target, outline = 0)
|
|
164
291
|
return nil unless horizontal_corner?(corner, source, target)
|
|
165
292
|
|
|
166
293
|
x, y = corner
|
|
167
294
|
|
|
168
|
-
source_y = y < source.y ? source.y : source.y + source.height
|
|
169
|
-
target_x = x < target.x ? target.x : target.x + target.width
|
|
295
|
+
source_y = y < source.y ? source.y - outline : source.y + source.height + outline
|
|
296
|
+
target_x = x < target.x ? target.x - outline : target.x + target.width + outline
|
|
170
297
|
[[x, source_y], corner, [target_x, y]]
|
|
171
298
|
end
|
|
172
299
|
|
|
@@ -175,13 +302,13 @@ module Ea
|
|
|
175
302
|
!within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
|
|
176
303
|
end
|
|
177
304
|
|
|
178
|
-
def vertical_corner_route(corner, source, target)
|
|
305
|
+
def vertical_corner_route(corner, source, target, outline = 0)
|
|
179
306
|
return nil unless vertical_corner?(corner, source, target)
|
|
180
307
|
|
|
181
308
|
x, y = corner
|
|
182
309
|
|
|
183
|
-
source_x = x < source.x ? source.x : source.x + source.width
|
|
184
|
-
target_y = y < target.y ? target.y : target.y + target.height
|
|
310
|
+
source_x = x < source.x ? source.x - outline : source.x + source.width + outline
|
|
311
|
+
target_y = y < target.y ? target.y - outline : target.y + target.height + outline
|
|
185
312
|
[[source_x, y], corner, [x, target_y]]
|
|
186
313
|
end
|
|
187
314
|
|
|
@@ -16,16 +16,11 @@ module Ea
|
|
|
16
16
|
|
|
17
17
|
attr_reader :database
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# seat (t_xref 'connector property' rows keyed by connector
|
|
22
|
-
# guid; VALU 0/empty both mean false).
|
|
19
|
+
# This flag requests the false annotation, not the property's Boolean
|
|
20
|
+
# value. An explicit true (-1) must not request a false annotation.
|
|
23
21
|
def substitutable_connector?(conn)
|
|
24
|
-
@substitutable_guids ||= database.collections[:xrefs]
|
|
25
|
-
|
|
26
|
-
x.xref_type == "connector property" &&
|
|
27
|
-
x.name == "CustomProperties" &&
|
|
28
|
-
x.description.to_s.include?("isSubstitutable")
|
|
22
|
+
@substitutable_guids ||= database.collections[:xrefs].select do |xref|
|
|
23
|
+
false_substitutability_xref?(xref)
|
|
29
24
|
end.map(&:client).to_set
|
|
30
25
|
@substitutable_guids.include?(conn.ea_guid)
|
|
31
26
|
end
|
|
@@ -99,6 +94,22 @@ module Ea
|
|
|
99
94
|
|
|
100
95
|
private
|
|
101
96
|
|
|
97
|
+
def false_substitutability_xref?(xref)
|
|
98
|
+
return false unless xref.xref_type == "connector property" && xref.name == "CustomProperties"
|
|
99
|
+
|
|
100
|
+
description = xref.description.to_s
|
|
101
|
+
description.include?("isSubstitutable") && !true_substitutability_property?(description)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def true_substitutability_property?(description)
|
|
105
|
+
description.scan(/@PROP=(.*?)@ENDPROP;/m).any? do |match|
|
|
106
|
+
property = match.first
|
|
107
|
+
property.include?("@NAME=isSubstitutable@ENDNAME;") &&
|
|
108
|
+
property.include?("@TYPE=Boolean@ENDTYPE;") &&
|
|
109
|
+
property[/@VALU=(.*?)@ENDVALU;/m, 1] == "-1"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
102
113
|
def build_elements(diagram_row)
|
|
103
114
|
objs = database.diagram_objects_for(diagram_row.diagram_id) || []
|
|
104
115
|
objs.map { |obj_row| build_element(obj_row, diagram_row) }
|
|
@@ -410,6 +421,7 @@ module Ea
|
|
|
410
421
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
411
422
|
is_substitutable: substitutable_connector?(connector)
|
|
412
423
|
), link_row, diagram_row, geom)
|
|
424
|
+
model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
|
|
413
425
|
correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
414
426
|
end
|
|
415
427
|
|
|
@@ -650,7 +662,8 @@ module Ea
|
|
|
650
662
|
src = bounds_from_rect(source_placement)
|
|
651
663
|
tgt = bounds_from_rect(target_placement)
|
|
652
664
|
tree_route = stored_square_route(link_row, connector, src, tgt) ||
|
|
653
|
-
stored_lateral_route(link_row, connector, src, tgt
|
|
665
|
+
stored_lateral_route(link_row, connector, src, tgt,
|
|
666
|
+
ported: source_port && target_port) ||
|
|
654
667
|
horizontal_tree_route(points, link_row, connector, src, tgt)
|
|
655
668
|
if tree_route
|
|
656
669
|
points = tree_route
|
|
@@ -669,8 +682,15 @@ module Ea
|
|
|
669
682
|
redock_parent_above(points, link_row, source_placement,
|
|
670
683
|
target_placement, strict: true)
|
|
671
684
|
end
|
|
685
|
+
if intermediate.any?
|
|
686
|
+
redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
|
|
687
|
+
end
|
|
672
688
|
end
|
|
673
689
|
|
|
690
|
+
if intermediate.any? && !tree_route
|
|
691
|
+
points = level_entry_route(points, link_row, connector, src, tgt)
|
|
692
|
+
end
|
|
693
|
+
points = drop_zero_length_tree_segments(points, link_row)
|
|
674
694
|
points.map do |x, y|
|
|
675
695
|
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
676
696
|
end
|
|
@@ -209,18 +209,46 @@ module Ea
|
|
|
209
209
|
divider_y_by_ref[target_ref]]
|
|
210
210
|
return nil unless bounds.all? && dividers.all?
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
preserve_source = explicit_horizontal_source_dock?(pairs, connector)
|
|
213
|
+
adjusted = row_slot_pairs(pairs, bounds, dividers, preserve_source: preserve_source)
|
|
213
214
|
if stroke_outline?(connector, bounds) &&
|
|
214
215
|
pairs == pairs_for(connector, diagram, model_index, bounds_map)
|
|
215
216
|
original = clipped_pairs(bounds, [connector.source_anchor, connector.target_anchor])
|
|
216
|
-
original_final = row_slot_pairs(original, bounds, dividers)
|
|
217
|
+
original_final = row_slot_pairs(original, bounds, dividers, preserve_source: preserve_source)
|
|
217
218
|
adjusted = original_final if original != pairs && !forward_route?(original_final, adjusted)
|
|
218
219
|
end
|
|
219
220
|
adjusted == pairs ? nil : adjusted
|
|
220
221
|
end
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
# A supplied horizontal port already specifies its row. Moving it
|
|
224
|
+
# to the next compartment row would discard that explicit coordinate.
|
|
225
|
+
def explicit_horizontal_source_dock?(pairs, connector)
|
|
226
|
+
return false unless explicit_horizontal_ray?(connector)
|
|
227
|
+
|
|
228
|
+
connector.source_anchor.y == connector.target_anchor.y &&
|
|
229
|
+
connector.source_port.y == connector.source_anchor.y &&
|
|
230
|
+
pairs.first[1] == connector.source_port.y
|
|
231
|
+
end
|
|
232
|
+
private_class_method :explicit_horizontal_source_dock?
|
|
233
|
+
|
|
234
|
+
def explicit_horizontal_ray?(connector)
|
|
235
|
+
explicit_horizontal_ray_style?(connector) && connector.has_geometry_offsets &&
|
|
236
|
+
connector.source_port && connector.target_port &&
|
|
237
|
+
connector.source_anchor && connector.target_anchor
|
|
238
|
+
end
|
|
239
|
+
private_class_method :explicit_horizontal_ray?
|
|
240
|
+
|
|
241
|
+
def explicit_horizontal_ray_style?(connector)
|
|
242
|
+
style = connector.style.to_h
|
|
243
|
+
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
244
|
+
style[:mode] == "3" && !style[:tree] && !style[:direct] && !style[:regenerated]
|
|
245
|
+
end
|
|
246
|
+
private_class_method :explicit_horizontal_ray_style?
|
|
247
|
+
|
|
248
|
+
def row_slot_pairs(pairs, bounds, dividers, preserve_source: false)
|
|
223
249
|
pairs.each_with_index.map do |(x, y), index|
|
|
250
|
+
next [x, y] if preserve_source && index.zero?
|
|
251
|
+
|
|
224
252
|
row_slot?(x, y, bounds[index], dividers[index]) ? [x, y + ROW_SLOT_PITCH] : [x, y]
|
|
225
253
|
end
|
|
226
254
|
end
|
|
@@ -22,6 +22,10 @@ module Ea
|
|
|
22
22
|
SEAT_FONT_SIZE = 7
|
|
23
23
|
SEAT_X_INSET = 3
|
|
24
24
|
SEAT_STEREOTYPE_GAP = 12
|
|
25
|
+
# EA keeps an upward property seat's left edge at diagram x >= 3;
|
|
26
|
+
# the stereotype line moves with the role. Measured only for the
|
|
27
|
+
# default Cambria 7pt boxed cluster.
|
|
28
|
+
MIN_ROLE_X = 3
|
|
25
29
|
|
|
26
30
|
attr_reader :canvas, :model_index, :document, :theme,
|
|
27
31
|
:font_family, :font_size, :font_unit
|
|
@@ -86,12 +90,15 @@ module Ea
|
|
|
86
90
|
stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
|
|
87
91
|
direction = leg_direction(end_kind, heading)
|
|
88
92
|
if direction && !(tree && zero_offset?(text_box, mult_box))
|
|
89
|
-
text_pos =
|
|
93
|
+
text_pos = plain_vertical_role(connector, direction, anchor, text_box, role) ||
|
|
94
|
+
source_property_seat(connector, direction, anchor, text_box, role) ||
|
|
90
95
|
plain_upward_seat(connector, direction, anchor, text_box, role) ||
|
|
91
96
|
role_seat(direction, anchor, text_box, role) || text_pos
|
|
92
97
|
mult_pos = directional_multiplicity(connector, direction, anchor, mult_box, mult) ||
|
|
93
98
|
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
94
99
|
elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
|
|
100
|
+
text_pos = target_vertical_role(connector, heading, anchor, text_box, role) ||
|
|
101
|
+
target_rightward_role(heading, anchor, text_box, role) || text_pos
|
|
95
102
|
mult_pos = target_multiplicity_seat(heading, anchor, mult_box) || mult_pos
|
|
96
103
|
end
|
|
97
104
|
|
|
@@ -185,7 +192,9 @@ module Ea
|
|
|
185
192
|
|
|
186
193
|
x, y = seat_corner(anchor, box)
|
|
187
194
|
inset, baseline = property_role_offsets(direction)
|
|
188
|
-
|
|
195
|
+
left = x - inset - width
|
|
196
|
+
left = [left, MIN_ROLE_X].max if direction == :up
|
|
197
|
+
[left, y + baseline]
|
|
189
198
|
end
|
|
190
199
|
|
|
191
200
|
def plain_upward_seat(connector, direction, anchor, box, role)
|
|
@@ -201,6 +210,61 @@ module Ea
|
|
|
201
210
|
[x - SEAT_X_INSET - width, y - 6]
|
|
202
211
|
end
|
|
203
212
|
|
|
213
|
+
def target_vertical_role(connector, heading, anchor, box, role)
|
|
214
|
+
return nil unless heading && heading[0].zero? && !heading[1].zero?
|
|
215
|
+
|
|
216
|
+
direction = heading[1].negative? ? :target_up : :target_down
|
|
217
|
+
plain_vertical_role(connector, direction, anchor, box, role)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# A target whose leg arrives from the right seats its role like a
|
|
221
|
+
# source whose leg leaves to the right.
|
|
222
|
+
def target_rightward_role(heading, anchor, box, role)
|
|
223
|
+
return nil unless heading && heading[1].zero? && heading[0].positive?
|
|
224
|
+
return nil unless seat_font? && cambria_box?(box) && !role_empty?(role)
|
|
225
|
+
|
|
226
|
+
x, y = seat_corner(anchor, box)
|
|
227
|
+
[x + SEAT_X_INSET, y - 7]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def plain_vertical_role(connector, direction, anchor, box, role)
|
|
231
|
+
return nil unless %i[down target_up target_down].include?(direction) &&
|
|
232
|
+
plain_vertical_box?(box)
|
|
233
|
+
|
|
234
|
+
end_kind = direction == :down ? :source : :target
|
|
235
|
+
return nil unless plain_vertical_connector?(connector, end_kind, direction)
|
|
236
|
+
|
|
237
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
238
|
+
return nil unless width
|
|
239
|
+
|
|
240
|
+
x, y = seat_corner(anchor, box)
|
|
241
|
+
[x - width - 8, y + (direction == :target_up ? -7 : 15)]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def plain_vertical_box?(box)
|
|
245
|
+
seat_font? && cambria_box?(box) && [0, 1].include?(box["alignment"])
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# A target leg that runs down seats only on the "LV" tree route
|
|
249
|
+
# with line width 2; every other vertical seat needs the plain
|
|
250
|
+
# width-0 route.
|
|
251
|
+
def plain_vertical_connector?(connector, end_kind, direction)
|
|
252
|
+
return false unless connector.connector_type == "Association" &&
|
|
253
|
+
connector_source_dest_stereotype(connector, end_kind).empty?
|
|
254
|
+
|
|
255
|
+
plain_vertical_style?(connector.style.to_h, direction)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def plain_vertical_style?(style, direction)
|
|
259
|
+
return false unless style[:mode] == "3"
|
|
260
|
+
|
|
261
|
+
if direction == :target_down
|
|
262
|
+
style[:lwidth] == "2" && style[:tree] == "LV"
|
|
263
|
+
else
|
|
264
|
+
style[:lwidth] == "0" && !style[:tree]
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
204
268
|
def plain_role_route?(style)
|
|
205
269
|
%w[1 3].include?(style[:mode]) && %w[0 2].include?(style[:lwidth]) &&
|
|
206
270
|
[nil, "V"].include?(style[:tree])
|
|
@@ -229,7 +293,7 @@ module Ea
|
|
|
229
293
|
case direction
|
|
230
294
|
when :left then left_property_route?(style)
|
|
231
295
|
when :down then downward_property_route?(style)
|
|
232
|
-
else style[:lwidth]
|
|
296
|
+
else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
|
|
233
297
|
end
|
|
234
298
|
end
|
|
235
299
|
|
|
@@ -96,6 +96,9 @@ module Ea
|
|
|
96
96
|
LINE_PITCH = 13
|
|
97
97
|
|
|
98
98
|
def label_position(connector, points)
|
|
99
|
+
name_seat = stored_name_position(connector, points)
|
|
100
|
+
return name_seat if name_seat
|
|
101
|
+
|
|
99
102
|
return midpoint(points) unless default_substitutable_seat?(connector) &&
|
|
100
103
|
upward_four_point_route?(points)
|
|
101
104
|
|
|
@@ -103,6 +106,40 @@ module Ea
|
|
|
103
106
|
[source[0] + 3, ((source[1] + bend[1]) / 2).floor - 1]
|
|
104
107
|
end
|
|
105
108
|
|
|
109
|
+
# At 7pt, a centered name's stored corner is 3px below its baseline.
|
|
110
|
+
def stored_name_position(connector, points)
|
|
111
|
+
box = stored_name_box(connector)
|
|
112
|
+
return nil unless box
|
|
113
|
+
|
|
114
|
+
width = Ea::Fonts::Metrics.text_length(midpoint_content(connector), font_size, family: font_family)
|
|
115
|
+
return nil unless width
|
|
116
|
+
|
|
117
|
+
x, y = midpoint(points)
|
|
118
|
+
offset_x, offset_y = box.values_at("ox", "oy").map(&:to_i)
|
|
119
|
+
[x + offset_x - (width / 2.0).floor, y + offset_y - 3]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def stored_name_box(connector)
|
|
123
|
+
return nil unless connector.is_a?(Ea::Model::DiagramConnector) && measured_name_font?
|
|
124
|
+
return nil if connector.is_substitutable || stereotype_label(connector)
|
|
125
|
+
return nil if midpoint_content(connector).include?(": ")
|
|
126
|
+
|
|
127
|
+
box = connector.label_boxes.to_h[:lmt]
|
|
128
|
+
box if default_name_box?(box)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def measured_name_font?
|
|
132
|
+
font_family.to_s.casecmp("Cambria").zero? && font_size == 7 && font_unit == "pt"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def default_name_box?(box)
|
|
136
|
+
return false unless box && box["alignment"] == 1
|
|
137
|
+
return false unless box.values_at("ox", "oy").all?
|
|
138
|
+
return false unless [nil, 0, 1].include?(box["direction"]) && [nil, -1].include?(box["color"])
|
|
139
|
+
|
|
140
|
+
box["rotation"].to_i.zero? && box.values_at("bold", "italic", "underline", "hidden").none?
|
|
141
|
+
end
|
|
142
|
+
|
|
106
143
|
def default_substitutable_seat?(connector)
|
|
107
144
|
connector.is_a?(Ea::Model::DiagramConnector) && connector.is_substitutable &&
|
|
108
145
|
connector.connector_type == "Generalization" && font_size == 7 &&
|
|
@@ -97,7 +97,7 @@ module Ea
|
|
|
97
97
|
mult_box: tgt_mb,
|
|
98
98
|
anchor: target_pt, connector: connector,
|
|
99
99
|
end_kind: :target, tree: tree && tree[:target],
|
|
100
|
-
heading: stored_tgt &&
|
|
100
|
+
heading: stored_tgt && target_heading(connector, points),
|
|
101
101
|
stored_labels: stored_labels).each { |t| texts << t }
|
|
102
102
|
texts
|
|
103
103
|
end
|
|
@@ -126,6 +126,19 @@ module Ea
|
|
|
126
126
|
stored_upward_heading(connector, heading)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# A target dock that clips an axis-aligned stored leg into a slight
|
|
130
|
+
# diagonal keeps the stored leg's direction.
|
|
131
|
+
def target_heading(connector, points)
|
|
132
|
+
heading = leg_heading(points.reverse)
|
|
133
|
+
return heading unless points.size == 2 && heading.none?(&:zero?)
|
|
134
|
+
|
|
135
|
+
stored = waypoint_pairs(connector)
|
|
136
|
+
return heading unless stored.size == 2
|
|
137
|
+
|
|
138
|
+
original = leg_heading(stored.reverse)
|
|
139
|
+
original.any?(&:zero?) ? original : heading
|
|
140
|
+
end
|
|
141
|
+
|
|
129
142
|
def stored_upward_heading(connector, heading)
|
|
130
143
|
return heading unless heading[1].negative?
|
|
131
144
|
|
data/lib/ea/version.rb
CHANGED