ea 0.6.92 → 0.6.94

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: 641441bf5b181aaadd045223f47a59069d916f48247f19ab4e6bc6c19cfeee03
4
- data.tar.gz: 0eb407d782f28301f0747a2f251ce3412f97cc60882f87cc7062567d1e7e4a64
3
+ metadata.gz: d2c872554c2534df450bc335593fb39ed9363f3fe20053ef001b1621c5cdec6b
4
+ data.tar.gz: 79cecbb5a25a07a38d5bac1df1aaf413ffbc471cdd39938cd64cdf8bca822ce7
5
5
  SHA512:
6
- metadata.gz: d705861a5209c8646e01fb097b4329e634744bdeef07355d00598a45bff1134ab186c69c0088250a3a3c5ece15d2c39281653c0e667a473fea80dbfd4bb9b9c0
7
- data.tar.gz: 84fce6fa28c77169812dda317ef720035c1ac9b17ac854238aa376480980308b7c506e6f3fa52e23807e7cc851546fe65b0c6b8bda72b937b1d9a99ab82953ad
6
+ metadata.gz: d24168a33acbf362b4feff14508abb00562139e6602caed154c7f4b5e8bfa2b0f773b045fd27585f0610946f8fb2e4e9e6dbf2b8667ab785fc1bd71f18e9aa83
7
+ data.tar.gz: 1559678bc826ecdb82d710a72723709fa195907e8a11bb138eed728ba8b9804b30fcab7f75a024f27f8df34558ba52ec7f373e18bf1f46e6c7a89f07af8cf70b
@@ -8,6 +8,21 @@ module Ea
8
8
  module Routing
9
9
  private
10
10
 
11
+ # A lateral tree leaves the side edge before turning upward.
12
+ # Keep that dock only when its first leg actually points outward.
13
+ def lateral_source_exit?(points, link_row, bounds)
14
+ return false unless points.size == 3 && DiagramStyleParser.parse(link_row.style)[:tree] == "LV"
15
+
16
+ anchor, bend = points.take(2)
17
+ anchor[1] == bend[1] && outward_side_exit?(anchor[0], bend[0], bounds)
18
+ end
19
+
20
+ def outward_side_exit?(anchor_x, bend_x, bounds)
21
+ left = bounds.x
22
+ right = left + bounds.width
23
+ (anchor_x == left && bend_x < left) || (anchor_x == right && bend_x > right)
24
+ end
25
+
11
26
  # Keep a vertical tree trunk whose bends still fit both boxes
12
27
  # and the open gap; the closest facing edges need not share its x.
13
28
  def preserve_stored_tree?(points, link_row, connector, src, tgt)
@@ -19,6 +34,50 @@ module Ea
19
34
  tree_bends_fit?(points[1], points[-2], src, tgt)
20
35
  end
21
36
 
37
+ # A width-2 Mode 3 horizontal tree keeps its stored vertical trunk
38
+ # and docks both ends on the facing sides' 1px outer outlines at
39
+ # the bend heights. Returns nil unless the trunk sits strictly
40
+ # inside the open gap and each bend lies inside its own box.
41
+ def horizontal_tree_route(points, link_row, connector, src, tgt)
42
+ return nil unless points.size == 4 && horizontal_tree_style?(link_row, connector)
43
+
44
+ first, last = points.values_at(1, 2)
45
+ src_x, tgt_x = facing_outline_xs(src, tgt)
46
+ return nil unless src_x && trunk_in_gap?(first, last, src_x, tgt_x)
47
+ return nil unless inside_height?(first[1], src) && inside_height?(last[1], tgt)
48
+
49
+ [[src_x, first[1]], first, last, [tgt_x, last[1]]]
50
+ end
51
+
52
+ def horizontal_tree_style?(link_row, connector)
53
+ style = DiagramStyleParser.parse(link_row.style)
54
+ %w[Generalization Aggregation].include?(connector.connector_type) &&
55
+ style[:tree] == "H" && style[:lwidth] == "2" && style[:mode] == "3"
56
+ end
57
+
58
+ def trunk_in_gap?(first, last, src_x, tgt_x)
59
+ first[0] == last[0] && first[1] != last[1] &&
60
+ first[0] > [src_x, tgt_x].min && first[0] < [src_x, tgt_x].max
61
+ end
62
+
63
+ def inside_height?(height, bounds)
64
+ height > bounds.y && height < bounds.y + bounds.height
65
+ end
66
+
67
+ # Source and target x on the facing sides' outer outlines, or nil
68
+ # when the boxes are not horizontally separated.
69
+ def facing_outline_xs(src, tgt)
70
+ source_left = src.x
71
+ source_right = source_left + src.width
72
+ target_left = tgt.x
73
+ target_right = target_left + tgt.width
74
+ if target_right < source_left
75
+ [source_left - 1, target_right + 1]
76
+ elsif source_right < target_left
77
+ [source_right + 1, target_left - 1]
78
+ end
79
+ end
80
+
22
81
  def tree_bends_fit?(first, last, source, target)
