ea 0.6.95 → 0.6.96

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: 87aea8f508b2ef7e8f98038519122caff0f87c967f3e59132838885124d0db84
4
- data.tar.gz: 381aa0a30e5c9c3c60f0885f4fd2ca737d909f8fc620e36725e787db909898ab
3
+ metadata.gz: 3b98c8ee8bf786f5160291763db47b38d8213a029ef4d647df1d73c386551dba
4
+ data.tar.gz: 3f0ee88fdb07d831f70e1ec51f634048140887a5231baa3dc0eea778a7f60d7a
5
5
  SHA512:
6
- metadata.gz: adba2730ddbc449a79ce0063273cb6f75c4fc88d219899e12e13bc4226d2d88d5ef438c062384d908296e62938d1b937c56b926043e89dc55b4d43a1d53c63c8
7
- data.tar.gz: b677683c53a71f27f68c2c80f7b150517288751bdbf586d78ad0fbe986b014931b57659235587e4ba61b8753656de72f6949363291fd6f1ab2d6e1a0a70aa5d1
6
+ metadata.gz: f0297306864b343dc4bdd5cae1c6cdaabeb7a5bb043cc592c76f11c5b42cfce4e7895bbfb3e92c51a263072203ca7e46b5eb3640633856474afbdc22798db2ba
7
+ data.tar.gz: de6096010a2802088ea9d8cee6fa9105c1b6622f4de8b89274668a5231645fd19727ad7633c74db66fbc6be04f70009f0d4d270f8ba4440189ff282f168d1313
@@ -10,6 +10,9 @@ font:
10
10
  weight_normal: 400
11
11
  weight_bold: 700
12
12
 
13
+ label_font:
14
+ family: Cambria
15
+
13
16
  # Element-level font (distinct from diagram-level frame font).
14
17
  # EA renders classifier compartments at this size regardless of
15
18
  # the diagram-level font.size.
@@ -0,0 +1,77 @@
1
+ {
2
+ " ": 2,
3
+ "(": 4,
4
+ ")": 4,
5
+ "*": 4,
6
+ "+": 6,
7
+ "-": 3,
8
+ ".": 2,
9
+ "0": 6,
10
+ "1": 6,
11
+ "2": 6,
12
+ "3": 6,
13
+ "4": 6,
14
+ "5": 6,
15
+ "6": 6,
16
+ "8": 6,
17
+ "9": 6,
18
+ ":": 3,
19
+ "<": 6,
20
+ "=": 6,
21
+ ">": 6,
22
+ "A": 6,
23
+ "B": 6,
24
+ "C": 6,
25
+ "D": 7,
26
+ "E": 6,
27
+ "F": 5,
28
+ "G": 6,
29
+ "H": 7,
30
+ "I": 3,
31
+ "K": 6,
32
+ "L": 5,
33
+ "M": 8,
34
+ "N": 7,
35
+ "O": 7,
36
+ "P": 6,
37
+ "Q": 7,
38
+ "R": 6,
39
+ "S": 5,
40
+ "T": 6,
41
+ "U": 6,
42
+ "V": 6,
43
+ "W": 9,
44
+ "X": 6,
45
+ "Y": 6,
46
+ "Z": 5,
47
+ "[": 4,
48
+ "]": 4,
49
+ "a": 5,
50
+ "b": 5,
51
+ "c": 4,
52
+ "d": 6,
53
+ "e": 5,
54
+ "f": 3,
55
+ "g": 5,
56
+ "h": 6,
57
+ "i": 3,
58
+ "j": 3,
59
+ "k": 5,
60
+ "l": 3,
61
+ "m": 8,
62
+ "n": 6,
63
+ "o": 5,
64
+ "p": 6,
65
+ "q": 5,
66
+ "r": 4,
67
+ "s": 4,
68
+ "t": 3,
69
+ "u": 6,
70
+ "v": 5,
71
+ "w": 8,
72
+ "x": 5,
73
+ "y": 5,
74
+ "z": 5,
75
+ "{": 4,
76
+ "}": 4
77
+ }
@@ -29,9 +29,18 @@ module Ea
29
29
  "calibri" => "Carlito",
