ea 0.6.97 → 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: c2ca3f0fd4381aec39af89849d601c85defa496b697f1fe625558c8a210aef64
4
- data.tar.gz: adbc3773a202f06ad7327ace0fbea64daacea45a4719afb28959332673dafdcd
3
+ metadata.gz: 4b9e7581bab2de7d7c5766a5f141c99885bb59cbb7ad974c313c26e86bfb96a2
4
+ data.tar.gz: 8933e36fb2422953eddb1d60069664e39177547beaf2ec17ed17c5b9f3aa4eac
5
5
  SHA512:
6
- metadata.gz: 99b878a17d58444e251964011cf2af59d46459223e657b8ffbf1a4602dc572af56e594bedd05c321cec0502d91e654cdd013d5b408277027d1c4f41c65885d3f
7
- data.tar.gz: 73ef9107ec26ea7b60f7c9e5d00b7fa09b4d08b10842345c589a75dfd13a62b553287b95b0ad1b71e0c45fc032ab30ff1299fecb03e2a3abac69027a69180e97
6
+ metadata.gz: ab6882700d57207a5fd2c1b27fa0befe5f287bb61f13387e99dc8f4429dbede75320d9fb6124a126094455d38d4dff7a41dbfc258633609c6695c5dcd216b36d
7
+ data.tar.gz: 82cd636abfe8c20a03dc8f6c5410192c65e674093eaaabc081e8fc84b7ec6544a87f3c87ff255f90fcaa1bc0f7704fdf1b2e46f344cf9288cd51ec4a7e8cfa97
@@ -8,6 +8,41 @@ module Ea
8
8
  module Routing
9
9
  private
10
10
 
11
+ # An empty custom-line Path has no authored corner to preserve.
12
+ # Keep normalized port routes; replace only the computed EDGE2 jog.
13
+ def direct_empty_path_ray(model, link_row, diagram_row, geom, context)
14
+ return model if context
15
+ return model unless empty_path_ray_candidate?(model, link_row, geom)
16
+
17
+ source, target = stored_ray_boxes(link_row, diagram_row)
18
+ return model unless empty_path_ray_gap?(source, target, geom)
19
+
20
+ model.waypoints = [model.source_port, model.target_port].map do |point|
21
+ Ea::Model::Waypoint.new(position: point)
22
+ end
23
+ set_stored_ray_origins(model, source, target, geom)
24
+ end
25
+
26
+ def empty_path_ray_gap?(source, target, geom)
27
+ return false unless source.x + source.width < target.x
28
+
29
+ [stored_ray_origin(source, geom[:sx], geom[:sy]),
30
+ stored_ray_origin(target, geom[:ex], geom[:ey])].all? do |origin|
31
+ within_height?(origin.y, source) && within_height?(origin.y, target)
32
+ end
33
+ end
34
+
35
+ def empty_path_ray_candidate?(model, link_row, geom)
36
+ %w[Generalization Aggregation].include?(model.connector_type) &&
37
+ model.waypoints.size == 3 && empty_path_ray_style?(model.style, link_row, geom) &&
38
+ model.has_geometry_offsets && model.source_port && model.target_port
39
+ end
40
+
41
+ def empty_path_ray_style?(style, link_row, geom)
42
+ style[:mode] == "3" && style[:lwidth] == "2" && !style[:tree] &&
43
+ geom[:edge] == 2 && link_row.path.to_s.empty?
44
+ end
45
+
11
46
  # Stored SY/EY use mathematical Y; ray origins use screen Y.
12
47
  # Computed-jog origins are already normalized to their derived ports.
13
48
  def correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
@@ -80,6 +115,85 @@ module Ea
80
115
  y: bounds.y + bounds.height / 2 - y_offset.to_i)
81
116
  end
82
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
+
83
197
  # Square routing keeps the stored orthogonal trunk, even when a
84
198
  # shorter facing-edge route exists between the same boxes.
85
199
  def stored_square_route(link_row, connector, source, target)
@@ -129,19 +243,29 @@ module Ea
129
243
 
130
244
  # A valid lateral corner joins one vertical leg and one horizontal
131
245
  # leg. Keep it even when the boxes also admit a shorter direct route.
132
- 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)
133
249
  return nil unless %w[Aggregation Association].include?(connector.connector_type)
134
250
 
135
251
  style = DiagramStyleParser.parse(link_row.style)
136
- return nil unless stored_lateral_style?(style)
252
+ return nil unless stored_lateral_style?(style, connector, ported)
137
253
 
