ea 0.6.99 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b9e7581bab2de7d7c5766a5f141c99885bb59cbb7ad974c313c26e86bfb96a2
4
- data.tar.gz: 8933e36fb2422953eddb1d60069664e39177547beaf2ec17ed17c5b9f3aa4eac
3
+ metadata.gz: 8f60cf58bb521e911eb26fb30b628162e149a9521027fae0e77756ef1b8354dd
4
+ data.tar.gz: c0580ced4122e41650fbc5053fb0fb534a40c8cae84f758039dd8b2f575422ad
5
5
  SHA512:
6
- metadata.gz: ab6882700d57207a5fd2c1b27fa0befe5f287bb61f13387e99dc8f4429dbede75320d9fb6124a126094455d38d4dff7a41dbfc258633609c6695c5dcd216b36d
7
- data.tar.gz: 82cd636abfe8c20a03dc8f6c5410192c65e674093eaaabc081e8fc84b7ec6544a87f3c87ff255f90fcaa1bc0f7704fdf1b2e46f344cf9288cd51ec4a7e8cfa97
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),
@@ -134,7 +312,7 @@ module Ea
134
312
 
135
313
  case connector.connector_type
136
314
  when "Aggregation"
137
- return if geometry_has_offsets?(link_row.geometry)
315
+ return unless style[:lwidth] == "0" || !geometry_has_offsets?(link_row.geometry)
138
316
 
139
317
  redock_child_below(points, bounds_from_rect(source_placement), bounds_from_rect(target_placement))
140
318
  when "Association"
@@ -334,14 +512,13 @@ module Ea
334
512
  # Keep a vertical tree trunk whose bends still fit both boxes
335
513
  # and the open gap; the closest facing edges need not share its x.
336
514
  def preserve_stored_tree?(points, link_row, connector, src, tgt)
337
- return false unless connector.connector_type == "Aggregation" && points.size == 4
515
+ return false unless points.size == 4 && stored_tree_type?(points, link_row, connector)
338
516
 
339
517
  style = DiagramStyleParser.parse(link_row.style)
340
518
  return false unless style[:lwidth] == "2" || flush_width_zero_tree?(link_row)
341
- return false unless style[:tree] == "V" ||
342
- (style[:mode] == "3" && style[:tree].nil?)
519
+ return false unless vertical_tree_style?(style)
343
520
 
344
- 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))
345
522
  end
346
523
 
347
524
  # A width-2 Mode 3 horizontal tree keeps its stored vertical trunk
@@ -412,6 +589,38 @@ module Ea
412
589
  model
413
590
  end
414
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
+
415
624
  def computed_jog_candidate?(model, link_row, geom)
416
625
  style = model.style.to_h
417
626
  model.connector_type == "Generalization" && geom[:edge] == 2 &&
@@ -438,14 +647,32 @@ module Ea
438
647
  style[:mode] == "3" && style[:tree] == "V" && style[:lwidth] == "0"
439
648
  end
440
649
 
441
- def tree_bends_fit?(first, last, source, target)
442
- first[0] != last[0] && first[1] == last[1] &&
443
- within_vertical_gap?(first[1], source, target) &&
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) &&
444
669
  within_horizontal_span?(first, source) && within_horizontal_span?(last, target)
445
670
  end
446
671
 
447
- def within_vertical_gap?(height, source, target)
448
- 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
449
676
  end
450
677
 
451
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
- correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
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
@@ -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
- def explicit_horizontal_ray?(connector)
235
- explicit_horizontal_ray_style?(connector) && connector.has_geometry_offsets &&
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
- %w[Generalization Aggregation].include?(connector.connector_type) &&
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?
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.99"
7
+ VERSION = "0.6.100"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.99
4
+ version: 0.6.100
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.