30
30
  "carlito" => "Carlito",
31
31
  "arial" => "Arial",
32
- "arial narrow" => "Arial Narrow"
32
+ "arial narrow" => "Arial Narrow",
33
+ "cambria" => "Cambria"
33
34
  }.freeze
34
35
 
36
+ # Cambria measurements are in SVG units at 7pt, not em advances.
37
+ # Unmeasured characters and other sizes/styles must use the fallback.
38
+ CAMBRIA_7_REGULAR_WIDTHS = JSON.parse(File.read(
39
+ File.expand_path("cambria_7_regular_widths.json", __dir__)
40
+ )).freeze
41
+ CAMBRIA_7_GUILLEMET_PAIR_WIDTH = 10
42
+ private_constant :CAMBRIA_7_REGULAR_WIDTHS, :CAMBRIA_7_GUILLEMET_PAIR_WIDTH
43
+
35
44
  # Advance used for codepoints missing from the tables.
36
45
  DEFAULT_ADVANCE = 0.25
37
46
 
@@ -64,6 +73,12 @@ module Ea
64
73
  return nil unless supported_family?(family)
65
74
  return nil if text.nil? || text.empty?
66
75
 
76
+ return cambria_text_length(text, size_pt, weight, style) if resolve_family(family) == "Cambria"
77
+
78
+ advance_text_length(text, size_pt, family, weight, style)
79
+ end
80
+
81
+ def advance_text_length(text, size_pt, family, weight, style)
67
82
  resolved = resolve_family(family)
68
83
  if GDI_HEIGHT_FAMILIES.include?(resolved)
69
84
  height = (size_pt.to_f * 10 / 7).round
@@ -86,6 +101,26 @@ module Ea
86
101
  cjk_count * CJK_ADVANCE * size_pt.to_f).round(3)
87
102
  end
88
103
 
104
+ private_class_method :advance_text_length
105
+
106
+ def cambria_text_length(text, size_pt, weight, style)
107
+ return nil unless size_pt.to_f.to_r == 7
108
+ return nil unless ["", "0", "400", "normal"].include?(weight.to_s)
109
+ return nil unless ["", "normal"].include?(style.to_s)
110
+
111
+ return cambria_ascii_length(text) unless text.start_with?("«") && text.end_with?("»")
112
+
113
+ inner_width = cambria_ascii_length(text[1...-1])
114
+ inner_width + CAMBRIA_7_GUILLEMET_PAIR_WIDTH if inner_width
115
+ end
116
+ private_class_method :cambria_text_length
117
+
118
+ def cambria_ascii_length(text)
119
+ widths = text.each_char.map { |char| CAMBRIA_7_REGULAR_WIDTHS[char] }
120
+ widths.sum unless widths.empty? || widths.include?(nil)
121
+ end
122
+ private_class_method :cambria_ascii_length
123
+
89
124
  # Families whose EA driver quantizes per-glyph advances
90
125
  # (fitted from published textLengths).
91
126
  GDI_HEIGHT_FAMILIES = %w[Carlito].freeze
@@ -8,6 +8,115 @@ module Ea
8
8
  module Routing
9
9
  private
10
10
 
