ea 0.6.98 → 0.6.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3c13e631927ad57c6168b6d7c02eaed8e933c89ad7dfcd0a055fb6b8a69e8e1
4
- data.tar.gz: 3edb35912780be1170fce402dd141823f4c5e44c92a6f84451fa136d3eb026a8
3
+ metadata.gz: 4b9e7581bab2de7d7c5766a5f141c99885bb59cbb7ad974c313c26e86bfb96a2
4
+ data.tar.gz: 8933e36fb2422953eddb1d60069664e39177547beaf2ec17ed17c5b9f3aa4eac
5
5
  SHA512:
6
- metadata.gz: 6544102e41ff1bde04737e18f93de5ea0ee657c47335f7eeb12316162ca33fbf2e0d5b0dc1572fccc5174373781b1368508d606641286c88ebe2d7f9f2232079
7
- data.tar.gz: eb3cb4702ea44ba8759ab9774a1ca445684b9d6ce7043153b4823657c191fff45c6db267ee74b1ed20cdec2064248b60a0f5112827b92edfa4aaecaa495fa093
6
+ metadata.gz: ab6882700d57207a5fd2c1b27fa0befe5f287bb61f13387e99dc8f4429dbede75320d9fb6124a126094455d38d4dff7a41dbfc258633609c6695c5dcd216b36d
7
+ data.tar.gz: 82cd636abfe8c20a03dc8f6c5410192c65e674093eaaabc081e8fc84b7ec6544a87f3c87ff255f90fcaa1bc0f7704fdf1b2e46f344cf9288cd51ec4a7e8cfa97
@@ -115,6 +115,85 @@ module Ea
115
115
  y: bounds.y + bounds.height / 2 - y_offset.to_i)
116
116
  end
117
117
 
118
+ # A vertical tree stores its trunk bend twice when the parent sits
119
+ # directly above; EA draws that as one bend.
120
+ def drop_zero_length_tree_segments(points, link_row)
121
+ return points unless points.size == 4 && points[1] == points[2] &&
122
+ DiagramStyleParser.parse(link_row.style)[:tree] == "V"
123
+
124
+ points.values_at(0, 1, 3)
125
+ end
126
+
127
+ # A stored vertical tree trunk docks both ends on the box edges that
128
+ # face its bends, at the adjacent bend's x: Aggregations without
129
+ # geometry offsets whose child sits below, and flush Associations
130
+ # whose parent sits above. Other shapes keep their stored docks.
131
+ def redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
132
+ style = DiagramStyleParser.parse(link_row.style)
133
+ return unless points.size == 4 && style[:tree] == "V"
134
+
135
+ case connector.connector_type
136
+ when "Aggregation"
137
+ return if geometry_has_offsets?(link_row.geometry)
138
+
139
+ redock_child_below(points, bounds_from_rect(source_placement), bounds_from_rect(target_placement))
140
+ when "Association"
141
+ return unless style[:lwidth] == "0"
142
+
143
+ redock_parent_above(points, link_row, source_placement, target_placement, strict: true)
144
+ end
145
+ end
146
+
147
+ def redock_child_below(points, src_b, tgt_b)
148
+ first_bend, last_bend = points.values_at(1, -2)
149
+ source_y = src_b.y + src_b.height
150
+ return points unless source_y <= tgt_b.y && first_bend[1] > source_y && last_bend[1] < tgt_b.y
151
+
152
+ points[0] = [clamp_to_span(first_bend[0], src_b), source_y]
153
+ points[-1] = [clamp_to_span(last_bend[0], tgt_b), tgt_b.y]
154
+ points
155
+ end
156
+
157
+ def clamp_to_span(position, bounds)
158
+ position.clamp(bounds.x, bounds.x + bounds.width)
159
+ end
160
+
161
+ # A stored-Path Mode 3 width-2 end whose adjacent bend lies level
162
+ # with its box (inside the box's span on one axis, outside on the
163
+ # other) enters the box orthogonally on the outer outline at that
164
+ # bend's coordinate. Bends that are not level keep the port and
165
+ # every authored corner stays.
166
+ def level_entry_route(points, link_row, connector, src, tgt)
167
+ return points unless points.size >= 3 && level_entry_style?(link_row, connector)
168
+
169
+ route = points.dup
170
+ first = level_entry_point(route[1], src)
171
+ last = level_entry_point(route[-2], tgt)
172
+ route[0] = first if first
173
+ route[-1] = last if last
174
+ route
175
+ end
176
+
177
+ def level_entry_style?(link_row, connector)
178
+ style = DiagramStyleParser.parse(link_row.style)
179
+ %w[Generalization Aggregation].include?(connector.connector_type) &&
180
+ style[:mode] == "3" && style[:lwidth] == "2" && !style.key?(:tree)
181
+ end
182
+
183
+ def level_entry_point(bend, box)
184
+ level_y = bend[1] > box.y && bend[1] < box.y + box.height
185
+ level_x = bend[0] > box.x && bend[0] < box.x + box.width
186
+ if level_y && outside_span?(bend[0], box.x, box.width)
187
+ [bend[0] < box.x ? box.x - 1 : box.x + box.width + 1, bend[1]]
188
+ elsif level_x && outside_span?(bend[1], box.y, box.height)
189
+ [bend[0], bend[1] < box.y ? box.y - 1 : box.y + box.height + 1]
190
+ end
191
+ end
192
+
193
+ def outside_span?(value, start, length)
194
+ value < start || value > start + length
195
+ end
196
+
118
197
  # Square routing keeps the stored orthogonal trunk, even when a
