ea 0.6.19 → 0.6.20

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: 93444f7054362eb7eacaee52caf7a5be9896fde1f3c2a9ef19e2850abb990a9d
4
- data.tar.gz: 9c224144cc0899efc1f8bb9c41f53d1fdf19104083f658915314cf609cf7d062
3
+ metadata.gz: 4414e6921d37d619d953382395e7ecf9c8ddb19da4cca389e57177eb7a4ab398
4
+ data.tar.gz: 3365590f62c21b9855d18d70656515b361c4c93def5fe1d8341af944ed01822c
5
5
  SHA512:
6
- metadata.gz: 39ffbc27aab19937893283c206ea1409b639357f0a27138b70b9f6c9d1ad8ac149cb5fc911f5ac39afb03b0f1a0d8ee1cb345d38bf02469f22c0460d1975e547
7
- data.tar.gz: 358e559f41b84f168c08008e598b858248a4d0b627c38b2f1771dde340790b565c54efe2874492830dac6f8395cd315cb745ae0081822977ba9f4b28481c302c
6
+ metadata.gz: 87930fec1aeee29cae1c4352527bcb896147f971427c78a1f58c053b3496d891b82d78afd5aadd7e43a6356534ac9393c21c405cdb991d2703d228706077c01b
7
+ data.tar.gz: 2d0bce26c6aad8b83004004b86e156306847331b2e357c1599793dfce22ce8f953ee179fae02dde2a327f2c408faf347bbfeab5f3551f520954bccd801b9e56a
@@ -4,9 +4,12 @@ module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
6
  module Compartment
7
- # Constraints compartment. Skipped when the classifier has no
8
- # constraints. Renders an italic "constraints" header and one
9
- # "{name}" line per constraint.
7
+ # Real (OCL) constraints compartment. Skipped when the
8
+ # classifier has no constraints — the {root}/{leaf} marker
9
+ # renders in its own Marker compartment between the divider
10
+ # and the attributes. EA seats OCL constraint lines after the
11
+ # content and suppresses them when they would overflow the
12
+ # stored box height (never truncates rows).
10
13
  module Constraints
11
14
  DEFAULT_TEXT_COLOR = "#000000"
12
15
 
@@ -16,10 +19,15 @@ module Ea
16
19
  return nil unless context.constraints&.any?
17
20
  return nil unless context.geometry.tagged_value_first_y
18
21
 