11
+ # Square routing keeps the stored orthogonal trunk, even when a
12
+ # shorter facing-edge route exists between the same boxes.
13
+ def stored_square_route(link_row, connector, source, target)
14
+ return nil unless stored_square_style?(link_row, connector)
15
+
16
+ corners = stored_square_corners(link_row)
17
+ square_corner_route(corners, source, target) if corners
18
+ end
19
+
20
+ def stored_square_style?(link_row, connector)
21
+ style = DiagramStyleParser.parse(link_row.style)
22
+ %w[Association Aggregation Generalization].include?(connector.connector_type) &&
23
+ style[:mode] == "3" && style[:tree] == "OS" && style[:lwidth] == "2"
24
+ end
25
+
26
+ def stored_square_corners(link_row)
27
+ return nil unless link_row.path.to_s.match?(/\A(?:-?\d+:-?\d+;)*-?\d+:-?\d+;?\z/)
28
+
29
+ corners = intermediate_waypoints(link_row)
30
+ corners if corners.each_cons(2).all? { |a, b| a[0] == b[0] || a[1] == b[1] }
31
+ end
32
+
33
+ def square_corner_route(corners, source, target)
34
+ first = square_corner_dock(corners.first, source)
35
+ last = square_corner_dock(corners.last, target)
36
+ return nil unless first && last
37
+
38
+ points = corners.dup
39
+ points.unshift(first) unless points.first == first
40
+ points.push(last) unless points.last == last
41
+ points if points.size >= 3
42
+ end
43
+
44
+ def square_corner_dock(corner, bounds)
45
+ x, y = corner
46
+ projected = [x.clamp(bounds.x, bounds.x + bounds.width),
47
+ y.clamp(bounds.y, bounds.y + bounds.height)]
48
+ return nil if projected[0] != x && projected[1] != y
49
+
50
+ projected if rectangle_edge?(projected, bounds)
51
+ end
52
+
53
+ def rectangle_edge?(point, bounds)
54
+ [bounds.x, bounds.x + bounds.width].include?(point[0]) ||
55
+ [bounds.y, bounds.y + bounds.height].include?(point[1])
56
+ end
57
+
58
+ # A valid lateral corner joins one vertical leg and one horizontal
59
+ # leg. Keep it even when the boxes also admit a shorter direct route.
60
+ def stored_lateral_route(link_row, connector, source, target)
61
+ return nil unless %w[Aggregation Association].include?(connector.connector_type)
62
+
63
+ style = DiagramStyleParser.parse(link_row.style)
64
+ return nil unless stored_lateral_style?(style)
65
+
66
+ corner = stored_lateral_corner(link_row)
67
+ return nil unless corner
68
+
69
+ if style[:tree] == "LH"
70
+ horizontal_corner_route(corner, source, target)
71
+ else
72
+ vertical_corner_route(corner, source, target)
73
+ end
74
+ end
75
+
76
+ def stored_lateral_corner(link_row)
77
+ return nil unless link_row.path.to_s.match?(/\A-?\d+:-?\d+;?\z/)
78
+
79
+ intermediate_waypoints(link_row).first
80
+ end
81
+
82
+ def stored_lateral_style?(style)
83
+ style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) && %w[LH LV].include?(style[:tree])
84
+ end
85
+
86
+ def horizontal_corner?(corner, source, target)
87
+ within_horizontal_span?(corner, source) && within_height?(corner[1], target) &&
88
+ !within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
89
+ end
90
+
91
+ def horizontal_corner_route(corner, source, target)
92
+ return nil unless horizontal_corner?(corner, source, target)
93
+
94
+ x, y = corner
95
+
96
+ source_y = y < source.y ? source.y : source.y + source.height
97
+ target_x = x < target.x ? target.x : target.x + target.width
98
+ [[x, source_y], corner, [target_x, y]]
99
+ end
100
+
101
+ def vertical_corner?(corner, source, target)
102
+ within_height?(corner[1], source) && within_horizontal_span?(corner, target) &&
103
+ !within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
104
+ end
105
+
106
+ def vertical_corner_route(corner, source, target)
107
+ return nil unless vertical_corner?(corner, source, target)
108
+
109
+ x, y = corner
110
+
111
+ source_x = x < source.x ? source.x : source.x + source.width
112
+ target_y = y < target.y ? target.y : target.y + target.height
113
+ [[source_x, y], corner, [x, target_y]]
114
+ end
115
+
116
+ def within_height?(height, bounds)
117
+ height.between?(bounds.y, bounds.y + bounds.height)
118
+ end
119
+
11
120
  # A lateral tree leaves the side edge before turning upward.
12
121
  # Keep that dock only when its first leg actually points outward.
13
122
  def lateral_source_exit?(points, link_row, bounds)
@@ -648,7 +648,9 @@ module Ea
648
648
  # EDGE=2 routes are excluded; they are already exact.
649
649
  src = bounds_from_rect(source_placement)
650
650
  tgt = bounds_from_rect(target_placement)
