ea 0.6.90 → 0.6.92

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: e78b8830cfb779be7d7724d11c984d11de76c2220aaae41502515f156e03aeca
4
- data.tar.gz: d694be35c46f90866280700653aa35af66da37e9e3ec9bcf0724805b947550b0
3
+ metadata.gz: 641441bf5b181aaadd045223f47a59069d916f48247f19ab4e6bc6c19cfeee03
4
+ data.tar.gz: 0eb407d782f28301f0747a2f251ce3412f97cc60882f87cc7062567d1e7e4a64
5
5
  SHA512:
6
- metadata.gz: 86c6dc4296a5c63c074178cdb01f19b171403e895b02dfb6e9897da9db87288a143228a3a0d7631953ddcf19ce2199a3060440fbd7472c472507af952206496c
7
- data.tar.gz: f0a27398ba13e4b352ccda7e3f810485577f5e2ac5154f2a4b43ab276ae6c2778877318f5c0c58e9ce92caa95ce2e27d10b11e829785cf61bb916fc1ac7bbccc
6
+ metadata.gz: d705861a5209c8646e01fb097b4329e634744bdeef07355d00598a45bff1134ab186c69c0088250a3a3c5ece15d2c39281653c0e667a473fea80dbfd4bb9b9c0
7
+ data.tar.gz: 84fce6fa28c77169812dda317ef720035c1ac9b17ac854238aa376480980308b7c506e6f3fa52e23807e7cc851546fe65b0c6b8bda72b937b1d9a99ab82953ad
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Sources
5
+ module Qea
6
+ class DiagramBuilder
7
+ # Internal geometry for choosing the facing edges of stored routes.
8
+ module Routing
9
+ private
10
+
11
+ # Keep a vertical tree trunk whose bends still fit both boxes
12
+ # and the open gap; the closest facing edges need not share its x.
13
+ def preserve_stored_tree?(points, link_row, connector, src, tgt)
14
+ return false unless connector.connector_type == "Aggregation" && points.size == 4
15
+
16
+ style = DiagramStyleParser.parse(link_row.style)
17
+ return false unless style[:tree] == "V" && style[:lwidth] == "2"
18
+
19
+ tree_bends_fit?(points[1], points[-2], src, tgt)
20
+ end
21
+
22
+ def tree_bends_fit?(first, last, source, target)
23
+ first[0] != last[0] && first[1] == last[1] &&
24
+ within_vertical_gap?(first[1], source, target) &&
25
+ within_horizontal_span?(first, source) && within_horizontal_span?(last, target)
26
+ end
27
+
28
+ def within_vertical_gap?(height, source, target)
29
+ height < source.y - 1 && height > target.y + target.height + 1
30
+ end
31
+
32
+ def within_horizontal_span?(point, bounds)
33
+ point[0].between?(bounds.x, bounds.x + bounds.width)
34
+ end
35
+
36
+ # Returns a replacement 2-point route between the closest
37
+ # facing edges, or the original points when: the boxes overlap
38
+ # on both axes, the current route already docks those edges,
39
+ # or no aligned dock fits the edges' overlap range.
40
+ def facing_edge_route(points, src, tgt)
41
+ v_gap = v_dir = h_gap = h_dir = nil
42
+ if src.y >= tgt.y + tgt.height
43
+ v_gap = src.y - (tgt.y + tgt.height)
44
+ v_dir = [:t, :b]
45
+ elsif tgt.y >= src.y + src.height
46
+ v_gap = tgt.y - (src.y + src.height)
47
+ v_dir = [:b, :t]
48
+ end
49
+ if src.x >= tgt.x + tgt.width
50
+ h_gap = src.x - (tgt.x + tgt.width)
51
+ h_dir = [:l, :r]
52
+ elsif tgt.x >= src.x + src.width
53
+ h_gap = tgt.x - (tgt.x + tgt.width)
54
+ h_dir = [:r, :l]
55
+ end
56
+ return points if v_gap.nil? && h_gap.nil?
57
+
58
+ if !v_gap.nil? && (h_gap.nil? || v_gap <= h_gap)
59
+ src_edge, tgt_edge = v_dir
60
+ else
61
+ src_edge, tgt_edge = h_dir
62
+ end
63
+ return points if docks_edges?(points, src, src_edge, tgt, tgt_edge)
64
+
65
+ if %i[t b].include?(src_edge)
66
+ lo = [src.x, tgt.x].max
67
+ hi = [src.x + src.width, tgt.x + tgt.width].min
68
+ return points if lo > hi
69
+
70
+ x = points.first[0].clamp(lo, hi)
71
+ [[x, src_edge == :t ? src.y : src.y + src.height],
72
+ [x, tgt_edge == :t ? tgt.y : tgt.y + tgt.height]]
73
+ else
74
+ lo = [src.y, tgt.y].max
75
+ hi = [src.y + src.height, tgt.y + tgt.height].min
76
+ return points if lo > hi
77
+
78
+ y = points.first[1].clamp(lo, hi)
79
+ [[src_edge == :l ? src.x : src.x + src.width, y],
80
+ [tgt_edge == :l ? tgt.x : tgt.x + tgt.width, y]]
81
+ end
82
+ end
83
+
84
+ def docks_edges?(points, src, src_edge, tgt, tgt_edge)
85
+ on_edge?(points.first, src, src_edge) && on_edge?(points.last, tgt, tgt_edge)
86
+ end
87
+
88
+ def on_edge?(pt, b, edge, tol = 2)
89
+ x, y = pt
90
+ case edge
91
+ when :l then (x - b.x).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
92
+ when :r then (x - (b.x + b.width)).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
93
+ when :t then (y - b.y).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
94
+ else (y - (b.y + b.height)).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -10,6 +10,10 @@ module Ea
10
10
  # DiagramConnectors (including pixel coordinates and parsed
