ea 0.6.99 → 0.6.101
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 +408 -10
- data/lib/ea/sources/qea/diagram_builder.rb +19 -3
- data/lib/ea/svg/ea_emitter/connector_redock.rb +105 -6
- 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: 7c722ef7c5b7c7968f5beb08538b31b8090a3ab1533b0418a9600752c2724ef3
|
|
4
|
+
data.tar.gz: 818694e4771383a00ae453f76a7b3f98e79b6b5a839d9459d415ec72129e4226
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b00b8fddbd7701f724dc20bd6640dfc53b7a99903d7a075581be94c9f3674254a114fa976a2a895d5ba231ca3b0c93f7b8ce241c42ef4d5c9e106f82c7b660a6
|
|
7
|
+
data.tar.gz: c41701279192e06adba33b39063dac26a9355dedf1a46fd687079013c341c880f8f3d83278876593cf27733c527f41e22032140b901246b0b6e335cf29dcb308
|
|
@@ -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,149 @@ 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, model.style.to_h[:lwidth])
|
|
117
|
+
return model unless origins
|
|
118
|
+
|
|
119
|
+
model.waypoints = port_waypoints(model)
|
|
120
|
+
model.source_anchor, model.target_anchor = origins
|
|
121
|
+
model.style = model.style.to_h.merge(stored_association_ray: true, association_ray_outline: true)
|
|
122
|
+
model
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def port_waypoints(model)
|
|
126
|
+
[model.source_port, model.target_port].map do |point|
|
|
127
|
+
Ea::Model::Waypoint.new(position: point)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Width-2 boxes also own their 1px outer stroke, so a stored
|
|
132
|
+
# origin on that outline is still inside its box.
|
|
133
|
+
def association_ray_origins(source, target, geom, width)
|
|
134
|
+
outline = width == "2" ? 1 : 0
|
|
135
|
+
origins = [stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
136
|
+
stored_ray_origin(target, geom[:ex], geom[:ey])]
|
|
137
|
+
boxes = [source, target].map { |box| grown_bounds(box, outline) }
|
|
138
|
+
inside = origins.zip(boxes).all? { |o, b| inside_box?(o, b) }
|
|
139
|
+
return unless inside && origins.first != origins.last
|
|
140
|
+
|
|
141
|
+
origins if width == "2" || sub_pixel_safe_origins?(origins, [source, target] + boxes)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def grown_bounds(bounds, amount)
|
|
145
|
+
Ea::Model::Bounds.new(x: bounds.x - amount, y: bounds.y - amount,
|
|
146
|
+
width: bounds.width + 2 * amount,
|
|
147
|
+
height: bounds.height + 2 * amount)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def association_ray_candidate?(model, link_row)
|
|
151
|
+
model.connector_type == "Association" && link_row.path.to_s.empty? &&
|
|
152
|
+
model.has_geometry_offsets && model.source_port && model.target_port &&
|
|
153
|
+
association_ray_style?(model.style.to_h)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def association_ray_style?(style)
|
|
157
|
+
style[:mode] == "3" && !style[:tree] && %w[0 2].include?(style[:lwidth]) &&
|
|
158
|
+
%i[direct regenerated].none? { |key| style.key?(key) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Width-0 origins are whole pixels while EA's box centers may sit
|
|
162
|
+
# on a half pixel. An origin on its border then clips differently
|
|
163
|
+
# unless the line is axis-aligned, which no rounding changes.
|
|
164
|
+
def sub_pixel_safe_origins?(origins, boxes)
|
|
165
|
+
first, last = origins
|
|
166
|
+
axis_aligned = first.x == last.x || first.y == last.y
|
|
167
|
+
axis_aligned || origins.none? { |point| boxes.any? { |box| on_border?(point, box) } }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def on_border?(point, bounds)
|
|
171
|
+
[bounds.x, bounds.x + bounds.width].include?(point.x) ||
|
|
172
|
+
[bounds.y, bounds.y + bounds.height].include?(point.y)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def inside_box?(point, bounds)
|
|
176
|
+
point.x.between?(bounds.x, bounds.x + bounds.width) &&
|
|
177
|
+
point.y.between?(bounds.y, bounds.y + bounds.height)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# A Mode 2 Association with equal SX/EX (vertical EDGE 1/3) or
|
|
181
|
+
# equal SY/EY (horizontal EDGE 4) and no custom Path docks as a
|
|
182
|
+
# straight line through the middle of the boxes' overlap, moved
|
|
183
|
+
# by the shared stored offset. Stored SY counts upward.
|
|
184
|
+
def overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
185
|
+
return model if context
|
|
186
|
+
return model unless overlap_ray_candidate?(model, link_row, geom)
|
|
187
|
+
|
|
188
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
189
|
+
origins = overlap_origins(source, target, geom)
|
|
190
|
+
return model unless origins
|
|
191
|
+
|
|
192
|
+
model.source_anchor, model.target_anchor = origins.map do |ox, oy|
|
|
193
|
+
Ea::Model::Point.new(x: ox, y: oy)
|
|
194
|
+
end
|
|
195
|
+
model.style[:overlap_ray] = true
|
|
196
|
+
model
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Both ray origins share the overlap coordinate on the axis the
|
|
200
|
+
# offsets move and sit at each box's centre on the other axis.
|
|
201
|
+
def overlap_origins(source, target, geom)
|
|
202
|
+
along, across, offset = overlap_axes(geom)
|
|
203
|
+
coordinate = overlap_ray_coordinate(*box_span(source, along), *box_span(target, along), offset)
|
|
204
|
+
return nil unless coordinate && disjoint_span?(*box_span(source, across), *box_span(target, across))
|
|
205
|
+
|
|
206
|
+
[source, target].map do |box|
|
|
207
|
+
start, size = box_span(box, across)
|
|
208
|
+
centre = start + size / 2.0
|
|
209
|
+
geom[:edge] == 4 ? [centre, coordinate] : [coordinate, centre]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# [axis the offsets move, axis the boxes face, stored offset]
|
|
214
|
+
def overlap_axes(geom)
|
|
215
|
+
return [%i[y height], %i[x width], -geom[:sy]] if geom[:edge] == 4
|
|
216
|
+
|
|
217
|
+
[%i[x width], %i[y height], geom[:sx]]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def box_span(box, keys)
|
|
221
|
+
keys.map { |key| box.public_send(key) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def overlap_ray_candidate?(model, link_row, geom)
|
|
225
|
+
model.connector_type == "Association" && model.style[:mode] == "2" &&
|
|
226
|
+
model.waypoints.size == 2 && link_row.path.to_s.empty? &&
|
|
227
|
+
model.has_geometry_offsets && uniform_overlap_offsets?(geom)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def uniform_overlap_offsets?(geom)
|
|
231
|
+
return false unless geom.values_at(:sx, :sy, :ex, :ey).all?
|
|
232
|
+
|
|
233
|
+
geom[:edge] == 4 ? geom[:sy] == geom[:ey] : [1, 3].include?(geom[:edge]) && geom[:sx] == geom[:ex]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Floor of the overlap midpoint plus the shared offset; nil
|
|
237
|
+
# when the boxes do not overlap on this axis.
|
|
238
|
+
def overlap_ray_coordinate(start_a, size_a, start_b, size_b, offset)
|
|
239
|
+
low = [start_a, start_b].max
|
|
240
|
+
high = [start_a + size_a, start_b + size_b].min
|
|
241
|
+
return nil if low > high
|
|
242
|
+
|
|
243
|
+
coordinate = ((low + high) / 2.0 + offset).floor
|
|
244
|
+
coordinate if coordinate.between?(low, high)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def disjoint_span?(start_a, size_a, start_b, size_b)
|
|
248
|
+
start_a + size_a < start_b || start_b + size_b < start_a
|
|
249
|
+
end
|
|
250
|
+
|
|
58
251
|
def stored_ray_boxes(link_row, diagram_row)
|
|
59
252
|
connector = database.find_connector(link_row.connectorid)
|
|
60
253
|
[source_placement_for(connector, diagram_row),
|
|
@@ -134,7 +327,7 @@ module Ea
|
|
|
134
327
|
|
|
135
328
|
case connector.connector_type
|
|
136
329
|
when "Aggregation"
|
|
137
|
-
return
|
|
330
|
+
return unless style[:lwidth] == "0" || !geometry_has_offsets?(link_row.geometry)
|
|
138
331
|
|
|
139
332
|
redock_child_below(points, bounds_from_rect(source_placement), bounds_from_rect(target_placement))
|
|
140
333
|
when "Association"
|
|
@@ -174,6 +367,14 @@ module Ea
|
|
|
174
367
|
route
|
|
175
368
|
end
|
|
176
369
|
|
|
370
|
+
# One stored corner that is level with both boxes already is the
|
|
371
|
+
# route (vertical leg out of the source, horizontal leg into the
|
|
372
|
+
# target); flattening it to the facing edges would lose it.
|
|
373
|
+
def level_corner_route?(points, link_row, connector, src, tgt)
|
|
374
|
+
points.size == 3 && level_entry_style?(link_row, connector) &&
|
|
375
|
+
!level_entry_point(points[1], src).nil? && !level_entry_point(points[1], tgt).nil?
|
|
376
|
+
end
|
|
377
|
+
|
|
177
378
|
def level_entry_style?(link_row, connector)
|
|
178
379
|
style = DiagramStyleParser.parse(link_row.style)
|
|
179
380
|
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
@@ -241,6 +442,62 @@ module Ea
|
|
|
241
442
|
[bounds.y, bounds.y + bounds.height].include?(point[1])
|
|
242
443
|
end
|
|
243
444
|
|
|
445
|
+
# A width-2 Mode 3 Aggregation without a tree style keeps an authored
|
|
446
|
+
# orthogonal stair-step of three or more corners. Each end docks on
|
|
447
|
+
# the 1px outline its adjacent leg points at, from the corner's own
|
|
448
|
+
# coordinate; any end that cannot do so leaves the route unchanged.
|
|
449
|
+
def stored_stair_route(link_row, connector, source, target)
|
|
450
|
+
return nil unless stair_style?(link_row, connector)
|
|
451
|
+
|
|
452
|
+
corners = stored_square_corners(link_row)
|
|
453
|
+
return nil unless corners && corners.size >= 3 && alternating_legs?(corners)
|
|
454
|
+
|
|
455
|
+
first = outline_dock(corners[0], corners[1], source)
|
|
456
|
+
last = outline_dock(corners[-1], corners[-2], target)
|
|
457
|
+
[first, *corners, last] if first && last
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def stair_style?(link_row, connector)
|
|
461
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
462
|
+
connector.connector_type == "Aggregation" && style[:mode] == "3" &&
|
|
463
|
+
style[:lwidth] == "2" && !style.key?(:tree)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Every leg is non-zero and runs along the opposite axis of the last.
|
|
467
|
+
def alternating_legs?(corners)
|
|
468
|
+
axes = corners.each_cons(2).map { |from, to| leg_axis(from, to) }
|
|
469
|
+
axes.none?(&:nil?) && axes.each_cons(2).all? { |a, b| a != b }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def leg_axis(from, to)
|
|
473
|
+
return :vertical if from[0] == to[0] && from[1] != to[1]
|
|
474
|
+
|
|
475
|
+
:horizontal if from[1] == to[1] && from[0] != to[0]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# The dock runs perpendicular to the corner's own leg.
|
|
479
|
+
def outline_dock(corner, neighbour, box)
|
|
480
|
+
if corner[1] == neighbour[1]
|
|
481
|
+
vertical_outline_dock(corner, box)
|
|
482
|
+
else
|
|
483
|
+
horizontal_outline_dock(corner, box)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def vertical_outline_dock(corner, box)
|
|
488
|
+
x, y = corner
|
|
489
|
+
return nil unless x.between?(box.x, box.x + box.width) && !within_height?(y, box)
|
|
490
|
+
|
|
491
|
+
[x, y < box.y ? box.y - 1 : box.y + box.height + 1]
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def horizontal_outline_dock(corner, box)
|
|
495
|
+
x, y = corner
|
|
496
|
+
return nil unless within_height?(y, box) && !x.between?(box.x, box.x + box.width)
|
|
497
|
+
|
|
498
|
+
[x < box.x ? box.x - 1 : box.x + box.width + 1, y]
|
|
499
|
+
end
|
|
500
|
+
|
|
244
501
|
# A valid lateral corner joins one vertical leg and one horizontal
|
|
245
502
|
# leg. Keep it even when the boxes also admit a shorter direct route.
|
|
246
503
|
# ported: both ends carry explicit SX/SY and EX/EY offsets. An
|
|
@@ -255,6 +512,66 @@ module Ea
|
|
|
255
512
|
corner && lateral_corner_route(style, corner, source, target)
|
|
256
513
|
end
|
|
257
514
|
|
|
515
|
+
# A horizontal lateral tree with no usable stored corner leaves
|
|
516
|
+
# the source's top or bottom edge at its center x, turns at the
|
|
517
|
+
# target's center y and enters the target's facing side edge.
|
|
518
|
+
# When that y lies inside the source's height the route is one
|
|
519
|
+
# horizontal line between the facing side edges. Every dock is
|
|
520
|
+
# flush on its box edge.
|
|
521
|
+
def derived_lateral_route(link_row, connector, source, target)
|
|
522
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
523
|
+
return nil unless style[:mode] == "3" && style[:tree] == "LH" &&
|
|
524
|
+
derived_lateral_path?(connector.connector_type, link_row.path.to_s)
|
|
525
|
+
|
|
526
|
+
x = source.x + source.width / 2
|
|
527
|
+
y = target.y + target.height / 2
|
|
528
|
+
edge = lateral_target_edge(x, target)
|
|
529
|
+
return nil unless edge
|
|
530
|
+
|
|
531
|
+
if within_height?(y, source)
|
|
532
|
+
side = target.x > x ? source.x + source.width : source.x
|
|
533
|
+
[[side, y], [edge, y]] if ((edge - side) * (edge - x)).positive?
|
|
534
|
+
else
|
|
535
|
+
[[x, y < source.y ? source.y : source.y + source.height], [x, y], [edge, y]]
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# An Association may carry one stored corner that no valid lateral
|
|
540
|
+
# route accepted; a Generalization derives only from an empty Path.
|
|
541
|
+
def derived_lateral_path?(connector_type, path)
|
|
542
|
+
case connector_type
|
|
543
|
+
when "Association" then path.match?(/\A(?:-?\d+:-?\d+;?)?\z/)
|
|
544
|
+
when "Generalization" then path.empty?
|
|
545
|
+
else false
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# A derived level line docks flush at its own two ends; anchoring
|
|
550
|
+
# the ray there keeps the emitter's clip from moving them.
|
|
551
|
+
def pin_derived_lateral_line(model, link_row, diagram_row, context)
|
|
552
|
+
style = model.style.to_h
|
|
553
|
+
return model if context || model.waypoints.size != 2 ||
|
|
554
|
+
style[:mode] != "3" || style[:tree] != "LH"
|
|
555
|
+
|
|
556
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
557
|
+
connector = database.find_connector(link_row.connectorid)
|
|
558
|
+
line = connector && derived_lateral_route(link_row, connector, source, target)
|
|
559
|
+
return model unless line && line.size == 2 &&
|
|
560
|
+
line == model.waypoints.map { |wp| [wp.position.x, wp.position.y] }
|
|
561
|
+
|
|
562
|
+
model.source_anchor, model.target_anchor = line.map { |x, y| Ea::Model::Point.new(x: x, y: y) }
|
|
563
|
+
model.style = style.merge(derived_lateral_line: true)
|
|
564
|
+
model
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# The target side edge a vertical leg at x faces, or nil when x
|
|
568
|
+
# lies within the target's span.
|
|
569
|
+
def lateral_target_edge(x_position, target)
|
|
570
|
+
return target.x if x_position < target.x
|
|
571
|
+
|
|
572
|
+
target.x + target.width if x_position > target.x + target.width
|
|
573
|
+
end
|
|
574
|
+
|
|
258
575
|
# An untyped Mode 3 Association corner takes whichever leg
|
|
259
576
|
# orientation its position fits; width-2 docks sit on the 1px
|
|
260
577
|
# outer outline.
|
|
@@ -334,14 +651,13 @@ module Ea
|
|
|
334
651
|
# Keep a vertical tree trunk whose bends still fit both boxes
|
|
335
652
|
# and the open gap; the closest facing edges need not share its x.
|
|
336
653
|
def preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
337
|
-
return false unless
|
|
654
|
+
return false unless points.size == 4 && stored_tree_type?(points, link_row, connector)
|
|
338
655
|
|
|
339
656
|
style = DiagramStyleParser.parse(link_row.style)
|
|
340
657
|
return false unless style[:lwidth] == "2" || flush_width_zero_tree?(link_row)
|
|
341
|
-
return false unless style
|
|
342
|
-
(style[:mode] == "3" && style[:tree].nil?)
|
|
658
|
+
return false unless vertical_tree_style?(style)
|
|
343
659
|
|
|
344
|
-
tree_bends_fit?(points[1], points[-2], src, tgt)
|
|
660
|
+
tree_bends_fit?(points[1], points[-2], src, tgt, flush_width_zero_tree?(link_row))
|
|
345
661
|
end
|
|
346
662
|
|
|
347
663
|
# A width-2 Mode 3 horizontal tree keeps its stored vertical trunk
|
|
@@ -412,6 +728,70 @@ module Ea
|
|
|
412
728
|
model
|
|
413
729
|
end
|
|
414
730
|
|
|
731
|
+
# Same one-pixel collapse for a two-waypoint computed line when only
|
|
732
|
+
# the target carries an explicit port (the source end is the default
|
|
733
|
+
# edge midpoint): the left-hand end's y wins.
|
|
734
|
+
def collapse_partial_port_jog(model, link_row, diagram_row)
|
|
735
|
+
return model unless partial_port_jog_candidate?(model, link_row)
|
|
736
|
+
|
|
737
|
+
connection = database.find_connector(link_row.connectorid)
|
|
738
|
+
src, tgt = [connection.start_object_id, connection.end_object_id].map do |id|
|
|
739
|
+
placement = diagram_object_placement(diagram_row.diagram_id, id)
|
|
740
|
+
placement && bounds_from_rect(placement)
|
|
741
|
+
end
|
|
742
|
+
from, to = model.waypoints.map(&:position)
|
|
743
|
+
return model unless src && tgt && facing_jog_ports?(from, to, src, tgt)
|
|
744
|
+
|
|
745
|
+
level = [from, to].min_by(&:x).y
|
|
746
|
+
ends = [[from.x, level], [to.x, level]].map do |x, y|
|
|
747
|
+
Ea::Model::Point.new(x: x, y: y)
|
|
748
|
+
end
|
|
749
|
+
model.waypoints = ends.map { |point| Ea::Model::Waypoint.new(position: point) }
|
|
750
|
+
model.source_anchor = model.source_port = ends.first
|
|
751
|
+
model.target_anchor = model.target_port = ends.last
|
|
752
|
+
model
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# A two-waypoint Mode 3 Aggregation with only a target port draws the
|
|
756
|
+
# line from the source center to the target's stored ray origin
|
|
757
|
+
# (center + EX, center - EY), clipped at the outlines; the boundary
|
|
758
|
+
# port is only the derived crossing of that origin's ray.
|
|
759
|
+
def correct_partial_port_origins(model, link_row, diagram_row, geom, context)
|
|
760
|
+
return model if context || !partial_port_origin_candidate?(model, link_row)
|
|
761
|
+
|
|
762
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
763
|
+
return model unless horizontal_ray_gap?(source, target)
|
|
764
|
+
|
|
765
|
+
model.source_anchor = stored_ray_origin(source, 0, 0)
|
|
766
|
+
model.target_anchor = stored_ray_origin(target, geom[:ex], geom[:ey])
|
|
767
|
+
model
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def partial_port_origin_candidate?(model, link_row)
|
|
771
|
+
style = model.style.to_h
|
|
772
|
+
model.connector_type == "Aggregation" && model.waypoints.size == 2 &&
|
|
773
|
+
link_row.path.to_s.empty? && style[:mode] == "3" && style[:lwidth] == "2" &&
|
|
774
|
+
!style.key?(:tree) &&
|
|
775
|
+
model.source_port.nil? && model.target_port
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def partial_port_jog_candidate?(model, link_row)
|
|
779
|
+
style = model.style.to_h
|
|
780
|
+
model.connector_type == "Aggregation" &&
|
|
781
|
+
link_row.path.to_s.empty? && style[:mode] == "3" &&
|
|
782
|
+
!style.key?(:tree) && model.waypoints.size == 2 &&
|
|
783
|
+
explicit_port_side?(model, style[:lwidth])
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Width 2 keeps the evidenced target-only port; width 0 accepts a
|
|
787
|
+
# single explicit port at either end.
|
|
788
|
+
def explicit_port_side?(model, width)
|
|
789
|
+
ports = [model.source_port, model.target_port]
|
|
790
|
+
return model.source_port.nil? && !model.target_port.nil? if width == "2"
|
|
791
|
+
|
|
792
|
+
width == "0" && ports.compact.size == 1
|
|
793
|
+
end
|
|
794
|
+
|
|
415
795
|
def computed_jog_candidate?(model, link_row, geom)
|
|
416
796
|
style = model.style.to_h
|
|
417
797
|
model.connector_type == "Generalization" && geom[:edge] == 2 &&
|
|
@@ -438,14 +818,32 @@ module Ea
|
|
|
438
818
|
style[:mode] == "3" && style[:tree] == "V" && style[:lwidth] == "0"
|
|
439
819
|
end
|
|
440
820
|
|
|
441
|
-
def
|
|
442
|
-
|
|
443
|
-
|
|
821
|
+
def vertical_tree_style?(style)
|
|
822
|
+
style[:tree] == "V" || (style[:mode] == "3" && style[:tree].nil?)
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def stored_tree_type?(points, link_row, connector)
|
|
826
|
+
connector.connector_type == "Aggregation" || flush_straight_association?(points, link_row, connector)
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# A flush Association keeps only a straight trunk (one bend stored twice).
|
|
830
|
+
def flush_straight_association?(points, link_row, connector)
|
|
831
|
+
connector.connector_type == "Association" && points[1] == points[-2] &&
|
|
832
|
+
flush_width_zero_tree?(link_row)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# A flush trunk may also hang below its parent; a bend repeated at
|
|
836
|
+
# one x is a straight trunk with a single bend.
|
|
837
|
+
def tree_bends_fit?(first, last, source, target, flush)
|
|
838
|
+
first[1] == last[1] &&
|
|
839
|
+
within_vertical_gap?(first[1], source, target, flush) &&
|
|
444
840
|
within_horizontal_span?(first, source) && within_horizontal_span?(last, target)
|
|
445
841
|
end
|
|
446
842
|
|
|
447
|
-
def within_vertical_gap?(height, source, target)
|
|
448
|
-
height < source.y - 1 && height > target.y + target.height + 1
|
|
843
|
+
def within_vertical_gap?(height, source, target, flush)
|
|
844
|
+
return true if height < source.y - 1 && height > target.y + target.height + 1
|
|
845
|
+
|
|
846
|
+
flush && height > source.y + source.height + 1 && height < target.y - 1
|
|
449
847
|
end
|
|
450
848
|
|
|
451
849
|
def within_horizontal_span?(point, bounds)
|
|
@@ -362,12 +362,20 @@ module Ea
|
|
|
362
362
|
target_stereotype: conn.deststereotype,
|
|
363
363
|
waypoints: waypoints,
|
|
364
364
|
label_boxes: DEFAULT_LABEL_BOXES,
|
|
365
|
-
style:
|
|
365
|
+
style: phantom_style(conn.connector_type),
|
|
366
366
|
hidden: false,
|
|
367
367
|
has_geometry_offsets: false
|
|
368
368
|
)
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
+
# Internal producer flag, like :direct. A model relationship with no
|
|
372
|
+
# link row is stroked at the 2px box outline, so its ends dock there.
|
|
373
|
+
def phantom_style(connector_type)
|
|
374
|
+
return { direct: true } if PHANTOM_DOUBLE_BACK_TYPES.include?(connector_type)
|
|
375
|
+
|
|
376
|
+
%w[Aggregation Association].include?(connector_type) ? { phantom_outline: true } : {}
|
|
377
|
+
end
|
|
378
|
+
|
|
371
379
|
# Computes the edge attachment point on source bounds that
|
|
372
380
|
# faces the target bounds. Uses the center-to-center direction
|
|
373
381
|
# to pick the nearest edge intersection.
|
|
@@ -421,8 +429,14 @@ module Ea
|
|
|
421
429
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
422
430
|
is_substitutable: substitutable_connector?(connector)
|
|
423
431
|
), link_row, diagram_row, geom)
|
|
432
|
+
model = collapse_partial_port_jog(model, link_row, diagram_row)
|
|
433
|
+
model = correct_partial_port_origins(model, link_row, diagram_row, geom, context)
|
|
424
434
|
model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
|
|
425
|
-
|
|
435
|
+
model = pin_derived_lateral_line(model, link_row, diagram_row, context)
|
|
436
|
+
model = mode2_overlap_ray(model, link_row, diagram_row, geom, context)
|
|
437
|
+
model = association_stored_ray(model, link_row, diagram_row, geom, context)
|
|
438
|
+
model = correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
439
|
+
overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
426
440
|
end
|
|
427
441
|
|
|
428
442
|
# Ray origin for one connector end: the element's center
|
|
@@ -662,15 +676,17 @@ module Ea
|
|
|
662
676
|
src = bounds_from_rect(source_placement)
|
|
663
677
|
tgt = bounds_from_rect(target_placement)
|
|
664
678
|
tree_route = stored_square_route(link_row, connector, src, tgt) ||
|
|
679
|
+
stored_stair_route(link_row, connector, src, tgt) ||
|
|
665
680
|
stored_lateral_route(link_row, connector, src, tgt,
|
|
666
681
|
ported: source_port && target_port) ||
|
|
682
|
+
derived_lateral_route(link_row, connector, src, tgt) ||
|
|
667
683
|
horizontal_tree_route(points, link_row, connector, src, tgt)
|
|
668
684
|
if tree_route
|
|
669
685
|
points = tree_route
|
|
670
686
|
elsif %w[Association Aggregation].include?(connector.connector_type) &&
|
|
671
687
|
edge_out != 2
|
|
672
688
|
stored_tree = preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
673
|
-
unless stored_tree
|
|
689
|
+
unless stored_tree || level_corner_route?(points, link_row, connector, src, tgt)
|
|
674
690
|
points = facing_edge_route(points, src, tgt)
|
|
675
691
|
end
|
|
676
692
|
# A width-2 Aggregation that keeps its stored bends is
|
|
@@ -39,10 +39,15 @@ module Ea
|
|
|
39
39
|
return target && [points[0], points[1], target]
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
return nil if flush_lateral_line?(connector, points, source_bounds, target_bounds)
|
|
43
|
+
|
|
42
44
|
anchors = [connector.source_anchor, connector.target_anchor]
|
|
43
45
|
bounds = [source_bounds, target_bounds]
|
|
44
46
|
original = clipped_pairs(bounds, anchors)
|
|
45
|
-
|
|
47
|
+
unless stroke_outline?(connector, bounds) || overlap_ray?(connector) ||
|
|
48
|
+
bordered_outline?(connector, diagram, [source_ref, target_ref], bounds)
|
|
49
|
+
return original
|
|
50
|
+
end
|
|
46
51
|
|
|
47
52
|
# EA docks these markers on the outer edge of the 2px box stroke.
|
|
48
53
|
outline = bounds.map do |box|
|
|
@@ -53,6 +58,29 @@ module Ea
|
|
|
53
58
|
forward_route?(original, candidate) ? candidate : original
|
|
54
59
|
end
|
|
55
60
|
|
|
61
|
+
# A horizontal lateral tree the QEA builder derived as one level
|
|
62
|
+
# line (internal style flag) keeps it between the facing side edges.
|
|
63
|
+
def flush_lateral_line?(connector, points, source, target)
|
|
64
|
+
style = connector.style.to_h
|
|
65
|
+
return false unless style[:derived_lateral_line] && style[:mode] == "3" &&
|
|
66
|
+
style[:tree] == "LH" && points.size == 2 &&
|
|
67
|
+
%w[Association Generalization].include?(connector.connector_type)
|
|
68
|
+
return false unless connector.source_anchor && connector.target_anchor
|
|
69
|
+
|
|
70
|
+
first, last = points
|
|
71
|
+
pinned = [[connector.source_anchor.x, connector.source_anchor.y],
|
|
72
|
+
[connector.target_anchor.x, connector.target_anchor.y]]
|
|
73
|
+
pinned == [first, last] && first[1] == last[1] &&
|
|
74
|
+
on_side_edge?(first, source) && on_side_edge?(last, target)
|
|
75
|
+
end
|
|
76
|
+
private_class_method :flush_lateral_line?
|
|
77
|
+
|
|
78
|
+
def on_side_edge?(point, bounds)
|
|
79
|
+
[bounds.x, bounds.x + bounds.width].include?(point[0]) &&
|
|
80
|
+
point[1].between?(bounds.y, bounds.y + bounds.height)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :on_side_edge?
|
|
83
|
+
|
|
56
84
|
def lateral_tree?(connector, points)
|
|
57
85
|
style = connector.style.to_h
|
|
58
86
|
points.size == 3 && style[:mode] == "3" && style[:lwidth] == "2" &&
|
|
@@ -97,15 +125,56 @@ module Ea
|
|
|
97
125
|
private_class_method :facing_coordinate
|
|
98
126
|
|
|
99
127
|
def stroke_outline?(connector, bounds)
|
|
100
|
-
connector.style.to_h[:lwidth] == "2" &&
|
|
128
|
+
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)) &&
|
|
101
129
|
bounds.all? { |box| box.width.positive? && box.height.positive? } &&
|
|
102
130
|
outline_connector?(connector, bounds)
|
|
103
131
|
end
|
|
104
132
|
private_class_method :stroke_outline?
|
|
105
133
|
|
|
134
|
+
# The QEA builder marks the Mode 2 Association overlap rays it
|
|
135
|
+
# produced; EA docks them on the outer stroke edge whatever the
|
|
136
|
+
# line width. Naturally aligned centres keep the legacy docking.
|
|
137
|
+
def overlap_ray?(connector)
|
|
138
|
+
connector.connector_type == "Association" && connector.style.to_h[:overlap_ray] == true
|
|
139
|
+
end
|
|
140
|
+
private_class_method :overlap_ray?
|
|
141
|
+
|
|
142
|
+
# The QEA builder flags a model relationship drawn without any
|
|
143
|
+
# diagram link row; EA strokes it at the 2px box outline.
|
|
144
|
+
def default_model_connector?(connector)
|
|
145
|
+
connector.style.to_h[:phantom_outline] == true && !connector.has_geometry_offsets &&
|
|
146
|
+
%w[Aggregation Association].include?(connector.connector_type)
|
|
147
|
+
end
|
|
148
|
+
private_class_method :default_model_connector?
|
|
149
|
+
|
|
150
|
+
# A thin straight Generalization/Aggregation/Association between two elements
|
|
151
|
+
# drawn with a 2px border stops on the outer edge of those borders.
|
|
152
|
+
def bordered_outline?(connector, diagram, refs, bounds)
|
|
153
|
+
style = connector.style.to_h
|
|
154
|
+
return false unless style[:lwidth] == "0" && !style[:tree]
|
|
155
|
+
return false unless %w[Generalization Aggregation Association].include?(connector.connector_type)
|
|
156
|
+
return false unless bounds.all? { |box| box.width.positive? && box.height.positive? }
|
|
157
|
+
|
|
158
|
+
ends = [[refs.first, style[:soid]], [refs.last, style[:eoid]]]
|
|
159
|
+
ends.all? { |ref, duid| border_two?(diagram, ref, duid) }
|
|
160
|
+
end
|
|
161
|
+
private_class_method :bordered_outline?
|
|
162
|
+
|
|
163
|
+
# Every placement of the element (the DUID-matched one when the
|
|
164
|
+
# connector names it) must declare a border width of 2.
|
|
165
|
+
def border_two?(diagram, ref, duid)
|
|
166
|
+
placed = Array(diagram&.elements).select { |el| el.model_element_ref == ref }
|
|
167
|
+
named = placed.select { |el| duid && el.duid.to_s.casecmp?(duid.to_s) }
|
|
168
|
+
placed = named unless named.empty?
|
|
169
|
+
placed.any? && placed.all? { |el| el.line_width == 2 || el.style.to_h[:lwth] == "2" }
|
|
170
|
+
end
|
|
171
|
+
private_class_method :border_two?
|
|
172
|
+
|
|
106
173
|
def outline_connector?(connector, bounds)
|
|
174
|
+
return true if default_model_connector?(connector)
|
|
107
175
|
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
108
176
|
|
|
177
|
+
return true if association_ray_outline?(connector)
|
|
109
178
|
return false unless stored_association?(connector) || offset_free_association?(connector)
|
|
110
179
|
|
|
111
180
|
# Diagonal Association rounding and tree docks keep their existing rules.
|
|
@@ -114,6 +183,15 @@ module Ea
|
|
|
114
183
|
end
|
|
115
184
|
private_class_method :outline_connector?
|
|
116
185
|
|
|
186
|
+
# The QEA builder marks the stored Association rays it produced;
|
|
187
|
+
# their diagonal docks sit on the outer stroke edge like the
|
|
188
|
+
# axis-aligned ones. Unmarked diagonals keep the legacy docking.
|
|
189
|
+
def association_ray_outline?(connector)
|
|
190
|
+
connector.connector_type == "Association" &&
|
|
191
|
+
connector.style.to_h[:association_ray_outline] == true
|
|
192
|
+
end
|
|
193
|
+
private_class_method :association_ray_outline?
|
|
194
|
+
|
|
117
195
|
def stored_association?(connector)
|
|
118
196
|
style = connector.style.to_h
|
|
119
197
|
connector.connector_type == "Association" && connector.has_geometry_offsets &&
|
|
@@ -197,6 +275,7 @@ module Ea
|
|
|
197
275
|
bounds_map, divider_y_by_ref)
|
|
198
276
|
return nil unless bounds_map && divider_y_by_ref
|
|
199
277
|
return nil unless pairs.size == 2
|
|
278
|
+
return nil if explicit_association_ray?(pairs, connector)
|
|
200
279
|
|
|
201
280
|
rel = relationship_for(connector, model_index)
|
|
202
281
|
return nil unless rel
|
|
@@ -231,16 +310,36 @@ module Ea
|
|
|
231
310
|
end
|
|
232
311
|
private_class_method :explicit_horizontal_source_dock?
|
|
233
312
|
|
|
234
|
-
|
|
235
|
-
|
|
313
|
+
# A stored Association ray whose source port certifies the shared
|
|
314
|
+
# anchor row is one explicit horizontal line: EA seats neither end
|
|
315
|
+
# in a compartment row, unlike Generalization/Aggregation targets.
|
|
316
|
+
def explicit_association_ray?(pairs, connector)
|
|
317
|
+
produced = connector.style.to_h[:stored_association_ray] == true
|
|
318
|
+
return true if produced && pairs.first[1] == pairs.last[1]
|
|
319
|
+
|
|
320
|
+
certified_association_ray?(pairs, connector)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def certified_association_ray?(pairs, connector)
|
|
324
|
+
return false unless explicit_horizontal_ray?(connector, %w[Association])
|
|
325
|
+
|
|
326
|
+
connector.source_anchor.y == connector.target_anchor.y &&
|
|
327
|
+
connector.source_port.y == connector.source_anchor.y &&
|
|
328
|
+
pairs.first[1] == pairs.last[1]
|
|
329
|
+
end
|
|
330
|
+
private_class_method :certified_association_ray?
|
|
331
|
+
private_class_method :explicit_association_ray?
|
|
332
|
+
|
|
333
|
+
def explicit_horizontal_ray?(connector, types = %w[Generalization Aggregation])
|
|
334
|
+
explicit_horizontal_ray_style?(connector, types) && connector.has_geometry_offsets &&
|
|
236
335
|
connector.source_port && connector.target_port &&
|
|
237
336
|
connector.source_anchor && connector.target_anchor
|
|
238
337
|
end
|
|
239
338
|
private_class_method :explicit_horizontal_ray?
|
|
240
339
|
|
|
241
|
-
def explicit_horizontal_ray_style?(connector)
|
|
340
|
+
def explicit_horizontal_ray_style?(connector, types)
|
|
242
341
|
style = connector.style.to_h
|
|
243
|
-
|
|
342
|
+
types.include?(connector.connector_type) &&
|
|
244
343
|
style[:mode] == "3" && !style[:tree] && !style[:direct] && !style[:regenerated]
|
|
245
344
|
end
|
|
246
345
|
private_class_method :explicit_horizontal_ray_style?
|
data/lib/ea/version.rb
CHANGED