ea 0.6.17 → 0.6.19
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/model/diagram_connector.rb +7 -0
- data/lib/ea/sources/qea/diagram_builder.rb +4 -0
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +11 -71
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +28 -2
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +5 -1
- data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +6 -2
- data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +4 -3
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +21 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +5 -13
- data/lib/ea/svg/ea_emitter/elements.rb +23 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +13 -22
- data/lib/ea/svg/ea_emitter/labels.rb +4 -0
- data/lib/ea/theme/definition.rb +1 -1
- data/lib/ea/version.rb +1 -1
- data/lib/tasks/fonts.rake +20 -15
- metadata +2 -2
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"
|
|
@@ -19,6 +19,11 @@ module Ea
|
|
|
19
19
|
attribute :target_edge, :integer
|
|
20
20
|
attribute :connector_type, :string
|
|
21
21
|
attribute :direction, :string
|
|
22
|
+
# t_connector.SourceStereotype / DestStereotype — EA renders
|
|
23
|
+
# the «Property» end-label exactly on the stereotyped end
|
|
24
|
+
# (corpus-verified; direction alone does not decide).
|
|
25
|
+
attribute :source_stereotype, :string
|
|
26
|
+
attribute :target_stereotype, :string
|
|
22
27
|
attribute :waypoints, Waypoint, collection: true, initialize_empty: true
|
|
23
28
|
attribute :label, :string
|
|
24
29
|
attribute :style, :hash, default: -> { {} }
|
|
@@ -54,6 +59,8 @@ module Ea
|
|
|
54
59
|
map "targetPort", to: :target_port
|
|
55
60
|
map "connectorType", to: :connector_type
|
|
56
61
|
map "direction", to: :direction
|
|
62
|
+
map "sourceStereotype", to: :source_stereotype
|
|
63
|
+
map "targetStereotype", to: :target_stereotype
|
|
57
64
|
map "waypoints", to: :waypoints, render_empty: true
|
|
58
65
|
map "label", to: :label
|
|
59
66
|
map "style", to: :style
|
|
@@ -199,6 +199,8 @@ module Ea
|
|
|
199
199
|
relationship_ref: ref_for_raw_connector(conn),
|
|
200
200
|
connector_type: conn.connector_type,
|
|
201
201
|
direction: conn.direction,
|
|
202
|
+
source_stereotype: conn.sourcestereotype,
|
|
203
|
+
target_stereotype: conn.deststereotype,
|
|
202
204
|
waypoints: waypoints,
|
|
203
205
|
label_boxes: DEFAULT_LABEL_BOXES,
|
|
204
206
|
style: {},
|
|
@@ -240,6 +242,8 @@ module Ea
|
|
|
240
242
|
relationship_ref: ref_for_connector(link_row),
|
|
241
243
|
connector_type: connector&.connector_type,
|
|
242
244
|
direction: connector&.direction,
|
|
245
|
+
source_stereotype: connector&.sourcestereotype,
|
|
246
|
+
target_stereotype: connector&.deststereotype,
|
|
243
247
|
waypoints: waypoints_for_link(link_row, diagram_row),
|
|
244
248
|
source_port: connection_port(diagram_row, connector, :source, geom),
|
|
245
249
|
target_port: connection_port(diagram_row, connector, :target, geom),
|
|
@@ -8,20 +8,14 @@ module Ea
|
|
|
8
8
|
# stays a small value-object with just coordinate translation
|
|
9
9
|
# and formatting concerns.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# Each source is a method returning an Array<[x, y]> points.
|
|
20
|
-
# Composing new sources = adding a method, no modification of
|
|
21
|
-
# existing ones (OCP).
|
|
11
|
+
# Corpus-verified against 3,663 EA-published reference SVGs:
|
|
12
|
+
# EA's canvas covers the ELEMENT RECTS only — connector
|
|
13
|
+
# waypoints, arrow markers, and package-tab overhangs do NOT
|
|
14
|
+
# extend it. Width = element extent + 85 is exact on 72% of
|
|
15
|
+
# the corpus; height = element extent + 76 exact on 50% (the
|
|
16
|
+
# remainder carry invisible per-diagram reservations below
|
|
17
|
+
# the content).
|
|
22
18
|
class BoundsCalculator
|
|
23
|
-
MARKER_EXTENT = 15
|
|
24
|
-
PACKAGE_TAB_HEIGHT = 20
|
|
25
19
|
# EA's canvas includes non-uniform frame insets around the
|
|
26
20
|
# element content. Reverse-engineered from reference SVG
|
|
27
21
|
# byte-diff: maintenance diagram has element 169x80 at
|
|
@@ -30,11 +24,12 @@ module Ea
|
|
|
30
24
|
# canvas_width = element_width + INSET_LEFT + INSET_RIGHT
|
|
31
25
|
# canvas_height = element_height + INSET_TOP + INSET_BOTTOM
|
|
32
26
|
# The top inset accommodates the frame tab + label space
|
|
33
|
-
# above the element.
|
|
27
|
+
# above the element. The bottom inset 36 matches the
|
|
28
|
+
# dominant corpus mode (50% of 3,663 diagrams).
|
|
34
29
|
INSET_LEFT = 35
|
|
35
30
|
INSET_RIGHT = 50
|
|
36
31
|
INSET_TOP = 40
|
|
37
|
-
INSET_BOTTOM =
|
|
32
|
+
INSET_BOTTOM = 36
|
|
38
33
|
|
|
39
34
|
attr_reader :diagram, :model_index
|
|
40
35
|
|
|
@@ -44,11 +39,7 @@ module Ea
|
|
|
44
39
|
end
|
|
45
40
|
|
|
46
41
|
def compute
|
|
47
|
-
points =
|
|
48
|
-
points.concat(element_points)
|
|
49
|
-
points.concat(connector_points)
|
|
50
|
-
points.concat(marker_points)
|
|
51
|
-
points.concat(package_tab_points)
|
|
42
|
+
points = element_points
|
|
52
43
|
return [0, 0, 1, 1] if points.empty?
|
|
53
44
|
|
|
54
45
|
xs = points.map(&:first)
|
|
@@ -85,57 +76,6 @@ module Ea
|
|
|
85
76
|
end
|
|
86
77
|
pts
|
|
87
78
|
end
|
|
88
|
-
|
|
89
|
-
# Package elements render with a tab polygon ABOVE the
|
|
90
|
-
# element's logical top edge (height PACKAGE_TAB_HEIGHT).
|
|
91
|
-
# Include those points so the canvas reserves space.
|
|
92
|
-
def package_tab_points
|
|
93
|
-
return [] if model_index.nil?
|
|
94
|
-
|
|
95
|
-
pts = []
|
|
96
|
-
(diagram.elements || []).each do |e|
|
|
97
|
-
ref = e.model_element_ref
|
|
98
|
-
next unless ref
|
|
99
|
-
|
|
100
|
-
candidate = model_index[ref]
|
|
101
|
-
next unless candidate.is_a?(Ea::Model::Package)
|
|
102
|
-
|
|
103
|
-
primary = e.bounds || e.image_bounds
|
|
104
|
-
next unless primary
|
|
105
|
-
|
|
106
|
-
pts << [primary.x, primary.y - PACKAGE_TAB_HEIGHT]
|
|
107
|
-
pts << [primary.x + primary.width, primary.y]
|
|
108
|
-
end
|
|
109
|
-
pts
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def connector_points
|
|
113
|
-
pts = []
|
|
114
|
-
(diagram.connectors || []).each do |c|
|
|
115
|
-
(c.waypoints || []).each do |wp|
|
|
116
|
-
next unless wp.position
|
|
117
|
-
|
|
118
|
-
pts << [wp.position.x, wp.position.y]
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
pts
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def marker_points
|
|
125
|
-
pts = []
|
|
126
|
-
(diagram.connectors || []).each do |c|
|
|
127
|
-
waypoints = (c.waypoints || []).map(&:position).compact
|
|
128
|
-
next unless waypoints.size >= 2
|
|
129
|
-
|
|
130
|
-
source = waypoints.first
|
|
131
|
-
target = waypoints.last
|
|
132
|
-
pts << [source.x - MARKER_EXTENT, source.y - MARKER_EXTENT]
|
|
133
|
-
pts << [source.x + MARKER_EXTENT, source.y + MARKER_EXTENT]
|
|
134
|
-
pts << [target.x - MARKER_EXTENT, target.y - MARKER_EXTENT]
|
|
135
|
-
pts << [target.x + MARKER_EXTENT, target.y + MARKER_EXTENT]
|
|
136
|
-
end
|
|
137
|
-
pts
|
|
138
|
-
end
|
|
139
79
|
end
|
|
140
80
|
end
|
|
141
81
|
end
|
|
@@ -21,10 +21,16 @@ module Ea
|
|
|
21
21
|
%(<g style="#{group_style(context)}">\n#{text_blocks.join("\n")}\n</g>)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# EA wraps note text by MEASURED width (Carlito advances),
|
|
25
|
+
# not character count: lines break at the last word whose
|
|
26
|
+
# cumulative integer textLength fits bounds.width - 15.
|
|
27
|
+
# Verified against EA-published reference SVGs.
|
|
24
28
|
def wrapped_lines(context, body)
|
|
25
|
-
|
|
29
|
+
usable = context.bounds.width - 15
|
|
30
|
+
size = context.size
|
|
31
|
+
family = context.family
|
|
26
32
|
body.to_s.split(/\n/).flat_map do |para|
|
|
27
|
-
para
|
|
33
|
+
wrap_paragraph(para, usable, size, family)
|
|
28
34
|
end.each_with_index.map do |line, idx|
|
|
29
35
|
y = context.bounds.y + context.theme.note.text_y_offset +
|
|
30
36
|
(idx * context.theme.note.line_height)
|
|
@@ -40,6 +46,26 @@ module Ea
|
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
def wrap_paragraph(para, usable, size, family)
|
|
50
|
+
words = para.strip.split(/\s+/)
|
|
51
|
+
lines = []
|
|
52
|
+
current = ""
|
|
53
|
+
words.each do |word|
|
|
54
|
+
candidate = current.empty? ? word : "#{current} #{word}"
|
|
55
|
+
width = TextRenderer.estimate_width(candidate, size, nil,
|
|
56
|
+
family: family).round
|
|
57
|
+
if current.empty? || width <= usable
|
|
58
|
+
current = candidate
|
|
59
|
+
else
|
|
60
|
+
lines << current
|
|
61
|
+
current = word
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
lines << current unless current.empty?
|
|
65
|
+
lines
|
|
66
|
+
end
|
|
67
|
+
module_function :wrap_paragraph
|
|
68
|
+
|
|
43
69
|
def group_style(context)
|
|
44
70
|
"stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; " \
|
|
45
71
|
"fill:#{context.theme.text_color};fill-opacity:1.00; " \
|
|
@@ -67,7 +67,11 @@ module Ea
|
|
|
67
67
|
lookup: lookup).to_s
|
|
68
68
|
end
|
|
69
69
|
else
|
|
70
|
-
|
|
70
|
+
# EA lists attribute rows alphabetically (case-
|
|
71
|
+
# insensitive) — corpus-verified: 991 boxes strictly
|
|
72
|
+
# alphabetical vs 24 strictly stored-Pos order.
|
|
73
|
+
props.sort_by { |p| [p.name.to_s.downcase, p.name.to_s] }
|
|
74
|
+
.flat_map do |prop|
|
|
71
75
|
lines = []
|
|
72
76
|
stereotype = property_stereotype(prop)
|
|
73
77
|
lines << "«#{stereotype}»" if stereotype
|
|
@@ -13,9 +13,13 @@ module Ea
|
|
|
13
13
|
# Rendered as a third compartment below the attribute
|
|
14
14
|
# compartment for Enumeration classifiers.
|
|
15
15
|
class EnumerationLiteralRenderer
|
|
16
|
+
# EA lists literals alphabetically (case-insensitive) and
|
|
17
|
+
# spaces rows at the same 13px pitch as attributes at 7pt
|
|
18
|
+
# (size + 6) — corpus-verified against EA-published SVGs.
|
|
16
19
|
def self.render(literals, bounds:, first_y:, family:, size:)
|
|
17
|
-
line_h = size +
|
|
18
|
-
|
|
20
|
+
line_h = size + 6
|
|
21
|
+
sorted = literals.sort_by { |l| [l.name.to_s.downcase, l.name.to_s] }
|
|
22
|
+
text_blocks = sorted.each_with_index.map do |literal, idx|
|
|
19
23
|
y = first_y + (idx * line_h)
|
|
20
24
|
build_text(bounds.x + 5, y, literal.name.to_s, family, size)
|
|
21
25
|
end
|
|
@@ -11,16 +11,17 @@ module Ea
|
|
|
11
11
|
#
|
|
12
12
|
# Context (Struct) carries everything a provider might need:
|
|
13
13
|
# classifier, diagram_package_id, visually_nested,
|
|
14
|
-
# umldi_keyword, bounds_width, font_size,
|
|
15
|
-
# off_canvas_parent_name
|
|
14
|
+
# umldi_keyword, bounds_width, font_size, family,
|
|
15
|
+
# off_canvas_parent_name, foreign_package_name
|
|
16
16
|
#
|
|
17
17
|
# Each provider implements:
|
|
18
18
|
# def self.call(context) → Array<[[text, style], ...]>
|
|
19
19
|
module HeaderLinePipeline
|
|
20
20
|
Context = Struct.new(:classifier, :diagram_package_id,
|
|
21
21
|
:visually_nested, :umldi_keyword,
|
|
22
|
-
:bounds_width, :font_size,
|
|
22
|
+
:bounds_width, :font_size, :family,
|
|
23
23
|
:off_canvas_parent_name,
|
|
24
|
+
:foreign_package_name,
|
|
24
25
|
keyword_init: true)
|
|
25
26
|
|
|
26
27
|
PROVIDERS = [
|
|
@@ -8,16 +8,19 @@ module Ea
|
|
|
8
8
|
# Emits the classifier's display name, with these behaviors:
|
|
9
9
|
#
|
|
10
10
|
# - Bold-italic for abstract Klass; bold otherwise.
|
|
11
|
-
# -
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# - Foreign elements (owning package ≠ the diagram's package,
|
|
12
|
+
# gated on t_diagram.ShowForeign) render the qualified
|
|
13
|
+
# name "OwningPackage::Name".
|
|
14
|
+
# - Qualified-name wrap: when the combined integer
|
|
15
|
+
# textLength exceeds the element bounds width, EA splits
|
|
16
|
+
# "pkg::Class" across two lines ("pkg::" then "Class"),
|
|
17
|
+
# each centered independently. Corpus-verified: every
|
|
18
|
+
# wrapped header overflows its box and every single-line
|
|
19
|
+
# header fits (tightest observed slack 6px).
|
|
14
20
|
#
|
|
15
21
|
# InstanceSpecification returns [] (InstanceSpec provider
|
|
16
22
|
# already emitted the full label).
|
|
17
23
|
class Name
|
|
18
|
-
QUALIFIED_WRAP_WIDTH_FACTOR = 0.55
|
|
19
|
-
QUALIFIED_WRAP_PADDING = 8
|
|
20
|
-
|
|
21
24
|
def self.call(context)
|
|
22
25
|
classifier = context.classifier
|
|
23
26
|
return [] if classifier.is_a?(Ea::Model::InstanceSpecification)
|
|
@@ -34,9 +37,9 @@ module Ea
|
|
|
34
37
|
|
|
35
38
|
def self.wrapped_name_lines(classifier, context, weight)
|
|
36
39
|
name = display_name(classifier)
|
|
40
|
+
name = "#{context.foreign_package_name}::#{name}" if context.foreign_package_name
|
|
37
41
|
unless context.bounds_width && name.include?("::") &&
|
|
38
|
-
name_exceeds_bounds?(name, context
|
|
39
|
-
context.font_size || 9)
|
|
42
|
+
name_exceeds_bounds?(name, context, weight)
|
|
40
43
|
return [[name, weight]]
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -44,15 +47,18 @@ module Ea
|
|
|
44
47
|
[["#{qualifier}::", weight], [base, weight]]
|
|
45
48
|
end
|
|
46
49
|
|
|
47
|
-
def self.name_exceeds_bounds?(name,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
def self.name_exceeds_bounds?(name, context, weight)
|
|
51
|
+
font_style = weight == :bold_italic ? "italic" : "normal"
|
|
52
|
+
len = TextRenderer.estimate_width(
|
|
53
|
+
name, context.font_size || 9, nil,
|
|
54
|
+
family: context.family, weight: "700", style: font_style
|
|
55
|
+
).round
|
|
56
|
+
len > context.bounds_width
|
|
51
57
|
end
|
|
52
58
|
|
|
53
|
-
# EA renders the plain classifier name in element headers
|
|
54
|
-
#
|
|
55
|
-
#
|
|
59
|
+
# EA renders the plain classifier name in element headers;
|
|
60
|
+
# qualification is layered on by the pipeline via
|
|
61
|
+
# context.foreign_package_name.
|
|
56
62
|
def self.display_name(classifier, *_)
|
|
57
63
|
classifier.name.to_s
|
|
58
64
|
end
|
|
@@ -51,20 +51,12 @@ module Ea
|
|
|
51
51
|
refs.map { |ref| "«#{canonical(ref)}»" }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# EA
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# against EA-published reference SVGs.
|
|
59
|
-
DISPLAY_OVERRIDES = {
|
|
60
|
-
"property" => "Property",
|
|
61
|
-
"featuretype" => "FeatureType",
|
|
62
|
-
"objecttype" => "ObjectType",
|
|
63
|
-
"codelist" => "CodeList"
|
|
64
|
-
}.freeze
|
|
65
|
-
|
|
54
|
+
# EA renders the stereotype string exactly as stored in
|
|
55
|
+
# t_object.Stereotype — corpus-verified: 3,284 placements
|
|
56
|
+
# render verbatim ('CodeList', 'codeList', 'codelist',
|
|
57
|
+
# 'featureType' …), zero case-canonicalized.
|
|
66
58
|
def self.canonical(ref)
|
|
67
|
-
|
|
59
|
+
ref
|
|
68
60
|
end
|
|
69
61
|
|
|
70
62
|
def self.fallback_name(classifier)
|
|
@@ -69,7 +69,9 @@ module Ea
|
|
|
69
69
|
umldi_keyword: element.umldi_keyword,
|
|
70
70
|
bounds_width: raw_bounds&.width,
|
|
71
71
|
font_size: size,
|
|
72
|
-
|
|
72
|
+
family: family,
|
|
73
|
+
off_canvas_parent_name: parent_name,
|
|
74
|
+
foreign_package_name: foreign_package_name_for(classifier)) : []
|
|
73
75
|
is_classifier = classifier.is_a?(Ea::Model::Classifier)
|
|
74
76
|
attr_lines = if is_classifier && show_attributes?
|
|
75
77
|
Element::AttributeRenderer.lines_for(
|
|
@@ -214,6 +216,26 @@ module Ea
|
|
|
214
216
|
Ea::Model::Constraint.new(name: name, kind: "Marker")
|
|
215
217
|
end
|
|
216
218
|
|
|
219
|
+
# EA qualifies a placed classifier's header with its owning
|
|
220
|
+
# package name ("OwningPackage::Name") exactly when the
|
|
221
|
+
# diagram's t_diagram.ShowForeign flag is set AND the
|
|
222
|
+
# classifier lives in a different package than the diagram
|
|
223
|
+
# (descendant packages included). Corpus-verified (450
|
|
224
|
+
# diagrams): ShowForeign=0 renders plain everywhere.
|
|
225
|
+
def foreign_package_name_for(classifier)
|
|
226
|
+
return nil unless diagram.show_foreign
|
|
227
|
+
return nil unless classifier.is_a?(Ea::Model::Classifier)
|
|
228
|
+
|
|
229
|
+
pkg_id = classifier.package_id
|
|
230
|
+
return nil if pkg_id.nil? || pkg_id == diagram.package_id
|
|
231
|
+
|
|
232
|
+
pkg = model_index ? model_index[pkg_id] : nil
|
|
233
|
+
return nil unless pkg.is_a?(Ea::Model::Package)
|
|
234
|
+
|
|
235
|
+
name = pkg.name.to_s
|
|
236
|
+
name.empty? ? nil : name
|
|
237
|
+
end
|
|
238
|
+
|
|
217
239
|
# EA renders the off-canvas parent classifier's name as an
|
|
218
240
|
# italic line at the top of a placed element's header when
|
|
219
241
|
# ALL of these hold:
|
|
@@ -61,7 +61,7 @@ module Ea
|
|
|
61
61
|
text_pos, mult_pos = tree_positions(end_kind, anchor, role,
|
|
62
62
|
text_pos, mult_pos)
|
|
63
63
|
end
|
|
64
|
-
show_property = property_label?(connector)
|
|
64
|
+
show_property = property_label?(connector, end_kind)
|
|
65
65
|
|
|
66
66
|
build_texts(role:, mult:, standalone:,
|
|
67
67
|
text_pos:, mult_pos:, show_property:)
|
|
@@ -113,27 +113,18 @@ module Ea
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# EA renders «
|
|
117
|
-
#
|
|
118
|
-
# t_connector.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# 137 diagrams).
|
|
129
|
-
UNDIRECTED_DIRECTIONS = %w[Unspecified Bi-Directional].freeze
|
|
130
|
-
|
|
131
|
-
def property_label?(connector)
|
|
132
|
-
direction = connector.direction.to_s
|
|
133
|
-
return false if direction.empty?
|
|
134
|
-
return false if UNDIRECTED_DIRECTIONS.include?(direction)
|
|
135
|
-
|
|
136
|
-
true
|
|
116
|
+
# EA renders «Property» between role name and multiplicity
|
|
117
|
+
# exactly on connector ends carrying
|
|
118
|
+
# t_connector.SourceStereotype / DestStereotype = "Property"
|
|
119
|
+
# (case-insensitive; stored variants include "property").
|
|
120
|
+
# Direction does not decide — Fig 11's directed aggregations
|
|
121
|
+
# carry no end stereotype and render none, while the CityGML
|
|
122
|
+
# profile diagrams mark ends explicitly.
|
|
123
|
+
PROPERTY_KEYWORD = "property"
|
|
124
|
+
|
|
125
|
+
def property_label?(connector, end_kind)
|
|
126
|
+
stereo = end_kind == :source ? connector.source_stereotype : connector.target_stereotype
|
|
127
|
+
stereo.to_s.downcase == PROPERTY_KEYWORD
|
|
137
128
|
end
|
|
138
129
|
|
|
139
130
|
def role_empty?(role)
|
|
@@ -47,6 +47,10 @@ module Ea
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def texts_for(connector)
|
|
50
|
+
# EA never labels NoteLink connectors — the note text lives
|
|
51
|
+
# in t_connector.Name but renders only inside the Note box.
|
|
52
|
+
return [] if connector.connector_type.to_s.casecmp("NoteLink").zero?
|
|
53
|
+
|
|
50
54
|
points = waypoint_pairs(connector)
|
|
51
55
|
return [] if points.size < 2
|
|
52
56
|
|
data/lib/ea/theme/definition.rb
CHANGED
|
@@ -62,7 +62,7 @@ module Ea
|
|
|
62
62
|
tab_label_padding: 10,
|
|
63
63
|
default_tab_width: 105).freeze
|
|
64
64
|
DEFAULT_NOTE = NoteSpec.new(fold_size: 12, text_x_offset: 5,
|
|
65
|
-
text_y_offset: 12, line_height:
|
|
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
68
|
font_size_unit: "pt", text_weight_normal: 400,
|
data/lib/ea/version.rb
CHANGED