22
+ first_y = context.geometry.tagged_value_first_y
23
+ line_h = context.size + 6
24
+ last_y = first_y + (context.constraints.size - 1) * line_h
25
+ return nil if last_y + 4 > context.bounds.y + context.bounds.height
26
+
19
27
  Element::ConstraintRenderer.render(
20
28
  context.constraints,
21
29
  bounds: context.bounds,
22
- first_y: context.geometry.tagged_value_first_y,
30
+ first_y: first_y,
23
31
  family: context.family, size: context.size,
24
32
  fill: DEFAULT_TEXT_COLOR
25
33
  )
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ module Compartment
7
+ # {root}/{leaf} marker compartment. EA seats the marker line
8
+ # DIRECTLY BELOW the header divider, BEFORE the attribute
9
+ # rows (each marker row shifts the attributes down one row).
10
+ # Real OCL constraints render trailing, after the content —
11
+ # the two are distinct compartments.
12
+ module Marker
13
+ module_function
14
+
15
+ def render(context)
16
+ return nil unless context.marker_lines&.any?
17
+ return nil unless context.geometry.marker_first_y
18
+
19
+ Element::ConstraintRenderer.render(
20
+ context.marker_lines,
21
+ bounds: context.bounds,
22
+ first_y: context.geometry.marker_first_y,
23
+ family: context.family, size: context.size
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -13,8 +13,6 @@ module Ea
13
13
  # (379 diagrams, 378 exact): classifiers never render the
14
14
  # subtitle, and ShowForeign=0 diagrams render none.
15
15
  module PackageFromParent
16
- Y_OFFSET_FROM_BOTTOM = 5
17
-
18
16
  module_function
19
17
 
20
18
  def render(context)
@@ -39,10 +37,14 @@ module Ea
39
37
 
40
38
  module_function :render
41
39
 
40
+ # EA draws the subtitle 15px BELOW the body bottom (outside
41
+ # the box) — verified against EA-published package SVGs.
42
+ Y_OFFSET_FROM_BOTTOM = 15
43
+
42
44
  def subtitle(text, context)
43
45
  bounds = context.bounds
44
46
  fill = context.theme.attribute_text_color
45
- y = bounds.y + bounds.height - Y_OFFSET_FROM_BOTTOM
47
+ y = bounds.y + bounds.height + Y_OFFSET_FROM_BOTTOM
46
48
  # EA centers the subtitle by its integer textLength inside
47
49
  # the package BODY and floors x.
48
50
  body_width = Element::PackageShapeRenderer.body_width_for(
@@ -20,6 +20,7 @@ module Ea
20
20
  autoload :EnumLiterals, "ea/svg/ea_emitter/compartment/enum_literals"
21
21
  autoload :TaggedValues, "ea/svg/ea_emitter/compartment/tagged_values"
22
22
  autoload :Constraints, "ea/svg/ea_emitter/compartment/constraints"
23
+ autoload :Marker, "ea/svg/ea_emitter/compartment/marker"
23
24
  autoload :PackageContents, "ea/svg/ea_emitter/compartment/package_contents"
24
25
  autoload :InstanceUnderline, "ea/svg/ea_emitter/compartment/instance_underline"
25
26
  autoload :InstanceSlots, "ea/svg/ea_emitter/compartment/instance_slots"
@@ -34,6 +35,7 @@ module Ea
34
35
  Header,
35
36
  HeaderDivider,
36
37
  StereotypeIcon,
38
+ Marker,
37
39
  Attributes,
38
40
  Operations,
39
41
  EnumLiterals,
@@ -21,6 +21,7 @@ module Ea
21
21
  :attr_lines_count, :op_lines_count,
22
22
  :tagged_values_count,
23
23
  :constraints_count,
24
+ :marker_count,
24
25
  :enum_literals_count,
25
26
  :header_top_padding, :header_line_offset,
26
27
  :divider_offset, :attr_line_offset,
@@ -30,6 +31,7 @@ module Ea
30
31
  attr_lines_count:, op_lines_count:,
31
32
  tagged_values_count:,
32
33
  constraints_count: 0,
34
+ marker_count: 0,
33
35
  enum_literals_count: 0,
34
36
  header_top_padding: 12,
35
37
  header_line_offset: 6,
@@ -43,6 +45,7 @@ module Ea
43
45
  @op_lines_count = op_lines_count
44
46
  @tagged_values_count = tagged_values_count
45
47
  @constraints_count = constraints_count
48
+ @marker_count = marker_count
46
49
  @enum_literals_count = enum_literals_count
47
50
  @header_top_padding = header_top_padding
48
51
  @header_line_offset = header_line_offset
@@ -67,6 +70,29 @@ module Ea
67
70
  end
68
71
 
69
72
  def attr_first_y
73
+ base = raw_attr_first_y
74
+ return base unless marker_count.to_i.positive?
75
+
76
+ # EA renders the {root}/{leaf} marker compartment BETWEEN
77
+ # the header divider and the attributes: its rows occupy
78
+ # (attr_base - 9) onward at the 13px pitch, and attributes
79
+ # shift down one row per marker line. Real OCL constraints
80
+ # do NOT shift attributes — they render trailing.
81
+ # Corpus-verified (TK_PositionType: name +13 {root} +22
82
+ # first-attr; MD_Identification keeps attrs at +22 despite
83
+ # carrying OCL constraints).
84
+ base + (size + 6) * marker_count
85
+ end
86
+
87
+ # First marker-line baseline: one row slot above where
88
+ # attributes would start without markers.
89
+ def marker_first_y
90
+ return nil unless marker_count.to_i.positive?
91
+
92
+ raw_attr_first_y - 9
93
+ end
94
+
95
+ def raw_attr_first_y
70
96
  return bounds.y + size + (header_top_padding || 12) + 12 unless divider_y
71
97
 
72
98
  divider_y + size + (attr_first_offset || 7)
@@ -106,15 +132,19 @@ module Ea
106
132
 
107
133
  # Tagged values appear after attributes (or after ops if
108
134
  # present). EA does not emit a separate divider — the
109
- # italic "tags" header marks the compartment.
135
+ # italic "tags" header marks the compartment. Constraints
136
+ # render ABOVE the attributes (constraints_first_y), not
137
+ # here.
110
138
  def tagged_value_first_y
111
- return nil unless tagged_values_count.to_i.positive? || constraints_count.to_i.positive?
139
+ return nil unless tagged_values_count.to_i.positive? ||
140
+ constraints_count.to_i.positive?
112
141
 
113
142
  content_bottom_y + size + 5
114
143
  end
115
144
 
116
145
  # The lowest content edge among attributes, enum literals,
117
- # and operations — the anchor for trailing compartments.
146
+ # operations, and constraints — the anchor for trailing
147
+ # compartments and the autosize height.
118
148
  def content_bottom_y
119
149
  bottoms = [attr_bottom_y]
120
150
  bottoms << enum_literals_bottom_y if enum_literals_count.to_i.positive?
@@ -5,47 +5,31 @@ module Ea
5
5
  module EaEmitter
6
6
  module Element
7
7
  # Emits the constraints compartment text `<g>`. EA renders
8
- # constraints as a sub-compartment with an italic "constraints"
9
- # header followed by `{name}` lines:
10
- #
11
- # <text style="...font-style:italic;...">constraints</text>
12
- # <text style="...font-style:normal;...">{pattern}</text>
8
+ # each constraint as a bare "{name}" line — no caption —
9
+ # RIGHT-ALIGNED to the element box's right edge at the 13px
10
+ # compartment pitch, seated between the header divider and
11
+ # the attribute rows. Corpus-verified (TK_PositionType /
12
+ # TK_Position: "{root}" x + textLength lands exactly on the
13
+ # box right edge).
13
14
  class ConstraintRenderer
14
- HEADER_TEXT = "constraints"
15
- HEADER_X_OFFSET = 5
16
- LINE_X_OFFSET = 5
17
-
18
15
  def self.render(constraints, bounds:, first_y:, family:, size:,
19
16
  fill: "#000000")
20
- line_h = size + 4
21
- text_blocks = []
22
- text_blocks << build_text(
23
- bounds.x + centered_x_offset(bounds, size), first_y,
24
- HEADER_TEXT, family, size, fill, style: "italic"
25
- )
26
- constraints.each_with_index do |constraint, idx|
27
- y = first_y + ((idx + 1) * line_h)
28
- text_blocks << build_text(
29
- bounds.x + LINE_X_OFFSET, y,
30
- "{#{constraint.name}}", family, size, fill
31
- )
17
+ line_h = size + 6
18
+ text_blocks = constraints.each_with_index.map do |constraint, idx|
19
+ content = "{#{constraint.name}}"
20
+ x = bounds.x + bounds.width -
21
+ TextRenderer.estimate_width(content, size, nil,
22
+ family: family).round
23
+ build_text(x, first_y + idx * line_h, content, family, size, fill)
32
24
  end
33
25
  %(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
34
26
  end
35
27
 
36
- def self.build_text(x, y, content, family, size, fill, style: "normal")
28
+ def self.build_text(x, y, content, family, size, fill)
37
29
  TextRenderer.new(content: content, x: x, y: y,
38
- family: family, size: size, fill: fill,
39
- style: style).to_svg
30
+ family: family, size: size, fill: fill).to_svg
40
31
  end
41
32
  private_class_method :build_text
42
-
43
- def self.centered_x_offset(bounds, size)
44
- text_width = HEADER_TEXT.length * (size / 2)
45
- centered = (bounds.width / 2 - text_width / 2).floor
46
- [HEADER_X_OFFSET, centered].max
47
- end
48
- private_class_method :centered_x_offset
49
33
  end
50
34
  end
51
35
  end
@@ -9,15 +9,18 @@ module Ea
9
9
  # Package as this folder-like silhouette regardless of diagram
10
10
  # type.
11
11
  #
12
- # Layout (element bounds cover tab + body together):
12
+ # Layout (element bounds cover tab + body together; the tab
13
+ # occupies the TOP of the stored rect — corpus-verified
14
+ # against EA-published package polygons, single tab 20px and
15
+ # stereotype tab 36px, both inside):
13
16
  #
14
17
  # ┌───┐
15
- # │tab│ ← top TAB_HEIGHT_SINGLE rows ABOVE element bounds;
16
- # │ │ when a stereotype is present, the tab grows to
17
- # └───┴──────────┐ TAB_HEIGHT_DOUBLE rows and extends INTO
18
- # │ │ the bounds; the name renders bold,
19
- # │ body │ left-aligned. The body always starts
20
- # │ │ where the tab ends.
18
+ # │tab│ ← top TAB_HEIGHT rows of the bounds;
19
+ # │ │ 36px when a stereotype is present
20
+ # ├───┴──────────┐
21
+ # │ │ the name renders bold, left-aligned;
22
+ # │ body │ the body always starts where the
23
+ # │ │ tab ends
21
24
  # └───────────────┘
22
25
  #
23
26
  class PackageShapeRenderer
@@ -128,11 +131,9 @@ module Ea
128
131
  tab_top_y + TAB_LABEL_Y_OFFSET
129
132
  end
130
133
 
131
- # Tab anchored 20px above the bounds top — matches EA's
132
- # folder silhouette regardless of whether the tab holds one
133
- # line or two.
134
+ # Tab occupies the top of the stored rect.
134
135
  def tab_top_y
135
- bounds.y - TAB_HEIGHT_SINGLE
136
+ bounds.y
136
137
  end
137
138
 
138
139
  def tab_height
@@ -153,12 +154,9 @@ module Ea
153
154
  ]
154
155
  end
155
156
 
156
- # Body extends from tab.bottom to (bounds.bottom - 20px
157
- # bottom margin). Total body height = bounds.height -
158
- # tab_height (because tab takes (tab_height - 20) from the
159
- # top of the bounds plus 20 above the bounds).
157
+ # Body extends from the tab bottom to the bounds bottom.
160
158
  def body_height(body_top)
161
- bounds.y + bounds.height - TAB_HEIGHT_SINGLE - body_top
159
+ bounds.y + bounds.height - body_top
162
160
  end
163
161
 
164
162
  def tab_points
@@ -86,29 +86,35 @@ module Ea
86
86
  tagged_values_for(classifier).size,
87
87
  header_lines,
88
88
  constraints_count: constraints_for(classifier).size,
89
+ marker_count: marker_lines_for(classifier).size,
89
90
  enum_literals_count: enum_literals_for(classifier).size)
