ea 0.6.121 → 0.6.123
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/model/diagram_element.rb +3 -0
- data/lib/ea/sources/qea/diagram_builder.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +22 -18
- data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +10 -1
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +12 -1
- data/lib/ea/svg/ea_emitter/connectors.rb +22 -9
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +44 -29
- data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +14 -2
- data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +21 -6
- data/lib/ea/svg/ea_emitter/elements.rb +27 -12
- 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/diamond.rb +9 -0
- data/lib/ea/svg/ea_emitter/markers.rb +11 -9
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d897b1e2165c9993420958c081ee3e2eedebbfbb8ac4db1ea8f160fd45913a3d
|
|
4
|
+
data.tar.gz: 667e5cce39c543c97f96fd7b64c994d72808daf3bf6a875ae112e0efe4b2623b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8789c4187a4c72534f2e4d17f1caf650e7da1748aa08bed49764fa6fde0dafdfc1bf8c6811ab7a33e31b6da1ffe0a71397b6ca60b1a8c2e7ee0e65a4798368d
|
|
7
|
+
data.tar.gz: 37bf77d8c14e921888081ee961506f835b5cd0de1bb28de790d5ba848003915922e83c83e2b29d6db0f58141f25dc1d643059f642950ee6cd2028da87d42b42f
|
|
@@ -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]),
|
|
@@ -5,12 +5,10 @@ module Ea
|
|
|
5
5
|
module EaEmitter
|
|
6
6
|
module Compartment
|
|
7
7
|
# Instance-name underline for object diagrams. EA draws a
|
|
8
|
-
# short horizontal line
|
|
9
|
-
#
|
|
10
|
-
# only when the diagram's `diagram_type` is "Object".
|
|
8
|
+
# short horizontal line below each instance name to denote
|
|
9
|
+
# that it is an instance (UML convention).
|
|
11
10
|
module InstanceUnderline
|
|
12
|
-
Y_OFFSET =
|
|
13
|
-
WIDTH_FACTOR = 0.65
|
|
11
|
+
Y_OFFSET = 1
|
|
14
12
|
|
|
15
13
|
module_function
|
|
16
14
|
|
|
@@ -19,11 +17,22 @@ module Ea
|
|
|
19
17
|
return nil unless object_diagram?(context)
|
|
20
18
|
return nil if context.header_lines.empty?
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
header = Element::HeaderRenderer.line_layout(
|
|
21
|
+
context.header_lines,
|
|
22
|
+
bounds: context.bounds,
|
|
23
|
+
first_y: context.geometry.header_first_y,
|
|
24
|
+
family: context.family,
|
|
25
|
+
size: context.size,
|
|
26
|
+
size_unit: context.size_unit,
|
|
27
|
+
logical_height: context.logical_height,
|
|
28
|
+
weight_normal: context.theme.text_weight_normal,
|
|
29
|
+
weight_bold: context.theme.text_weight_bold,
|
|
30
|
+
width_factor: context.theme.text_width_factor,
|
|
31
|
+
line_offset: context.theme.compartments.header_line_offset
|
|
32
|
+
).last
|
|
33
|
+
return nil unless header && !header.text.to_s.empty?
|
|
25
34
|
|
|
26
|
-
underline_path(
|
|
35
|
+
underline_path(header)
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
def object_diagram?(context)
|
|
@@ -31,18 +40,13 @@ module Ea
|
|
|
31
40
|
end
|
|
32
41
|
module_function :object_diagram?
|
|
33
42
|
|
|
34
|
-
def underline_path(
|
|
35
|
-
|
|
36
|
-
text_width = TextRenderer.estimate_width(text, context.size,
|
|
37
|
-
WIDTH_FACTOR)
|
|
38
|
-
x_start = bounds.x + (bounds.width - text_width) / 2.0
|
|
39
|
-
y_pos = context.geometry.header_first_y + Y_OFFSET
|
|
40
|
-
build_path_tag(x_start, y_pos, text_width)
|
|
43
|
+
def underline_path(header)
|
|
44
|
+
build_path_tag(header.x, header.y + Y_OFFSET, header.text_length)
|
|
41
45
|
end
|
|
42
46
|
module_function :underline_path
|
|
43
47
|
|
|
44
|
-
def build_path_tag(x_start, y_pos,
|
|
45
|
-
x_end = x_start +
|
|
48
|
+
def build_path_tag(x_start, y_pos, text_length)
|
|
49
|
+
x_end = x_start + text_length
|
|
46
50
|
coords = format_coords(x_start, y_pos, x_end)
|
|
47
51
|
d = "M #{coords[:x1]} #{coords[:y1]} L #{coords[:x2]} #{coords[:y1]}"
|
|
48
52
|
style = "stroke-width:1;stroke-linecap:square;" \
|
|
@@ -14,6 +14,7 @@ module Ea
|
|
|
14
14
|
ROW_HEIGHT = 16
|
|
15
15
|
ICON_X_OFFSET = 5
|
|
16
16
|
TEXT_X_OFFSET = 23
|
|
17
|
+
FIRST_BASELINE_OFFSET = 15
|
|
17
18
|
|
|
18
19
|
module_function
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ module Ea
|
|
|
22
23
|
return nil if rows.nil? || rows.empty?
|
|
23
24
|
|
|
24
25
|
bounds = context.bounds
|
|
25
|
-
first_y = context
|
|
26
|
+
first_y = first_baseline(context)
|
|
26
27
|
blocks = rows.each_with_index.flat_map do |row, idx|
|
|
27
28
|
y_top = first_y - 11 + (idx * ROW_HEIGHT)
|
|
28
29
|
y_text = first_y + (idx * ROW_HEIGHT)
|
|
@@ -34,6 +35,14 @@ module Ea
|
|
|
34
35
|
wrap_group(blocks.join("\n"))
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def first_baseline(context)
|
|
39
|
+
context.bounds.y + Element::PackageShapeRenderer.tab_height_for(
|
|
40
|
+
stereotype: Shape.package_stereotype(context.model_element),
|
|
41
|
+
family: context.family, size: context.size, size_unit: context.size_unit
|
|
42
|
+
) + FIRST_BASELINE_OFFSET
|
|
43
|
+
end
|
|
44
|
+
private_class_method :first_baseline
|
|
45
|
+
|
|
37
46
|
def wrap_group(body)
|
|
38
47
|
%(<g style="#{group_style}">\n#{body}\n</g>)
|
|
39
48
|
end
|
|
@@ -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.
|
|
@@ -50,14 +50,14 @@ module Ea
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def layers
|
|
53
|
-
|
|
53
|
+
styles = paths_with_styles
|
|
54
54
|
|
|
55
55
|
if grouped
|
|
56
|
-
|
|
57
|
-
line_layer(width, entries.map(&:last).join("\n "))
|
|
56
|
+
styles.group_by(&:first).map do |(width, dashed), entries|
|
|
57
|
+
line_layer(width, dashed, entries.map(&:last).join("\n "))
|
|
58
58
|
end
|
|
59
59
|
else
|
|
60
|
-
|
|
60
|
+
styles.map { |(width, dashed), path| line_layer(width, dashed, path) }
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -73,16 +73,29 @@ module Ea
|
|
|
73
73
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
|
-
def
|
|
76
|
+
def paths_with_styles
|
|
77
77
|
visible_connectors.filter_map do |connector|
|
|
78
78
|
path = path_for(connector)
|
|
79
|
-
|
|
79
|
+
next unless path
|
|
80
|
+
|
|
81
|
+
[[self.class.stroke_width_for(connector, stroke_width), dashed?(connector)], path]
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
def line_layer(width, body)
|
|
84
|
-
|
|
85
|
-
|
|
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"
|
|
86
99
|
end
|
|
87
100
|
|
|
88
101
|
def visible_connectors
|
|
@@ -11,55 +11,70 @@ module Ea
|
|
|
11
11
|
# the left edge from text-width approximation since SVG text
|
|
12
12
|
# x= is the start position, not the center).
|
|
13
13
|
class HeaderRenderer
|
|
14
|
+
HeaderLine = Struct.new(:text, :style, :x, :y, :text_length,
|
|
15
|
+
:weight, :font_style, keyword_init: true)
|
|
16
|
+
|
|
14
17
|
def self.render(lines, bounds:, first_y:, family:,
|
|
15
18
|
size:, size_unit: "pt", logical_height: nil,
|
|
16
19
|
fill: "#000000", weight_normal: 400, weight_bold: 700,
|
|
17
20
|
stroke_in_text: "#000000", width_factor: 0.612,
|
|
18
21
|
line_offset: 6)
|
|
19
22
|
logical_height = nil unless size_unit == "pt"
|
|
23
|
+
layout = line_layout(
|
|
24
|
+
lines, bounds: bounds, first_y: first_y, family: family,
|
|
25
|
+
size: size, size_unit: size_unit, logical_height: logical_height,
|
|
26
|
+
weight_normal: weight_normal, weight_bold: weight_bold,
|
|
27
|
+
width_factor: width_factor, line_offset: line_offset
|
|
28
|
+
)
|
|
29
|
+
text_blocks = layout.map do |line|
|
|
30
|
+
TextRenderer.new(
|
|
31
|
+
content: line.text,
|
|
32
|
+
x: line.x, y: line.y,
|
|
33
|
+
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
34
|
+
weight: line.weight, style: line.font_style, fill: fill,
|
|
35
|
+
stroke_in_text: stroke_in_text, width_factor: width_factor,
|
|
36
|
+
text_length: line.text_length
|
|
37
|
+
).to_svg
|
|
38
|
+
end
|
|
39
|
+
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the exact integer placement and width used for each
|
|
43
|
+
# emitted header line so dependent compartments can align to it.
|
|
44
|
+
def self.line_layout(lines, bounds:, first_y:, family:, size:,
|
|
45
|
+
size_unit: "pt", logical_height: nil,
|
|
46
|
+
weight_normal: 400, weight_bold: 700,
|
|
47
|
+
width_factor: 0.612, line_offset: 6)
|
|
48
|
+
logical_height = nil unless size_unit == "pt"
|
|
20
49
|
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
21
|
-
size, family: family, unit: size_unit, offset: line_offset,
|
|
50
|
+
size, family: family, unit: size_unit, offset: line_offset,
|
|
51
|
+
logical_height: logical_height
|
|
22
52
|
)
|
|
23
53
|
ghost_shift = HeaderLines.parent_ghost_gap(lines)
|
|
24
|
-
|
|
25
|
-
weight =
|
|
26
|
-
when :bold, :bold_italic then weight_bold
|
|
27
|
-
else weight_normal
|
|
28
|
-
end
|
|
54
|
+
lines.each_with_index.map do |(text, style), idx|
|
|
55
|
+
weight = %i[bold bold_italic].include?(style) ? weight_bold : weight_normal
|
|
29
56
|
font_style = %i[italic italic_tight bold_italic].include?(style) ? "italic" : "normal"
|
|
57
|
+
text_length = TextRenderer.estimate_width(
|
|
58
|
+
text, size, width_factor, family: family, weight: weight,
|
|
59
|
+
style: font_style, logical_height: logical_height
|
|
60
|
+
).round
|
|
30
61
|
y = first_y + (idx * line_h) +
|
|
31
62
|
(idx >= 1 && ghost_shift.positive? ? ghost_shift : 0)
|
|
32
63
|
x = if %i[italic italic_tight].include?(style)
|
|
33
|
-
|
|
34
|
-
bounds.x + bounds.width - 3 -
|
|
35
|
-
Ea::Svg::EaEmitter::TextRenderer.estimate_width(
|
|
36
|
-
text, size, width_factor,
|
|
37
|
-
family: family, weight: weight, style: font_style, logical_height: logical_height
|
|
38
|
-
).round
|
|
64
|
+
bounds.x + bounds.width - 3 - text_length
|
|
39
65
|
else
|
|
40
|
-
center_x_for(
|
|
41
|
-
family: family, weight: weight,
|
|
42
|
-
style: font_style, logical_height: logical_height)
|
|
66
|
+
center_x_for(text_length, bounds)
|
|
43
67
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
48
|
-
weight: weight, style: font_style, fill: fill,
|
|
49
|
-
stroke_in_text: stroke_in_text, width_factor: width_factor
|
|
50
|
-
).to_svg
|
|
68
|
+
HeaderLine.new(text: text, style: style, x: x, y: y,
|
|
69
|
+
text_length: text_length, weight: weight,
|
|
70
|
+
font_style: font_style)
|
|
51
71
|
end
|
|
52
|
-
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
53
72
|
end
|
|
54
73
|
|
|
55
74
|
# EA centers text by its INTEGER textLength and floors the
|
|
56
75
|
# resulting x (verified exact against EA-published SVGs).
|
|
57
|
-
def self.center_x_for(
|
|
58
|
-
|
|
59
|
-
len = Ea::Svg::EaEmitter::TextRenderer.estimate_width(
|
|
60
|
-
text, size, width_factor, family: family, weight: weight, style: style, logical_height: logical_height
|
|
61
|
-
).round
|
|
62
|
-
(bounds.x + (bounds.width - len) / 2.0).floor
|
|
76
|
+
def self.center_x_for(text_length, bounds)
|
|
77
|
+
(bounds.x + (bounds.width - text_length) / 2.0).floor
|
|
63
78
|
end
|
|
64
79
|
private_class_method :center_x_for
|
|
65
80
|
end
|
|
@@ -26,9 +26,10 @@ module Ea
|
|
|
26
26
|
text_blocks = []
|
|
27
27
|
has_receptions = operations.any?(&:is_reception)
|
|
28
28
|
if has_receptions
|
|
29
|
+
heading_width = reception_heading_width(family, size, size_unit, logical_height)
|
|
29
30
|
text_blocks << TextRenderer.new(
|
|
30
31
|
content: "receptions",
|
|
31
|
-
x: bounds.x + (bounds.width / 2.0) -
|
|
32
|
+
x: bounds.x + (bounds.width / 2.0) - heading_width / 2.0,
|
|
32
33
|
y: first_y,
|
|
33
34
|
family: family, size: size, size_unit: size_unit, logical_height: logical_height,
|
|
34
35
|
style: "italic", fill: "#000000"
|
|
@@ -36,12 +37,14 @@ module Ea
|
|
|
36
37
|
end
|
|
37
38
|
ops = has_receptions ? operations : operations
|
|
38
39
|
offset = has_receptions ? 1 : 0
|
|
40
|
+
content_x_offset = CONTENT_X_OFFSET + 2 * (size - 7)
|
|
41
|
+
content_x_offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
|
|
39
42
|
ops.each_with_index do |op, idx|
|
|
40
43
|
y = first_y + ((idx + offset) * line_h)
|
|
41
44
|
text_blocks << build_text(bounds.x + VISIBILITY_X_OFFSET, y,
|
|
42
45
|
visibility_prefix(op), family, size, size_unit, logical_height: logical_height)
|
|
43
46
|
text = operation_text(op, parameter_detail: parameter_detail)
|
|
44
|
-
text_blocks << build_text(bounds.x +
|
|
47
|
+
text_blocks << build_text(bounds.x + content_x_offset, y,
|
|
45
48
|
text, family, size, size_unit, logical_height: logical_height)
|
|
46
49
|
end
|
|
47
50
|
%(<g style="#{Style::TEXT_GROUP}">\n#{text_blocks.join("\n")}\n</g>)
|
|
@@ -102,6 +105,15 @@ module Ea
|
|
|
102
105
|
end
|
|
103
106
|
private_class_method :reception_params
|
|
104
107
|
|
|
108
|
+
def self.reception_heading_width(family, size, size_unit, logical_height)
|
|
109
|
+
TextRenderer.estimate_width(
|
|
110
|
+
"receptions", size,
|
|
111
|
+
family: family, weight: 400, style: "italic",
|
|
112
|
+
logical_height: size_unit == "pt" ? logical_height : nil
|
|
113
|
+
).round
|
|
114
|
+
end
|
|
115
|
+
private_class_method :reception_heading_width
|
|
116
|
+
|
|
105
117
|
def self.build_text(x, y, content, family, size, size_unit, logical_height: nil)
|
|
106
118
|
TextRenderer.new(content: content, x: x, y: y,
|
|
107
119
|
family: family, size: size, size_unit: size_unit,
|
|
@@ -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
|
|
@@ -46,6 +45,21 @@ module Ea
|
|
|
46
45
|
family, size, size_unit, text_fill).to_svg
|
|
47
46
|
end
|
|
48
47
|
|
|
48
|
+
def self.tab_height_for(stereotype:, family:, size:, size_unit: "pt")
|
|
49
|
+
rows = stereotype && !stereotype.empty? ? 2 : 1
|
|
50
|
+
nominal_height = rows == 2 ? TAB_HEIGHT_DOUBLE : TAB_HEIGHT_SINGLE
|
|
51
|
+
line_height = if size_unit == "pt"
|
|
52
|
+
Ea::Fonts::Metrics.compartment_line_height(
|
|
53
|
+
size, family: family, unit: size_unit, offset: TAB_TEXT_PADDING
|
|
54
|
+
)
|
|
55
|
+
else
|
|
56
|
+
TAB_LABEL_Y_OFFSET
|
|
57
|
+
end
|
|
58
|
+
# Preserve the nominal padding while each row follows the same
|
|
59
|
+
# font line height used to place the package header text.
|
|
60
|
+
nominal_height + rows * (line_height - TAB_LABEL_Y_OFFSET)
|
|
61
|
+
end
|
|
62
|
+
|
|
49
63
|
def initialize(bounds, fill, stroke, stroke_width, label,
|
|
50
64
|
stereotype, family, size, size_unit, text_fill)
|
|
51
65
|
@bounds = bounds
|
|
@@ -148,7 +162,8 @@ module Ea
|
|
|
148
162
|
end
|
|
149
163
|
|
|
150
164
|
def tab_height
|
|
151
|
-
|
|
165
|
+
self.class.tab_height_for(stereotype: stereotype, family: family,
|
|
166
|
+
size: size, size_unit: size_unit)
|
|
152
167
|
end
|
|
153
168
|
|
|
154
169
|
def body_points
|
|
@@ -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,
|
|
@@ -249,7 +251,8 @@ module Ea
|
|
|
249
251
|
# Real attributes grow the DRAWN box past the stored
|
|
250
252
|
# height instead of being dropped (0CABA7D7: stored h44
|
|
251
253
|
# draws at h59). Grown height = last row baseline + EA's
|
|
252
|
-
#
|
|
254
|
+
# trailing padding (7px for compact Cambria, 8px otherwise).
|
|
255
|
+
# Render-local: the diagram-level
|
|
253
256
|
# canvas still derives from stored bounds.
|
|
254
257
|
# content_bottom_y is nil when no compartment has rows;
|
|
255
258
|
# op_bottom_y is NOT a sentinel (it reports the op-divider
|
|
@@ -268,7 +271,7 @@ module Ea
|
|
|
268
271
|
bounds = Ea::Model::Bounds.new(
|
|
269
272
|
x: bounds.x, y: bounds.y,
|
|
270
273
|
width: bounds.width,
|
|
271
|
-
height: grown_bottom +
|
|
274
|
+
height: grown_bottom + compartment_bottom_padding(size, family, size_unit) - bounds.y
|
|
272
275
|
)
|
|
273
276
|
end
|
|
274
277
|
RenderContext.new(
|
|
@@ -298,18 +301,30 @@ module Ea
|
|
|
298
301
|
)
|
|
299
302
|
end
|
|
300
303
|
|
|
301
|
-
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)
|
|
302
316
|
return nil if lines.to_a.empty?
|
|
303
317
|
|
|
304
|
-
|
|
318
|
+
lines.filter_map do |line|
|
|
305
319
|
text = line.sub(/\A\S /, "")
|
|
306
|
-
Ea::Fonts::Metrics.text_length(text, size,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
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
|
|
311
324
|
|
|
312
|
-
|
|
325
|
+
offset = line.match?(/\A[-+~#]\s/) ? font_options.fetch(:content_x_offset, 22) : 22
|
|
326
|
+
offset + length + right_pad
|
|
327
|
+
end.max
|
|
313
328
|
end
|
|
314
329
|
|
|
315
330
|
def compartment_geometry(bounds, size, attr_lines, op_lines,
|
|
@@ -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)
|
|
@@ -125,7 +125,7 @@ module Ea
|
|
|
125
125
|
# Unknown faces retain the stored-upward fallback.
|
|
126
126
|
def source_heading(connector, points)
|
|
127
127
|
heading = leg_heading(points)
|
|
128
|
-
return heading unless
|
|
128
|
+
return heading unless heading.none?(&:zero?) && source_face_route?(connector, points)
|
|
129
129
|
|
|
130
130
|
box = source_dock_bounds(connector)
|
|
131
131
|
face = box && source_dock_heading(connector, points.first, box)
|
|
@@ -134,6 +134,15 @@ module Ea
|
|
|
134
134
|
stored_upward_heading(connector, heading)
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
# Explicit source boxes preserve the heading of a manual bend. An
|
|
138
|
+
# omitted source box uses the same dock seat as a straight route.
|
|
139
|
+
def source_face_route?(connector, points)
|
|
140
|
+
return true if points.size == 2
|
|
141
|
+
|
|
142
|
+
boxes = connector.label_boxes || {}
|
|
143
|
+
end_boxes(boxes, :llt, :llb).nil? && !end_boxes(boxes, :lrt, :lrb).nil?
|
|
144
|
+
end
|
|
145
|
+
|
|
137
146
|
# A target dock that clips an axis-aligned stored leg into a slight
|
|
138
147
|
# diagonal keeps the stored leg's direction.
|
|
139
148
|
def target_heading(connector, points)
|
|
@@ -150,15 +159,24 @@ module Ea
|
|
|
150
159
|
# A saved external ray enters the source through the face it first
|
|
151
160
|
# meets, even when the clipped leg then runs back across the box.
|
|
152
161
|
def face_leads_leg?(connector, face, heading, box)
|
|
153
|
-
|
|
154
|
-
if face == [0, 1] && anchor && anchor.y > box.y + box.height &&
|
|
155
|
-
connector.style.to_h[:stored_association_ray] == true
|
|
156
|
-
return true
|
|
157
|
-
end
|
|
162
|
+
return true if saved_ray_beyond_face?(connector, face, box)
|
|
158
163
|
|
|
159
164
|
face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
|
|
160
165
|
end
|
|
161
166
|
|
|
167
|
+
# A saved ray from beyond the bottom or right face meets that face
|
|
168
|
+
# first. The top face is excluded: its clipped leg keeps the anchor seat.
|
|
169
|
+
def saved_ray_beyond_face?(connector, face, box)
|
|
170
|
+
anchor = connector.source_anchor
|
|
171
|
+
return false unless anchor && connector.style.to_h[:stored_association_ray] == true
|
|
172
|
+
|
|
173
|
+
case face
|
|
174
|
+
when [0, 1] then anchor.y > box.y + box.height
|
|
175
|
+
when [1, 0] then anchor.x > box.x + box.width
|
|
176
|
+
else false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
162
180
|
def stored_upward_heading(connector, heading)
|
|
163
181
|
return heading unless heading[1].negative?
|
|
164
182
|
|
|
@@ -20,6 +20,7 @@ module Ea
|
|
|
20
20
|
anchor: whole_anchor,
|
|
21
21
|
base: whole_base)]
|
|
22
22
|
return specs unless whole_at_target
|
|
23
|
+
return specs if arrow_on_target_diamond?(connector, relationship)
|
|
23
24
|
|
|
24
25
|
specs.concat(direction_specs(connector, source, target,
|
|
25
26
|
before_target, after_source))
|
|
@@ -67,6 +68,14 @@ module Ea
|
|
|
67
68
|
!whole_end_at_source?(connector)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
# EA draws no arrow where a directed, dest-flagged aggregation
|
|
72
|
+
# would put it on its own target diamond.
|
|
73
|
+
def self.arrow_on_target_diamond?(connector, relationship)
|
|
74
|
+
connector.direction == "Source -> Destination" &&
|
|
75
|
+
dest_flagged?(relationship)
|
|
76
|
+
end
|
|
77
|
+
private_class_method :arrow_on_target_diamond?
|
|
78
|
+
|
|
70
79
|
def self.dest_flagged?(relationship)
|
|
71
80
|
relationship &&
|
|
72
81
|
%w[shared composite].include?(relationship.target_aggregation.to_s)
|
|
@@ -22,7 +22,7 @@ module Ea
|
|
|
22
22
|
DIAMOND_HALF_W = 5
|
|
23
23
|
DIAMOND_HALF_H = 10
|
|
24
24
|
TRI_HALF_BASE = 6
|
|
25
|
-
TRI_HEIGHT =
|
|
25
|
+
TRI_HEIGHT = 16
|
|
26
26
|
# EA's path arrowheads are height 15 corpus-wide, with
|
|
27
27
|
# distinct bases per connector kind: association
|
|
28
28
|
# navigability arrows base 12, dependency arrows base 10
|
|
@@ -364,10 +364,10 @@ module Ea
|
|
|
364
364
|
left_x = tx - ux * DIAMOND_HALF_H - (-uy) * DIAMOND_HALF_W
|
|
365
365
|
left_y = ty - uy * DIAMOND_HALF_H - ux * DIAMOND_HALF_W
|
|
366
366
|
pts = [
|
|
367
|
-
Canvas.coord(tx), Canvas.coord(ty),
|
|
368
|
-
Canvas.coord(right_x), Canvas.coord(right_y),
|
|
369
|
-
Canvas.coord(back_x), Canvas.coord(back_y),
|
|
370
|
-
Canvas.coord(left_x), Canvas.coord(left_y)
|
|
367
|
+
Canvas.coord(tx.round), Canvas.coord(ty.round),
|
|
368
|
+
Canvas.coord(right_x.round), Canvas.coord(right_y.round),
|
|
369
|
+
Canvas.coord(back_x.round), Canvas.coord(back_y.round),
|
|
370
|
+
Canvas.coord(left_x.round), Canvas.coord(left_y.round)
|
|
371
371
|
].join(" ")
|
|
372
372
|
%(<polygon points="#{pts}" shape-rendering="auto" style="fill-rule:evenodd;"/>)
|
|
373
373
|
end
|
|
@@ -385,9 +385,9 @@ module Ea
|
|
|
385
385
|
w2_x = back_x - (-uy) * TRI_HALF_BASE
|
|
386
386
|
w2_y = back_y - ux * TRI_HALF_BASE
|
|
387
387
|
pts = [
|
|
388
|
-
Canvas.coord(tx), Canvas.coord(ty),
|
|
389
|
-
Canvas.coord(w1_x), Canvas.coord(w1_y),
|
|
390
|
-
Canvas.coord(w2_x), Canvas.coord(w2_y)
|
|
388
|
+
Canvas.coord(tx.round), Canvas.coord(ty.round),
|
|
389
|
+
Canvas.coord(w1_x.round), Canvas.coord(w1_y.round),
|
|
390
|
+
Canvas.coord(w2_x.round), Canvas.coord(w2_y.round)
|
|
391
391
|
].join(" ")
|
|
392
392
|
%(<polygon points="#{pts}" shape-rendering="auto" style="fill-rule:evenodd;"/>)
|
|
393
393
|
end
|
|
@@ -404,7 +404,9 @@ module Ea
|
|
|
404
404
|
w1_y = back_y + ux * ARROW_HALF_BASE
|
|
405
405
|
w2_x = back_x - (-uy) * ARROW_HALF_BASE
|
|
406
406
|
w2_y = back_y - ux * ARROW_HALF_BASE
|
|
407
|
-
d = "M #{Canvas.coord(w1_x)} #{Canvas.coord(w1_y
|
|
407
|
+
d = "M #{Canvas.coord(w1_x.round)} #{Canvas.coord(w1_y.round)} " \
|
|
408
|
+
"L #{Canvas.coord(tx.round)} #{Canvas.coord(ty.round)} " \
|
|
409
|
+
"L #{Canvas.coord(w2_x.round)} #{Canvas.coord(w2_y.round)}"
|
|
408
410
|
%(<path d="#{d}" shape-rendering="auto"/>)
|
|
409
411
|
end
|
|
410
412
|
|
data/lib/ea/version.rb
CHANGED