ea 0.6.100 → 0.6.102
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 +268 -22
- data/lib/ea/sources/qea/diagram_builder.rb +16 -3
- data/lib/ea/svg/ea_emitter/connector_redock.rb +121 -3
- 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: c453170a8a782786ce0a1061623c8de93ed75536228bb99d9b6c1857163f58f3
|
|
4
|
+
data.tar.gz: 19c5dc1e01e47508e51dfc87add4cd24059b4035b83ed45c5aa98d46d4edcb8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26fbc91c4d39c834e0e9a19c443501953b487210ea88922be813bf167dfb56c6d2cc55cb3238441f58a08a6cd7511560e495f787a05a566466e9e7c5f45b06b0
|
|
7
|
+
data.tar.gz: 79f6d211068cf179e2fee6eea24e2e0ae60385be826db2254dcb6ce3d3aab345d68c743f529d430b242f8ea0cc3fdda7425e6cf880cf984abf1b7aeec145d74c
|
|
@@ -23,6 +23,16 @@ module Ea
|
|
|
23
23
|
set_stored_ray_origins(model, source, target, geom)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def mark_default_square_ray(model, link_row, geom, context)
|
|
27
|
+
style = model.style.to_h
|
|
28
|
+
return model if context || !link_row.path.to_s.empty? || model.waypoints.size != 2
|
|
29
|
+
return model unless style[:mode] == "3" && style[:tree] == "OS" && style[:lwidth] == "2"
|
|
30
|
+
return model unless geom[:sx].nil? && geom[:sy].nil? && [geom[:ex], geom[:ey]] == [0, 0]
|
|
31
|
+
|
|
32
|
+
model.style = style.merge(default_square_ray: true)
|
|
33
|
+
model
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
# A Mode 2 Generalization whose stored Path is a horizontal jog
|
|
27
37
|
# between vertically separated boxes that overlap in x is EA's
|
|
28
38
|
# auto-route: one vertical ray at the overlap midpoint plus SX.
|
|
@@ -100,40 +110,108 @@ module Ea
|
|
|
100
110
|
return model unless stored_ray_candidate?(model, link_row, geom)
|
|
101
111
|
|
|
102
112
|
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
103
|
-
|
|
113
|
+
allowed = if normalized_port_anchors?(model)
|
|
114
|
+
normalized_level_ray?(model, source, target, geom)
|
|
115
|
+
else
|
|
116
|
+
stored_ray_gap?(source, target, model.style[:mode], geom[:edge])
|
|
117
|
+
end
|
|
118
|
+
return model unless allowed
|
|
104
119
|
|
|
105
120
|
set_stored_ray_origins(model, source, target, geom)
|
|
106
121
|
end
|
|
107
122
|
|
|
108
|
-
# An empty-Path Mode 3 Association
|
|
109
|
-
#
|
|
110
|
-
#
|
|
123
|
+
# An empty-Path Mode 3 Association follows the line between its
|
|
124
|
+
# stored endpoint origins; an omitted offset defaults to the center.
|
|
125
|
+
# The target origin must lie inside its own stroked outline.
|
|
111
126
|
def association_stored_ray(model, link_row, diagram_row, geom, context)
|
|
112
127
|
return model if context
|
|
113
128
|
return model unless association_ray_candidate?(model, link_row)
|
|
114
129
|
|
|
115
130
|
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
116
|
-
|
|
131
|
+
width = association_outline_width(model, link_row, diagram_row)
|
|
132
|
+
origins = association_ray_origins(source, target, geom, width)
|
|
117
133
|
return model unless origins
|
|
118
134
|
|
|
119
|
-
model.waypoints =
|
|
120
|
-
Ea::Model::Waypoint.new(position: point)
|
|
121
|
-
end
|
|
135
|
+
model.waypoints = port_waypoints(model, origins)
|
|
122
136
|
model.source_anchor, model.target_anchor = origins
|
|
137
|
+
model.style = model.style.to_h.merge(stored_association_ray: true, association_ray_outline: true)
|
|
138
|
+
if geom[:edge] == 1 && !inside_box?(origins.first, grown_bounds(source, width == "2" ? 1 : 0))
|
|
139
|
+
model.style[:stored_top_dock] = true
|
|
140
|
+
end
|
|
123
141
|
model
|
|
124
142
|
end
|
|
125
143
|
|
|
126
|
-
def
|
|
144
|
+
def port_waypoints(model, origins)
|
|
145
|
+
[model.source_port || origins.first, model.target_port || origins.last].map do |point|
|
|
146
|
+
Ea::Model::Waypoint.new(position: point)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Width-2 boxes also own their 1px outer stroke, so a stored
|
|
151
|
+
# origin on that outline is still inside its box.
|
|
152
|
+
def association_ray_origins(source, target, geom, width)
|
|
153
|
+
outline = width == "2" ? 1 : 0
|
|
127
154
|
origins = [stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
128
155
|
stored_ray_origin(target, geom[:ex], geom[:ey])]
|
|
129
|
-
|
|
130
|
-
origins
|
|
156
|
+
boxes = [source, target].map { |box| grown_bounds(box, outline) }
|
|
157
|
+
return unless origins.first != origins.last && association_origin_pair?(origins, boxes, width, geom[:edge])
|
|
158
|
+
|
|
159
|
+
origins if width == "2" || sub_pixel_safe_origins?(origins, [source, target] + boxes)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# An external source origin is a virtual point on the saved ray.
|
|
163
|
+
# It is usable only when that segment actually crosses the source.
|
|
164
|
+
def association_origin_pair?(origins, boxes, width, edge)
|
|
165
|
+
return false unless inside_box?(origins.last, boxes.last)
|
|
166
|
+
return true if inside_box?(origins.first, boxes.first)
|
|
167
|
+
|
|
168
|
+
return segment_crosses_box?(origins.first, origins.last, boxes.first) if width == "2"
|
|
169
|
+
return false unless edge == 1
|
|
170
|
+
|
|
171
|
+
source = boxes.first
|
|
172
|
+
top = Ea::Model::Bounds.new(x: source.x, y: source.y, width: source.width, height: 0)
|
|
173
|
+
segment_crosses_box?(origins.first, origins.last, top)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def segment_crosses_box?(first, last, box)
|
|
177
|
+
interval = [0.0, 1.0]
|
|
178
|
+
axes = [[first.x, last.x, box.x, box.x + box.width],
|
|
179
|
+
[first.y, last.y, box.y, box.y + box.height]]
|
|
180
|
+
axes.all? do |origin, finish, low, high|
|
|
181
|
+
if origin == finish
|
|
182
|
+
origin.between?(low, high)
|
|
183
|
+
else
|
|
184
|
+
times = [(low - origin).to_f / (finish - origin),
|
|
185
|
+
(high - origin).to_f / (finish - origin)].minmax
|
|
186
|
+
interval = [[interval.first, times.first].max, [interval.last, times.last].min]
|
|
187
|
+
interval.first <= interval.last
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def grown_bounds(bounds, amount)
|
|
193
|
+
Ea::Model::Bounds.new(x: bounds.x - amount, y: bounds.y - amount,
|
|
194
|
+
width: bounds.width + 2 * amount,
|
|
195
|
+
height: bounds.height + 2 * amount)
|
|
131
196
|
end
|
|
132
197
|
|
|
133
198
|
def association_ray_candidate?(model, link_row)
|
|
134
199
|
model.connector_type == "Association" && link_row.path.to_s.empty? &&
|
|
135
|
-
model.
|
|
136
|
-
association_ray_style?(model.style.to_h)
|
|
200
|
+
model.source_anchor && model.target_anchor &&
|
|
201
|
+
(model.source_port || model.target_port) && association_ray_style?(model.style.to_h)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A thin connector still meets the outer stroke of explicitly
|
|
205
|
+
# bordered elements. Ambiguous placements must agree on that border.
|
|
206
|
+
def association_outline_width(model, link_row, diagram_row)
|
|
207
|
+
width = model.style[:lwidth]
|
|
208
|
+
return width if width == "2"
|
|
209
|
+
|
|
210
|
+
connector = database.find_connector(link_row.connectorid)
|
|
211
|
+
ids = [connector.start_object_id, connector.end_object_id]
|
|
212
|
+
placements = database.diagram_objects_for(diagram_row.diagram_id)
|
|
213
|
+
borders = placements.select { |placement| ids.include?(placement.ea_object_id) }
|
|
214
|
+
borders.all? { |placement| DiagramStyleParser.parse(placement.objectstyle)[:lwth] == "2" } ? "2" : width
|
|
137
215
|
end
|
|
138
216
|
|
|
139
217
|
def association_ray_style?(style)
|
|
@@ -141,15 +219,13 @@ module Ea
|
|
|
141
219
|
%i[direct regenerated].none? { |key| style.key?(key) }
|
|
142
220
|
end
|
|
143
221
|
|
|
144
|
-
#
|
|
145
|
-
# half pixel. An origin on its border then clips differently
|
|
222
|
+
# Width-0 origins are whole pixels while EA's box centers may sit
|
|
223
|
+
# on a half pixel. An origin on its border then clips differently
|
|
146
224
|
# unless the line is axis-aligned, which no rounding changes.
|
|
147
225
|
def sub_pixel_safe_origins?(origins, boxes)
|
|
148
226
|
first, last = origins
|
|
149
|
-
return false if first == last
|
|
150
|
-
|
|
151
227
|
axis_aligned = first.x == last.x || first.y == last.y
|
|
152
|
-
axis_aligned || origins.
|
|
228
|
+
axis_aligned || origins.none? { |point| boxes.any? { |box| on_border?(point, box) } }
|
|
153
229
|
end
|
|
154
230
|
|
|
155
231
|
def on_border?(point, bounds)
|
|
@@ -248,7 +324,21 @@ module Ea
|
|
|
248
324
|
def stored_ray_candidate?(model, link_row, geom)
|
|
249
325
|
%w[Generalization Aggregation].include?(model.connector_type) &&
|
|
250
326
|
model.waypoints.size == 2 && stored_ray_style?(model.style, link_row) &&
|
|
251
|
-
stored_ray_geometry?(model, geom)
|
|
327
|
+
stored_ray_geometry?(model, geom)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Boundary-port rounding can move a computed jog off a level saved
|
|
331
|
+
# ray. Equal stored origin rows specify the line before clipping.
|
|
332
|
+
def normalized_level_ray?(model, source, target, geom)
|
|
333
|
+
style = model.style.to_h
|
|
334
|
+
return false unless style[:mode] == "3" && style[:lwidth] == "2" &&
|
|
335
|
+
horizontal_ray_gap?(source, target)
|
|
336
|
+
|
|
337
|
+
origins = [stored_ray_origin(source, geom[:sx], geom[:sy]),
|
|
338
|
+
stored_ray_origin(target, geom[:ex], geom[:ey])]
|
|
339
|
+
boxes = [source, target].map { |box| grown_bounds(box, 1) }
|
|
340
|
+
origins.first.y == origins.last.y &&
|
|
341
|
+
origins.zip(boxes).all? { |origin, box| inside_box?(origin, box) }
|
|
252
342
|
end
|
|
253
343
|
|
|
254
344
|
def stored_ray_style?(style, link_row)
|
|
@@ -352,6 +442,14 @@ module Ea
|
|
|
352
442
|
route
|
|
353
443
|
end
|
|
354
444
|
|
|
445
|
+
# One stored corner that is level with both boxes already is the
|
|
446
|
+
# route (vertical leg out of the source, horizontal leg into the
|
|
447
|
+
# target); flattening it to the facing edges would lose it.
|
|
448
|
+
def level_corner_route?(points, link_row, connector, src, tgt)
|
|
449
|
+
points.size == 3 && level_entry_style?(link_row, connector) &&
|
|
450
|
+
!level_entry_point(points[1], src).nil? && !level_entry_point(points[1], tgt).nil?
|
|
451
|
+
end
|
|
452
|
+
|
|
355
453
|
def level_entry_style?(link_row, connector)
|
|
356
454
|
style = DiagramStyleParser.parse(link_row.style)
|
|
357
455
|
%w[Generalization Aggregation].include?(connector.connector_type) &&
|
|
@@ -419,6 +517,62 @@ module Ea
|
|
|
419
517
|
[bounds.y, bounds.y + bounds.height].include?(point[1])
|
|
420
518
|
end
|
|
421
519
|
|
|
520
|
+
# A width-2 Mode 3 Aggregation without a tree style keeps an authored
|
|
521
|
+
# orthogonal stair-step of three or more corners. Each end docks on
|
|
522
|
+
# the 1px outline its adjacent leg points at, from the corner's own
|
|
523
|
+
# coordinate; any end that cannot do so leaves the route unchanged.
|
|
524
|
+
def stored_stair_route(link_row, connector, source, target)
|
|
525
|
+
return nil unless stair_style?(link_row, connector)
|
|
526
|
+
|
|
527
|
+
corners = stored_square_corners(link_row)
|
|
528
|
+
return nil unless corners && corners.size >= 3 && alternating_legs?(corners)
|
|
529
|
+
|
|
530
|
+
first = outline_dock(corners[0], corners[1], source)
|
|
531
|
+
last = outline_dock(corners[-1], corners[-2], target)
|
|
532
|
+
[first, *corners, last] if first && last
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def stair_style?(link_row, connector)
|
|
536
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
537
|
+
connector.connector_type == "Aggregation" && style[:mode] == "3" &&
|
|
538
|
+
style[:lwidth] == "2" && !style.key?(:tree)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Every leg is non-zero and runs along the opposite axis of the last.
|
|
542
|
+
def alternating_legs?(corners)
|
|
543
|
+
axes = corners.each_cons(2).map { |from, to| leg_axis(from, to) }
|
|
544
|
+
axes.none?(&:nil?) && axes.each_cons(2).all? { |a, b| a != b }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def leg_axis(from, to)
|
|
548
|
+
return :vertical if from[0] == to[0] && from[1] != to[1]
|
|
549
|
+
|
|
550
|
+
:horizontal if from[1] == to[1] && from[0] != to[0]
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# The dock runs perpendicular to the corner's own leg.
|
|
554
|
+
def outline_dock(corner, neighbour, box)
|
|
555
|
+
if corner[1] == neighbour[1]
|
|
556
|
+
vertical_outline_dock(corner, box)
|
|
557
|
+
else
|
|
558
|
+
horizontal_outline_dock(corner, box)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def vertical_outline_dock(corner, box)
|
|
563
|
+
x, y = corner
|
|
564
|
+
return nil unless x.between?(box.x, box.x + box.width) && !within_height?(y, box)
|
|
565
|
+
|
|
566
|
+
[x, y < box.y ? box.y - 1 : box.y + box.height + 1]
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def horizontal_outline_dock(corner, box)
|
|
570
|
+
x, y = corner
|
|
571
|
+
return nil unless within_height?(y, box) && !x.between?(box.x, box.x + box.width)
|
|
572
|
+
|
|
573
|
+
[x < box.x ? box.x - 1 : box.x + box.width + 1, y]
|
|
574
|
+
end
|
|
575
|
+
|
|
422
576
|
# A valid lateral corner joins one vertical leg and one horizontal
|
|
423
577
|
# leg. Keep it even when the boxes also admit a shorter direct route.
|
|
424
578
|
# ported: both ends carry explicit SX/SY and EX/EY offsets. An
|
|
@@ -433,6 +587,66 @@ module Ea
|
|
|
433
587
|
corner && lateral_corner_route(style, corner, source, target)
|
|
434
588
|
end
|
|
435
589
|
|
|
590
|
+
# A horizontal lateral tree with no usable stored corner leaves
|
|
591
|
+
# the source's top or bottom edge at its center x, turns at the
|
|
592
|
+
# target's center y and enters the target's facing side edge.
|
|
593
|
+
# When that y lies inside the source's height the route is one
|
|
594
|
+
# horizontal line between the facing side edges. Every dock is
|
|
595
|
+
# flush on its box edge.
|
|
596
|
+
def derived_lateral_route(link_row, connector, source, target)
|
|
597
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
598
|
+
return nil unless style[:mode] == "3" && style[:tree] == "LH" &&
|
|
599
|
+
derived_lateral_path?(connector.connector_type, link_row.path.to_s)
|
|
600
|
+
|
|
601
|
+
x = source.x + source.width / 2
|
|
602
|
+
y = target.y + target.height / 2
|
|
603
|
+
edge = lateral_target_edge(x, target)
|
|
604
|
+
return nil unless edge
|
|
605
|
+
|
|
606
|
+
if within_height?(y, source)
|
|
607
|
+
side = target.x > x ? source.x + source.width : source.x
|
|
608
|
+
[[side, y], [edge, y]] if ((edge - side) * (edge - x)).positive?
|
|
609
|
+
else
|
|
610
|
+
[[x, y < source.y ? source.y : source.y + source.height], [x, y], [edge, y]]
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# An Association may carry one stored corner that no valid lateral
|
|
615
|
+
# route accepted; a Generalization derives only from an empty Path.
|
|
616
|
+
def derived_lateral_path?(connector_type, path)
|
|
617
|
+
case connector_type
|
|
618
|
+
when "Association" then path.match?(/\A(?:-?\d+:-?\d+;?)?\z/)
|
|
619
|
+
when "Generalization" then path.empty?
|
|
620
|
+
else false
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# A derived level line docks flush at its own two ends; anchoring
|
|
625
|
+
# the ray there keeps the emitter's clip from moving them.
|
|
626
|
+
def pin_derived_lateral_line(model, link_row, diagram_row, context)
|
|
627
|
+
style = model.style.to_h
|
|
628
|
+
return model if context || model.waypoints.size != 2 ||
|
|
629
|
+
style[:mode] != "3" || style[:tree] != "LH"
|
|
630
|
+
|
|
631
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
632
|
+
connector = database.find_connector(link_row.connectorid)
|
|
633
|
+
line = connector && derived_lateral_route(link_row, connector, source, target)
|
|
634
|
+
return model unless line && line.size == 2 &&
|
|
635
|
+
line == model.waypoints.map { |wp| [wp.position.x, wp.position.y] }
|
|
636
|
+
|
|
637
|
+
model.source_anchor, model.target_anchor = line.map { |x, y| Ea::Model::Point.new(x: x, y: y) }
|
|
638
|
+
model.style = style.merge(derived_lateral_line: true)
|
|
639
|
+
model
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# The target side edge a vertical leg at x faces, or nil when x
|
|
643
|
+
# lies within the target's span.
|
|
644
|
+
def lateral_target_edge(x_position, target)
|
|
645
|
+
return target.x if x_position < target.x
|
|
646
|
+
|
|
647
|
+
target.x + target.width if x_position > target.x + target.width
|
|
648
|
+
end
|
|
649
|
+
|
|
436
650
|
# An untyped Mode 3 Association corner takes whichever leg
|
|
437
651
|
# orientation its position fits; width-2 docks sit on the 1px
|
|
438
652
|
# outer outline.
|
|
@@ -613,12 +827,44 @@ module Ea
|
|
|
613
827
|
model
|
|
614
828
|
end
|
|
615
829
|
|
|
830
|
+
# A two-waypoint Mode 3 Aggregation with only a target port draws the
|
|
831
|
+
# line from the source center to the target's stored ray origin
|
|
832
|
+
# (center + EX, center - EY), clipped at the outlines; the boundary
|
|
833
|
+
# port is only the derived crossing of that origin's ray.
|
|
834
|
+
def correct_partial_port_origins(model, link_row, diagram_row, geom, context)
|
|
835
|
+
return model if context || !partial_port_origin_candidate?(model, link_row)
|
|
836
|
+
|
|
837
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
838
|
+
return model unless horizontal_ray_gap?(source, target)
|
|
839
|
+
|
|
840
|
+
model.source_anchor = stored_ray_origin(source, 0, 0)
|
|
841
|
+
model.target_anchor = stored_ray_origin(target, geom[:ex], geom[:ey])
|
|
842
|
+
model
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def partial_port_origin_candidate?(model, link_row)
|
|
846
|
+
style = model.style.to_h
|
|
847
|
+
model.connector_type == "Aggregation" && model.waypoints.size == 2 &&
|
|
848
|
+
link_row.path.to_s.empty? && style[:mode] == "3" && style[:lwidth] == "2" &&
|
|
849
|
+
!style.key?(:tree) &&
|
|
850
|
+
model.source_port.nil? && model.target_port
|
|
851
|
+
end
|
|
852
|
+
|
|
616
853
|
def partial_port_jog_candidate?(model, link_row)
|
|
617
854
|
style = model.style.to_h
|
|
618
855
|
model.connector_type == "Aggregation" &&
|
|
619
|
-
link_row.path.to_s.empty? && style[:mode] == "3" &&
|
|
620
|
-
!style.key?(:tree) && model.
|
|
621
|
-
model
|
|
856
|
+
link_row.path.to_s.empty? && style[:mode] == "3" &&
|
|
857
|
+
!style.key?(:tree) && model.waypoints.size == 2 &&
|
|
858
|
+
explicit_port_side?(model, style[:lwidth])
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Width 2 keeps the evidenced target-only port; width 0 accepts a
|
|
862
|
+
# single explicit port at either end.
|
|
863
|
+
def explicit_port_side?(model, width)
|
|
864
|
+
ports = [model.source_port, model.target_port]
|
|
865
|
+
return model.source_port.nil? && !model.target_port.nil? if width == "2"
|
|
866
|
+
|
|
867
|
+
width == "0" && ports.compact.size == 1
|
|
622
868
|
end
|
|
623
869
|
|
|
624
870
|
def computed_jog_candidate?(model, link_row, geom)
|
|
@@ -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.
|
|
@@ -422,11 +430,14 @@ module Ea
|
|
|
422
430
|
is_substitutable: substitutable_connector?(connector)
|
|
423
431
|
), link_row, diagram_row, geom)
|
|
424
432
|
model = collapse_partial_port_jog(model, link_row, diagram_row)
|
|
433
|
+
model = correct_partial_port_origins(model, link_row, diagram_row, geom, context)
|
|
425
434
|
model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
|
|
435
|
+
model = pin_derived_lateral_line(model, link_row, diagram_row, context)
|
|
426
436
|
model = mode2_overlap_ray(model, link_row, diagram_row, geom, context)
|
|
427
437
|
model = association_stored_ray(model, link_row, diagram_row, geom, context)
|
|
428
438
|
model = correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
429
|
-
overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
439
|
+
model = overlap_ray_origins(model, link_row, diagram_row, geom, context)
|
|
440
|
+
mark_default_square_ray(model, link_row, geom, context)
|
|
430
441
|
end
|
|
431
442
|
|
|
432
443
|
# Ray origin for one connector end: the element's center
|
|
@@ -666,15 +677,17 @@ module Ea
|
|
|
666
677
|
src = bounds_from_rect(source_placement)
|
|
667
678
|
tgt = bounds_from_rect(target_placement)
|
|
668
679
|
tree_route = stored_square_route(link_row, connector, src, tgt) ||
|
|
680
|
+
stored_stair_route(link_row, connector, src, tgt) ||
|
|
669
681
|
stored_lateral_route(link_row, connector, src, tgt,
|
|
670
682
|
ported: source_port && target_port) ||
|
|
683
|
+
derived_lateral_route(link_row, connector, src, tgt) ||
|
|
671
684
|
horizontal_tree_route(points, link_row, connector, src, tgt)
|
|
672
685
|
if tree_route
|
|
673
686
|
points = tree_route
|
|
674
687
|
elsif %w[Association Aggregation].include?(connector.connector_type) &&
|
|
675
688
|
edge_out != 2
|
|
676
689
|
stored_tree = preserve_stored_tree?(points, link_row, connector, src, tgt)
|
|
677
|
-
unless stored_tree
|
|
690
|
+
unless stored_tree || level_corner_route?(points, link_row, connector, src, tgt)
|
|
678
691
|
points = facing_edge_route(points, src, tgt)
|
|
679
692
|
end
|
|
680
693
|
# A width-2 Aggregation that keeps its stored bends is
|
|
@@ -39,10 +39,16 @@ 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
|
+
pairs = default_square_pairs(connector, points, source_bounds, target_bounds, original)
|
|
50
|
+
return stored_top_pairs(connector, source_bounds, pairs)
|
|
51
|
+
end
|
|
46
52
|
|
|
47
53
|
# EA docks these markers on the outer edge of the 2px box stroke.
|
|
48
54
|
outline = bounds.map do |box|
|
|
@@ -50,8 +56,70 @@ module Ea
|
|
|
50
56
|
width: box.width + 2, height: box.height + 2)
|
|
51
57
|
end
|
|
52
58
|
candidate = clipped_pairs(outline, anchors)
|
|
53
|
-
forward_route?(original, candidate)
|
|
59
|
+
expanded = forward_route?(original, candidate)
|
|
60
|
+
pairs = expanded ? candidate : original
|
|
61
|
+
target = expanded ? outline.last : target_bounds
|
|
62
|
+
pairs = default_square_pairs(connector, points, source_bounds, target, pairs)
|
|
63
|
+
stored_top_pairs(connector, expanded ? outline.first : source_bounds, pairs)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# EDGE=1 explicitly seats an external source ray on the top edge;
|
|
67
|
+
# its first contact can instead be the bottom when its origin is below.
|
|
68
|
+
def stored_top_pairs(connector, source, pairs)
|
|
69
|
+
return pairs unless connector.connector_type == "Association" &&
|
|
70
|
+
connector.style.to_h[:stored_top_dock] == true
|
|
71
|
+
|
|
72
|
+
first = connector.source_anchor
|
|
73
|
+
last = connector.target_anchor
|
|
74
|
+
return pairs unless first && last && first.y != last.y
|
|
75
|
+
|
|
76
|
+
ratio = (source.y - first.y).to_f / (last.y - first.y)
|
|
77
|
+
x_position = first.x + ratio * (last.x - first.x)
|
|
78
|
+
return pairs unless ratio.between?(0, 1) &&
|
|
79
|
+
x_position.between?(source.x, source.x + source.width)
|
|
80
|
+
|
|
81
|
+
proposed = [[x_position.floor, source.y], pairs.last]
|
|
82
|
+
forward_route?(pairs, proposed) ? proposed : pairs
|
|
83
|
+
end
|
|
84
|
+
private_class_method :stored_top_pairs
|
|
85
|
+
|
|
86
|
+
# A square ray with an omitted source offset keeps its chosen side
|
|
87
|
+
# edge and projects that end to the clipped target's horizontal row.
|
|
88
|
+
def default_square_pairs(connector, points, source, target, pairs)
|
|
89
|
+
return pairs unless connector.style.to_h[:default_square_ray] == true
|
|
90
|
+
return pairs unless points.first[1] == points.last[1] &&
|
|
91
|
+
on_side_edge?(points.first, source) && on_side_edge?(pairs.last, target)
|
|
92
|
+
|
|
93
|
+
first = [points.first[0], pairs.last[1]]
|
|
94
|
+
return pairs unless on_side_edge?(first, source)
|
|
95
|
+
|
|
96
|
+
proposed = [first, pairs.last]
|
|
97
|
+
forward_route?(points, proposed) ? proposed : pairs
|
|
98
|
+
end
|
|
99
|
+
private_class_method :default_square_pairs
|
|
100
|
+
|
|
101
|
+
# A horizontal lateral tree the QEA builder derived as one level
|
|
102
|
+
# line (internal style flag) keeps it between the facing side edges.
|
|
103
|
+
def flush_lateral_line?(connector, points, source, target)
|
|
104
|
+
style = connector.style.to_h
|
|
105
|
+
return false unless style[:derived_lateral_line] && style[:mode] == "3" &&
|
|
106
|
+
style[:tree] == "LH" && points.size == 2 &&
|
|
107
|
+
%w[Association Generalization].include?(connector.connector_type)
|
|
108
|
+
return false unless connector.source_anchor && connector.target_anchor
|
|
109
|
+
|
|
110
|
+
first, last = points
|
|
111
|
+
pinned = [[connector.source_anchor.x, connector.source_anchor.y],
|
|
112
|
+
[connector.target_anchor.x, connector.target_anchor.y]]
|
|
113
|
+
pinned == [first, last] && first[1] == last[1] &&
|
|
114
|
+
on_side_edge?(first, source) && on_side_edge?(last, target)
|
|
54
115
|
end
|
|
116
|
+
private_class_method :flush_lateral_line?
|
|
117
|
+
|
|
118
|
+
def on_side_edge?(point, bounds)
|
|
119
|
+
[bounds.x, bounds.x + bounds.width].include?(point[0]) &&
|
|
120
|
+
point[1].between?(bounds.y, bounds.y + bounds.height)
|
|
121
|
+
end
|
|
122
|
+
private_class_method :on_side_edge?
|
|
55
123
|
|
|
56
124
|
def lateral_tree?(connector, points)
|
|
57
125
|
style = connector.style.to_h
|
|
@@ -97,7 +165,7 @@ module Ea
|
|
|
97
165
|
private_class_method :facing_coordinate
|
|
98
166
|
|
|
99
167
|
def stroke_outline?(connector, bounds)
|
|
100
|
-
connector.style.to_h[:lwidth] == "2" &&
|
|
168
|
+
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)) &&
|
|
101
169
|
bounds.all? { |box| box.width.positive? && box.height.positive? } &&
|
|
102
170
|
outline_connector?(connector, bounds)
|
|
103
171
|
end
|
|
@@ -111,9 +179,42 @@ module Ea
|
|
|
111
179
|
end
|
|
112
180
|
private_class_method :overlap_ray?
|
|
113
181
|
|
|
182
|
+
# The QEA builder flags a model relationship drawn without any
|
|
183
|
+
# diagram link row; EA strokes it at the 2px box outline.
|
|
184
|
+
def default_model_connector?(connector)
|
|
185
|
+
connector.style.to_h[:phantom_outline] == true && !connector.has_geometry_offsets &&
|
|
186
|
+
%w[Aggregation Association].include?(connector.connector_type)
|
|
187
|
+
end
|
|
188
|
+
private_class_method :default_model_connector?
|
|
189
|
+
|
|
190
|
+
# A thin straight Generalization/Aggregation/Association between two elements
|
|
191
|
+
# drawn with a 2px border stops on the outer edge of those borders.
|
|
192
|
+
def bordered_outline?(connector, diagram, refs, bounds)
|
|
193
|
+
style = connector.style.to_h
|
|
194
|
+
return false unless style[:lwidth] == "0" && !style[:tree]
|
|
195
|
+
return false unless %w[Generalization Aggregation Association].include?(connector.connector_type)
|
|
196
|
+
return false unless bounds.all? { |box| box.width.positive? && box.height.positive? }
|
|
197
|
+
|
|
198
|
+
ends = [[refs.first, style[:soid]], [refs.last, style[:eoid]]]
|
|
199
|
+
ends.all? { |ref, duid| border_two?(diagram, ref, duid) }
|
|
200
|
+
end
|
|
201
|
+
private_class_method :bordered_outline?
|
|
202
|
+
|
|
203
|
+
# Every placement of the element (the DUID-matched one when the
|
|
204
|
+
# connector names it) must declare a border width of 2.
|
|
205
|
+
def border_two?(diagram, ref, duid)
|
|
206
|
+
placed = Array(diagram&.elements).select { |el| el.model_element_ref == ref }
|
|
207
|
+
named = placed.select { |el| duid && el.duid.to_s.casecmp?(duid.to_s) }
|
|
208
|
+
placed = named unless named.empty?
|
|
209
|
+
placed.any? && placed.all? { |el| el.line_width == 2 || el.style.to_h[:lwth] == "2" }
|
|
210
|
+
end
|
|
211
|
+
private_class_method :border_two?
|
|
212
|
+
|
|
114
213
|
def outline_connector?(connector, bounds)
|
|
214
|
+
return true if default_model_connector?(connector)
|
|
115
215
|
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
116
216
|
|
|
217
|
+
return true if association_ray_outline?(connector)
|
|
117
218
|
return false unless stored_association?(connector) || offset_free_association?(connector)
|
|
118
219
|
|
|
119
220
|
# Diagonal Association rounding and tree docks keep their existing rules.
|
|
@@ -122,6 +223,15 @@ module Ea
|
|
|
122
223
|
end
|
|
123
224
|
private_class_method :outline_connector?
|
|
124
225
|
|
|
226
|
+
# The QEA builder marks the stored Association rays it produced;
|
|
227
|
+
# their diagonal docks sit on the outer stroke edge like the
|
|
228
|
+
# axis-aligned ones. Unmarked diagonals keep the legacy docking.
|
|
229
|
+
def association_ray_outline?(connector)
|
|
230
|
+
connector.connector_type == "Association" &&
|
|
231
|
+
connector.style.to_h[:association_ray_outline] == true
|
|
232
|
+
end
|
|
233
|
+
private_class_method :association_ray_outline?
|
|
234
|
+
|
|
125
235
|
def stored_association?(connector)
|
|
126
236
|
style = connector.style.to_h
|
|
127
237
|
connector.connector_type == "Association" && connector.has_geometry_offsets &&
|
|
@@ -244,12 +354,20 @@ module Ea
|
|
|
244
354
|
# anchor row is one explicit horizontal line: EA seats neither end
|
|
245
355
|
# in a compartment row, unlike Generalization/Aggregation targets.
|
|
246
356
|
def explicit_association_ray?(pairs, connector)
|
|
357
|
+
produced = connector.style.to_h[:stored_association_ray] == true
|
|
358
|
+
return true if produced && pairs.first[1] == pairs.last[1]
|
|
359
|
+
|
|
360
|
+
certified_association_ray?(pairs, connector)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def certified_association_ray?(pairs, connector)
|
|
247
364
|
return false unless explicit_horizontal_ray?(connector, %w[Association])
|
|
248
365
|
|
|
249
366
|
connector.source_anchor.y == connector.target_anchor.y &&
|
|
250
367
|
connector.source_port.y == connector.source_anchor.y &&
|
|
251
368
|
pairs.first[1] == pairs.last[1]
|
|
252
369
|
end
|
|
370
|
+
private_class_method :certified_association_ray?
|
|
253
371
|
private_class_method :explicit_association_ray?
|
|
254
372
|
|
|
255
373
|
def explicit_horizontal_ray?(connector, types = %w[Generalization Aggregation])
|
data/lib/ea/version.rb
CHANGED