90
- # EA suppresses the whole attribute compartment when it does
91
- # not fit the stored box height (never truncates rows) - the
92
- # AOS box on CityFurniture hides its association property
93
- # this way. Verified against EA-published reference SVGs.
91
+ # EA suppresses the whole attribute compartment when its
92
+ # rows do not fit the stored box height (never truncates
93
+ # rows) - the AOS box on CityFurniture hides its
94
+ # association property this way. The gate compares the last
95
+ # baseline + text descent (~4px) against the box bottom:
96
+ # TK_PositionType keeps 7 rows ending 6px above the bottom,
97
+ # while a 13px-tighter box loses them all.
94
98
  if attr_lines.any? &&
95
- geometry.attr_bottom_y.to_i + size > bounds.y + bounds.height
99
+ geometry.attr_bottom_y.to_i + 4 > bounds.y + bounds.height
96
100
  attr_lines = []
97
101
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
98
102
  tagged_values_for(classifier).size,
99
103
  header_lines,
100
104
  constraints_count: constraints_for(classifier).size,
105
+ marker_count: marker_lines_for(classifier).size,
101
106
  enum_literals_count: enum_literals_for(classifier).size)
102
107
  end
103
108
  # EA suppresses the operations compartment when its rows
104
109
  # overflow the stored box height (same rule as attributes).