23
82
  first[0] != last[0] && first[1] == last[1] &&
24
83
  within_vertical_gap?(first[1], source, target) &&
@@ -577,9 +577,8 @@ module Ea
577
577
  # describe the source-side and target-side bend offsets
578
578
  # relative to the edge attachment points.
579
579
  #
580
- # Path takes precedence when present — it's the authoritative
581
- # routing. SX/SY/EX/EY are fallback for auto-routed direct
582
- # connectors with at most one bend on each end.
580
+ # Custom-line Path takes precedence when present. Direct mode
581
+ # ignores old custom bends; SX/SY/EX/EY supply its endpoint ports.
583
582
  #
584
583
  # If we can't resolve the source or target element placement
585
584
  # for this diagram, we emit no waypoints (the connector will
@@ -607,8 +606,10 @@ module Ea
607
606
  element_edge_point(target_placement, :target, edge_out)
608
607
 
609
608
  intermediate = intermediate_waypoints(link_row)
610
- if edge_out == 2 && source_port && target_port &&
611
- !valid_tree_path?(intermediate, source_placement)
609
+ if DiagramStyleParser.parse(link_row.style)[:mode] == "1"
610
+ points = [source_point, target_point]
611
+ elsif edge_out == 2 && source_port && target_port &&
612
+ !valid_tree_path?(intermediate, source_placement)
612
613
  # A stale tree Path (not on the source's facing edge) is
613
614
  # recomputed: source port -> corner at (target dock x,
614
615
  # source exit y) -> target port.
@@ -645,10 +646,13 @@ module Ea
645
646
  # Between DISJOINT boxes, a stored route that does not dock the
646
647
  # closest facing edge pair is redrawn straight between them.
647
648
  # EDGE=2 routes are excluded; they are already exact.
648
- if %w[Association Aggregation].include?(connector.connector_type) &&
649
- edge_out != 2
650
- src = bounds_from_rect(source_placement)
651
- tgt = bounds_from_rect(target_placement)
649
+ src = bounds_from_rect(source_placement)
650
+ tgt = bounds_from_rect(target_placement)
651
+ tree_route = horizontal_tree_route(points, link_row, connector, src, tgt)
652
+ if tree_route
653
+ points = tree_route
654
+ elsif %w[Association Aggregation].include?(connector.connector_type) &&
655
+ edge_out != 2
652
656
  unless preserve_stored_tree?(points, link_row, connector, src, tgt)
653
657
  points = facing_edge_route(points, src, tgt)
654
658
  end
@@ -923,7 +927,7 @@ module Ea
923
927
  target_y = tgt_b.y + tgt_b.height
924
928
  source_y -= inset if points[1][1] < source_y - inset
925
929
  target_y += inset if points[-2][1] > target_y + inset
926
- unless strict && points[1][1] >= source_y
930
+ unless lateral_source_exit?(points, link_row, src_b) || (strict && points[1][1] >= source_y)
927
931
  points[0] = [points[1][0].clamp(src_b.x, src_b.x + src_b.width), source_y]
928
932
  end
929
933
  unless strict && points[-2][1] <= target_y
@@ -3,7 +3,7 @@
3
3
  module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
- # Re-docks DIRECT (auto-routed straight, 2-waypoint) connectors
6
+ # Re-docks direct connectors and lateral tree target legs
7
7
  # against the DRAWN element outlines captured during the element
8
8
  # render pass. EA clips connector rays at drawn boxes — grown
9
9
  # classifier heights and autosized package bodies — never the
@@ -13,17 +13,15 @@ module Ea
13
13
  module ConnectorRedock
14
14
  module_function
15
15
 
16
- # Returns replacement waypoint pairs [source_edge, target_edge]
16
+ # Returns replacement waypoint pairs for direct or lateral tree routes,
17
17
  # for the connector, or nil when the connector must keep its
18
18
  # parsed waypoints (not direct, not 2-point, unresolved ends,
19
19
  # self-loop, or no drawn bounds for either end).