651
- tree_route = horizontal_tree_route(points, link_row, connector, src, tgt)
651
+ tree_route = stored_square_route(link_row, connector, src, tgt) ||
652
+ stored_lateral_route(link_row, connector, src, tgt) ||
653
+ horizontal_tree_route(points, link_row, connector, src, tgt)
652
654
  if tree_route
653
655
  points = tree_route
654
656
  elsif %w[Association Aggregation].include?(connector.connector_type) &&
@@ -75,11 +75,8 @@ module Ea
75
75
  text_pos, mult_pos = tree_positions(end_kind, anchor, role,
76
76
  text_pos, mult_pos)
77
77
  elsif mult_box && stored_offsets_zero?(mult_box)
78
- # A stored mult box with OX=0:OY=0 means "EA default":
79
- # the multiplicity sits 12px right of and 15px below the
80
- # endpoint anchor, regardless of line direction
81
- # (87-sample corpus fit: dy = +15 constant, dx = +12
82
- # dominant with a +3 minority).
78
+ # Start at the legacy default; measured directional seats
79
+ # below replace it for default Cambria labels.
83
80
  mult_pos = [anchor[0] + 12, anchor[1] + 15]
84
81
  end
85
82
  # The end's stored stereotype renders verbatim («Type» on
@@ -89,8 +86,11 @@ module Ea
89
86
  stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
90
87
  direction = leg_direction(end_kind, heading)
91
88
  if direction && !(tree && zero_offset?(text_box, mult_box))
92
- text_pos = role_seat(direction, anchor, text_box, role) || text_pos
93
- mult_pos = mult_seat(direction, anchor, mult_box) || mult_pos
89
+ text_pos = source_property_seat(connector, direction, anchor, text_box, role) ||
90
+ plain_upward_seat(connector, direction, anchor, text_box, role) ||
91
+ role_seat(direction, anchor, text_box, role) || text_pos
92
+ mult_pos = directional_multiplicity(connector, direction, anchor, mult_box, mult) ||
93
+ mult_seat(direction, anchor, mult_box) || mult_pos
94
94
  end
95
95
 
