ea 0.6.95 → 0.6.97
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 +3 -0
- data/config/themes/39.yml +8 -0
- data/lib/ea/fonts/cambria_7_regular_widths.json +77 -0
- data/lib/ea/fonts/metrics.rb +36 -1
- data/lib/ea/sources/qea/diagram_builder/routing.rb +181 -0
- data/lib/ea/sources/qea/diagram_builder.rb +5 -2
- data/lib/ea/svg/ea_emitter/connector_redock.rb +15 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +108 -10
- data/lib/ea/svg/ea_emitter/labels.rb +72 -5
- data/lib/ea/theme/definition.rb +29 -0
- data/lib/ea/theme/loader.rb +5 -0
- data/lib/ea/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2ca3f0fd4381aec39af89849d601c85defa496b697f1fe625558c8a210aef64
|
|
4
|
+
data.tar.gz: adbc3773a202f06ad7327ace0fbea64daacea45a4719afb28959332673dafdcd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99b878a17d58444e251964011cf2af59d46459223e657b8ffbf1a4602dc572af56e594bedd05c321cec0502d91e654cdd013d5b408277027d1c4f41c65885d3f
|
|
7
|
+
data.tar.gz: 73ef9107ec26ea7b60f7c9e5d00b7fa09b4d08b10842345c589a75dfd13a62b553287b95b0ad1b71e0c45fc032ab30ff1299fecb03e2a3abac69027a69180e97
|
data/config/themes/119.yml
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
" ": 2,
|
|
3
|
+
"(": 4,
|
|
4
|
+
")": 4,
|
|
5
|
+
"*": 4,
|
|
6
|
+
"+": 6,
|
|
7
|
+
"-": 3,
|
|
8
|
+
".": 2,
|
|
9
|
+
"0": 6,
|
|
10
|
+
"1": 6,
|
|
11
|
+
"2": 6,
|
|
12
|
+
"3": 6,
|
|
13
|
+
"4": 6,
|
|
14
|
+
"5": 6,
|
|
15
|
+
"6": 6,
|
|
16
|
+
"8": 6,
|
|
17
|
+
"9": 6,
|
|
18
|
+
":": 3,
|
|
19
|
+
"<": 6,
|
|
20
|
+
"=": 6,
|
|
21
|
+
">": 6,
|
|
22
|
+
"A": 6,
|
|
23
|
+
"B": 6,
|
|
24
|
+
"C": 6,
|
|
25
|
+
"D": 7,
|
|
26
|
+
"E": 6,
|
|
27
|
+
"F": 5,
|
|
28
|
+
"G": 6,
|
|
29
|
+
"H": 7,
|
|
30
|
+
"I": 3,
|
|
31
|
+
"K": 6,
|
|
32
|
+
"L": 5,
|
|
33
|
+
"M": 8,
|
|
34
|
+
"N": 7,
|
|
35
|
+
"O": 7,
|
|
36
|
+
"P": 6,
|
|
37
|
+
"Q": 7,
|
|
38
|
+
"R": 6,
|
|
39
|
+
"S": 5,
|
|
40
|
+
"T": 6,
|
|
41
|
+
"U": 6,
|
|
42
|
+
"V": 6,
|
|
43
|
+
"W": 9,
|
|
44
|
+
"X": 6,
|
|
45
|
+
"Y": 6,
|
|
46
|
+
"Z": 5,
|
|
47
|
+
"[": 4,
|
|
48
|
+
"]": 4,
|
|
49
|
+
"a": 5,
|
|
50
|
+
"b": 5,
|
|
51
|
+
"c": 4,
|
|
52
|
+
"d": 6,
|
|
53
|
+
"e": 5,
|
|
54
|
+
"f": 3,
|
|
55
|
+
"g": 5,
|
|
56
|
+
"h": 6,
|
|
57
|
+
"i": 3,
|
|
58
|
+
"j": 3,
|
|
59
|
+
"k": 5,
|
|
60
|
+
"l": 3,
|
|
61
|
+
"m": 8,
|
|
62
|
+
"n": 6,
|
|
63
|
+
"o": 5,
|
|
64
|
+
"p": 6,
|
|
65
|
+
"q": 5,
|
|
66
|
+
"r": 4,
|
|
67
|
+
"s": 4,
|
|
68
|
+
"t": 3,
|
|
69
|
+
"u": 6,
|
|
70
|
+
"v": 5,
|
|
71
|
+
"w": 8,
|
|
72
|
+
"x": 5,
|
|
73
|
+
"y": 5,
|
|
74
|
+
"z": 5,
|
|
75
|
+
"{": 4,
|
|
76
|
+
"}": 4
|
|
77
|
+
}
|
data/lib/ea/fonts/metrics.rb
CHANGED
|
@@ -29,9 +29,18 @@ module Ea
|
|
|
29
29
|
"calibri" => "Carlito",
|
|
30
30
|
"carlito" => "Carlito",
|
|
31
31
|
"arial" => "Arial",
|
|
32
|
-
"arial narrow" => "Arial Narrow"
|
|
32
|
+
"arial narrow" => "Arial Narrow",
|
|
33
|
+
"cambria" => "Cambria"
|
|
33
34
|
}.freeze
|
|
34
35
|
|
|
36
|
+
# Cambria measurements are in SVG units at 7pt, not em advances.
|
|
37
|
+
# Unmeasured characters and other sizes/styles must use the fallback.
|
|
38
|
+
CAMBRIA_7_REGULAR_WIDTHS = JSON.parse(File.read(
|
|
39
|
+
File.expand_path("cambria_7_regular_widths.json", __dir__)
|
|
40
|
+
)).freeze
|
|
41
|
+
CAMBRIA_7_GUILLEMET_PAIR_WIDTH = 10
|
|
42
|
+
private_constant :CAMBRIA_7_REGULAR_WIDTHS, :CAMBRIA_7_GUILLEMET_PAIR_WIDTH
|
|
43
|
+
|
|
35
44
|
# Advance used for codepoints missing from the tables.
|
|
36
45
|
DEFAULT_ADVANCE = 0.25
|
|
37
46
|
|
|
@@ -64,6 +73,12 @@ module Ea
|
|
|
64
73
|
return nil unless supported_family?(family)
|
|
65
74
|
return nil if text.nil? || text.empty?
|
|
66
75
|
|
|
76
|
+
return cambria_text_length(text, size_pt, weight, style) if resolve_family(family) == "Cambria"
|
|
77
|
+
|
|
78
|
+
advance_text_length(text, size_pt, family, weight, style)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def advance_text_length(text, size_pt, family, weight, style)
|
|
67
82
|
resolved = resolve_family(family)
|
|
68
83
|
if GDI_HEIGHT_FAMILIES.include?(resolved)
|
|
69
84
|
height = (size_pt.to_f * 10 / 7).round
|
|
@@ -86,6 +101,26 @@ module Ea
|
|
|
86
101
|
cjk_count * CJK_ADVANCE * size_pt.to_f).round(3)
|
|
87
102
|
end
|
|
88
103
|
|
|
104
|
+
private_class_method :advance_text_length
|
|
105
|
+
|
|
106
|
+
def cambria_text_length(text, size_pt, weight, style)
|
|
107
|
+
return nil unless size_pt.to_f.to_r == 7
|
|
108
|
+
return nil unless ["", "0", "400", "normal"].include?(weight.to_s)
|
|
109
|
+
return nil unless ["", "normal"].include?(style.to_s)
|
|
110
|
+
|
|
111
|
+
return cambria_ascii_length(text) unless text.start_with?("«") && text.end_with?("»")
|
|
112
|
+
|
|
113
|
+
inner_width = cambria_ascii_length(text[1...-1])
|
|
114
|
+
inner_width + CAMBRIA_7_GUILLEMET_PAIR_WIDTH if inner_width
|
|
115
|
+
end
|
|
116
|
+
private_class_method :cambria_text_length
|
|
117
|
+
|
|
118
|
+
def cambria_ascii_length(text)
|
|
119
|
+
widths = text.each_char.map { |char| CAMBRIA_7_REGULAR_WIDTHS[char] }
|
|
120
|
+
widths.sum unless widths.empty? || widths.include?(nil)
|
|
121
|
+
end
|
|
122
|
+
private_class_method :cambria_ascii_length
|
|
123
|
+
|
|
89
124
|
# Families whose EA driver quantizes per-glyph advances
|
|
90
125
|
# (fitted from published textLengths).
|
|
91
126
|
GDI_HEIGHT_FAMILIES = %w[Carlito].freeze
|
|
@@ -8,6 +8,187 @@ module Ea
|
|
|
8
8
|
module Routing
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
# Stored SY/EY use mathematical Y; ray origins use screen Y.
|
|
12
|
+
# Computed-jog origins are already normalized to their derived ports.
|
|
13
|
+
def correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
14
|
+
return model if context
|
|
15
|
+
return model unless stored_ray_candidate?(model, link_row, geom)
|
|
16
|
+
|
|
17
|
+
source, target = stored_ray_boxes(link_row, diagram_row)
|
|
18
|
+
return model unless stored_ray_gap?(source, target, model.style[:mode], geom[:edge])
|
|
19
|
+
|
|
20
|
+
set_stored_ray_origins(model, source, target, geom)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stored_ray_boxes(link_row, diagram_row)
|
|
24
|
+
connector = database.find_connector(link_row.connectorid)
|
|
25
|
+
[source_placement_for(connector, diagram_row),
|
|
26
|
+
target_placement_for(connector, diagram_row)].map { |placement| bounds_from_rect(placement) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_stored_ray_origins(model, source, target, geom)
|
|
30
|
+
model.source_anchor = stored_ray_origin(source, geom[:sx], geom[:sy])
|
|
31
|
+
model.target_anchor = stored_ray_origin(target, geom[:ex], geom[:ey])
|
|
32
|
+
model
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stored_ray_candidate?(model, link_row, geom)
|
|
36
|
+
%w[Generalization Aggregation].include?(model.connector_type) &&
|
|
37
|
+
model.waypoints.size == 2 && stored_ray_style?(model.style, link_row) &&
|
|
38
|
+
stored_ray_geometry?(model, geom) && !normalized_port_anchors?(model)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stored_ray_style?(style, link_row)
|
|
42
|
+
%w[1 3].include?(style[:mode]) && !style[:tree] &&
|
|
43
|
+
(style[:mode] == "1" || link_row.path.to_s.empty?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stored_ray_geometry?(model, geom)
|
|
47
|
+
model.has_geometry_offsets && model.source_port && model.target_port &&
|
|
48
|
+
(!geom[:sy].to_i.zero? || !geom[:ey].to_i.zero?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def normalized_port_anchors?(model)
|
|
52
|
+
same_port_anchor?(model.source_anchor, model.source_port) &&
|
|
53
|
+
same_port_anchor?(model.target_anchor, model.target_port)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def same_port_anchor?(anchor, port)
|
|
57
|
+
anchor.x == port.x && anchor.y == port.y
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stored_ray_gap?(source, target, mode, edge)
|
|
61
|
+
return horizontal_ray_gap?(source, target) || vertical_ray_gap?(source, target) if mode == "1"
|
|
62
|
+
|
|
63
|
+
if edge == 1
|
|
64
|
+
target.y + target.height < source.y
|
|
65
|
+
elsif edge == 4
|
|
66
|
+
target.x + target.width < source.x
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def horizontal_ray_gap?(source, target)
|
|
71
|
+
source.x + source.width < target.x || target.x + target.width < source.x
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def vertical_ray_gap?(source, target)
|
|
75
|
+
source.y + source.height < target.y || target.y + target.height < source.y
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def stored_ray_origin(bounds, x_offset, y_offset)
|
|
79
|
+
Ea::Model::Point.new(x: bounds.x + bounds.width / 2 + x_offset.to_i,
|
|
80
|
+
y: bounds.y + bounds.height / 2 - y_offset.to_i)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Square routing keeps the stored orthogonal trunk, even when a
|
|
84
|
+
# shorter facing-edge route exists between the same boxes.
|
|
85
|
+
def stored_square_route(link_row, connector, source, target)
|
|
86
|
+
return nil unless stored_square_style?(link_row, connector)
|
|
87
|
+
|
|
88
|
+
corners = stored_square_corners(link_row)
|
|
89
|
+
square_corner_route(corners, source, target) if corners
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def stored_square_style?(link_row, connector)
|
|
93
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
94
|
+
%w[Association Aggregation Generalization].include?(connector.connector_type) &&
|
|
95
|
+
style[:mode] == "3" && style[:tree] == "OS" && style[:lwidth] == "2"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def stored_square_corners(link_row)
|
|
99
|
+
return nil unless link_row.path.to_s.match?(/\A(?:-?\d+:-?\d+;)*-?\d+:-?\d+;?\z/)
|
|
100
|
+
|
|
101
|
+
corners = intermediate_waypoints(link_row)
|
|
102
|
+
corners if corners.each_cons(2).all? { |a, b| a[0] == b[0] || a[1] == b[1] }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def square_corner_route(corners, source, target)
|
|
106
|
+
first = square_corner_dock(corners.first, source)
|
|
107
|
+
last = square_corner_dock(corners.last, target)
|
|
108
|
+
return nil unless first && last
|
|
109
|
+
|
|
110
|
+
points = corners.dup
|
|
111
|
+
points.unshift(first) unless points.first == first
|
|
112
|
+
points.push(last) unless points.last == last
|
|
113
|
+
points if points.size >= 3
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def square_corner_dock(corner, bounds)
|
|
117
|
+
x, y = corner
|
|
118
|
+
projected = [x.clamp(bounds.x, bounds.x + bounds.width),
|
|
119
|
+
y.clamp(bounds.y, bounds.y + bounds.height)]
|
|
120
|
+
return nil if projected[0] != x && projected[1] != y
|
|
121
|
+
|
|
122
|
+
projected if rectangle_edge?(projected, bounds)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def rectangle_edge?(point, bounds)
|
|
126
|
+
[bounds.x, bounds.x + bounds.width].include?(point[0]) ||
|
|
127
|
+
[bounds.y, bounds.y + bounds.height].include?(point[1])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A valid lateral corner joins one vertical leg and one horizontal
|
|
131
|
+
# leg. Keep it even when the boxes also admit a shorter direct route.
|
|
132
|
+
def stored_lateral_route(link_row, connector, source, target)
|
|
133
|
+
return nil unless %w[Aggregation Association].include?(connector.connector_type)
|
|
134
|
+
|
|
135
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
136
|
+
return nil unless stored_lateral_style?(style)
|
|
137
|
+
|
|
138
|
+
corner = stored_lateral_corner(link_row)
|
|
139
|
+
return nil unless corner
|
|
140
|
+
|
|
141
|
+
if style[:tree] == "LH"
|
|
142
|
+
horizontal_corner_route(corner, source, target)
|
|
143
|
+
else
|
|
144
|
+
vertical_corner_route(corner, source, target)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def stored_lateral_corner(link_row)
|
|
149
|
+
return nil unless link_row.path.to_s.match?(/\A-?\d+:-?\d+;?\z/)
|
|
150
|
+
|
|
151
|
+
intermediate_waypoints(link_row).first
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def stored_lateral_style?(style)
|
|
155
|
+
style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) && %w[LH LV].include?(style[:tree])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def horizontal_corner?(corner, source, target)
|
|
159
|
+
within_horizontal_span?(corner, source) && within_height?(corner[1], target) &&
|
|
160
|
+
!within_height?(corner[1], source) && !within_horizontal_span?(corner, target)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def horizontal_corner_route(corner, source, target)
|
|
164
|
+
return nil unless horizontal_corner?(corner, source, target)
|
|
165
|
+
|
|
166
|
+
x, y = corner
|
|
167
|
+
|
|
168
|
+
source_y = y < source.y ? source.y : source.y + source.height
|
|
169
|
+
target_x = x < target.x ? target.x : target.x + target.width
|
|
170
|
+
[[x, source_y], corner, [target_x, y]]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def vertical_corner?(corner, source, target)
|
|
174
|
+
within_height?(corner[1], source) && within_horizontal_span?(corner, target) &&
|
|
175
|
+
!within_horizontal_span?(corner, source) && !within_height?(corner[1], target)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def vertical_corner_route(corner, source, target)
|
|
179
|
+
return nil unless vertical_corner?(corner, source, target)
|
|
180
|
+
|
|
181
|
+
x, y = corner
|
|
182
|
+
|
|
183
|
+
source_x = x < source.x ? source.x : source.x + source.width
|
|
184
|
+
target_y = y < target.y ? target.y : target.y + target.height
|
|
185
|
+
[[source_x, y], corner, [x, target_y]]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def within_height?(height, bounds)
|
|
189
|
+
height.between?(bounds.y, bounds.y + bounds.height)
|
|
190
|
+
end
|
|
191
|
+
|
|
11
192
|
# A lateral tree leaves the side edge before turning upward.
|
|
12
193
|
# Keep that dock only when its first leg actually points outward.
|
|
13
194
|
def lateral_source_exit?(points, link_row, bounds)
|
|
@@ -385,7 +385,7 @@ module Ea
|
|
|
385
385
|
geom = parse_geometry_fields(link_row.geometry)
|
|
386
386
|
context = ContextDiagramSupport.package_context(diagram_row) ||
|
|
387
387
|
ContextDiagramSupport.element_regenerate?(diagram_row)
|
|
388
|
-
collapse_computed_jog(Ea::Model::DiagramConnector.new(
|
|
388
|
+
model = collapse_computed_jog(Ea::Model::DiagramConnector.new(
|
|
389
389
|
id: IdNormalizer.synthetic("dc", diagram_row.diagram_id,
|
|
390
390
|
link_row.instance_id),
|
|
391
391
|
diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
|
|
@@ -410,6 +410,7 @@ module Ea
|
|
|
410
410
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
411
411
|
is_substitutable: substitutable_connector?(connector)
|
|
412
412
|
), link_row, diagram_row, geom)
|
|
413
|
+
correct_stored_ray_origins(model, link_row, diagram_row, geom, context)
|
|
413
414
|
end
|
|
414
415
|
|
|
415
416
|
# Ray origin for one connector end: the element's center
|
|
@@ -648,7 +649,9 @@ module Ea
|
|
|
648
649
|
# EDGE=2 routes are excluded; they are already exact.
|
|
649
650
|
src = bounds_from_rect(source_placement)
|
|
650
651
|
tgt = bounds_from_rect(target_placement)
|
|
651
|
-
tree_route =
|
|
652
|
+
tree_route = stored_square_route(link_row, connector, src, tgt) ||
|
|
653
|
+
stored_lateral_route(link_row, connector, src, tgt) ||
|
|
654
|
+
horizontal_tree_route(points, link_row, connector, src, tgt)
|
|
652
655
|
if tree_route
|
|
653
656
|
points = tree_route
|
|
654
657
|
elsif %w[Association Aggregation].include?(connector.connector_type) &&
|
|
@@ -106,7 +106,7 @@ module Ea
|
|
|
106
106
|
def outline_connector?(connector, bounds)
|
|
107
107
|
return true if %w[Generalization Aggregation].include?(connector.connector_type)
|
|
108
108
|
|
|
109
|
-
return false unless stored_association?(connector)
|
|
109
|
+
return false unless stored_association?(connector) || offset_free_association?(connector)
|
|
110
110
|
|
|
111
111
|
# Diagonal Association rounding and tree docks keep their existing rules.
|
|
112
112
|
pairs = clipped_pairs(bounds, [connector.source_anchor, connector.target_anchor])
|
|
@@ -122,6 +122,20 @@ module Ea
|
|
|
122
122
|
end
|
|
123
123
|
private_class_method :stored_association?
|
|
124
124
|
|
|
125
|
+
def offset_free_association?(connector)
|
|
126
|
+
style = connector.style.to_h
|
|
127
|
+
connector.connector_type == "Association" && style[:mode] == "3" && !style[:tree] &&
|
|
128
|
+
%i[direct regenerated].none? { |key| style.key?(key) } && offset_free_anchors?(connector)
|
|
129
|
+
end
|
|
130
|
+
private_class_method :offset_free_association?
|
|
131
|
+
|
|
132
|
+
def offset_free_anchors?(connector)
|
|
133
|
+
!connector.has_geometry_offsets &&
|
|
134
|
+
[connector.source_port, connector.target_port].all?(&:nil?) &&
|
|
135
|
+
[connector.source_anchor, connector.target_anchor].all?
|
|
136
|
+
end
|
|
137
|
+
private_class_method :offset_free_anchors?
|
|
138
|
+
|
|
125
139
|
def clipped_pairs(bounds, anchors)
|
|
126
140
|
source, target = bounds
|
|
127
141
|
source_anchor, target_anchor = anchors
|
|
@@ -75,11 +75,8 @@ module Ea
|
|
|
75
75
|
text_pos, mult_pos = tree_positions(end_kind, anchor, role,
|
|
76
76
|
text_pos, mult_pos)
|
|
77
77
|
elsif mult_box && stored_offsets_zero?(mult_box)
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# endpoint anchor, regardless of line direction
|
|
81
|
-
# (87-sample corpus fit: dy = +15 constant, dx = +12
|
|
82
|
-
# dominant with a +3 minority).
|
|
78
|
+
# Start at the legacy default; measured directional seats
|
|
79
|
+
# below replace it for default Cambria labels.
|
|
83
80
|
mult_pos = [anchor[0] + 12, anchor[1] + 15]
|
|
84
81
|
end
|
|
85
82
|
# The end's stored stereotype renders verbatim («Type» on
|
|
@@ -89,8 +86,13 @@ module Ea
|
|
|
89
86
|
stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
|
|
90
87
|
direction = leg_direction(end_kind, heading)
|
|
91
88
|
if direction && !(tree && zero_offset?(text_box, mult_box))
|
|
92
|
-
text_pos =
|
|
93
|
-
|
|
89
|
+
text_pos = source_property_seat(connector, direction, anchor, text_box, role) ||
|
|
90
|
+
plain_upward_seat(connector, direction, anchor, text_box, role) ||
|
|
91
|
+
role_seat(direction, anchor, text_box, role) || text_pos
|
|
92
|
+
mult_pos = directional_multiplicity(connector, direction, anchor, mult_box, mult) ||
|
|
93
|
+
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
94
|
+
elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
|
|
95
|
+
mult_pos = target_multiplicity_seat(heading, anchor, mult_box) || mult_pos
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
build_texts(role:, mult:, standalone:,
|
|
@@ -147,19 +149,22 @@ module Ea
|
|
|
147
149
|
# right: the multiplicity 6px above it on an upward leg and
|
|
148
150
|
# 15px below it on a rightward one, the role 7px above it on
|
|
149
151
|
# a rightward one. Other sizes, headings, the target end,
|
|
150
|
-
# custom presentation keep their offsets
|
|
152
|
+
# custom presentation keep their offsets. Zero multiplicity boxes
|
|
153
|
+
# follow the direction for Cambria and keep the default otherwise.
|
|
151
154
|
def leg_direction(end_kind, heading)
|
|
152
155
|
return nil unless source_seat?(end_kind, heading)
|
|
153
156
|
|
|
154
157
|
dx, dy = heading
|
|
155
158
|
return :up if dx.zero? && dy.negative?
|
|
156
159
|
return :right if dy.zero? && dx.positive?
|
|
160
|
+
return :left if dy.zero? && dx.negative?
|
|
161
|
+
return :down if dx.zero? && dy.positive?
|
|
157
162
|
|
|
158
163
|
nil
|
|
159
164
|
end
|
|
160
165
|
|
|
161
166
|
# Role text on a rightward leg is left-aligned at the corner,
|
|
162
|
-
# 7px above it.
|
|
167
|
+
# 7px above it.
|
|
163
168
|
def role_seat(direction, anchor, box, role)
|
|
164
169
|
return nil unless direction == :right
|
|
165
170
|
return nil if box.nil? || role_empty?(role)
|
|
@@ -169,8 +174,101 @@ module Ea
|
|
|
169
174
|
[x + SEAT_X_INSET, y - 7]
|
|
170
175
|
end
|
|
171
176
|
|
|
177
|
+
# Property-role clusters use a direction-specific right edge;
|
|
178
|
+
# alignment controls the stereotype line within that cluster.
|
|
179
|
+
def source_property_seat(connector, direction, anchor, box, role)
|
|
180
|
+
return nil unless %i[up left down].include?(direction) && property_role_box?(box, direction)
|
|
181
|
+
return nil unless source_property_font?(connector, direction)
|
|
182
|
+
|
|
183
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
184
|
+
return nil unless width
|
|
185
|
+
|
|
186
|
+
x, y = seat_corner(anchor, box)
|
|
187
|
+
inset, baseline = property_role_offsets(direction)
|
|
188
|
+
[x - inset - width, y + baseline]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def plain_upward_seat(connector, direction, anchor, box, role)
|
|
192
|
+
return nil unless direction == :up && cambria_box?(box)
|
|
193
|
+
return nil unless [0, 1].include?(box["alignment"])
|
|
194
|
+
return nil unless connector_source_dest_stereotype(connector, :source).empty?
|
|
195
|
+
return nil unless plain_role_route?(connector.style.to_h)
|
|
196
|
+
|
|
197
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
198
|
+
return nil unless width
|
|
199
|
+
|
|
200
|
+
x, y = seat_corner(anchor, box)
|
|
201
|
+
[x - SEAT_X_INSET - width, y - 6]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def plain_role_route?(style)
|
|
205
|
+
%w[1 3].include?(style[:mode]) && %w[0 2].include?(style[:lwidth]) &&
|
|
206
|
+
[nil, "V"].include?(style[:tree])
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def property_role_box?(box, direction)
|
|
210
|
+
alignments = direction == :down ? [1] : [0, 1]
|
|
211
|
+
box && alignments.include?(box["alignment"]) && default_presentation?(box)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def property_role_offsets(direction)
|
|
215
|
+
case direction
|
|
216
|
+
when :up then [60, -6]
|
|
217
|
+
when :left then [60, -7]
|
|
218
|
+
else [65, 15]
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def source_property_font?(connector, direction)
|
|
223
|
+
font_family.to_s.casecmp("Cambria").zero? &&
|
|
224
|
+
property_label?(connector, :source) &&
|
|
225
|
+
source_property_route?(connector.style.to_h, direction)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def source_property_route?(style, direction)
|
|
229
|
+
case direction
|
|
230
|
+
when :left then left_property_route?(style)
|
|
231
|
+
when :down then downward_property_route?(style)
|
|
232
|
+
else style[:lwidth] == "2" && style[:tree] != "OS"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def left_property_route?(style)
|
|
237
|
+
style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
|
|
238
|
+
[nil, "H", "LV"].include?(style[:tree])
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def downward_property_route?(style)
|
|
242
|
+
%w[1 3].include?(style[:mode]) && style[:lwidth] == "2" && !style[:tree]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def directional_multiplicity(connector, direction, anchor, box, mult)
|
|
246
|
+
return nil unless cambria_box?(box)
|
|
247
|
+
|
|
248
|
+
x, y = seat_corner(anchor, box)
|
|
249
|
+
return [x + 8, y + 15] if direction == :down
|
|
250
|
+
return nil unless direction == :left && source_property_font?(connector, direction)
|
|
251
|
+
|
|
252
|
+
width = Ea::Fonts::Metrics.text_length(mult, font_size, family: font_family)
|
|
253
|
+
[x - width - 3, y + 15] if width
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def target_multiplicity_seat(heading, anchor, box)
|
|
257
|
+
return nil unless seat_font?
|
|
258
|
+
return nil unless heading && heading[0].zero? && heading[1].negative?
|
|
259
|
+
return nil unless cambria_box?(box) && stored_offsets_zero?(box)
|
|
260
|
+
|
|
261
|
+
[anchor[0] + 8, anchor[1] - 7]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def cambria_box?(box)
|
|
265
|
+
box && default_presentation?(box) && font_family.to_s.casecmp("Cambria").zero?
|
|
266
|
+
end
|
|
267
|
+
|
|
172
268
|
def mult_seat(direction, anchor, box)
|
|
173
|
-
return nil
|
|
269
|
+
return nil unless %i[up right].include?(direction)
|
|
270
|
+
return nil if box.nil?
|
|
271
|
+
return nil if stored_offsets_zero?(box) && !cambria_box?(box)
|
|
174
272
|
return nil unless default_presentation?(box)
|
|
175
273
|
|
|
176
274
|
x, y = seat_corner(anchor, box)
|
|
@@ -91,12 +91,13 @@ module Ea
|
|
|
91
91
|
end_renderer.texts(text_box: src_tb, mult_box: src_mb,
|
|
92
92
|
anchor: source_pt, connector: connector,
|
|
93
93
|
end_kind: :source, tree: tree && tree[:source],
|
|
94
|
-
heading: stored_src &&
|
|
94
|
+
heading: stored_src && source_heading(connector, points),
|
|
95
95
|
stored_labels: stored_labels).each { |t| texts << t }
|
|
96
96
|
end_renderer.texts(text_box: tgt_tb,
|
|
97
97
|
mult_box: tgt_mb,
|
|
98
98
|
anchor: target_pt, connector: connector,
|
|
99
99
|
end_kind: :target, tree: tree && tree[:target],
|
|
100
|
+
heading: stored_tgt && leg_heading(points.reverse),
|
|
100
101
|
stored_labels: stored_labels).each { |t| texts << t }
|
|
101
102
|
texts
|
|
102
103
|
end
|
|
@@ -112,6 +113,72 @@ module Ea
|
|
|
112
113
|
{ source: true, target: true }
|
|
113
114
|
end
|
|
114
115
|
|
|
116
|
+
# A diagonal clipped leg keeps the seat of its unambiguous exit face.
|
|
117
|
+
# Unknown faces retain the stored-upward fallback.
|
|
118
|
+
def source_heading(connector, points)
|
|
119
|
+
heading = leg_heading(points)
|
|
120
|
+
return heading unless points.size == 2 && heading.none?(&:zero?)
|
|
121
|
+
|
|
122
|
+
box = source_dock_bounds(connector)
|
|
123
|
+
face = box && dock_face_heading(points.first, box)
|
|
124
|
+
return face if face && face.zip(heading).any? { |axis, delta| (axis * delta).positive? }
|
|
125
|
+
|
|
126
|
+
stored_upward_heading(connector, heading)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stored_upward_heading(connector, heading)
|
|
130
|
+
return heading unless heading[1].negative?
|
|
131
|
+
|
|
132
|
+
stored = waypoint_pairs(connector)
|
|
133
|
+
return heading unless stored.size == 2
|
|
134
|
+
|
|
135
|
+
original = leg_heading(stored)
|
|
136
|
+
original[0].zero? && original[1].negative? ? original : heading
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def source_dock_bounds(connector)
|
|
140
|
+
return nil unless model_index && drawn_bounds
|
|
141
|
+
|
|
142
|
+
relationship = model_index[connector.relationship_ref]
|
|
143
|
+
return nil unless relationship
|
|
144
|
+
|
|
145
|
+
source, target = ConnectorRedock.end_refs(relationship)
|
|
146
|
+
return nil if source == target
|
|
147
|
+
return nil unless drawn_bounds[target]
|
|
148
|
+
|
|
149
|
+
drawn_bounds[source]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def dock_face_heading(point, box)
|
|
153
|
+
return nil unless box.width.positive? && box.height.positive?
|
|
154
|
+
|
|
155
|
+
vertical_dock_heading(point, box) || horizontal_dock_heading(point, box)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def vertical_dock_heading(point, box)
|
|
159
|
+
x, y = point
|
|
160
|
+
left = box.x
|
|
161
|
+
return nil unless x > left && x < left + box.width
|
|
162
|
+
|
|
163
|
+
top = box.y
|
|
164
|
+
return [0, -1] if [top, top - 1].include?(y)
|
|
165
|
+
|
|
166
|
+
bottom = top + box.height
|
|
167
|
+
[0, 1] if [bottom, bottom + 1].include?(y)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def horizontal_dock_heading(point, box)
|
|
171
|
+
x, y = point
|
|
172
|
+
top = box.y
|
|
173
|
+
return nil unless y > top && y < top + box.height
|
|
174
|
+
|
|
175
|
+
left = box.x
|
|
176
|
+
return [-1, 0] if [left, left - 1].include?(x)
|
|
177
|
+
|
|
178
|
+
right = left + box.width
|
|
179
|
+
[1, 0] if [right, right + 1].include?(x)
|
|
180
|
+
end
|
|
181
|
+
|
|
115
182
|
def leg_heading(points)
|
|
116
183
|
[points[1][0] - points[0][0], points[1][1] - points[0][1]]
|
|
117
184
|
end
|
|
@@ -159,7 +226,7 @@ module Ea
|
|
|
159
226
|
theme: theme,
|
|
160
227
|
font_family: label_font_family,
|
|
161
228
|
font_size: label_font_size,
|
|
162
|
-
font_unit: theme.
|
|
229
|
+
font_unit: theme.label_font_size_unit
|
|
163
230
|
)
|
|
164
231
|
end
|
|
165
232
|
|
|
@@ -169,7 +236,7 @@ module Ea
|
|
|
169
236
|
model_index: model_index,
|
|
170
237
|
font_family: label_font_family,
|
|
171
238
|
font_size: label_font_size,
|
|
172
|
-
font_unit: theme.
|
|
239
|
+
font_unit: theme.label_font_size_unit
|
|
173
240
|
)
|
|
174
241
|
end
|
|
175
242
|
|
|
@@ -195,11 +262,11 @@ module Ea
|
|
|
195
262
|
end
|
|
196
263
|
|
|
197
264
|
def label_font_family
|
|
198
|
-
theme.
|
|
265
|
+
theme.label_font_family || DEFAULT_FAMILY
|
|
199
266
|
end
|
|
200
267
|
|
|
201
268
|
def label_font_size
|
|
202
|
-
theme.
|
|
269
|
+
theme.label_font_size || DEFAULT_SIZE
|
|
203
270
|
end
|
|
204
271
|
end
|
|
205
272
|
end
|
data/lib/ea/theme/definition.rb
CHANGED
|
@@ -65,6 +65,8 @@ module Ea
|
|
|
65
65
|
text_y_offset: 12, line_height: 13).freeze
|
|
66
66
|
|
|
67
67
|
def initialize(id:, name: nil, font_family: nil, font_size: nil,
|
|
68
|
+
label_font_family: nil, label_font_size: nil,
|
|
69
|
+
label_font_size_unit: nil,
|
|
68
70
|
font_size_unit: "pt", text_weight_normal: 400,
|
|
69
71
|
text_weight_bold: 700,
|
|
70
72
|
element_font_size: 9, element_font_unit: "pt",
|
|
@@ -81,6 +83,9 @@ module Ea
|
|
|
81
83
|
@id = id.to_s
|
|
82
84
|
@name = name
|
|
83
85
|
@font_family = font_family
|
|
86
|
+
@label_font_family = label_font_family
|
|
87
|
+
@label_font_size = label_font_size
|
|
88
|
+
@label_font_size_unit = label_font_size_unit
|
|
84
89
|
@font_size = font_size
|
|
85
90
|
@font_size_unit = font_size_unit
|
|
86
91
|
@text_weight_normal = text_weight_normal
|
|
@@ -119,7 +124,22 @@ module Ea
|
|
|
119
124
|
fills[classifier.class.name]
|
|
120
125
|
end
|
|
121
126
|
|
|
127
|
+
def label_font_family
|
|
128
|
+
@label_font_family || font_family
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def label_font_size
|
|
132
|
+
@label_font_size || font_size
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def label_font_size_unit
|
|
136
|
+
@label_font_size_unit || font_size_unit
|
|
137
|
+
end
|
|
138
|
+
|
|
122
139
|
def with(**overrides)
|
|
140
|
+
clear_label_override(overrides, :font_family, :label_font_family)
|
|
141
|
+
clear_label_override(overrides, :font_size, :label_font_size)
|
|
142
|
+
clear_label_override(overrides, :font_size_unit, :label_font_size_unit)
|
|
123
143
|
Definition.new(**to_h.merge(overrides))
|
|
124
144
|
end
|
|
125
145
|
|
|
@@ -151,6 +171,9 @@ module Ea
|
|
|
151
171
|
id: id, name: name,
|
|
152
172
|
font_family: font_family, font_size: font_size,
|
|
153
173
|
font_size_unit: font_size_unit,
|
|
174
|
+
label_font_family: @label_font_family,
|
|
175
|
+
label_font_size: @label_font_size,
|
|
176
|
+
label_font_size_unit: @label_font_size_unit,
|
|
154
177
|
text_weight_normal: text_weight_normal,
|
|
155
178
|
text_weight_bold: text_weight_bold,
|
|
156
179
|
element_font_size: element_font_size,
|
|
@@ -171,6 +194,12 @@ module Ea
|
|
|
171
194
|
package: package, note: note, fills: fills
|
|
172
195
|
}
|
|
173
196
|
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def clear_label_override(overrides, global_key, label_key)
|
|
201
|
+
overrides[label_key] = nil if overrides.key?(global_key) && !overrides.key?(label_key)
|
|
202
|
+
end
|
|
174
203
|
end
|
|
175
204
|
end
|
|
176
205
|
end
|
data/lib/ea/theme/loader.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Ea
|
|
|
10
10
|
# name: EA White Theme
|
|
11
11
|
# font: { family: Carlito, size: 7, unit: pt, weight_normal: 400, ... }
|
|
12
12
|
# element_font: { size: 9, unit: pt }
|
|
13
|
+
# label_font: { family: Cambria, size: 7, unit: pt }
|
|
13
14
|
# colors: { text: "#000000", attribute_text: "#66413F", ... }
|
|
14
15
|
# strokes: { element_border: 2, connector_line: 2, ... }
|
|
15
16
|
# text_metrics: { width_factor: 0.65 }
|
|
@@ -38,6 +39,7 @@ module Ea
|
|
|
38
39
|
def from_hash(data)
|
|
39
40
|
font = data["font"] || {}
|
|
40
41
|
elem_font = data["element_font"] || {}
|
|
42
|
+
label_font = data["label_font"] || {}
|
|
41
43
|
colors = data["colors"] || {}
|
|
42
44
|
strokes = data["strokes"] || {}
|
|
43
45
|
text_metrics = data["text_metrics"] || {}
|
|
@@ -53,6 +55,9 @@ module Ea
|
|
|
53
55
|
id: data["id"],
|
|
54
56
|
name: data["name"],
|
|
55
57
|
font_family: font["family"],
|
|
58
|
+
label_font_family: label_font["family"],
|
|
59
|
+
label_font_size: label_font["size"],
|
|
60
|
+
label_font_size_unit: label_font["unit"],
|
|
56
61
|
font_size: font["size"],
|
|
57
62
|
font_size_unit: font["unit"] || "pt",
|
|
58
63
|
text_weight_normal: font["weight_normal"] || 400,
|
data/lib/ea/version.rb
CHANGED
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.97
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -501,6 +501,7 @@ files:
|
|
|
501
501
|
- config/qea_schema.yml
|
|
502
502
|
- config/stereotype_colors.yml
|
|
503
503
|
- config/themes/119.yml
|
|
504
|
+
- config/themes/39.yml
|
|
504
505
|
- config/themes/default.yml
|
|
505
506
|
- docs/ea_to_uml_type_mapping.md
|
|
506
507
|
- docs/xmi_qea_conversion_capabilities.md
|
|
@@ -615,6 +616,7 @@ files:
|
|
|
615
616
|
- lib/ea/export/xsd/generator.rb
|
|
616
617
|
- lib/ea/export/xsd/namespace_registry.rb
|
|
617
618
|
- lib/ea/fonts.rb
|
|
619
|
+
- lib/ea/fonts/cambria_7_regular_widths.json
|
|
618
620
|
- lib/ea/fonts/carlito_metrics.json
|
|
619
621
|
- lib/ea/fonts/metrics.rb
|
|
620
622
|
- lib/ea/guid_format.rb
|