105
110
  if op_lines.any? && geometry.op_first_y &&
106
- geometry.op_bottom_y.to_i + size > bounds.y + bounds.height
111
+ geometry.op_bottom_y.to_i + 4 > bounds.y + bounds.height
107
112
  op_lines = []
108
113
  geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
109
114
  tagged_values_for(classifier).size,
110
115
  header_lines,
111
116
  constraints_count: constraints_for(classifier).size,
117
+ marker_count: marker_lines_for(classifier).size,
112
118
  enum_literals_count: enum_literals_for(classifier).size)
113
119
  end
114
120
  RenderContext.new(
@@ -127,6 +133,7 @@ module Ea
127
133
  enum_literals: enum_literals_for(classifier),
128
134
  tagged_values: element.show_tagged_values ? tagged_values_for(classifier) : [],
129
135
  constraints: constraints_for(classifier),
136
+ marker_lines: marker_lines_for(classifier),
130
137
  package_content_lines: package_content_lines_for(model_element),
131
138
  geometry: geometry,
132
139
  theme: theme,
@@ -139,13 +146,15 @@ module Ea
139
146
 
140
147
  def compartment_geometry(bounds, size, attr_lines, op_lines,
141
148
  tagged_count, header_lines,
142
- constraints_count: 0, enum_literals_count: 0)
149
+ constraints_count: 0, marker_count: 0,
150
+ enum_literals_count: 0)
143
151
  CompartmentGeometry.new(bounds: bounds, size: size,
144
152
  header_lines_count: header_lines.size,
145
153
  attr_lines_count: attr_lines.size,
146
154
  op_lines_count: op_lines.size,
147
155
  tagged_values_count: tagged_count,
148
156
  constraints_count: constraints_count,
157
+ marker_count: marker_count,
149
158
  enum_literals_count: enum_literals_count,
150
159
  header_top_padding: header_padding_for(header_lines),
151
160
  header_line_offset: theme.compartments.header_line_offset,
@@ -193,12 +202,20 @@ module Ea
193
202
  classifier.tagged_values || []
194
203
  end
195
204
 
205
+ # Real OCL constraints render trailing, after the content
206
+ # compartment; the {root}/{leaf} marker renders its own
207
+ # compartment between the divider and the attributes.
196
208
  def constraints_for(classifier)
197
209
  return [] unless classifier.is_a?(Ea::Model::Classifier)
198
210
 
211
+ classifier.constraints || []
212
+ end
213
+
214
+ def marker_lines_for(classifier)
215
+ return [] unless classifier.is_a?(Ea::Model::Classifier)
216
+
199
217
  marker_line = root_leaf_marker_for(classifier)
200
- marker = marker_line ? [marker_constraint(marker_line)] : []
201
- marker + (classifier.constraints || [])
218
+ marker_line ? [marker_constraint(marker_line)] : []
202
219
  end
203
220
 
204
221
  # EA renders the element's Root/Leaf checkboxes as {root},
@@ -16,7 +16,7 @@ module Ea
16
16
  :fill, :stroke, :stroke_width, :text_fill,
17
17
  :family, :size, :size_unit,
18
18
  :header_lines, :attr_lines, :op_lines,
19
- :enum_literals, :tagged_values, :constraints,
19
+ :enum_literals, :tagged_values, :constraints, :marker_lines,
20
20
  :package_content_lines,
21
21
  :geometry, :theme, :canvas, :diagram, :model_index,
22
22
  :off_canvas_parent_name,
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.19"
7
+ VERSION = "0.6.20"
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.19
4
+ version: 0.6.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -884,6 +884,7 @@ files:
884
884
  - lib/ea/svg/ea_emitter/compartment/header_divider.rb
885
885
  - lib/ea/svg/ea_emitter/compartment/instance_slots.rb
886
886
  - lib/ea/svg/ea_emitter/compartment/instance_underline.rb
887
+ - lib/ea/svg/ea_emitter/compartment/marker.rb
887
888
  - lib/ea/svg/ea_emitter/compartment/note_body.rb
888
889
  - lib/ea/svg/ea_emitter/compartment/note_text.rb
889
890
  - lib/ea/svg/ea_emitter/compartment/operations.rb