96
96
  build_texts(role:, mult:, standalone:,
@@ -147,19 +147,22 @@ module Ea
147
147
  # right: the multiplicity 6px above it on an upward leg and
148
148
  # 15px below it on a rightward one, the role 7px above it on
149
149
  # a rightward one. Other sizes, headings, the target end,
150
- # custom presentation keep their offsets; zero multiplicity boxes keep their default seat.
150
+ # custom presentation keep their offsets. Zero multiplicity boxes
151
+ # follow the direction for Cambria and keep the default otherwise.
151
152
  def leg_direction(end_kind, heading)
152
153
  return nil unless source_seat?(end_kind, heading)
153
154
 
154
155
  dx, dy = heading
155
156
  return :up if dx.zero? && dy.negative?
156
157
  return :right if dy.zero? && dx.positive?
158
+ return :left if dy.zero? && dx.negative?
159
+ return :down if dx.zero? && dy.positive?
157
160
 
158
161
  nil
159
162
  end
160
163
 
161
164
  # Role text on a rightward leg is left-aligned at the corner,
162
- # 7px above it. Upward legs keep the plain role offset.
165
+ # 7px above it.
163
166
  def role_seat(direction, anchor, box, role)
164
167
  return nil unless direction == :right
165
168
  return nil if box.nil? || role_empty?(role)
@@ -169,8 +172,93 @@ module Ea
169
172
  [x + SEAT_X_INSET, y - 7]
170
173
  end
171
174
 
175
+ # Property-role clusters use a direction-specific right edge;
176
+ # alignment controls the stereotype line within that cluster.
177
+ def source_property_seat(connector, direction, anchor, box, role)
178
+ return nil unless %i[up left down].include?(direction) && property_role_box?(box, direction)
179
+ return nil unless source_property_font?(connector, direction)
180
+
181
+ width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
182
+ return nil unless width
183
+
184
+ x, y = seat_corner(anchor, box)
185
+ inset, baseline = property_role_offsets(direction)
186
+ [x - inset - width, y + baseline]
187
+ end
188
+
189
+ def plain_upward_seat(connector, direction, anchor, box, role)
190
+ return nil unless direction == :up && cambria_box?(box)
191
+ return nil unless [0, 1].include?(box["alignment"])
192
+ return nil unless connector_source_dest_stereotype(connector, :source).empty?
193
+ return nil unless plain_role_route?(connector.style.to_h)
194
+
195
+ width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
196
+ return nil unless width
197
+
198
+ x, y = seat_corner(anchor, box)
199
+ [x - SEAT_X_INSET - width, y - 6]
200
+ end
201
+
202
+ def plain_role_route?(style)
203
+ %w[1 3].include?(style[:mode]) && %w[0 2].include?(style[:lwidth]) &&
204
+ [nil, "V"].include?(style[:tree])
205
+ end
206
+
207
+ def property_role_box?(box, direction)
208
+ alignments = direction == :down ? [1] : [0, 1]
209
+ box && alignments.include?(box["alignment"]) && default_presentation?(box)
210
+ end
211
+
212
+ def property_role_offsets(direction)
213
+ case direction
214
+ when :up then [60, -6]
215
+ when :left then [60, -7]
216
+ else [65, 15]
217
+ end
218
+ end
219
+
220
+ def source_property_font?(connector, direction)
221
+ font_family.to_s.casecmp("Cambria").zero? &&
222
+ property_label?(connector, :source) &&
223
+ source_property_route?(connector.style.to_h, direction)
224
+ end
225
+
226
+ def source_property_route?(style, direction)
227
+ case direction
228
+ when :left then left_property_route?(style)
229
+ when :down then downward_property_route?(style)
230
+ else style[:lwidth] == "2" && style[:tree] != "OS"
231
+ end
232
+ end
233
+
234
+ def left_property_route?(style)
235
+ style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
236
+ [nil, "H", "LV"].include?(style[:tree])
237
+ end
238
+
239
+ def downward_property_route?(style)
240
+ %w[1 3].include?(style[:mode]) && style[:lwidth] == "2" && !style[:tree]
241
+ end
242
+
243
+ def directional_multiplicity(connector, direction, anchor, box, mult)
244
+ return nil unless cambria_box?(box)
245
+
246
+ x, y = seat_corner(anchor, box)
247
+ return [x + 8, y + 15] if direction == :down
248
+ return nil unless direction == :left && source_property_font?(connector, direction)
249
+
250
+ width = Ea::Fonts::Metrics.text_length(mult, font_size, family: font_family)
251
+ [x - width - 3, y + 15] if width
252
+ end
253
+
254
+ def cambria_box?(box)
255
+ box && default_presentation?(box) && font_family.to_s.casecmp("Cambria").zero?
256
+ end
257
+
172
258
  def mult_seat(direction, anchor, box)
173
- return nil if box.nil? || stored_offsets_zero?(box)
259
+ return nil unless %i[up right].include?(direction)
260
+ return nil if box.nil?
261
+ return nil if stored_offsets_zero?(box) && !cambria_box?(box)
174
262
  return nil unless default_presentation?(box)
175
263
 
176
264
  x, y = seat_corner(anchor, box)
@@ -91,7 +91,7 @@ module Ea
91
91
  end_renderer.texts(text_box: src_tb, mult_box: src_mb,
92
92
  anchor: source_pt, connector: connector,
93
93
  end_kind: :source, tree: tree && tree[:source],
94
- heading: stored_src && leg_heading(points),
94
+ heading: stored_src && source_heading(connector, points),
95
95
  stored_labels: stored_labels).each { |t| texts << t }
96
96
  end_renderer.texts(text_box: tgt_tb,
97
97
  mult_box: tgt_mb,
@@ -112,6 +112,19 @@ module Ea
112
112
  { source: true, target: true }
113
113
  end
114
114
 
115
+ # Clipping can slant a stored vertical leg. Keep its label direction
116
+ # while the label position follows the final emitted endpoint.
117
+ def source_heading(connector, points)
118
+ heading = leg_heading(points)
119
+ return heading unless points.size == 2 && heading[0].nonzero? && heading[1].negative?
120
+
121
+ stored = waypoint_pairs(connector)
122
+ return heading unless stored.size == 2
123
+
124
+ original = leg_heading(stored)
125
+ original[0].zero? && original[1].negative? ? original : heading
126
+ end
127
+
115
128
  def leg_heading(points)
116
129
  [points[1][0] - points[0][0], points[1][1] - points[0][1]]
117
130
  end
@@ -195,7 +208,7 @@ module Ea
195
208
  end
196
209
 
197
210
  def label_font_family
198
- theme.font_family || DEFAULT_FAMILY
211
+ theme.label_font_family || DEFAULT_FAMILY
199
212
  end
200
213
 
201
214
  def label_font_size
@@ -65,6 +65,7 @@ module Ea
65
65
  text_y_offset: 12, line_height: 13).freeze
66
66
 
67
67
  def initialize(id:, name: nil, font_family: nil, font_size: nil,
68
+ label_font_family: nil,
68
69
  font_size_unit: "pt", text_weight_normal: 400,
69
70
  text_weight_bold: 700,
70
71
  element_font_size: 9, element_font_unit: "pt",
@@ -81,6 +82,7 @@ module Ea
81
82
  @id = id.to_s
82
83
  @name = name
83
84
  @font_family = font_family
85
+ @label_font_family = label_font_family
84
86
  @font_size = font_size
85
87
  @font_size_unit = font_size_unit
86
88
  @text_weight_normal = text_weight_normal
@@ -119,7 +121,12 @@ module Ea
119
121
  fills[classifier.class.name]
120
122
  end
121
123
 
124
+ def label_font_family
125
+ @label_font_family || font_family
126
+ end
127
+
122
128
  def with(**overrides)
129
+ overrides[:label_font_family] = nil if overrides.key?(:font_family) && !overrides.key?(:label_font_family)
123
130
  Definition.new(**to_h.merge(overrides))
124
131
  end
125
132
 
@@ -151,6 +158,7 @@ module Ea
151
158
  id: id, name: name,
152
159
  font_family: font_family, font_size: font_size,
153
160
  font_size_unit: font_size_unit,
161
+ label_font_family: @label_font_family,
154
162
  text_weight_normal: text_weight_normal,
155
163
  text_weight_bold: text_weight_bold,
156
164
  element_font_size: element_font_size,
@@ -10,6 +10,7 @@ module Ea
10
10
  # name: EA White Theme
11
11
  # font: { family: Carlito, size: 7, unit: pt, weight_normal: 400, ... }
12
12
  # element_font: { size: 9, unit: pt }
13
+ # label_font: { family: Cambria }
13
14
  # colors: { text: "#000000", attribute_text: "#66413F", ... }
14
15
  # strokes: { element_border: 2, connector_line: 2, ... }
15
16
  # text_metrics: { width_factor: 0.65 }
@@ -38,6 +39,7 @@ module Ea
38
39
  def from_hash(data)
39
40
  font = data["font"] || {}
40
41
  elem_font = data["element_font"] || {}
42
+ label_font = data["label_font"] || {}
41
43
  colors = data["colors"] || {}
42
44
  strokes = data["strokes"] || {}
43
45
  text_metrics = data["text_metrics"] || {}
@@ -53,6 +55,7 @@ module Ea
53
55
  id: data["id"],
54
56
  name: data["name"],
55
57
  font_family: font["family"],
58
+ label_font_family: label_font["family"],
56
59
  font_size: font["size"],
57
60
  font_size_unit: font["unit"] || "pt",
58
61
  text_weight_normal: font["weight_normal"] || 400,
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.95"
7
+ VERSION = "0.6.96"
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.95
4
+ version: 0.6.96
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -615,6 +615,7 @@ files:
615
615
  - lib/ea/export/xsd/generator.rb
616
616
  - lib/ea/export/xsd/namespace_registry.rb
617
617
  - lib/ea/fonts.rb
618
+ - lib/ea/fonts/cambria_7_regular_widths.json
618
619
  - lib/ea/fonts/carlito_metrics.json
619
620
  - lib/ea/fonts/metrics.rb
620
621
  - lib/ea/guid_format.rb