119
198
  # shorter facing-edge route exists between the same boxes.
120
199
  def stored_square_route(link_row, connector, source, target)
@@ -164,19 +243,29 @@ module Ea
164
243
 
165
244
  # A valid lateral corner joins one vertical leg and one horizontal
166
245
  # leg. Keep it even when the boxes also admit a shorter direct route.
167
- def stored_lateral_route(link_row, connector, source, target)
246
+ # ported: both ends carry explicit SX/SY and EX/EY offsets. An
247
+ # untyped corner without them may be an authored diagonal.
248
+ def stored_lateral_route(link_row, connector, source, target, ported: false)
168
249
  return nil unless %w[Aggregation Association].include?(connector.connector_type)
169
250
 
170
251
  style = DiagramStyleParser.parse(link_row.style)
171
- return nil unless stored_lateral_style?(style)
252
+ return nil unless stored_lateral_style?(style, connector, ported)
172
253
 
173
254
  corner = stored_lateral_corner(link_row)
174
- return nil unless corner
255
+ corner && lateral_corner_route(style, corner, source, target)
256
+ end
175
257
 
176
- if style[:tree] == "LH"
177
- horizontal_corner_route(corner, source, target)
258
+ # An untyped Mode 3 Association corner takes whichever leg
259
+ # orientation its position fits; width-2 docks sit on the 1px
260
+ # outer outline.
261
+ def lateral_corner_route(style, corner, source, target)
262
+ case style[:tree]
263
+ when "LH" then horizontal_corner_route(corner, source, target)
264
+ when "LV" then vertical_corner_route(corner, source, target)
178
265
  else
179
- vertical_corner_route(corner, source, target)
266
+ outline = style[:lwidth] == "2" ? 1 : 0
267
+ horizontal_corner_route(corner, source, target, outline) ||
268
+ vertical_corner_route(corner, source, target, outline)
180
269
  end
181
270
  end
182
271
 
@@ -186,8 +275,11 @@ module Ea
186
275
  intermediate_waypoints(link_row).first
187
276
  end
188
277
 
189
- def stored_lateral_style?(style)
190
- style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) && %w[LH LV].include?(style[:tree])
278
+ def stored_lateral_style?(style, connector, ported)
279
+ return false unless style[:mode] == "3" && %w[0 2].include?(style[:lwidth])
280
+
281
+ %w[LH LV].include?(style[:tree]) ||
282
+ (style[:tree].nil? && ported && connector.connector_type == "Association")
191
283
  end
192
284
 
193
285
  def horizontal_corner?(corner, source, target)
@@ -195,13 +287,13 @@ module Ea
195
287
  !within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