138
254
  corner = stored_lateral_corner(link_row)
139
- return nil unless corner
255
+ corner && lateral_corner_route(style, corner, source, target)
256
+ end
140
257
 
141
- if style[:tree] == "LH"
142
- 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)
143
265
  else
144
- 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)
145
269
  end
146
270
  end
147
271
 
@@ -151,8 +275,11 @@ module Ea
151
275
  intermediate_waypoints(link_row).first
152
276
  end
153
277
 
154
- def stored_lateral_style?(style)
155
- 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")
156
283
  end
157
284
 
158
285
  def horizontal_corner?(corner, source, target)
@@ -160,13 +287,13 @@ module Ea
160
287
  !within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
161
288
  end
162
289
 
163
- def horizontal_corner_route(corner, source, target)
290
+ def horizontal_corner_route(corner, source, target, outline = 0)
164
291
  return nil unless horizontal_corner?(corner, source, target)
165
292
 
166
293
  x, y = corner
167
294
 
168
- source_y = y < source.y ? source.y : source.y + source.height
169
- 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
170
297
  [[x, source_y], corner, [target_x, y]]
171
298
  end
172
299
 
@@ -175,13 +302,13 @@ module Ea
175
302
  !within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
176
303
  end
177
304
 
178
- def vertical_corner_route(corner, source, target)
305
+ def vertical_corner_route(corner, source, target, outline = 0)
179
306
  return nil unless vertical_corner?(corner, source, target)
180
307
 
181
308
  x, y = corner
182
309
 
183
- source_x = x < source.x ? source.x : source.x + source.width
184
- 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
185
312
  [[source_x, y], corner, [x, target_y]]
186
313
  end
187
314
 
@@ -16,16 +16,11 @@ module Ea
16
16
 
17
17
  attr_reader :database
18
18
 
19
- # Generalizations whose EA CustomProperties carry
20
- # isSubstitutable draw '{isSubstitutable=false}' at the label
21
- # seat (t_xref 'connector property' rows keyed by connector
22
- # guid; VALU 0/empty both mean false).
19
+ # This flag requests the false annotation, not the property's Boolean
20
+ # value. An explicit true (-1) must not request a false annotation.
23
21
  def substitutable_connector?(conn)
24
- @substitutable_guids ||= database.collections[:xrefs]
25
- .select do |x|
26
- x.xref_type == "connector property" &&
27
- x.name == "CustomProperties" &&
28
- x.description.to_s.include?("isSubstitutable")
22
+ @substitutable_guids ||= database.collections[:xrefs].select do |xref|
23
+ false_substitutability_xref?(xref)
29
24
  end.map(&:client).to_set
30
25
  @substitutable_guids.include?(conn.ea_guid)
31
26
  end
@@ -99,6 +94,22 @@ module Ea
99
94
 
100
95
  private
101
96
 
97
+ def false_substitutability_xref?(xref)
98
+ return false unless xref.xref_type == "connector property" && xref.name == "CustomProperties"
99
+
100
+ description = xref.description.to_s
101
+ description.include?("isSubstitutable") && !true_substitutability_property?(description)
102
+ end
103
+
104
+ def true_substitutability_property?(description)
105
+ description.scan(/@PROP=(.*?)@ENDPROP;/m).any? do |match|
106
+ property = match.first
107
+ property.include?("@NAME=isSubstitutable@ENDNAME;") &&
108
+ property.include?("@TYPE=Boolean@ENDTYPE;") &&
109
+ property[/@VALU=(.*?)@ENDVALU;/m, 1] == "-1"
110
+ end
111
+ end
112
+
102
113
  def build_elements(diagram_row)
103
114
  objs = database.diagram_objects_for(diagram_row.diagram_id) || []
104
115
  objs.map { |obj_row| build_element(obj_row, diagram_row) }
@@ -410,6 +421,7 @@ module Ea
410
421
  has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
411
422
  is_substitutable: substitutable_connector?(connector)
412
423
  ), link_row, diagram_row, geom)
424
+ model = direct_empty_path_ray(model, link_row, diagram_row, geom, context)
413
425
  correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
414
426
  end
415
427
 
@@ -650,7 +662,8 @@ module Ea
650
662
  src = bounds_from_rect(source_placement)
651
663
  tgt = bounds_from_rect(target_placement)
652
664
  tree_route = stored_square_route(link_row, connector, src, tgt) ||
653
- stored_lateral_route(link_row, connector, src, tgt) ||
665
+ stored_lateral_route(link_row, connector, src, tgt,
666
+ ported: source_port && target_port) ||
654
667
  horizontal_tree_route(points, link_row, connector, src, tgt)