20
20
  def pairs_for(connector, diagram, model_index, bounds_map)
21
21
  return nil unless bounds_map
22
- return nil unless direct?(connector) || two_point?(connector)
23
-
24
22
  points = waypoint_pairs(connector)
25
- return nil unless points.size == 2
26
-
23
+ lateral = lateral_tree?(connector, points)
24
+ return nil unless points.size == 2 || lateral
27
25
 
28
26
  rel = relationship_for(connector, model_index)
29
27
  return nil unless rel
@@ -36,6 +34,11 @@ module Ea
36
34
  return nil unless source_bounds && target_bounds
37
35
  return nil if source_ref == target_ref
38
36
 
37
+ if lateral
38
+ target = lateral_target(points[1], target_bounds, connector.style[:tree])
39
+ return target && [points[0], points[1], target]
40
+ end
41
+
39
42
  anchors = [connector.source_anchor, connector.target_anchor]
40
43
  bounds = [source_bounds, target_bounds]
41
44
  original = clipped_pairs(bounds, anchors)
@@ -50,6 +53,49 @@ module Ea
50
53
  forward_route?(original, candidate) ? candidate : original
51
54
  end
52
55
 
56
+ def lateral_tree?(connector, points)
57
+ style = connector.style.to_h
58
+ points.size == 3 && style[:mode] == "3" && style[:lwidth] == "2" &&
59
+ %w[LV LH].include?(style[:tree]) &&
60
+ %w[Generalization Aggregation].include?(connector.connector_type)
61
+ end
62
+ private_class_method :lateral_tree?
63
+
64
+ # The bend selects the near target edge even when the boxes overlap.
65
+ # Preserve the source leg and bend; only the final dock changes.
66
+ def lateral_target(bend, bounds, tree)
67
+ return nil unless bounds.width.positive? && bounds.height.positive?
68
+
69
+ tree == "LV" ? vertical_target(bend, bounds) : horizontal_target(bend, bounds)
70
+ end
71
+ private_class_method :lateral_target
72
+
73
+ def vertical_target(bend, bounds)
74
+ x, y = bend
75
+ return nil unless x.between?(bounds.x, bounds.x + bounds.width)
76
+
77
+ edge = facing_coordinate(y, bounds.y, bounds.height)
78
+ [x, edge] if edge
79
+ end
80
+ private_class_method :vertical_target
81
+
82
+ def horizontal_target(bend, bounds)
83
+ x, y = bend
84
+ return nil unless y.between?(bounds.y, bounds.y + bounds.height)
85
+
86
+ edge = facing_coordinate(x, bounds.x, bounds.width)
87
+ [edge, y] if edge
88
+ end
89
+ private_class_method :horizontal_target
90
+
91
+ def facing_coordinate(bend, start, extent)
92
+ return start if bend < start
93
+ return start + extent if bend > start + extent
94
+
95
+ nil
96
+ end
97
+ private_class_method :facing_coordinate
98
+
53
99
  def stroke_outline?(connector, bounds)
54
100
  connector.style.to_h[:lwidth] == "2" &&
55
101
  %w[Generalization Aggregation].include?(connector.connector_type) &&
@@ -21,6 +21,7 @@ module Ea
21
21
  PROPERTY_STEREOTYPE = "«Property»"
22
22
  SEAT_FONT_SIZE = 7
23
23
  SEAT_X_INSET = 3
24
+ SEAT_STEREOTYPE_GAP = 12
24
25
 
25
26
  attr_reader :canvas, :model_index, :document, :theme,
26
27
  :font_family, :font_size, :font_unit
@@ -44,12 +45,22 @@ module Ea
44
45
  # connector - the DiagramConnector (for relationship lookup)
45
46
  # end_kind - :source or :target
46
47
  # heading - [dx, dy] of the first leg leaving the anchor
48
+ # stored_labels - the connector has stored label boxes at
49
+ # either end; false keeps the opposite-end
50
+ # fallback
47
51
  def texts(text_box:, mult_box:, anchor:, connector:, end_kind:,
48
- tree: false, heading: nil)
52
+ tree: false, heading: nil, stored_labels: false)
49
53
  from_property, role, mult = role_and_multiplicity(connector, end_kind)
50
54
  if role_empty?(role) && mult_empty?(mult)
51
55
  other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
52
56
  opposite(end_kind))
57
+ if stored_labels
58
+ return [] if role_empty?(other_role) && mult_empty?(other_mult)
59
+
60
+ return own_stereotype_only(text_box, anchor, connector,
61
+ end_kind)
62
+ end
63
+
53
64
  role = other_role if role_empty?(role)