11
11
  # styles).
12
12
  class DiagramBuilder
13
+ autoload :Routing, "ea/sources/qea/diagram_builder/routing"
14
+ include Routing
15
+ private_constant :Routing
16
+
13
17
  attr_reader :database
14
18
 
15
19
  # Generalizations whose EA CustomProperties carry
@@ -643,9 +647,11 @@ module Ea
643
647
  # EDGE=2 routes are excluded; they are already exact.
644
648
  if %w[Association Aggregation].include?(connector.connector_type) &&
645
649
  edge_out != 2
646
- points = facing_edge_route(points,
647
- bounds_from_rect(source_placement),
648
- bounds_from_rect(target_placement))
650
+ src = bounds_from_rect(source_placement)
651
+ tgt = bounds_from_rect(target_placement)
652
+ unless preserve_stored_tree?(points, link_row, connector, src, tgt)
653
+ points = facing_edge_route(points, src, tgt)
654
+ end
649
655
  # A width-2 Aggregation that keeps its stored bends is
650
656
  # docked like a Generalization one.
651
657
  if connector.connector_type == "Aggregation" &&
@@ -661,68 +667,6 @@ module Ea
661
667
  end
662
668
  end
663
669
 
664
- # Returns a replacement 2-point route between the closest
665
- # facing edges, or the original points when: the boxes overlap
666
- # on both axes, the current route already docks those edges,
667
- # or no aligned dock fits the edges' overlap range.
668
- def facing_edge_route(points, src, tgt)
669
- v_gap = v_dir = h_gap = h_dir = nil
670
- if src.y >= tgt.y + tgt.height
671
- v_gap = src.y - (tgt.y + tgt.height)
672
- v_dir = [:t, :b]
673
- elsif tgt.y >= src.y + src.height
674
- v_gap = tgt.y - (src.y + src.height)
675
- v_dir = [:b, :t]
676
- end
677
- if src.x >= tgt.x + tgt.width
678
- h_gap = src.x - (tgt.x + tgt.width)
679
- h_dir = [:l, :r]
680
- elsif tgt.x >= src.x + src.width
681
- h_gap = tgt.x - (tgt.x + tgt.width)
682
- h_dir = [:r, :l]
683
- end
684
- return points if v_gap.nil? && h_gap.nil?
685
-
686
- if !v_gap.nil? && (h_gap.nil? || v_gap <= h_gap)
687
- src_edge, tgt_edge = v_dir
688
- else
689
- src_edge, tgt_edge = h_dir
690
- end
691
- return points if docks_edges?(points, src, src_edge, tgt, tgt_edge)
692
-
693
- if %i[t b].include?(src_edge)
694
- lo = [src.x, tgt.x].max
695
- hi = [src.x + src.width, tgt.x + tgt.width].min
696
- return points if lo > hi
697
-
698
- x = points.first[0].clamp(lo, hi)
699
- [[x, src_edge == :t ? src.y : src.y + src.height],
700
- [x, tgt_edge == :t ? tgt.y : tgt.y + tgt.height]]
701
- else
702
- lo = [src.y, tgt.y].max
703
- hi = [src.y + src.height, tgt.y + tgt.height].min
704
- return points if lo > hi
705
-
706
- y = points.first[1].clamp(lo, hi)
707
- [[src_edge == :l ? src.x : src.x + src.width, y],
708
- [tgt_edge == :l ? tgt.x : tgt.x + tgt.width, y]]
709
- end
710
- end
711
-
712
- def docks_edges?(points, src, src_edge, tgt, tgt_edge)
713
- on_edge?(points.first, src, src_edge) && on_edge?(points.last, tgt, tgt_edge)
714
- end
715
-
716
- def on_edge?(pt, b, edge, tol = 2)
717
- x, y = pt
718
- case edge
719
- when :l then (x - b.x).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
720
- when :r then (x - (b.x + b.width)).abs <= tol && y.between?(b.y - tol, b.y + b.height + tol)
721
- when :t then (y - b.y).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
722
- else (y - (b.y + b.height)).abs <= tol && x.between?(b.x - tol, b.x + b.width + tol)
723
- end
724
- end
725
-
726
670
  # A tree Path is current when its bend height still falls