655
668
  if tree_route
656
669
  points = tree_route
@@ -669,8 +682,15 @@ module Ea
669
682
  redock_parent_above(points, link_row, source_placement,
670
683
  target_placement, strict: true)
671
684
  end
685
+ if intermediate.any?
686
+ redock_vertical_tree_trunk(points, link_row, connector, source_placement, target_placement)
687
+ end
672
688
  end
673
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)
674
694
  points.map do |x, y|
675
695
  Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
676
696
  end
@@ -209,18 +209,46 @@ module Ea
209
209
  divider_y_by_ref[target_ref]]
210
210
  return nil unless bounds.all? && dividers.all?
211
211
 
212
- adjusted = row_slot_pairs(pairs, bounds, dividers)
212
+ preserve_source = explicit_horizontal_source_dock?(pairs, connector)
213
+ adjusted = row_slot_pairs(pairs, bounds, dividers, preserve_source: preserve_source)
213
214
  if stroke_outline?(connector, bounds) &&
214
215
  pairs == pairs_for(connector, diagram, model_index, bounds_map)
215
216
  original = clipped_pairs(bounds, [connector.source_anchor, connector.target_anchor])
216
- original_final = row_slot_pairs(original, bounds, dividers)
217
+ original_final = row_slot_pairs(original, bounds, dividers, preserve_source: preserve_source)
217
218
  adjusted = original_final if original != pairs && !forward_route?(original_final, adjusted)
218
219
  end
219
220
  adjusted == pairs ? nil : adjusted
220
221
  end
221
222
 
222
- def row_slot_pairs(pairs, bounds, dividers)
223
+ # A supplied horizontal port already specifies its row. Moving it
224
+ # to the next compartment row would discard that explicit coordinate.
225
+ def explicit_horizontal_source_dock?(pairs, connector)
226
+ return false unless explicit_horizontal_ray?(connector)
227
+
228
+ connector.source_anchor.y == connector.target_anchor.y &&
229
+ connector.source_port.y == connector.source_anchor.y &&
230
+ pairs.first[1] == connector.source_port.y
231
+ end
232
+ private_class_method :explicit_horizontal_source_dock?
233
+
234
+ def explicit_horizontal_ray?(connector)
235
+ explicit_horizontal_ray_style?(connector) && connector.has_geometry_offsets &&
236
+ connector.source_port && connector.target_port &&
237
+ connector.source_anchor && connector.target_anchor
238
+ end
239
+ private_class_method :explicit_horizontal_ray?
240
+
241
+ def explicit_horizontal_ray_style?(connector)
242
+ style = connector.style.to_h
243
+ %w[Generalization Aggregation].include?(connector.connector_type) &&
244
+ style[:mode] == "3" && !style[:tree] && !style[:direct] && !style[:regenerated]
245
+ end
246
+ private_class_method :explicit_horizontal_ray_style?
247
+
248
+ def row_slot_pairs(pairs, bounds, dividers, preserve_source: false)
223
249
  pairs.each_with_index.map do |(x, y), index|
250
+ next [x, y] if preserve_source && index.zero?
251
+
224
252
  row_slot?(x, y, bounds[index], dividers[index]) ? [x, y + ROW_SLOT_PITCH] : [x, y]
225
253
  end
226
254
  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
@@ -86,12 +90,15 @@ module Ea
86
90
  stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
87
91
  direction = leg_direction(end_kind, heading)
88
92
  if direction && !(tree && zero_offset?(text_box, mult_box))
89
- text_pos = source_property_seat(connector, direction, anchor, text_box, role) ||
93
+ text_pos = plain_vertical_role(connector, direction, anchor, text_box, role) ||
94
+ source_property_seat(connector, direction, anchor, text_box, role) ||
90
95
  plain_upward_seat(connector, direction, anchor, text_box, role) ||
91
96
  role_seat(direction, anchor, text_box, role) || text_pos
92
97
  mult_pos = directional_multiplicity(connector, direction, anchor, mult_box, mult) ||
93
98
  mult_seat(direction, anchor, mult_box) || mult_pos
94
99
  elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
100
+ text_pos = target_vertical_role(connector, heading, anchor, text_box, role) ||
101
+ target_rightward_role(heading, anchor, text_box, role) || text_pos
95
102
  mult_pos = target_multiplicity_seat(heading, anchor, mult_box) || mult_pos
96
103
  end
97
104
 
