ea 0.6.18 → 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 +4 -4
- data/lib/ea/fonts/carlito_metrics.json +1 -1
- data/lib/ea/fonts/metrics.rb +74 -34
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +12 -4
- data/lib/ea/svg/ea_emitter/compartment/marker.rb +30 -0
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +5 -3
- data/lib/ea/svg/ea_emitter/compartment.rb +2 -0
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +33 -3
- data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +15 -31
- data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +14 -16
- data/lib/ea/svg/ea_emitter/elements.rb +26 -9
- data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
- data/lib/ea/version.rb +1 -1
- data/lib/tasks/fonts.rake +20 -15
- metadata +2 -1
data/lib/ea/fonts/metrics.rb
CHANGED
|
@@ -4,12 +4,14 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Ea
|
|
6
6
|
module Fonts
|
|
7
|
-
# Advance-width metrics for
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# Advance-width metrics for the fonts EA's published SVGs use:
|
|
8
|
+
# Carlito (metric-compatible with Calibri), Arial, and Arial
|
|
9
|
+
# Narrow. EA's exported textLength values encode per-family,
|
|
10
|
+
# per-style, per-POINT-SIZE scale factors applied to the font's
|
|
11
|
+
# design advances (fitted by exact-count maximization against
|
|
12
|
+
# EA-published SVGs), so text widths computed here match EA's
|
|
13
|
+
# output to sub-pixel precision — far tighter than any legacy
|
|
14
|
+
# fitted-codepoint table, and style/size aware.
|
|
13
15
|
module Metrics
|
|
14
16
|
DATA_PATH = File.expand_path("carlito_metrics.json", __dir__)
|
|
15
17
|
|
|
@@ -21,16 +23,23 @@ module Ea
|
|
|
21
23
|
[true, true] => :bold_italic
|
|
22
24
|
}.freeze
|
|
23
25
|
|
|
24
|
-
# Families
|
|
25
|
-
|
|
26
|
+
# Families metric-compatible with a data-file family (the key
|
|
27
|
+
# is rendered even when the QEA named the other one).
|
|
28
|
+
FAMILY_ALIASES = {
|
|
29
|
+
"calibri" => "Carlito",
|
|
30
|
+
"carlito" => "Carlito",
|
|
31
|
+
"arial" => "Arial",
|
|
32
|
+
"arial narrow" => "Arial Narrow"
|
|
33
|
+
}.freeze
|
|
26
34
|
|
|
27
35
|
# Advance used for codepoints missing from the tables.
|
|
28
36
|
DEFAULT_ADVANCE = 0.25
|
|
29
37
|
|
|
30
|
-
# Carlito
|
|
31
|
-
# proportional CJK font whose advances measure ~0.709em at
|
|
32
|
-
# NOMINAL size (not scaled by the per-style factors) —
|
|
33
|
-
# from EA-published textLengths (52 mixed-script
|
|
38
|
+
# Carlito/Arial have no CJK glyphs; EA's renderer falls back to
|
|
39
|
+
# a proportional CJK font whose advances measure ~0.709em at
|
|
40
|
+
# the NOMINAL size (not scaled by the per-style factors) —
|
|
41
|
+
# fitted from EA-published textLengths (52 mixed-script
|
|
42
|
+
# samples).
|
|
34
43
|
CJK_ADVANCE = 0.709
|
|
35
44
|
CJK_RANGES = [
|
|
36
45
|
(0x3000..0x303F), # CJK punctuation
|
|
@@ -48,18 +57,23 @@ module Ea
|
|
|
48
57
|
return nil unless supported_family?(family)
|
|
49
58
|
return nil if text.nil? || text.empty?
|
|
50
59
|
|
|
51
|
-
latin, cjk_count = split_advance(text,
|
|
52
|
-
|
|
60
|
+
latin, cjk_count = split_advance(text, family: family,
|
|
61
|
+
weight: weight, style: style)
|
|
62
|
+
(latin * size_pt.to_f * factor_for(family, size_pt, weight, style) +
|
|
53
63
|
cjk_count * CJK_ADVANCE * size_pt.to_f).round(3)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
def supported_family?(family)
|
|
57
|
-
|
|
67
|
+
!resolve_family(family).nil?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_family(family)
|
|
71
|
+
FAMILY_ALIASES[family.to_s.downcase]
|
|
58
72
|
end
|
|
59
73
|
|
|
60
74
|
# Returns [latin_em (factor-scaled later), cjk_glyph_count].
|
|
61
|
-
def split_advance(text, weight: nil, style: nil)
|
|
62
|
-
table = table_for(weight, style)
|
|
75
|
+
def split_advance(text, family: nil, weight: nil, style: nil)
|
|
76
|
+
table = table_for(family, weight, style)
|
|
63
77
|
latin = 0.0
|
|
64
78
|
cjk = 0
|
|
65
79
|
text.each_char.each do |ch|
|
|
@@ -76,12 +90,20 @@ module Ea
|
|
|
76
90
|
CJK_RANGES.any? { |range| range.cover?(cp) }
|
|
77
91
|
end
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
# EA's scale factor varies by point size (GDI quantization):
|
|
94
|
+
# Carlito regular fits 1.390 at 7pt but 1.432 at 9pt. Sizes
|
|
95
|
+
# without a fitted entry use the style's default (7pt, the
|
|
96
|
+
# corpus's dominant size).
|
|
97
|
+
def factor_for(family, size_pt, weight, style)
|
|
98
|
+
style = STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s
|
|
99
|
+
table = data["factors"].fetch(resolve_family(family), {})
|
|
100
|
+
.fetch(style, {})
|
|
101
|
+
table[size_pt.to_i.to_s] || table["default"] || 1.0
|
|
81
102
|
end
|
|
82
103
|
|
|
83
|
-
def table_for(weight, style)
|
|
84
|
-
|
|
104
|
+
def table_for(family, weight, style)
|
|
105
|
+
style = STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s
|
|
106
|
+
data["advance"].fetch(resolve_family(family), {}).fetch(style, {})
|
|
85
107
|
end
|
|
86
108
|
|
|
87
109
|
def data
|
|
@@ -100,28 +122,46 @@ module Ea
|
|
|
100
122
|
format("U+%04X", ch.ord)
|
|
101
123
|
end
|
|
102
124
|
|
|
103
|
-
# Resolves
|
|
104
|
-
# (system index + downloaded formulas).
|
|
105
|
-
#
|
|
106
|
-
|
|
125
|
+
# Resolves a family's four style TTFs through the fontist gem
|
|
126
|
+
# (system index + downloaded formulas). Style variants are
|
|
127
|
+
# identified from each file's head.macStyle bits — filenames
|
|
128
|
+
# vary across fontist formulas and system copies ("ArialBd",
|
|
129
|
+
# "Arial Narrow Bold"), so name-matching is unreliable. The
|
|
130
|
+
# first file matching each style wins (fontist orders the
|
|
131
|
+
# downloaded formulas ahead of system copies). Empty when
|
|
132
|
+
# fontist is unavailable or the family is not installed.
|
|
133
|
+
def font_files_for(family)
|
|
107
134
|
require "fontist"
|
|
108
|
-
|
|
109
|
-
Fontist::SystemFont.find("Carlito").filter_map do |path|
|
|
135
|
+
Fontist::SystemFont.find(family).each_with_object({}) do |path, out|
|
|
110
136
|
next unless path.downcase.end_with?(".ttf")
|
|
111
137
|
|
|
112
|
-
style =
|
|
113
|
-
|
|
114
|
-
next unless match
|
|
138
|
+
style = style_from_mac_style(path)
|
|
139
|
+
next unless style
|
|
115
140
|
|
|
116
|
-
[
|
|
117
|
-
end
|
|
141
|
+
out[style] ||= path
|
|
142
|
+
end
|
|
118
143
|
rescue LoadError, StandardError => e
|
|
119
|
-
raise if ENV[
|
|
144
|
+
raise if ENV["EA_FONTS_DEBUG"]
|
|
120
145
|
|
|
121
146
|
{}
|
|
122
147
|
end
|
|
123
148
|
|
|
124
|
-
#
|
|
149
|
+
# head.macStyle: bit 0 = BOLD, bit 1 = ITALIC.
|
|
150
|
+
def style_from_mac_style(path)
|
|
151
|
+
require "fontisan"
|
|
152
|
+
font = Fontisan::FontLoader.load(path)
|
|
153
|
+
mac_style = font.table("head").mac_style
|
|
154
|
+
key = [mac_style & 1 == 1, mac_style & 2 == 2]
|
|
155
|
+
STYLE_KEYS[key]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Carlito's data-file name (historical) differs from the
|
|
159
|
+
# fontist family name used to resolve its TTFs.
|
|
160
|
+
def carlito_font_files
|
|
161
|
+
font_files_for("Carlito")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Reads advance widths from a TTF via the fontisan gem (used by
|
|
125
165
|
# `rake fonts:validate` to re-derive the tables).
|
|
126
166
|
def advances_from_font(path)
|
|
127
167
|
require "fontisan"
|
|
@@ -4,9 +4,12 @@ module Ea
|
|
|
4
4
|
module Svg
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Compartment
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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:
|
|
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
|
|
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? ||
|
|
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
|
|
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
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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 +
|
|
21
|
-
text_blocks =
|
|
22
|
-
|
|
23
|
-
bounds.x +
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
16
|
-
# │ │ when a stereotype is present
|
|
17
|
-
#
|
|
18
|
-
# │ │
|
|
19
|
-
# │ body │
|
|
20
|
-
# │ │
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
91
|
-
# not fit the stored box height (never truncates
|
|
92
|
-
# AOS box on CityFurniture hides its
|
|
93
|
-
# this way.
|
|
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 +
|
|
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 +
|
|
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,
|
|
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
|
-
|
|
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
data/lib/tasks/fonts.rake
CHANGED
|
@@ -3,30 +3,35 @@
|
|
|
3
3
|
require "ea/fonts/metrics"
|
|
4
4
|
|
|
5
5
|
namespace :fonts do
|
|
6
|
-
desc "Validate the shipped
|
|
6
|
+
desc "Validate the shipped font metrics against Fontist-resolved TTFs"
|
|
7
7
|
task :validate do
|
|
8
|
-
files = Ea::Fonts::Metrics.carlito_font_files
|
|
9
|
-
abort "Carlito not resolvable via Fontist — install it first" if files.empty?
|
|
10
|
-
|
|
11
8
|
shipped = JSON.parse(File.read(Ea::Fonts::Metrics::DATA_PATH))["advance"]
|
|
12
9
|
style_key = { "regular" => "regular", "bold" => "bold",
|
|
13
10
|
"italic" => "italic", "bold_italic" => "bold_italic",
|
|
14
11
|
"bolditalic" => "bold_italic" }
|
|
15
12
|
drift = []
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
shipped.keys.each do |family|
|
|
15
|
+
files = Ea::Fonts::Metrics.font_files_for(family)
|
|
16
|
+
if files.empty?
|
|
17
|
+
puts "#{family}: NOT RESOLVABLE VIA FONTIST — skipped"
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
files.each do |style, path|
|
|
22
|
+
table = shipped[family][style_key[style.downcase]] || {}
|
|
23
|
+
actual = Ea::Fonts::Metrics.advances_from_font(path)
|
|
24
|
+
missing = actual.keys.count { |cp| table[format("U+%04X", cp)].nil? }
|
|
25
|
+
puts " (shipped #{family}/#{style} table lacks #{missing} of #{actual.size} codepoints — skipped)"
|
|
26
|
+
actual.each do |cp, adv|
|
|
27
|
+
key = format("U+%04X", cp)
|
|
28
|
+
ours = table[key]
|
|
29
|
+
next if ours.nil? || (ours - adv).abs <= 0.0005
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
drift << format("%s/%s %s: json=%s font=%s", family, style, key, ours, adv)
|
|
32
|
+
end
|
|
33
|
+
puts "#{family}/#{style}: #{path} — #{actual.size} codepoints checked"
|
|
28
34
|
end
|
|
29
|
-
puts "#{style}: #{path} — #{actual.size} codepoints checked"
|
|
30
35
|
end
|
|
31
36
|
|
|
32
37
|
if drift.empty?
|
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.
|
|
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
|