727
671
  # within the source element's y-range (the exit height EA
728
672
  # records); a bend outside the element is stale and EA
@@ -19,6 +19,8 @@ module Ea
19
19
  # for those — only simple role names render.
20
20
  class EndLabel
21
21
  PROPERTY_STEREOTYPE = "«Property»"
22
+ SEAT_FONT_SIZE = 7
23
+ SEAT_X_INSET = 3
22
24
 
23
25
  attr_reader :canvas, :model_index, :document, :theme,
24
26
  :font_family, :font_size, :font_unit
@@ -41,8 +43,9 @@ module Ea
41
43
  # anchor - [x, y] of the connector endpoint
42
44
  # connector - the DiagramConnector (for relationship lookup)
43
45
  # end_kind - :source or :target
46
+ # heading - [dx, dy] of the first leg leaving the anchor
44
47
  def texts(text_box:, mult_box:, anchor:, connector:, end_kind:,
45
- tree: false)
48
+ tree: false, heading: nil)
46
49
  from_property, role, mult = role_and_multiplicity(connector, end_kind)
47
50
  if role_empty?(role) && mult_empty?(mult)
48
51
  other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
@@ -73,6 +76,11 @@ module Ea
73
76
  # the end has none.
74
77
  stereo = connector_source_dest_stereotype(connector, end_kind)
75
78
  stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
79
+ direction = leg_direction(end_kind, heading)
80
+ if direction && !(tree && zero_offset?(text_box, mult_box))
81
+ text_pos = role_seat(direction, anchor, text_box, role) || text_pos
82
+ mult_pos = mult_seat(direction, anchor, mult_box) || mult_pos
83
+ end
76
84
 
77
85
  build_texts(role:, mult:, standalone:,
78
86
  text_pos:, mult_pos:, stereo:, text_box:)
@@ -109,6 +117,63 @@ module Ea
109
117
  end
110
118
  end
111
119
 