@@ -185,7 +192,9 @@ module Ea
185
192
 
186
193
  x, y = seat_corner(anchor, box)
187
194
  inset, baseline = property_role_offsets(direction)
188
- [x - inset - width, y + baseline]
195
+ left = x - inset - width
196
+ left = [left, MIN_ROLE_X].max if direction == :up
197
+ [left, y + baseline]
189
198
  end
190
199
 
191
200
  def plain_upward_seat(connector, direction, anchor, box, role)
@@ -201,6 +210,61 @@ module Ea
201
210
  [x - SEAT_X_INSET - width, y - 6]
202
211
  end
203
212
 
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
219
+
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]
228
+ end
229
+
230
+ def plain_vertical_role(connector, direction, anchor, box, role)
231
+ return nil unless %i[down target_up target_down].include?(direction) &&
232
+ plain_vertical_box?(box)
233
+
234
+ end_kind = direction == :down ? :source : :target
235
+ return nil unless plain_vertical_connector?(connector, end_kind, direction)
236
+
237
+ width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
238
+ return nil unless width
239
+
240
+ x, y = seat_corner(anchor, box)
241
+ [x - width - 8, y + (direction == :target_up ? -7 : 15)]
242
+ end
243
+
244
+ def plain_vertical_box?(box)
245
+ seat_font? && cambria_box?(box) && [0, 1].include?(box["alignment"])
246
+ end
247
+
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
266
+ end
267
+
204
268
  def plain_role_route?(style)
205
269
  %w[1 3].include?(style[:mode]) && %w[0 2].include?(style[:lwidth]) &&
206
270
  [nil, "V"].include?(style[:tree])
@@ -229,7 +293,7 @@ module Ea
229
293
  case direction
230
294
  when :left then left_property_route?(style)
231
295
  when :down then downward_property_route?(style)
232
- else style[:lwidth] == "2" && style[:tree] != "OS"
296
+ else %w[0 2].include?(style[:lwidth]) && style[:tree] != "OS"
233
297
  end
234
298
  end
235
299
 
@@ -96,6 +96,9 @@ module Ea
96
96
  LINE_PITCH = 13
97
97
 
98
98
  def label_position(connector, points)
99
+ name_seat = stored_name_position(connector, points)
100
+ return name_seat if name_seat
101
+
99
102
  return midpoint(points) unless default_substitutable_seat?(connector) &&
100
103
  upward_four_point_route?(points)
101
104
 
@@ -103,6 +106,40 @@ module Ea
103
106
  [source[0] + 3, ((source[1] + bend[1]) / 2).floor - 1]
104
107
  end
105
108
 
109
+ # At 7pt, a centered name's stored corner is 3px below its baseline.
110
+ def stored_name_position(connector, points)
111
+ box = stored_name_box(connector)
112
+ return nil unless box
113
+
114
+ width = Ea::Fonts::Metrics.text_length(midpoint_content(connector), font_size, family: font_family)
115
+ return nil unless width
116
+
117
+ x, y = midpoint(points)
118
+ offset_x, offset_y = box.values_at("ox", "oy").map(&:to_i)
119
+ [x + offset_x - (width / 2.0).floor, y + offset_y - 3]
120
+ end
121
+
122
+ def stored_name_box(connector)
123
+ return nil unless connector.is_a?(Ea::Model::DiagramConnector) && measured_name_font?
124
+ return nil if connector.is_substitutable || stereotype_label(connector)
125
+ return nil if midpoint_content(connector).include?(": ")
126
+
127
+ box = connector.label_boxes.to_h[:lmt]
128
+ box if default_name_box?(box)
129
+ end
130
+
131
+ def measured_name_font?
132
+ font_family.to_s.casecmp("Cambria").zero? && font_size == 7 && font_unit == "pt"
133
+ end
134
+
135
+ def default_name_box?(box)
136
+ return false unless box && box["alignment"] == 1
137
+ return false unless box.values_at("ox", "oy").all?
138
+ return false unless [nil, 0, 1].include?(box["direction"]) && [nil, -1].include?(box["color"])
139
+
140
+ box["rotation"].to_i.zero? && box.values_at("bold", "italic", "underline", "hidden").none?
141
+ end
142
+
106
143
  def default_substitutable_seat?(connector)
107
144
  connector.is_a?(Ea::Model::DiagramConnector) && connector.is_substitutable &&
108
145
  connector.connector_type == "Generalization" && font_size == 7 &&
@@ -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.97"
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.97
4
+ version: 0.6.99
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.