196
288
  end
197
289
 
198
- def horizontal_corner_route(corner, source, target)
290
+ def horizontal_corner_route(corner, source, target, outline = 0)
199
291
  return nil unless horizontal_corner?(corner, source, target)
200
292
 
201
293
  x, y = corner
202
294
 
203
- source_y = y < source.y ? source.y : source.y + source.height
204
- target_x = x < target.x ? target.x : target.x + target.width
295
+ source_y = y < source.y ? source.y - outline : source.y + source.height + outline
296
+ target_x = x < target.x ? target.x - outline : target.x + target.width + outline
205
297
  [[x, source_y], corner, [target_x, y]]
206
298
  end
207
299
 
@@ -210,13 +302,13 @@ module Ea
210
302
  !within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
211
303
  end
212
304
 
213
- def vertical_corner_route(corner, source, target)
305
+ def vertical_corner_route(corner, source, target, outline = 0)
214
306
  return nil unless vertical_corner?(corner, source, target)
215
307
 
216
308
  x, y = corner
217
309
 
218
- source_x = x < source.x ? source.x : source.x + source.width
219
- target_y = y < target.y ? target.y : target.y + target.height
310
+ source_x = x < source.x ? source.x - outline : source.x + source.width + outline
311
+ target_y = y < target.y ? target.y - outline : target.y + target.height + outline
220
312
  [[source_x, y], corner, [x, target_y]]
221
313
  end
222
314
 
@@ -662,7 +662,8 @@ module Ea
662
662
  src = bounds_from_rect(source_placement)
663
663
  tgt = bounds_from_rect(target_placement)
664
664
  tree_route = stored_square_route(link_row, connector, src, tgt) ||
665
- stored_lateral_route(link_row, connector, src, tgt) ||
665
+ stored_lateral_route(link_row, connector, src, tgt,
666
+ ported: source_port && target_port) ||
666
667
  horizontal_tree_route(points, link_row, connector, src, tgt)
667
668
  if tree_route
668
669
  points = tree_route
@@ -681,8 +682,15 @@ module Ea
681
682
  redock_parent_above(points, link_row, source_placement,
682
683
  target_placement, strict: true)
683
684
  end
685
+ if intermediate.any?
686
+ redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
687
+ end
684
688
  end
685
689
 
690
+ if intermediate.any? && !tree_route
691
+ points = level_entry_route(points, link_row, connector, src, tgt)
692
+ end
693
+ points = drop_zero_length_tree_segments(points, link_row)
686
694
  points.map do |x, y|
687
695
  Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
688
696
  end
@@ -22,6 +22,10 @@ module Ea
22
22
  SEAT_FONT_SIZE = 7
23
23
  SEAT_X_INSET = 3
24
24
  SEAT_STEREOTYPE_GAP = 12
25
+ # EA keeps an upward property seat's left edge at diagram x >= 3;
26
+ # the stereotype line moves with the role. Measured only for the
27
+ # default Cambria 7pt boxed cluster.
28
+ MIN_ROLE_X = 3
25
29
 
26
30
  attr_reader :canvas, :model_index, :document, :theme,
27
31
  :font_family, :font_size, :font_unit
@@ -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 = target_upward_role(connector, heading, anchor, text_box, role) || 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
- [x - inset - width, y + baseline]
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 target_upward_role(connector, heading, anchor, box, role)
207
- return nil unless heading && heading[0].zero? && heading[1].negative?
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
- plain_vertical_role(connector, :target_up, anchor, box, role)
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) && plain_vertical_box?(box)
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 + (end_kind == :source ? 15 : -7)]
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
- def plain_vertical_connector?(connector, end_kind)
230
- style = connector.style.to_h
231
- connector.connector_type == "Association" &&
232
- connector_source_dest_stereotype(connector, end_kind).empty? &&
233
- style[:mode] == "3" && style[:lwidth] == "0" && !style[:tree]
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] == "2" && style[:tree] != "OS"
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 && leg_heading(points.reverse),
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
@@ -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.98"
7
+ VERSION = "0.6.99"
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.98
4
+ version: 0.6.99
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.