54
65
  mult ||= other_mult
55
66
  from_property ||= other_from_prop
@@ -88,6 +99,19 @@ module Ea
88
99
 
89
100
  private
90
101
 
102
+ # With stored boxes on the connector, an end that carries no
103
+ # data of its own draws only its own stereotype; the data end
104
+ # draws the role and multiplicity; with no data anywhere nothing
105
+ # is drawn. Connectors with no stored box keep the opposite-end
106
+ # fallback.
107
+ def own_stereotype_only(text_box, anchor, connector, end_kind)
108
+ stereo = connector_source_dest_stereotype(connector, end_kind)
109
+ pos = position_at(text_box, anchor)
110
+ return [] if stereo.nil? || stereo.empty? || pos.nil?
111
+
112
+ [text_at(pos[0], pos[1], "«#{stereo}»")]
113
+ end
114
+
91
115
  # EA's tree-route label clusters (hand-verified against the
92
116
  # EA-published reference, SY 8542 with canvas=(0,0)):
93
117
  # source-end pairs stack at the exit point (role +3/-23,
@@ -154,8 +178,11 @@ module Ea
154
178
  end
155
179
 
156
180
  def source_seat?(end_kind, heading)
157
- end_kind == :source && heading &&
158
- font_size == SEAT_FONT_SIZE && font_unit == "pt"
181
+ end_kind == :source && heading && seat_font?
182
+ end
183
+
184
+ def seat_font?
185
+ font_size == SEAT_FONT_SIZE && font_unit == "pt"
159
186
  end
160
187
 
161
188
  # Boxes with explicit string-keyed OX/OY (the QEA shape) that
@@ -181,7 +208,7 @@ module Ea
181
208
  texts = [text_at(x, y, role)]
182
209
  if stereo
183
210
  stereo_x = x + stereotype_offset(role, stereo, text_box)
184
- texts << text_at(stereo_x, y + property_y_offset, stereo)
211
+ texts << text_at(stereo_x, y + property_y_offset(text_box), stereo)
185
212
  end
186
213
  if mult && !mult.empty? && mult_pos
187
214
  texts << text_at(*mult_pos, mult)
@@ -368,7 +395,13 @@ module Ea
368
395
  [anchor[0] + offset_x.to_i, anchor[1] - offset_y.to_i]
369
396
  end
370
397
 
371
- def property_y_offset
398
+ # The stereotype line sits 12px below the role on 7pt labels
399
+ # with default presentation;
400
+ # other sizes and custom presentation keep size + 6.
401
+ def property_y_offset(text_box)
402
+ return SEAT_STEREOTYPE_GAP if seat_font? && text_box &&
403
+ default_presentation?(text_box)
404
+
372
405
  font_size + 6
373
406
  end
374
407
 
@@ -79,19 +79,22 @@ module Ea
79
79
  tree = tree_shaped?(points)
80
80
 
81
81
  stored_src = end_boxes(boxes, :llt, :llb)
82
+ stored_tgt = end_boxes(boxes, :lrt, :lrb)
83
+ stored_labels = !(stored_src.nil? && stored_tgt.nil?)
82
84
  src_tb, src_mb = stored_src || default_label_boxes(points, :source)
83
- tgt_tb, tgt_mb = end_boxes(boxes, :lrt, :lrb) ||
84
- default_label_boxes(points, :target)
85
+ tgt_tb, tgt_mb = stored_tgt || default_label_boxes(points, :target)
85
86
 
86
87
  texts = []
87
88
  end_renderer.texts(text_box: src_tb, mult_box: src_mb,
88
89
  anchor: source_pt, connector: connector,
89
90
  end_kind: :source, tree: tree && tree[:source],
90
- heading: stored_src && leg_heading(points)).each { |t| texts << t }
91
+ heading: stored_src && leg_heading(points),
92
+ stored_labels: stored_labels).each { |t| texts << t }
91
93
  end_renderer.texts(text_box: tgt_tb,
92
94
  mult_box: tgt_mb,
93
95
  anchor: target_pt, connector: connector,
94
- end_kind: :target, tree: tree && tree[:target]).each { |t| texts << t }
96
+ end_kind: :target, tree: tree && tree[:target],
97
+ stored_labels: stored_labels).each { |t| texts << t }
95
98
  texts
96
99
  end
97
100
 
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.92"
7
+ VERSION = "0.6.94"
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.92
4
+ version: 0.6.94
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.