ea 0.6.98 → 0.6.100
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 +342 -23
- data/lib/ea/sources/qea/diagram_builder.rb +14 -2
- data/lib/ea/svg/ea_emitter/connector_redock.rb +26 -5
- data/lib/ea/svg/ea_emitter/label/end_label.rb +46 -14
- 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: 8f60cf58bb521e911eb26fb30b628162e149a9521027fae0e77756ef1b8354dd
|
|
4
|
+
data.tar.gz: c0580ced4122e41650fbc5053fb0fb534a40c8cae84f758039dd8b2f575422ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6460bd84357b97c350c485526bcc62bc74309d924eacd9e7983347b101c22d1fcfc246dc7b76d443eb04cb360e2be890eaf1cffedf49103e9f2419b5205fe3a7
|
|
7
|
+
data.tar.gz: 2a8ce3556ffa2b5531e432002ab7a67439cd4f4ed171c1f84f141b264e9fa6913af3ffd8983dbb0be70c9282407fdb3695efb07728c0217c656688ed38f8d023
|
|
@@ -23,6 +23,56 @@ module Ea
|
|
|
23
23
|
set_stored_ray_origins(model, source, target, geom)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# A Mode 2 Generalization whose stored Path is a horizontal jog
|
|
27
|
+
# between vertically separated boxes that overlap in x is EA's
|
|
28
|
+
# auto-route: one vertical ray at the overlap midpoint plus SX.
|
|
29
|
+
# The stored jog is stale; the ray docks on both 1px outlines.
|
|
30
|
+
def mode2_overlap_ray(model, link_row, diagram_row, geom, context)
|
|
31
|
+
return model if context || !mode2_overlap_candidate?(model, link_row, geom)
|
|
32
|
+
|
|
33
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
34
|
+
column = overlap_ray_coordinate(source.x, source.width, target.x, target.width, geom[:sx])
|
|
35
|
+
return model unless column && vertical_separation?(source, target)
|
|
36
|
+
|
|
37
|
+
set_overlap_ray(model, column, source, target)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def set_overlap_ray(model, column, source, target)
|
|
41
|
+
model.waypoints = overlap_ray_ends(column, source, target).map do |x, y|
|
|
42
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
43
|
+
end
|
|
44
|
+
model.source_anchor, model.target_anchor = [source, target].map do |box|
|
|
45
|
+
Ea::Model::Point.new(x: column, y: box.y + box.height / 2)
|
|
46
|
+
end
|
|
47
|
+
model
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mode2_overlap_candidate?(model, link_row, geom)
|
|
51
|
+
model.connector_type == "Generalization" && model.has_geometry_offsets &&
|
|
52
|
+
mode2_overlap_style?(model.style.to_h, geom) && model.waypoints.size == 4 &&
|
|
53
|
+
horizontal_jog?(intermediate_waypoints(link_row))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def mode2_overlap_style?(style, geom)
|
|
57
|
+
style[:mode] == "2" && style[:lwidth] == "2" && !style[:tree] && geom[:edge] == 1 && geom[:sx]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def horizontal_jog?(corners)
|
|
61
|
+
corners.size == 2 && corners[0][1] == corners[1][1]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def vertical_separation?(source, target)
|
|
65
|
+
source.y + source.height < target.y || target.y + target.height < source.y
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def overlap_ray_ends(column, source, target)
|
|
69
|
+
if target.y + target.height < source.y
|
|
70
|
+
[[column, source.y - 1], [column, target.y + target.height + 1]]
|
|
71
|
+
else
|
|
72
|
+
[[column, source.y + source.height + 1], [column, target.y - 1]]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
26
76
|
def empty_path_ray_gap?(source, target, geom)
|
|
27
77
|
return false unless source.x + source.width < target.x
|
|
28
78
|
|
|
@@ -55,6 +105,134 @@ module Ea
|
|
|
55
105
|
set_stored_ray_origins(model, source, target, geom)
|
|
56
106
|
end
|
|
57
107
|
|
|
108
|
+
# An empty-Path Mode 3 Association with explicit offsets on both
|
|
109
|
+
# ends is the straight line between the two stored ray origins.
|
|
110
|
+
# Origins outside their own box are not a stored ray.
|
|
111
|
+
def association_stored_ray(model, link_row, diagram_row, geom, context)
|
|
112
|
+
return model if context
|
|
113
|
+
return model unless association_ray_candidate?(model, link_row)
|
|
114
|
+
|
|
115
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
116
|
+
origins = association_ray_origins(source, target, geom)
|
|
117
|
+
return model unless origins
|
|
118
|
+
|
|
119
|
+
model.waypoints = [model.source_port, model.target_port].map do |point|
|
|
120
|
+
Ea::Model::Waypoint.new(position: point)
|
|
121
|
+
end
|
|
122
|
+
model.source_anchor, model.target_anchor = origins
|
|
123
|
+
model
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def association_ray_origins(source, target, geom)
|
|
127
|
+
origins = [stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
128
|
+
stored_ray_origin(target, geom[:ex], geom[:ey])]
|
|
129
|
+
inside = origins.zip([source, target]).all? { |o, b| inside_box?(o, b) }
|
|
130
|
+
origins if inside && sub_pixel_safe_origins?(origins, [source, target])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def association_ray_candidate?(model, link_row)
|
|
134
|
+
model.connector_type == "Association" && link_row.path.to_s.empty? &&
|
|
135
|
+
model.has_geometry_offsets && model.source_port && model.target_port &&
|
|
136
|
+
association_ray_style?(model.style.to_h)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def association_ray_style?(style)
|
|
140
|
+
style[:mode] == "3" && !style[:tree] && %w[0 2].include?(style[:lwidth]) &&
|
|
141
|
+
%i[direct regenerated].none? { |key| style.key?(key) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Origins are whole pixels while EA's box centers may sit on a
|
|
145
|
+
# half pixel. An origin on its border then clips differently
|
|
146
|
+
# unless the line is axis-aligned, which no rounding changes.
|
|
147
|
+
def sub_pixel_safe_origins?(origins, boxes)
|
|
148
|
+
first, last = origins
|
|
149
|
+
return false if first == last
|
|
150
|
+
|
|
151
|
+
axis_aligned = first.x == last.x || first.y == last.y
|
|
152
|
+
axis_aligned || origins.zip(boxes).none? { |point, box| on_border?(point, box) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def on_border?(point, bounds)
|
|
156
|
+
[bounds.x, bounds.x + bounds.width].include?(point.x) ||
|
|
157
|
+
[bounds.y, bounds.y + bounds.height].include?(point.y)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def inside_box?(point, bounds)
|
|
161
|
+
point.x.between?(bounds.x, bounds.x + bounds.width) &&
|
|
162
|
+
point.y.between?(bounds.y, bounds.y + bounds.height)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# A Mode 2 Association with equal SX/EX (vertical EDGE 1/3) or
|
|
166
|
+
# equal SY/EY (horizontal EDGE 4) and no custom Path docks as a
|
|
167
|
+
# straight line through the middle of the boxes' overlap, moved
|
|
168
|
+
# by the shared stored offset. Stored SY counts upward.
|
|
169
|
+
def overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
170
|
+
return model if context
|
|
171
|
+
return model unless overlap_ray_candidate?(model, link_row, geom)
|
|
172
|
+
|
|
173
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
174
|
+
origins = overlap_origins(source, target, geom)
|
|
175
|
+
return model unless origins
|
|
176
|
+
|
|
177
|
+
model.source_anchor, model.target_anchor = origins.map do |ox, oy|
|
|
178
|
+
Ea::Model::Point.new(x: ox, y: oy)
|
|
179
|
+
end
|
|
180
|
+
model.style[:overlap_ray] = true
|
|
181
|
+
model
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Both ray origins share the overlap coordinate on the axis the
|
|
185
|
+
# offsets move and sit at each box's centre on the other axis.
|
|
186
|
+
def overlap_origins(source, target, geom)
|
|
187
|
+
along, across, offset = overlap_axes(geom)
|
|
188
|
+
coordinate = overlap_ray_coordinate(*box_span(source, along), *box_span(target, along), offset)
|
|
189
|
+
return nil unless coordinate && disjoint_span?(*box_span(source, across), *box_span(target, across))
|
|
190
|
+
|
|
191
|
+
[source, target].map do |box|
|
|
192
|
+
start, size = box_span(box, across)
|
|
193
|
+
centre = start + size / 2.0
|
|
194
|
+
geom[:edge] == 4 ? [centre, coordinate] : [coordinate, centre]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# [axis the offsets move, axis the boxes face, stored offset]
|
|
199
|
+
def overlap_axes(geom)
|
|
200
|
+
return [%i[y height], %i[x width], -geom[:sy]] if geom[:edge] == 4
|
|
201
|
+
|
|
202
|
+
[%i[x width], %i[y height], geom[:sx]]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def box_span(box, keys)
|
|
206
|
+
keys.map { |key| box.public_send(key) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def overlap_ray_candidate?(model, link_row, geom)
|
|
210
|
+
model.connector_type == "Association" && model.style[:mode] == "2" &&
|
|
211
|
+
model.waypoints.size == 2 && link_row.path.to_s.empty? &&
|
|
212
|
+
model.has_geometry_offsets && uniform_overlap_offsets?(geom)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def uniform_overlap_offsets?(geom)
|
|
216
|
+
return false unless geom.values_at(:sx, :sy, :ex, :ey).all?
|
|
217
|
+
|
|
218
|
+
geom[:edge] == 4 ? geom[:sy] == geom[:ey] : [1, 3].include?(geom[:edge]) && geom[:sx] == geom[:ex]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Floor of the overlap midpoint plus the shared offset; nil
|
|
222
|
+
# when the boxes do not overlap on this axis.
|
|
223
|
+
def overlap_ray_coordinate(start_a, size_a, start_b, size_b, offset)
|
|
224
|
+
low = [start_a, start_b].max
|
|
225
|
+
high = [start_a + size_a, start_b + size_b].min
|
|
226
|
+
return nil if low > high
|
|
227
|
+
|
|
228
|
+
coordinate = ((low + high) / 2.0 + offset).floor
|
|
229
|
+
coordinate if coordinate.between?(low, high)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def disjoint_span?(start_a, size_a, start_b, size_b)
|
|
233
|
+
start_a + size_a < start_b || start_b + size_b < start_a
|
|
234
|
+
end
|
|
235
|
+
|
|
58
236
|
def stored_ray_boxes(link_row, diagram_row)
|
|
59
237
|
connector = database.find_connector(link_row.connectorid)
|
|
60
238
|
[source_placement_for(connector, diagram_row),
|
|
@@ -115,6 +293,85 @@ module Ea
|
|
|
115
293
|
y: bounds.y + bounds.height / 2 - y_offset.to_i)
|
|
116
294
|
end
|
|
117
295
|
|
|
296
|
+
# A vertical tree stores its trunk bend twice when the parent sits
|
|
297
|
+
# directly above; EA draws that as one bend.
|
|
298
|
+
def drop_zero_length_tree_segments(points, link_row)
|
|
299
|
+
return points unless points.size == 4 && points[1] == points[2] &&
|
|
300
|
+
DiagramStyleParser.parse(link_row.style)[:tree] == "V"
|
|
301
|
+
|
|
302
|
+
points.values_at(0, 1, 3)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# A stored vertical tree trunk docks both ends on the box edges that
|
|
306
|
+
# face its bends, at the adjacent bend's x: Aggregations without
|
|
307
|
+
# geometry offsets whose child sits below, and flush Associations
|
|
308
|
+
# whose parent sits above. Other shapes keep their stored docks.
|
|
309
|
+
def redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
|
|
310
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
311
|
+
return unless points.size == 4 && style[:tree] == "V"
|
|
312
|
+
|
|
313
|
+
case connector.connector_type
|
|
314
|
+
when "Aggregation"
|
|
315
|
+
return unless style[:lwidth] == "0" || !geometry_has_offsets?(link_row.geometry)
|
|
316
|
+
|
|
317
|
+
redock_child_below(points, bounds_from_rect(source_placement), bounds_from_rect(target_placement))
|
|
318
|
+
when "Association"
|
|
319
|
+
return unless style[:lwidth] == "0"
|
|
320
|
+
|
|
321
|
+
redock_parent_above(points, link_row, source_placement, target_placement, strict: true)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def redock_child_below(points, src_b, tgt_b)
|
|
326
|
+
first_bend, last_bend = points.values_at(1, -2)
|
|
327
|
+
source_y = src_b.y + src_b.height
|
|
328
|
+
return points unless source_y <= tgt_b.y && first_bend[1] > source_y && last_bend[1] < tgt_b.y
|
|
329
|
+
|
|
330
|
+
points[0] = [clamp_to_span(first_bend[0], src_b), source_y]
|
|
331
|
+
points[-1] = [clamp_to_span(last_bend[0], tgt_b), tgt_b.y]
|
|
332
|
+
points
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def clamp_to_span(position, bounds)
|
|
336
|
+
position.clamp(bounds.x, bounds.x + bounds.width)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# A stored-Path Mode 3 width-2 end whose adjacent bend lies level
|
|
340
|
+
# with its box (inside the box's span on one axis, outside on the
|
|
341
|
+
# other) enters the box orthogonally on the outer outline at that
|
|
342
|
+
# bend's coordinate. Bends that are not level keep the port and
|
|
343
|
+
# every authored corner stays.
|
|
344
|
+
def level_entry_route(points, link_row, connector, src, tgt)
|
|
345
|
+
return points unless points.size >= 3 && level_entry_style?(link_row, connector)
|
|
346
|
+
|
|
347
|
+
route = points.dup
|
|
348
|
+
first = level_entry_point(route[1], src)
|
|
349
|
+
last = level_entry_point(route[-2], tgt)
|
|
350
|
+
route[0] = first if first
|
|
351
|
+
route[-1] = last if last
|
|
352
|
+
route
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def level_entry_style?(link_row, connector)
|
|
356
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
357
|
+
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
358
|
+
style[:mode] == "3" && style[:lwidth] == "2" && !style.key?(:tree)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def level_entry_point(bend, box)
|
|
362
|
+
level_y = bend[1] > box.y && bend[1] < box.y + box.height
|
|
363
|
+
level_x = bend[0] > box.x && bend[0] < box.x + box.width
|
|
364
|
+
if level_y && outside_span?(bend[0], box.x, box.width)
|
|
365
|
+
[bend[0] < box.x ? box.x - 1 : box.x + box.width + 1, bend[1]]
|
|
366
|
+
elsif level_x && outside_span?(bend[1], box.y, box.height)
|
|
367
|
+
[bend[0], bend[1] < box.y ? box.y - 1 : box.y + box.height + 1]
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def outside_span?(value, start, length)
|
|
372
|
+
value < start || value > start + length
|
|
373
|
+
end
|
|
374
|
+
|
|
118
375
|
# Square routing keeps the stored orthogonal trunk, even when a
|
|
119
376
|
# shorter facing-edge route exists between the same boxes.
|
|
120
377
|
def stored_square_route(link_row, connector, source, target)
|
|
@@ -164,19 +421,29 @@ module Ea
|
|
|
164
421
|
|
|
165
422
|
# A valid lateral corner joins one vertical leg and one horizontal
|
|
166
423
|
# leg. Keep it even when the boxes also admit a shorter direct route.
|
|
167
|
-
|
|
424
|
+
# ported: both ends carry explicit SX/SY and EX/EY offsets. An
|
|
425
|
+
# untyped corner without them may be an authored diagonal.
|
|
426
|
+
def stored_lateral_route(link_row, connector, source, target, ported: false)
|
|
168
427
|
return nil unless %w[Aggregation Association].include?(connector.connector_type)
|
|
169
428
|
|
|
170
429
|
style = DiagramStyleParser.parse(link_row.style)
|
|
171
|
-
return nil unless stored_lateral_style?(style)
|
|
430
|
+
return nil unless stored_lateral_style?(style, connector, ported)
|
|
172
431
|
|
|
173
432
|
corner = stored_lateral_corner(link_row)
|
|
174
|
-
|
|
433
|
+
corner && lateral_corner_route(style, corner, source, target)
|
|
434
|
+
end
|
|
175
435
|
|
|
176
|
-
|
|
177
|
-
|
|
436
|
+
# An untyped Mode 3 Association corner takes whichever leg
|
|
437
|
+
# orientation its position fits; width-2 docks sit on the 1px
|
|
438
|
+
# outer outline.
|
|
439
|
+
def lateral_corner_route(style, corner, source, target)
|
|
440
|
+
case style[:tree]
|
|
441
|
+
when "LH" then horizontal_corner_route(corner, source, target)
|
|
442
|
+
when "LV" then vertical_corner_route(corner, source, target)
|
|
178
443
|
else
|
|
179
|
-
|
|
444
|
+
outline = style[:lwidth] == "2" ? 1 : 0
|
|
445
|
+
horizontal_corner_route(corner, source, target, outline) ||
|
|
446
|
+
vertical_corner_route(corner, source, target, outline)
|
|
180
447
|
end
|
|
181
448
|
end
|
|
182
449
|
|
|
@@ -186,8 +453,11 @@ module Ea
|
|
|
186
453
|
intermediate_waypoints(link_row).first
|
|
187
454
|
end
|
|
188
455
|
|
|
189
|
-
def stored_lateral_style?(style)
|
|
190
|
-
style[:mode] == "3" && %w[0 2].include?(style[:lwidth])
|
|
456
|
+
def stored_lateral_style?(style, connector, ported)
|
|
457
|
+
return false unless style[:mode] == "3" && %w[0 2].include?(style[:lwidth])
|
|
458
|
+
|
|
459
|
+
%w[LH LV].include?(style[:tree]) ||
|
|
460
|
+
(style[:tree].nil? && ported && connector.connector_type == "Association")
|
|
191
461
|
end
|
|
192
462
|
|
|
193
463
|
def horizontal_corner?(corner, source, target)
|
|
@@ -195,13 +465,13 @@ module Ea
|
|
|
195
465
|
!within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
|
|
196
466
|
end
|
|
197
467
|
|
|
198
|
-
def horizontal_corner_route(corner, source, target)
|
|
468
|
+
def horizontal_corner_route(corner, source, target, outline = 0)
|
|
199
469
|
return nil unless horizontal_corner?(corner, source, target)
|
|
200
470
|
|
|
201
471
|
x, y = corner
|
|
202
472
|
|
|
203
|
-
source_y = y < source.y ? source.y : source.y + source.height
|
|
204
|
-
target_x = x < target.x ? target.x : target.x + target.width
|
|
473
|
+
source_y = y < source.y ? source.y - outline : source.y + source.height + outline
|
|
474
|
+
target_x = x < target.x ? target.x - outline : target.x + target.width + outline
|
|
205
475
|
[[x, source_y], corner, [target_x, y]]
|
|
206
476
|
end
|
|
207
477
|
|
|
@@ -210,13 +480,13 @@ module Ea
|
|
|
210
480
|
!within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
|
|
211
481
|
end
|
|
212
482
|
|
|
213
|
-
def vertical_corner_route(corner, source, target)
|
|
483
|
+
def vertical_corner_route(corner, source, target, outline = 0)
|
|
214
484
|
return nil unless vertical_corner?(corner, source, target)
|
|
215
485
|
|
|
216
486
|
x, y = corner
|
|
217
487
|
|
|
218
|
-
source_x = x < source.x ? source.x : source.x + source.width
|
|
219
|
-
target_y = y < target.y ? target.y : target.y + target.height
|
|
488
|
+
source_x = x < source.x ? source.x - outline : source.x + source.width + outline
|
|
489
|
+
target_y = y < target.y ? target.y - outline : target.y + target.height + outline
|
|
220
490
|
[[source_x, y], corner, [x, target_y]]
|
|
221
491
|
end
|
|
222
492
|
|
|
@@ -242,14 +512,13 @@ module Ea
|
|
|
242
512
|
# Keep a vertical tree trunk whose bends still fit both boxes
|
|
243
513
|
# and the open gap; the closest facing edges need not share its x.
|
|
244
514
|
def preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
245
|
-
return false unless
|
|
515
|
+
return false unless points.size == 4 && stored_tree_type?(points, link_row, connector)
|
|
246
516
|
|
|
247
517
|
style = DiagramStyleParser.parse(link_row.style)
|
|
248
518
|
return false unless style[:lwidth] == "2" || flush_width_zero_tree?(link_row)
|
|
249
|
-
return false unless style
|
|
250
|
-
(style[:mode] == "3" && style[:tree].nil?)
|
|
519
|
+
return false unless vertical_tree_style?(style)
|
|
251
520
|
|
|
252
|
-
tree_bends_fit?(points[1], points[-2], src, tgt)
|
|
521
|
+
tree_bends_fit?(points[1], points[-2], src, tgt, flush_width_zero_tree?(link_row))
|
|
253
522
|
end
|
|
254
523
|
|
|
255
524
|
# A width-2 Mode 3 horizontal tree keeps its stored vertical trunk
|
|
@@ -320,6 +589,38 @@ module Ea
|
|
|
320
589
|
model
|
|
321
590
|
end
|
|
322
591
|
|
|
592
|
+
# Same one-pixel collapse for a two-waypoint computed line when only
|
|
593
|
+
# the target carries an explicit port (the source end is the default
|
|
594
|
+
# edge midpoint): the left-hand end's y wins.
|
|
595
|
+
def collapse_partial_port_jog(model, link_row, diagram_row)
|
|
596
|
+
return model unless partial_port_jog_candidate?(model, link_row)
|
|
597
|
+
|
|
598
|
+
connection = database.find_connector(link_row.connectorid)
|
|
599
|
+
src, tgt = [connection.start_object_id, connection.end_object_id].map do |id|
|
|
600
|
+
placement = diagram_object_placement(diagram_row.diagram_id, id)
|
|
601
|
+
placement && bounds_from_rect(placement)
|
|
602
|
+
end
|
|
603
|
+
from, to = model.waypoints.map(&:position)
|
|
604
|
+
return model unless src && tgt && facing_jog_ports?(from, to, src, tgt)
|
|
605
|
+
|
|
606
|
+
level = [from, to].min_by(&:x).y
|
|
607
|
+
ends = [[from.x, level], [to.x, level]].map do |x, y|
|
|
608
|
+
Ea::Model::Point.new(x: x, y: y)
|
|
609
|
+
end
|
|
610
|
+
model.waypoints = ends.map { |point| Ea::Model::Waypoint.new(position: point) }
|
|
611
|
+
model.source_anchor = model.source_port = ends.first
|
|
612
|
+
model.target_anchor = model.target_port = ends.last
|
|
613
|
+
model
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def partial_port_jog_candidate?(model, link_row)
|
|
617
|
+
style = model.style.to_h
|
|
618
|
+
model.connector_type == "Aggregation" &&
|
|
619
|
+
link_row.path.to_s.empty? && style[:mode] == "3" && style[:lwidth] == "2" &&
|
|
620
|
+
!style.key?(:tree) && model.source_port.nil? && model.target_port &&
|
|
621
|
+
model.waypoints.size == 2
|
|
622
|
+
end
|
|
623
|
+
|
|
323
624
|
def computed_jog_candidate?(model, link_row, geom)
|
|
324
625
|
style = model.style.to_h
|
|
325
626
|
model.connector_type == "Generalization" && geom[:edge] == 2 &&
|
|
@@ -346,14 +647,32 @@ module Ea
|
|
|
346
647
|
style[:mode] == "3" && style[:tree] == "V" && style[:lwidth] == "0"
|
|
347
648
|
end
|
|
348
649
|
|
|
349
|
-
def
|
|
350
|
-
|
|
351
|
-
|
|
650
|
+
def vertical_tree_style?(style)
|
|
651
|
+
style[:tree] == "V" || (style[:mode] == "3" && style[:tree].nil?)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def stored_tree_type?(points, link_row, connector)
|
|
655
|
+
connector.connector_type == "Aggregation" || flush_straight_association?(points, link_row, connector)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# A flush Association keeps only a straight trunk (one bend stored twice).
|
|
659
|
+
def flush_straight_association?(points, link_row, connector)
|
|
660
|
+
connector.connector_type == "Association" && points[1] == points[-2] &&
|
|
661
|
+
flush_width_zero_tree?(link_row)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# A flush trunk may also hang below its parent; a bend repeated at
|
|
665
|
+
# one x is a straight trunk with a single bend.
|
|
666
|
+
def tree_bends_fit?(first, last, source, target, flush)
|
|
667
|
+
first[1] == last[1] &&
|
|
668
|
+
within_vertical_gap?(first[1], source, target, flush) &&
|
|
352
669
|
within_horizontal_span?(first, source) && within_horizontal_span?(last, target)
|
|
353
670
|
end
|
|
354
671
|
|
|
355
|
-
def within_vertical_gap?(height, source, target)
|
|
356
|
-
height < source.y - 1 && height > target.y + target.height + 1
|
|
672
|
+
def within_vertical_gap?(height, source, target, flush)
|
|
673
|
+
return true if height < source.y - 1 && height > target.y + target.height + 1
|
|
674
|
+
|
|
675
|
+
flush && height > source.y + source.height + 1 && height < target.y - 1
|
|
357
676
|
end
|
|
358
677
|
|
|
359
678
|
def within_horizontal_span?(point, bounds)
|
|
@@ -421,8 +421,12 @@ module Ea
|
|
|
421
421
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
422
422
|
is_substitutable: substitutable_connector?(connector)
|
|
423
423
|
), link_row, diagram_row, geom)
|
|
424
|
+
model = collapse_partial_port_jog(model, link_row, diagram_row)
|
|
424
425
|
model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
|
|
425
|
-
|
|
426
|
+
model = mode2_overlap_ray(model, link_row, diagram_row, geom, context)
|
|
427
|
+
model = association_stored_ray(model, link_row, diagram_row, geom, context)
|
|
428
|
+
model = correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
429
|
+
overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
426
430
|
end
|
|
427
431
|
|
|
428
432
|
# Ray origin for one connector end: the element's center
|
|
@@ -662,7 +666,8 @@ module Ea
|
|
|
662
666
|
src = bounds_from_rect(source_placement)
|
|
663
667
|
tgt = bounds_from_rect(target_placement)
|
|
664
668
|
tree_route = stored_square_route(link_row, connector, src, tgt) ||
|
|
665
|
-
stored_lateral_route(link_row, connector, src, tgt
|
|
669
|
+
stored_lateral_route(link_row, connector, src, tgt,
|
|
670
|
+
ported: source_port && target_port) ||
|
|
666
671
|
horizontal_tree_route(points, link_row, connector, src, tgt)
|
|
667
672
|
if tree_route
|
|
668
673
|
points = tree_route
|
|
@@ -681,8 +686,15 @@ module Ea
|
|
|
681
686
|
redock_parent_above(points, link_row, source_placement,
|
|
682
687
|
target_placement, strict: true)
|
|
683
688
|
end
|
|
689
|
+
if intermediate.any?
|
|
690
|
+
redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
|
|
691
|
+
end
|
|
684
692
|
end
|
|
685
693
|
|
|
694
|
+
if intermediate.any? && !tree_route
|
|
695
|
+
points = level_entry_route(points, link_row, connector, src, tgt)
|
|
696
|
+
end
|
|
697
|
+
points = drop_zero_length_tree_segments(points, link_row)
|
|
686
698
|
points.map do |x, y|
|
|
687
699
|
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
688
700
|
end
|
|
@@ -42,7 +42,7 @@ module Ea
|
|
|
42
42
|
anchors = [connector.source_anchor, connector.target_anchor]
|
|
43
43
|
bounds = [source_bounds, target_bounds]
|
|
44
44
|
original = clipped_pairs(bounds, anchors)
|
|
45
|
-
return original unless stroke_outline?(connector, bounds)
|
|
45
|
+
return original unless stroke_outline?(connector, bounds) || overlap_ray?(connector)
|
|
46
46
|
|
|
47
47
|
# EA docks these markers on the outer edge of the 2px box stroke.
|
|
48
48
|
outline = bounds.map do |box|
|
|
@@ -103,6 +103,14 @@ module Ea
|
|
|
103
103
|
end
|
|
104
104
|
private_class_method :stroke_outline?
|
|
105
105
|
|
|
106
|
+
# The QEA builder marks the Mode 2 Association overlap rays it
|
|
107
|
+
# produced; EA docks them on the outer stroke edge whatever the
|
|
108
|
+
# line width. Naturally aligned centres keep the legacy docking.
|
|
109
|
+
def overlap_ray?(connector)
|
|
110
|
+
connector.connector_type == "Association" && connector.style.to_h[:overlap_ray] == true
|
|
111
|
+
end
|
|
112
|
+
private_class_method :overlap_ray?
|
|
113
|
+
|
|
106
114
|
def outline_connector?(connector, bounds)
|
|
107
115
|
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
108
116
|
|
|
@@ -197,6 +205,7 @@ module Ea
|
|
|
197
205
|
bounds_map, divider_y_by_ref)
|
|
198
206
|
return nil unless bounds_map && divider_y_by_ref
|
|
199
207
|
return nil unless pairs.size == 2
|
|
208
|
+
return nil if explicit_association_ray?(pairs, connector)
|
|
200
209
|
|
|
201
210
|
rel = relationship_for(connector, model_index)
|
|
202
211
|
return nil unless rel
|
|
@@ -231,16 +240,28 @@ module Ea
|
|
|
231
240
|
end
|
|
232
241
|
private_class_method :explicit_horizontal_source_dock?
|
|
233
242
|
|
|
234
|
-
|
|
235
|
-
|
|
243
|
+
# A stored Association ray whose source port certifies the shared
|
|
244
|
+
# anchor row is one explicit horizontal line: EA seats neither end
|
|
245
|
+
# in a compartment row, unlike Generalization/Aggregation targets.
|
|
246
|
+
def explicit_association_ray?(pairs, connector)
|
|
247
|
+
return false unless explicit_horizontal_ray?(connector, %w[Association])
|
|
248
|
+
|
|
249
|
+
connector.source_anchor.y == connector.target_anchor.y &&
|
|
250
|
+
connector.source_port.y == connector.source_anchor.y &&
|
|
251
|
+
pairs.first[1] == pairs.last[1]
|
|
252
|
+
end
|
|
253
|
+
private_class_method :explicit_association_ray?
|
|
254
|
+
|
|
255
|
+
def explicit_horizontal_ray?(connector, types = %w[Generalization Aggregation])
|
|
256
|
+
explicit_horizontal_ray_style?(connector, types) && connector.has_geometry_offsets &&
|
|
236
257
|
connector.source_port && connector.target_port &&
|
|
237
258
|
connector.source_anchor && connector.target_anchor
|
|
238
259
|
end
|
|
239
260
|
private_class_method :explicit_horizontal_ray?
|
|
240
261
|
|
|
241
|
-
def explicit_horizontal_ray_style?(connector)
|
|
262
|
+
def explicit_horizontal_ray_style?(connector, types)
|
|
242
263
|
style = connector.style.to_h
|
|
243
|
-
|
|
264
|
+
types.include?(connector.connector_type) &&
|
|
244
265
|
style[:mode] == "3" && !style[:tree] && !style[:direct] && !style[:regenerated]
|
|
245
266
|
end
|
|
246
267
|
private_class_method :explicit_horizontal_ray_style?
|
|
@@ -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
|
|
@@ -93,7 +97,8 @@ module Ea
|
|
|
93
97
|
mult_pos = directional_multiplicity(connector, direction, anchor, mult_box, mult) ||
|
|
94
98
|
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
95
99
|
elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
|
|
96
|
-
text_pos =
|
|
100
|
+
text_pos = target_vertical_role(connector, heading, anchor, text_box, role) ||
|
|
101
|
+
target_rightward_role(heading, anchor, text_box, role) || text_pos
|
|
97
102
|
mult_pos = target_multiplicity_seat(heading, anchor, mult_box) || mult_pos
|
|
98
103
|
end
|
|
99
104
|
|
|
@@ -187,7 +192,9 @@ module Ea
|
|
|
187
192
|
|
|
188
193
|
x, y = seat_corner(anchor, box)
|
|
189
194
|
inset, baseline = property_role_offsets(direction)
|
|
190
|
-
|
|
195
|
+
left = x - inset - width
|
|
196
|
+
left = [left, MIN_ROLE_X].max if direction == :up
|
|
197
|
+
[left, y + baseline]
|
|
191
198
|
end
|
|
192
199
|
|
|
193
200
|
def plain_upward_seat(connector, direction, anchor, box, role)
|
|
@@ -203,34 +210,59 @@ module Ea
|
|
|
203
210
|
[x - SEAT_X_INSET - width, y - 6]
|
|
204
211
|
end
|
|
205
212
|
|
|
206
|
-
def
|
|
207
|
-
return nil unless heading && heading[0].zero? && heading[1].
|
|
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
|
|
208
219
|
|
|
209
|
-
|
|
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]
|
|
210
228
|
end
|
|
211
229
|
|
|
212
230
|
def plain_vertical_role(connector, direction, anchor, box, role)
|
|
213
|
-
return nil unless %i[down target_up].include?(direction) &&
|
|
231
|
+
return nil unless %i[down target_up target_down].include?(direction) &&
|
|
232
|
+
plain_vertical_box?(box)
|
|
214
233
|
|
|
215
234
|
end_kind = direction == :down ? :source : :target
|
|
216
|
-
return nil unless plain_vertical_connector?(connector, end_kind)
|
|
235
|
+
return nil unless plain_vertical_connector?(connector, end_kind, direction)
|
|
217
236
|
|
|
218
237
|
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
219
238
|
return nil unless width
|
|
220
239
|
|
|
221
240
|
x, y = seat_corner(anchor, box)
|
|
222
|
-
[x - width - 8, y + (
|
|
241
|
+
[x - width - 8, y + (direction == :target_up ? -7 : 15)]
|
|
223
242
|
end
|
|
224
243
|
|
|
225
244
|
def plain_vertical_box?(box)
|
|
226
245
|
seat_font? && cambria_box?(box) && [0, 1].include?(box["alignment"])
|
|
227
246
|
end
|
|
228
247
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
234
266
|
end
|
|
235
267
|
|
|
236
268
|
def plain_role_route?(style)
|
|
@@ -261,7 +293,7 @@ module Ea
|
|
|
261
293
|
case direction
|
|
262
294
|
when :left then left_property_route?(style)
|
|
263
295
|
when :down then downward_property_route?(style)
|
|
264
|
-
else style[:lwidth]
|
|
296
|
+
else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
|
|
265
297
|
end
|
|
266
298
|
end
|
|
267
299
|
|
|
@@ -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