120
+ # Stored source-end boxes on a 7pt label whose first leg
121
+ # leaves straight up or straight right are seated at the box
122
+ # corner, anchor + (OX, OY) with no sign flip, 3px to its
123
+ # right: the multiplicity 6px above it on an upward leg and
124
+ # 15px below it on a rightward one, the role 7px above it on
125
+ # a rightward one. Other sizes, headings, the target end,
126
+ # custom presentation keep their offsets; zero multiplicity boxes keep their default seat.
127
+ def leg_direction(end_kind, heading)
128
+ return nil unless source_seat?(end_kind, heading)
129
+
130
+ dx, dy = heading
131
+ return :up if dx.zero? && dy.negative?
132
+ return :right if dy.zero? && dx.positive?
133
+
134
+ nil
135
+ end
136
+
137
+ # Role text on a rightward leg is left-aligned at the corner,
138
+ # 7px above it. Upward legs keep the plain role offset.
139
+ def role_seat(direction, anchor, box, role)
140
+ return nil unless direction == :right
141
+ return nil if box.nil? || role_empty?(role)
142
+ return nil unless default_presentation?(box)
143
+
144
+ x, y = seat_corner(anchor, box)
145
+ [x + SEAT_X_INSET, y - 7]
146
+ end
147
+
148
+ def mult_seat(direction, anchor, box)
149
+ return nil if box.nil? || stored_offsets_zero?(box)
150
+ return nil unless default_presentation?(box)
151
+
152
+ x, y = seat_corner(anchor, box)
153
+ [x + SEAT_X_INSET, direction == :right ? y + 15 : y - 6]
154
+ end
155
+
156
+ def source_seat?(end_kind, heading)
157
+ end_kind == :source && heading &&
158
+ font_size == SEAT_FONT_SIZE && font_unit == "pt"
159
+ end
160
+
161
+ # Boxes with explicit string-keyed OX/OY (the QEA shape) that
162
+ # are unrotated, unhidden, unstyled and left/center aligned.
163
+ def default_presentation?(box)
164
+ return false unless box["ox"] && box["oy"]
165
+ return false unless [nil, 0, 1].include?(box["alignment"])
166
+ return false unless [nil, -1].include?(box["color"])
167
+
168
+ box["rotation"].to_i.zero? && box["direction"].to_i.zero? &&
169
+ box.values_at("hidden", "bold", "italic", "underline")
170
+ .none?
171
+ end
172
+
173
+ def seat_corner(anchor, box)
174
+ [anchor[0] + box["ox"].to_i, anchor[1] + box["oy"].to_i]
175
+ end
176
+
112
177
  def build_texts(role:, mult:, standalone:, text_pos:, mult_pos:,
113
178
  stereo:, text_box:)
114
179
  if role && !role.empty? && text_pos
@@ -78,15 +78,16 @@ module Ea
78
78
  boxes = connector.label_boxes || {}
79
79
  tree = tree_shaped?(points)
80
80
 
81
- src_tb, src_mb = end_boxes(boxes, :llt, :llb) ||
82
- default_label_boxes(points, :source)
81
+ stored_src = end_boxes(boxes, :llt, :llb)
82
+ src_tb, src_mb = stored_src || default_label_boxes(points, :source)
83
83
  tgt_tb, tgt_mb = end_boxes(boxes, :lrt, :lrb) ||
84
84
  default_label_boxes(points, :target)
85
85
 
86
86
  texts = []
87
87
  end_renderer.texts(text_box: src_tb, mult_box: src_mb,
88
88
  anchor: source_pt, connector: connector,
89
- end_kind: :source, tree: tree && tree[:source]).each { |t| texts << t }
89
+ end_kind: :source, tree: tree && tree[:source],
90
+ heading: stored_src && leg_heading(points)).each { |t| texts << t }
90
91
  end_renderer.texts(text_box: tgt_tb,
91
92
  mult_box: tgt_mb,
92
93
  anchor: target_pt, connector: connector,
@@ -105,6 +106,10 @@ module Ea
105
106
  { source: true, target: true }
106
107
  end
107
108
 
109
+ def leg_heading(points)
110
+ [points[1][0] - points[0][0], points[1][1] - points[0][1]]
111
+ end
112
+
108
113
  def end_boxes(boxes, text_key, mult_key)
109
114
  return nil if boxes[text_key].nil? && boxes[mult_key].nil?
110
115
 
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.90"
7
+ VERSION = "0.6.92"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.90
4
+ version: 0.6.92
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -815,6 +815,7 @@ files:
815
815
  - lib/ea/sources/qea/connector_relationship_map.rb
816
816
  - lib/ea/sources/qea/context_diagram_support.rb
817
817
  - lib/ea/sources/qea/diagram_builder.rb
818
+ - lib/ea/sources/qea/diagram_builder/routing.rb
818
819
  - lib/ea/sources/qea/diagram_style_parser.rb
819
820
  - lib/ea/sources/qea/id_normalizer.rb
820
821
  - lib/ea/sources/qea/instance_builder.rb