ea 0.6.120 → 0.6.122
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/config/themes/119.yml +2 -4
- data/lib/ea/fonts/carlito_metrics.json +1 -1
- data/lib/ea/fonts/metrics.rb +12 -2
- data/lib/ea/model/diagram_element.rb +3 -0
- data/lib/ea/sources/qea/diagram_builder.rb +3 -2
- data/lib/ea/svg/ea_emitter/color_resolver.rb +1 -1
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +13 -12
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +12 -1
- data/lib/ea/svg/ea_emitter/connector_redock.rb +3 -2
- data/lib/ea/svg/ea_emitter/connectors.rb +51 -9
- data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +9 -6
- data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +4 -44
- data/lib/ea/svg/ea_emitter/elements.rb +34 -14
- data/lib/ea/svg/ea_emitter/font_resolver.rb +18 -3
- data/lib/ea/svg/ea_emitter/label/end_label.rb +69 -12
- data/lib/ea/svg/ea_emitter/labels.rb +24 -6
- data/lib/ea/svg/ea_emitter/marker/arrow_path.rb +12 -1
- data/lib/ea/svg/ea_emitter/marker/diamond.rb +48 -3
- data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +1 -1
- data/lib/ea/svg/ea_emitter/marker/package_import.rb +6 -19
- data/lib/ea/svg/ea_emitter/marker/plus.rb +9 -17
- data/lib/ea/svg/ea_emitter/markers.rb +64 -73
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
data/lib/ea/fonts/metrics.rb
CHANGED
|
@@ -51,6 +51,9 @@ module Ea
|
|
|
51
51
|
# Advance used for codepoints missing from the tables.
|
|
52
52
|
DEFAULT_ADVANCE = 0.25
|
|
53
53
|
|
|
54
|
+
# Inferred subpixel steps that reproduce EA's near-half-pixel rounding.
|
|
55
|
+
GDI_SUBPIXELS = 64
|
|
56
|
+
|
|
54
57
|
# Carlito/Arial have no CJK glyphs; EA's renderer falls back to
|
|
55
58
|
# a proportional CJK font whose advances measure ~0.709em at
|
|
56
59
|
# the NOMINAL size (not scaled by the per-style factors) —
|
|
@@ -89,6 +92,13 @@ module Ea
|
|
|
89
92
|
(size.to_f * 10 / 7).round
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
# Quantize before rounding: near-half-pixel Carlito advances
|
|
96
|
+
# round up in EA's output. The 1/64px step is inferred.
|
|
97
|
+
def gdi_pixel_advance(advance, height)
|
|
98
|
+
((advance * height * GDI_SUBPIXELS).round / GDI_SUBPIXELS.to_f).round
|
|
99
|
+
end
|
|
100
|
+
private_class_method :gdi_pixel_advance
|
|
101
|
+
|
|
92
102
|
def compact_compartment_font?(size, family:, unit: "pt")
|
|
93
103
|
family.to_s.casecmp("Cambria").zero? && size == 7 && unit == "pt"
|
|
94
104
|
end
|
|
@@ -122,7 +132,7 @@ module Ea
|
|
|
122
132
|
if cjk?(ch.ord)
|
|
123
133
|
cjk_count += 1
|
|
124
134
|
else
|
|
125
|
-
latin += (
|
|
135
|
+
latin += gdi_pixel_advance(table[cp_key(ch)] || DEFAULT_ADVANCE, height)
|
|
126
136
|
end
|
|
127
137
|
end
|
|
128
138
|
return latin + cjk_count * CJK_ADVANCE * size_pt.to_f
|
|
@@ -166,7 +176,7 @@ module Ea
|
|
|
166
176
|
|
|
167
177
|
# Families whose EA driver quantizes per-glyph advances
|
|
168
178
|
# (fitted from published textLengths).
|
|
169
|
-
GDI_HEIGHT_FAMILIES = %w[Carlito].freeze
|
|
179
|
+
GDI_HEIGHT_FAMILIES = %w[Carlito Arial].freeze
|
|
170
180
|
|
|
171
181
|
def supported_family?(family)
|
|
172
182
|
!resolve_family(family).nil?
|
|
@@ -22,6 +22,8 @@ module Ea
|
|
|
22
22
|
attribute :line_width, :integer
|
|
23
23
|
attribute :font_family, :string
|
|
24
24
|
attribute :font_size, :integer
|
|
25
|
+
# nil: unspecified provenance; true: source delegates to default resolution.
|
|
26
|
+
attribute :font_size_inherited, :boolean
|
|
25
27
|
attribute :font_logical_height, :integer # original GDI pixel height before point normalization
|
|
26
28
|
attribute :font_size_unit, :string # source font size unit, before theme fallback
|
|
27
29
|
attribute :font_bold, :boolean
|
|
@@ -55,6 +57,7 @@ module Ea
|
|
|
55
57
|
map "lineWidth", to: :line_width
|
|
56
58
|
map "fontFamily", to: :font_family
|
|
57
59
|
map "fontSize", to: :font_size
|
|
60
|
+
map "fontSizeInherited", to: :font_size_inherited, render_default: true
|
|
58
61
|
map "fontSizeUnit", to: :font_size_unit
|
|
59
62
|
map "fontLogicalHeight", to: :font_logical_height
|
|
60
63
|
map "fontBold", to: :font_bold, render_default: true
|
|
@@ -138,6 +138,7 @@ module Ea
|
|
|
138
138
|
style: style_hash,
|
|
139
139
|
font_family: presence(font_style[:font]),
|
|
140
140
|
font_size: font_size_from(font_style),
|
|
141
|
+
font_size_inherited: font_size_from(font_style).nil?,
|
|
141
142
|
font_size_unit: "pt",
|
|
142
143
|
font_logical_height: font_logical_height_from(font_style),
|
|
143
144
|
font_bold: truthy?(style_hash[:bold]),
|
|
@@ -150,14 +151,14 @@ module Ea
|
|
|
150
151
|
)
|
|
151
152
|
end
|
|
152
153
|
|
|
153
|
-
# Negative widths inherit
|
|
154
|
+
# Negative widths inherit; QEA zero widths request a one-pixel cosmetic pen.
|
|
154
155
|
def effective_line_width(obj_row, style)
|
|
155
156
|
object = database.find_object(obj_row.ea_object_id)
|
|
156
157
|
[style[:lwth], object&.borderwidth].each do |raw|
|
|
157
158
|
next if raw.nil? || raw.to_s.empty?
|
|
158
159
|
|
|
159
160
|
width = raw.to_i
|
|
160
|
-
return width if width >= 0
|
|
161
|
+
return [width, 1].max if width >= 0
|
|
161
162
|
end
|
|
162
163
|
nil
|
|
163
164
|
end
|
|
@@ -6,7 +6,7 @@ module Ea
|
|
|
6
6
|
# Single source of truth for fill + stroke color decisions.
|
|
7
7
|
#
|
|
8
8
|
# Theme :119 applies:
|
|
9
|
-
# -
|
|
9
|
+
# - Black default element strokes
|
|
10
10
|
# - Method color for operation text
|
|
11
11
|
# Theme :119 does NOT apply:
|
|
12
12
|
# - Per-type fill colors (elements use default #FFFFFF)
|
|
@@ -21,13 +21,8 @@ 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.
|
|
28
24
|
def wrapped_lines(context, body)
|
|
29
|
-
#
|
|
30
|
-
# (9BA1CE54: width 214, longest line tl 210). Pitch and
|
|
25
|
+
# Lines fit bounds.width - 4. Pitch and
|
|
31
26
|
# first-line offset scale with the note font (9pt: pitch
|
|
32
27
|
# 16 = size+7, first line at bounds.y + 18 = size+9), and
|
|
33
28
|
# each paragraph break adds one blank pitch row
|
|
@@ -41,7 +36,8 @@ module Ea
|
|
|
41
36
|
y_index = 0
|
|
42
37
|
paragraphs = NoteHtml.paragraphs(body)
|
|
43
38
|
paragraphs.flat_map do |para|
|
|
44
|
-
lines = wrap_paragraph(para.text, usable, size, context.family
|
|
39
|
+
lines = wrap_paragraph(para.text, usable, size, context.family,
|
|
40
|
+
logical_height: context.logical_height)
|
|
45
41
|
paragraph_svg = lines.each_with_index.map do |line, i|
|
|
46
42
|
y = context.bounds.y + first_offset + ((y_index + i) * pitch)
|
|
47
43
|
TextRenderer.new(
|
|
@@ -50,6 +46,7 @@ module Ea
|
|
|
50
46
|
y: y,
|
|
51
47
|
family: context.family, size: context.size,
|
|
52
48
|
size_unit: context.size_unit,
|
|
49
|
+
logical_height: context.logical_height,
|
|
53
50
|
fill: context.theme.text_color,
|
|
54
51
|
stroke_in_text: context.theme.stroke_in_text_color,
|
|
55
52
|
width_factor: context.theme.text_width_factor
|
|
@@ -60,14 +57,18 @@ module Ea
|
|
|
60
57
|
end
|
|
61
58
|
end
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
# Wrap at whitespace until native script-specific break metrics are
|
|
61
|
+
# established. Every word but the paragraph's last carries its
|
|
62
|
+
# trailing space into the fit test.
|
|
63
|
+
def wrap_paragraph(para, usable, size, family, logical_height: nil)
|
|
65
64
|
lines = []
|
|
66
65
|
current = ""
|
|
67
|
-
words
|
|
66
|
+
words = para.strip.split(/\s+/)
|
|
67
|
+
words.each_with_index do |word, index|
|
|
68
68
|
candidate = current.empty? ? word : "#{current} #{word}"
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
trailing = index < words.size - 1 ? " " : ""
|
|
70
|
+
width = TextRenderer.estimate_width("#{candidate}#{trailing}", size, nil,
|
|
71
|
+
family: family, logical_height: logical_height).round
|
|
71
72
|
if current.empty? || width <= usable
|
|
72
73
|
current = candidate
|
|
73
74
|
else
|
|
@@ -15,7 +15,7 @@ module Ea
|
|
|
15
15
|
context.bounds,
|
|
16
16
|
legend: context.model_element.legend,
|
|
17
17
|
family: context.family,
|
|
18
|
-
item_font: { size: context
|
|
18
|
+
item_font: { size: legend_item_size(context), unit: context.size_unit,
|
|
19
19
|
logical_height: context.logical_height }
|
|
20
20
|
)
|
|
21
21
|
elsif context.model_element.is_a?(Ea::Model::Note)
|
|
@@ -54,6 +54,17 @@ module Ea
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def legend_item_size(context)
|
|
58
|
+
element = context.element
|
|
59
|
+
return unless element
|
|
60
|
+
return element.font_size if element.font_size&.nonzero?
|
|
61
|
+
|
|
62
|
+
# Parsed source defaults inherit; unspecified callers keep 9pt.
|
|
63
|
+
inherited = element.font_size_inherited || element.style&.fetch(:fontsz, nil).to_s == "0"
|
|
64
|
+
context.size if inherited
|
|
65
|
+
end
|
|
66
|
+
private_class_method :legend_item_size
|
|
67
|
+
|
|
57
68
|
# A Text element with LegendOpts= renders as the legend
|
|
58
69
|
# block rather than a folder silhouette. Dispatch on the
|
|
59
70
|
# presence of the attached Legend payload.
|
|
@@ -233,7 +233,8 @@ module Ea
|
|
|
233
233
|
private_class_method :facing_coordinate
|
|
234
234
|
|
|
235
235
|
def stroke_outline?(connector, bounds)
|
|
236
|
-
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector)
|
|
236
|
+
(connector.style.to_h[:lwidth] == "2" || default_model_connector?(connector) ||
|
|
237
|
+
connector.connector_type == "NoteLink") &&
|
|
237
238
|
bounds.all? { |box| box.width.positive? && box.height.positive? } &&
|
|
238
239
|
outline_connector?(connector, bounds)
|
|
239
240
|
end
|
|
@@ -311,7 +312,7 @@ module Ea
|
|
|
311
312
|
|
|
312
313
|
def outline_connector?(connector, bounds)
|
|
313
314
|
return true if default_model_connector?(connector)
|
|
314
|
-
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
315
|
+
return true if %w[Generalization Aggregation NoteLink].include?(connector.connector_type)
|
|
315
316
|
|
|
316
317
|
return true if association_ray_outline?(connector)
|
|
317
318
|
return false unless stored_association?(connector) || offset_free_association?(connector)
|
|
@@ -31,22 +31,39 @@ module Ea
|
|
|
31
31
|
@divider_y_by_ref = divider_y_by_ref
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# EA draws each connector (and its markers) with the pen width
|
|
35
|
+
# stored in the diagram link style (`LWidth`); links without
|
|
36
|
+
# the field keep the diagram default.
|
|
37
|
+
def self.stroke_width_for(connector, default)
|
|
38
|
+
style = connector.style || {}
|
|
39
|
+
raw = style[:lwidth] || style["lwidth"]
|
|
40
|
+
return default if raw.to_s.strip.empty?
|
|
41
|
+
|
|
42
|
+
[raw.to_i, 1].max
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Layer key for a style group: the plain key at the default
|
|
46
|
+
# width, suffixed with the width otherwise so layers of
|
|
47
|
+
# different widths never merge.
|
|
48
|
+
def self.layer_key(key, width, default)
|
|
49
|
+
width == default ? key : :"#{key}_w#{width}"
|
|
50
|
+
end
|
|
51
|
+
|
|
34
52
|
def layers
|
|
35
|
-
|
|
36
|
-
return [] if paths.empty?
|
|
53
|
+
styles = paths_with_styles
|
|
37
54
|
|
|
38
55
|
if grouped
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
paths.map do |path|
|
|
43
|
-
Layer.new(style_key: LINE_STYLE_KEY, style: line_style, body: path)
|
|
56
|
+
styles.group_by(&:first).map do |(width, dashed), entries|
|
|
57
|
+
line_layer(width, dashed, entries.map(&:last).join("\n "))
|
|
44
58
|
end
|
|
59
|
+
else
|
|
60
|
+
styles.map { |(width, dashed), path| line_layer(width, dashed, path) }
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
|
|
48
|
-
def line_style
|
|
49
|
-
"stroke-width:#{
|
|
64
|
+
def line_style(width = stroke_width)
|
|
65
|
+
"stroke-width:#{width};stroke-linecap:round;stroke-linejoin:bevel; " \
|
|
66
|
+
"fill:#000000;fill-opacity:0.00; stroke:#000000; stroke-opacity:1.00"
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
# Backwards-compatible render — joins all layers' `<g>`.
|
|
@@ -56,6 +73,31 @@ module Ea
|
|
|
56
73
|
|
|
57
74
|
private
|
|
58
75
|
|
|
76
|
+
def paths_with_styles
|
|
77
|
+
visible_connectors.filter_map do |connector|
|
|
78
|
+
path = path_for(connector)
|
|
79
|
+
next unless path
|
|
80
|
+
|
|
81
|
+
[[self.class.stroke_width_for(connector, stroke_width), dashed?(connector)], path]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def line_layer(width, dashed, body)
|
|
86
|
+
key = dashed ? :connector_line_dashed : LINE_STYLE_KEY
|
|
87
|
+
style = line_style(width)
|
|
88
|
+
style += ";stroke-dasharray:6,3" if dashed
|
|
89
|
+
Layer.new(style_key: self.class.layer_key(key, width, stroke_width),
|
|
90
|
+
style: style, body: body)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def dashed?(connector)
|
|
94
|
+
return true if connector.connector_type == "Dependency"
|
|
95
|
+
return false unless connector.connector_type == "Package"
|
|
96
|
+
|
|
97
|
+
relationship = model_index && model_index[connector.relationship_ref]
|
|
98
|
+
relationship&.stereotype == "import"
|
|
99
|
+
end
|
|
100
|
+
|
|
59
101
|
def visible_connectors
|
|
60
102
|
(diagram.connectors || []).select(&:renderable?)
|
|
61
103
|
end
|
|
@@ -10,13 +10,12 @@ module Ea
|
|
|
10
10
|
# type.
|
|
11
11
|
#
|
|
12
12
|
# Layout (element bounds cover tab + body together; the tab
|
|
13
|
-
# occupies the TOP of the stored rect
|
|
14
|
-
#
|
|
15
|
-
# stereotype tab 36px, both inside):
|
|
13
|
+
# occupies the TOP of the stored rect, with its height adjusted
|
|
14
|
+
# by the font line height for each header row):
|
|
16
15
|
#
|
|
17
16
|
# ┌───┐
|
|
18
|
-
# │tab│ ←
|
|
19
|
-
# │ │
|
|
17
|
+
# │tab│ ← one name row, or two rows with a stereotype
|
|
18
|
+
# │ │
|
|
20
19
|
# ├───┴──────────┐
|
|
21
20
|
# │ │ the name renders bold, left-aligned;
|
|
22
21
|
# │ body │ the body always starts where the
|
|
@@ -148,7 +147,11 @@ module Ea
|
|
|
148
147
|
end
|
|
149
148
|
|
|
150
149
|
def tab_height
|
|
151
|
-
has_stereotype? ? TAB_HEIGHT_DOUBLE : TAB_HEIGHT_SINGLE
|
|
150
|
+
nominal_height = has_stereotype? ? TAB_HEIGHT_DOUBLE : TAB_HEIGHT_SINGLE
|
|
151
|
+
rows = has_stereotype? ? 2 : 1
|
|
152
|
+
# Preserve the nominal padding while each row follows the same
|
|
153
|
+
# font line height used to place the package header text.
|
|
154
|
+
nominal_height + rows * (tab_line_height - TAB_LABEL_Y_OFFSET)
|
|
152
155
|
end
|
|
153
156
|
|
|
154
157
|
def body_points
|
|
@@ -9,32 +9,15 @@ module Ea
|
|
|
9
9
|
# chevron) when a stereotype provides a custom icon via
|
|
10
10
|
# ShapeScript.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
# common GML stereotypes (FeatureType, Type) since the source
|
|
14
|
-
# ShapeScript definitions live in EA's encrypted
|
|
15
|
-
# InternalTechnologies (TODO.diagrams/87). Future enhancement:
|
|
16
|
-
# read shapescript from MDG files when available.
|
|
12
|
+
# Only MDG-provided ShapeScript is rendered.
|
|
17
13
|
class StereotypeIconRenderer
|
|
18
|
-
# Hardcoded fallback icons keyed by stereotype name.
|
|
19
|
-
# Each entry is [fill, stroke, points_array].
|
|
20
|
-
FALLBACK_ICONS = {
|
|
21
|
-
"FeatureType" => ["#FAF1EC", "#69738C",
|
|
22
|
-
[[0, -5], [5, 0], [0, 5], [-5, 0]]],
|
|
23
|
-
"Type" => ["#FAF1EC", "#69738C",
|
|
24
|
-
[[0, -5], [5, 0], [0, 5], [-5, 0]]]
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
# Default position offset from element's center
|
|
28
|
-
OFFSET_X = 0
|
|
29
|
-
OFFSET_Y = 0
|
|
30
|
-
|
|
31
14
|
attr_reader :classifier, :bounds, :canvas, :mdg_registry
|
|
32
15
|
|
|
33
16
|
# @param mdg_registry [Ea::Mdg::Registry, nil] optional
|
|
34
17
|
# registry to look up MDG-provided ShapeScript for the
|
|
35
18
|
# classifier's stereotype. When provided and the matching
|
|
36
19
|
# stereotype has a ShapeScript body, it's parsed and
|
|
37
|
-
# rendered.
|
|
20
|
+
# rendered.
|
|
38
21
|
def initialize(classifier:, bounds:, canvas: nil, mdg_registry: nil)
|
|
39
22
|
@classifier = classifier
|
|
40
23
|
@bounds = bounds
|
|
@@ -51,12 +34,12 @@ module Ea
|
|
|
51
34
|
return "" unless classifier && bounds
|
|
52
35
|
return "" unless stereotype_name
|
|
53
36
|
|
|
54
|
-
shapescript_svg
|
|
37
|
+
shapescript_svg.to_s
|
|
55
38
|
end
|
|
56
39
|
|
|
57
40
|
private
|
|
58
41
|
|
|
59
|
-
#
|
|
42
|
+
# Renders MDG-provided ShapeScript for the stereotype.
|
|
60
43
|
# @return [String, nil] SVG fragment from parsed ShapeScript
|
|
61
44
|
def shapescript_svg
|
|
62
45
|
return nil unless mdg_registry
|
|
@@ -83,35 +66,12 @@ module Ea
|
|
|
83
66
|
nil
|
|
84
67
|
end
|
|
85
68
|
|
|
86
|
-
# @return [String] hardcoded fallback polygon, or "" if none
|
|
87
|
-
def fallback_svg
|
|
88
|
-
spec = FALLBACK_ICONS[stereotype_name]
|
|
89
|
-
return "" unless spec
|
|
90
|
-
|
|
91
|
-
fill, stroke, points = spec
|
|
92
|
-
cx = bounds.x + bounds.width / 2 + OFFSET_X
|
|
93
|
-
cy = bounds.y + bounds.height / 2 + OFFSET_Y
|
|
94
|
-
translated = points.map { |x, y| translate_point(cx + x, cy + y) }
|
|
95
|
-
pts_str = translated.map { |x, y| coord(x) + "," + coord(y) }.join(" ")
|
|
96
|
-
%(<polygon points="#{pts_str}" fill="#{fill}" stroke="#{stroke}" stroke-width="1"/>)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
69
|
def stereotype_name
|
|
100
70
|
return nil unless classifier.is_a?(Ea::Model::Classifier)
|
|
101
71
|
|
|
102
72
|
refs = classifier.stereotype_refs
|
|
103
73
|
refs&.first
|
|
104
74
|
end
|
|
105
|
-
|
|
106
|
-
def translate_point(x, y)
|
|
107
|
-
return [x, y] unless canvas
|
|
108
|
-
|
|
109
|
-
[canvas.translate_x(x), canvas.translate_y(y)]
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def coord(value)
|
|
113
|
-
Ea::Svg::EaEmitter::Canvas.coord(value)
|
|
114
|
-
end
|
|
115
75
|
end
|
|
116
76
|
end
|
|
117
77
|
end
|
|
@@ -11,7 +11,7 @@ module Ea
|
|
|
11
11
|
class Elements
|
|
12
12
|
DEFAULT_FILL = "#FFFFFF"
|
|
13
13
|
DEFAULT_STROKE = "#000000"
|
|
14
|
-
DEFAULT_STROKE_WIDTH =
|
|
14
|
+
DEFAULT_STROKE_WIDTH = 1
|
|
15
15
|
DEFAULT_TEXT_COLOR = "#000000"
|
|
16
16
|
|
|
17
17
|
attr_reader :diagram, :model_index, :canvas, :document
|
|
@@ -152,8 +152,10 @@ module Ea
|
|
|
152
152
|
[]
|
|
153
153
|
end
|
|
154
154
|
# Titles wrap; content rows grow the drawn box to fit their insets.
|
|
155
|
-
row_w = [row_content_width(attr_lines, 5, size, family,
|
|
156
|
-
|
|
155
|
+
row_w = [row_content_width(attr_lines, 5, size, family,
|
|
156
|
+
logical_height: logical_height,
|
|
157
|
+
content_x_offset: attribute_content_x_offset(size, family, size_unit)),
|
|
158
|
+
row_content_width(op_lines, 3, size, family, logical_height: logical_height),
|
|
157
159
|
Compartment::EnumLiterals.content_width(
|
|
158
160
|
enum_literals_for(classifier),
|
|
159
161
|
theme: theme, size: size, family: family,
|
|
@@ -178,6 +180,9 @@ module Ea
|
|
|
178
180
|
off_canvas_parent_name: parent_name,
|
|
179
181
|
foreign_package_name: foreign_package_name_for(classifier),
|
|
180
182
|
suppress_stereotypes: suppress_stereotypes?) : []
|
|
183
|
+
header_lines = header_lines.map do |text, style|
|
|
184
|
+
[text, font_resolver.header_style_for(style, element)]
|
|
185
|
+
end
|
|
181
186
|
geometry = compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
182
187
|
tagged_values_for(classifier).size,
|
|
183
188
|
header_lines, classifier: classifier, family: family,
|
|
@@ -246,7 +251,8 @@ module Ea
|
|
|
246
251
|
# Real attributes grow the DRAWN box past the stored
|
|
247
252
|
# height instead of being dropped (0CABA7D7: stored h44
|
|
248
253
|
# draws at h59). Grown height = last row baseline + EA's
|
|
249
|
-
#
|
|
254
|
+
# trailing padding (7px for compact Cambria, 8px otherwise).
|
|
255
|
+
# Render-local: the diagram-level
|
|
250
256
|
# canvas still derives from stored bounds.
|
|
251
257
|
# content_bottom_y is nil when no compartment has rows;
|
|
252
258
|
# op_bottom_y is NOT a sentinel (it reports the op-divider
|
|
@@ -257,13 +263,15 @@ module Ea
|
|
|
257
263
|
# Bag': Set enum box stored h59 -> EA 223 fitting six
|
|
258
264
|
# literals); op-only boxes keep the no-growth behavior
|
|
259
265
|
# (op_bottom_y is a divider sentinel, not content).
|
|
266
|
+
# Saved boxes may already fit rows with less than the growth
|
|
267
|
+
# padding. Test the baseline for overflow before adding padding.
|
|
260
268
|
if (attr_lines.any? || enum_literals_for(classifier).any?) &&
|
|
261
269
|
grown_bottom.positive? &&
|
|
262
|
-
grown_bottom
|
|
270
|
+
grown_bottom > bounds.y + bounds.height
|
|
263
271
|
bounds = Ea::Model::Bounds.new(
|
|
264
272
|
x: bounds.x, y: bounds.y,
|
|
265
273
|
width: bounds.width,
|
|
266
|
-
height: grown_bottom +
|
|
274
|
+
height: grown_bottom + compartment_bottom_padding(size, family, size_unit) - bounds.y
|
|
267
275
|
)
|
|
268
276
|
end
|
|
269
277
|
RenderContext.new(
|
|
@@ -293,18 +301,30 @@ module Ea
|
|
|
293
301
|
)
|
|
294
302
|
end
|
|
295
303
|
|
|
296
|
-
def
|
|
304
|
+
def attribute_content_x_offset(size, family, size_unit)
|
|
305
|
+
offset = (theme.attribute_spec.content_x_offset || Element::AttributeRenderer::DEFAULT_CONTENT_X_OFFSET) +
|
|
306
|
+
2 * (size - 7)
|
|
307
|
+
offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
|
|
308
|
+
offset
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def compartment_bottom_padding(size, family, size_unit)
|
|
312
|
+
Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit) ? 7 : 8
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def row_content_width(lines, right_pad, size, family, **font_options)
|
|
297
316
|
return nil if lines.to_a.empty?
|
|
298
317
|
|
|
299
|
-
|
|
318
|
+
lines.filter_map do |line|
|
|
300
319
|
text = line.sub(/\A\S /, "")
|
|
301
|
-
Ea::Fonts::Metrics.text_length(text, size,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return nil unless tl
|
|
320
|
+
length = Ea::Fonts::Metrics.text_length(text, size,
|
|
321
|
+
family: family,
|
|
322
|
+
weight: "400", logical_height: font_options[:logical_height])&.round
|
|
323
|
+
next unless length
|
|
306
324
|
|
|
307
|
-
|
|
325
|
+
offset = line.match?(/\A[-+~#]\s/) ? font_options.fetch(:content_x_offset, 22) : 22
|
|
326
|
+
offset + length + right_pad
|
|
327
|
+
end.max
|
|
308
328
|
end
|
|
309
329
|
|
|
310
330
|
def compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
@@ -43,7 +43,7 @@ module Ea
|
|
|
43
43
|
return explicit
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
default_family_for(explicit)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def size_for(element)
|
|
@@ -72,14 +72,29 @@ module Ea
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def style_for(element)
|
|
75
|
-
|
|
75
|
+
return "italic" if element&.font_italic
|
|
76
|
+
return "normal" if element&.style&.key?(:italic)
|
|
77
|
+
|
|
78
|
+
diagram.hand_draw ? "italic" : "normal"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def header_style_for(style, element)
|
|
82
|
+
return style unless diagram.hand_draw && style_for(element) == "italic"
|
|
83
|
+
|
|
84
|
+
style == :bold ? :bold_italic : style
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
private
|
|
79
88
|
|
|
89
|
+
def default_family_for(explicit)
|
|
90
|
+
return "Source Sans Pro" if diagram.hand_draw
|
|
91
|
+
|
|
92
|
+
theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
|
|
93
|
+
end
|
|
94
|
+
|
|
80
95
|
def theme_family_for(explicit)
|
|
81
96
|
return unless theme.themed?
|
|
82
|
-
return theme.font_family if DEFAULT_MARKER_FAMILIES.include?(explicit)
|
|
97
|
+
return theme.font_family || "Carlito" if DEFAULT_MARKER_FAMILIES.include?(explicit)
|
|
83
98
|
|
|
84
99
|
theme.element_font_family || theme.font_family
|
|
85
100
|
end
|
|
@@ -79,6 +79,7 @@ module Ea
|
|
|
79
79
|
text_pos, mult_pos = tree_positions(end_kind, anchor, role,
|
|
80
80
|
text_pos, mult_pos)
|
|
81
81
|
mult_pos = tree_left_multiplicity(end_kind, heading, anchor, mult_box, mult) || mult_pos
|
|
82
|
+
text_pos = tree_left_property_role(connector, end_kind, heading, anchor, text_box, role) || text_pos
|
|
82
83
|
elsif mult_box && stored_offsets_zero?(mult_box)
|
|
83
84
|
# Start at the legacy default; measured directional seats
|
|
84
85
|
# below replace it for default Cambria labels.
|
|
@@ -210,8 +211,9 @@ module Ea
|
|
|
210
211
|
|
|
211
212
|
def source_property_seat?(connector, direction, box)
|
|
212
213
|
phantom_property_box?(connector, direction, box) ||
|
|
213
|
-
(%i[up left right down].include?(direction) &&
|
|
214
|
-
|
|
214
|
+
(%i[up left right down].include?(direction) &&
|
|
215
|
+
property_role_box?(box, direction, connector.style.to_h) &&
|
|
216
|
+
source_property_font?(connector, direction, box))
|
|
215
217
|
end
|
|
216
218
|
|
|
217
219
|
# Phantom outlines carry synthesized zero boxes without alignment.
|
|
@@ -392,9 +394,23 @@ module Ea
|
|
|
392
394
|
[nil, "V"].include?(style[:tree])
|
|
393
395
|
end
|
|
394
396
|
|
|
395
|
-
def property_role_box?(box, direction)
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
def property_role_box?(box, direction, style = {})
|
|
398
|
+
return false unless box && default_presentation?(box)
|
|
399
|
+
return [0, 1].include?(box["alignment"]) unless direction == :down
|
|
400
|
+
|
|
401
|
+
case box["alignment"]
|
|
402
|
+
when 1 then true
|
|
403
|
+
when 0 then persisted_plain_down_box?(box, style)
|
|
404
|
+
else false
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Persisted alignment-0 boxes on untreed routes use the downward
|
|
409
|
+
# seat, including saved rays; the "OS" route also seats absent boxes.
|
|
410
|
+
def persisted_plain_down_box?(box, style)
|
|
411
|
+
return true if orthogonal_stub_route?(style)
|
|
412
|
+
|
|
413
|
+
!stored_offsets_zero?(box) && style[:tree].nil?
|
|
398
414
|
end
|
|
399
415
|
|
|
400
416
|
def property_role_offsets(direction)
|
|
@@ -405,26 +421,58 @@ module Ea
|
|
|
405
421
|
end
|
|
406
422
|
end
|
|
407
423
|
|
|
408
|
-
def source_property_font?(connector, direction)
|
|
424
|
+
def source_property_font?(connector, direction, box)
|
|
409
425
|
source_property_cambria?(connector) &&
|
|
410
|
-
source_property_route?(connector.style.to_h, direction)
|
|
426
|
+
source_property_route?(connector.style.to_h, direction, box)
|
|
411
427
|
end
|
|
412
428
|
|
|
413
429
|
def source_property_cambria?(connector)
|
|
414
430
|
font_family.to_s.casecmp("Cambria").zero? && property_label?(connector, :source)
|
|
415
431
|
end
|
|
416
432
|
|
|
417
|
-
def source_property_route?(style, direction)
|
|
433
|
+
def source_property_route?(style, direction, box)
|
|
418
434
|
case direction
|
|
419
|
-
when :left then left_property_route?(style)
|
|
420
435
|
when :right then style.values_at(:mode, :lwidth, :tree) == %w[3 2 H]
|
|
421
|
-
when :
|
|
422
|
-
|
|
436
|
+
when :up then upward_property_route?(style)
|
|
437
|
+
when :left then orthogonal_stub_route?(style) || left_property_route?(style)
|
|
438
|
+
else downward_source_route?(style, box)
|
|
423
439
|
end
|
|
424
440
|
end
|
|
425
441
|
|
|
442
|
+
def downward_source_route?(style, box)
|
|
443
|
+
[orthogonal_stub_route?(style), lateral_horizontal_route?(style), mode_two_route?(style),
|
|
444
|
+
downward_property_route?(style), persisted_width_zero_down_box?(box, style)].any?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def persisted_width_zero_down_box?(box, style)
|
|
448
|
+
box["alignment"]&.zero? && !stored_offsets_zero?(box) && style[:stored_association_ray] != true &&
|
|
449
|
+
style.values_at(:mode, :lwidth, :tree) == ["3", "0", nil]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Mode 3, width 2 on the horizontal lateral tree.
|
|
453
|
+
def lateral_horizontal_route?(style)
|
|
454
|
+
style.values_at(:mode, :lwidth, :tree) == %w[3 2 LH]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Mode 2, width 2 without a tree seats like the other width-2 modes.
|
|
458
|
+
def mode_two_route?(style)
|
|
459
|
+
style.values_at(:mode, :lwidth, :tree) == ["2", "2", nil]
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def upward_property_route?(style)
|
|
463
|
+
return orthogonal_stub_route?(style) if style[:tree] == "OS"
|
|
464
|
+
|
|
465
|
+
%w[0 2].include?(style[:lwidth])
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# The "OS" route seats its Property roles at the same directional
|
|
469
|
+
# corner seats as the untreed width-2 route.
|
|
470
|
+
def orthogonal_stub_route?(style)
|
|
471
|
+
style.values_at(:mode, :lwidth, :tree) == %w[3 2 OS]
|
|
472
|
+
end
|
|
473
|
+
|
|
426
474
|
def left_property_route?(style)
|
|
427
|
-
downward_property_route?(style) ||
|
|
475
|
+
downward_property_route?(style) || mode_two_route?(style) ||
|
|
428
476
|
(style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
|
|
429
477
|
[nil, "H", "LV"].include?(style[:tree]))
|
|
430
478
|
end
|
|
@@ -456,6 +504,15 @@ module Ea
|
|
|
456
504
|
[x + SEAT_X_INSET + width, y + 15] if width
|
|
457
505
|
end
|
|
458
506
|
|
|
507
|
+
# An absent source box on a leftward tree leg seats a Property role
|
|
508
|
+
# at the leftward Property seat, not the rightward tree default.
|
|
509
|
+
def tree_left_property_role(connector, end_kind, heading, anchor, box, role)
|
|
510
|
+
return nil unless leg_direction(end_kind, heading) == :left && box&.[]("alignment")&.zero?
|
|
511
|
+
return nil unless connector.style.to_h.values_at(:mode, :lwidth) == %w[3 2]
|
|
512
|
+
|
|
513
|
+
source_property_seat(connector, :left, anchor, box, role)
|
|
514
|
+
end
|
|
515
|
+
|
|
459
516
|
# A tree-route source leaving left seats its multiplicity like any
|
|
460
517
|
# other leftward source rather than at the rightward tree default.
|
|
461
518
|
def tree_left_multiplicity(end_kind, heading, anchor, box, mult)
|