ea 0.6.127 → 0.6.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ea/fonts/carlito_gdi_widths.json +1 -0
- data/lib/ea/fonts/cjk_fallback_widths.json +1 -0
- data/lib/ea/fonts/metrics.rb +73 -15
- data/lib/ea/fonts/vertical_metrics.json +154 -0
- data/lib/ea/sources/qea/diagram_builder/association_class.rb +52 -0
- data/lib/ea/sources/qea/diagram_builder/auto_routing.rb +26 -4
- data/lib/ea/sources/qea/diagram_builder/routing.rb +90 -4
- data/lib/ea/sources/qea/diagram_builder.rb +29 -5
- data/lib/ea/sources/qea/relationship_builder.rb +6 -6
- data/lib/ea/sources/xmi/relationship_builder.rb +15 -8
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -1
- data/lib/ea/svg/ea_emitter/compartment/header.rb +10 -1
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +24 -5
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +56 -20
- data/lib/ea/svg/ea_emitter/compartment/note_html.rb +18 -31
- data/lib/ea/svg/ea_emitter/connector_redock.rb +52 -3
- data/lib/ea/svg/ea_emitter/connectors.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +13 -3
- data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +14 -5
- data/lib/ea/svg/ea_emitter/elements.rb +13 -1
- data/lib/ea/svg/ea_emitter/font_resolver.rb +6 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +182 -10
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +28 -8
- data/lib/ea/svg/ea_emitter/label/plain_source_seat.rb +29 -0
- data/lib/ea/svg/ea_emitter/label/plain_target_seat.rb +28 -0
- data/lib/ea/svg/ea_emitter/label/source_stair_seat.rb +60 -0
- data/lib/ea/svg/ea_emitter/label/target_bend_seat.rb +45 -0
- data/lib/ea/svg/ea_emitter/label.rb +4 -0
- data/lib/ea/svg/ea_emitter/labels.rb +19 -3
- data/lib/ea/svg/ea_emitter/marker/diamond.rb +29 -5
- data/lib/ea/theme/definition.rb +9 -1
- data/lib/ea/version.rb +1 -1
- metadata +9 -1
|
@@ -18,13 +18,14 @@ module Ea
|
|
|
18
18
|
size:, size_unit: "pt", logical_height: nil,
|
|
19
19
|
fill: "#000000", weight_normal: 400, weight_bold: 700,
|
|
20
20
|
stroke_in_text: "#000000", width_factor: 0.612,
|
|
21
|
-
line_offset: 6)
|
|
21
|
+
line_offset: 6, default_signal_header: false)
|
|
22
22
|
logical_height = nil unless size_unit == "pt"
|
|
23
23
|
layout = line_layout(
|
|
24
24
|
lines, bounds: bounds, first_y: first_y, family: family,
|
|
25
25
|
size: size, size_unit: size_unit, logical_height: logical_height,
|
|
26
26
|
weight_normal: weight_normal, weight_bold: weight_bold,
|
|
27
|
-
width_factor: width_factor, line_offset: line_offset
|
|
27
|
+
width_factor: width_factor, line_offset: line_offset,
|
|
28
|
+
default_signal_header: default_signal_header
|
|
28
29
|
)
|
|
29
30
|
text_blocks = layout.map do |line|
|
|
30
31
|
TextRenderer.new(
|
|
@@ -44,12 +45,21 @@ module Ea
|
|
|
44
45
|
def self.line_layout(lines, bounds:, first_y:, family:, size:,
|
|
45
46
|
size_unit: "pt", logical_height: nil,
|
|
46
47
|
weight_normal: 400, weight_bold: 700,
|
|
47
|
-
width_factor: 0.612, line_offset: 6)
|
|
48
|
+
width_factor: 0.612, line_offset: 6, default_signal_header: false)
|
|
48
49
|
logical_height = nil unless size_unit == "pt"
|
|
49
50
|
line_h = Ea::Fonts::Metrics.compartment_line_height(
|
|
50
51
|
size, family: family, unit: size_unit, offset: line_offset,
|
|
51
52
|
logical_height: logical_height
|
|
52
53
|
)
|
|
54
|
+
# Native default Cambria 7pt Signals use a 15px header pitch.
|
|
55
|
+
# Keep explicit fonts, logical heights and layout overrides intact.
|
|
56
|
+
if default_signal_header && family == "Cambria" && size == 7 &&
|
|
57
|
+
size_unit == "pt" && logical_height.nil? && line_offset == 6 &&
|
|
58
|
+
first_y == bounds.y + 13 && lines.size == 2 &&
|
|
59
|
+
lines.first == ["«signal»", :normal] && lines.last.last == :bold
|
|
60
|
+
first_y -= 1
|
|
61
|
+
line_h = 15
|
|
62
|
+
end
|
|
53
63
|
ghost_shift = HeaderLines.parent_ghost_gap(lines)
|
|
54
64
|
lines.each_with_index.map do |(text, style), idx|
|
|
55
65
|
weight = %i[bold bold_italic].include?(style) ? weight_bold : weight_normal
|
|
@@ -19,8 +19,8 @@ module Ea
|
|
|
19
19
|
def self.render(tagged_values, bounds:, first_y:, family:, size:, fill: "#000000",
|
|
20
20
|
size_unit: "px", logical_height: nil)
|
|
21
21
|
line_h = line_height(size, family, size_unit, logical_height)
|
|
22
|
-
text_blocks = [build_text(bounds.x + centered_x_offset(bounds, size
|
|
23
|
-
HEADER_TEXT, family, size, fill,
|
|
22
|
+
text_blocks = [build_text(bounds.x + centered_x_offset(bounds, family, size, size_unit, logical_height),
|
|
23
|
+
first_y, HEADER_TEXT, family, size, fill,
|
|
24
24
|
style: "italic", size_unit: size_unit, logical_height: logical_height)]
|
|
25
25
|
tagged_values.each_with_index do |tv, idx|
|
|
26
26
|
text_blocks << build_text(bounds.x + LINE_X_OFFSET, first_y + ((idx + 1) * line_h),
|
|
@@ -48,9 +48,18 @@ module Ea
|
|
|
48
48
|
private_class_method :line_height
|
|
49
49
|
|
|
50
50
|
# Center the "tags" header text within the bounds width.
|
|
51
|
-
def self.centered_x_offset(bounds, size)
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
def self.centered_x_offset(bounds, family, size, size_unit, logical_height)
|
|
52
|
+
# QEA point captions center on the same integer width emitted in SVG.
|
|
53
|
+
# Retain the legacy pixel API's placement.
|
|
54
|
+
text_width = if size_unit == "pt"
|
|
55
|
+
TextRenderer.estimate_width(
|
|
56
|
+
HEADER_TEXT, size, family: family, weight: 400,
|
|
57
|
+
style: "italic", logical_height: logical_height
|
|
58
|
+
).round
|
|
59
|
+
else
|
|
60
|
+
HEADER_TEXT.length * (size / 2)
|
|
61
|
+
end
|
|
62
|
+
centered = ((bounds.width - text_width) / 2.0).floor
|
|
54
63
|
[HEADER_X_OFFSET, centered].max
|
|
55
64
|
end
|
|
56
65
|
private_class_method :centered_x_offset
|
|
@@ -156,6 +156,7 @@ module Ea
|
|
|
156
156
|
logical_height: logical_height,
|
|
157
157
|
content_x_offset: attribute_content_x_offset(size, family, size_unit)),
|
|
158
158
|
row_content_width(op_lines, 3, size, family, logical_height: logical_height),
|
|
159
|
+
tag_content_width(element, classifier, size, family, logical_height: logical_height),
|
|
159
160
|
Compartment::EnumLiterals.content_width(
|
|
160
161
|
enum_literals_for(classifier),
|
|
161
162
|
theme: theme, size: size, family: family,
|
|
@@ -302,7 +303,7 @@ module Ea
|
|
|
302
303
|
end
|
|
303
304
|
|
|
304
305
|
def attribute_content_x_offset(size, family, size_unit)
|
|
305
|
-
offset = (theme.
|
|
306
|
+
offset = (theme.attribute_content_x_offset || Element::AttributeRenderer::DEFAULT_CONTENT_X_OFFSET) +
|
|
306
307
|
2 * (size - 7)
|
|
307
308
|
offset -= 1 if Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit)
|
|
308
309
|
offset
|
|
@@ -312,6 +313,17 @@ module Ea
|
|
|
312
313
|
Ea::Fonts::Metrics.compact_compartment_font?(size, family: family, unit: size_unit) ? 7 : 8
|
|
313
314
|
end
|
|
314
315
|
|
|
316
|
+
def tag_content_width(element, classifier, size, family, logical_height: nil)
|
|
317
|
+
return nil unless element.show_tagged_values
|
|
318
|
+
|
|
319
|
+
tagged_values_for(classifier).map do |tag|
|
|
320
|
+
TextRenderer.estimate_width("#{tag.key} = #{tag.value}", size,
|
|
321
|
+
family: family, weight: 400, style: "normal",
|
|
322
|
+
logical_height: logical_height).round +
|
|
323
|
+
2 * Element::TaggedValueRenderer::LINE_X_OFFSET
|
|
324
|
+
end.max
|
|
325
|
+
end
|
|
326
|
+
|
|
315
327
|
def row_content_width(lines, right_pad, size, family, **font_options)
|
|
316
328
|
return nil if lines.to_a.empty?
|
|
317
329
|
|
|
@@ -17,6 +17,11 @@ module Ea
|
|
|
17
17
|
# text-emitting layer.
|
|
18
18
|
class FontResolver
|
|
19
19
|
DEFAULT_FONT_FAMILY = "Yu Gothic UI"
|
|
20
|
+
# EA draws every text kind of a hand-drawn diagram (names, notes,
|
|
21
|
+
# connector labels) in this family.
|
|
22
|
+
HAND_DRAW_FAMILY = "Source Sans Pro"
|
|
23
|
+
# Native hand-drawn connector end labels carry font-weight:0.
|
|
24
|
+
HAND_DRAW_LABEL_WEIGHT = 0
|
|
20
25
|
# EA's default element compartment font size. EA-published
|
|
21
26
|
# reference SVGs render body text at 7pt (the current EA
|
|
22
27
|
# theme size) for every element whose objectstyle fontsz is
|
|
@@ -87,7 +92,7 @@ module Ea
|
|
|
87
92
|
private
|
|
88
93
|
|
|
89
94
|
def default_family_for(explicit)
|
|
90
|
-
return
|
|
95
|
+
return HAND_DRAW_FAMILY if diagram.hand_draw
|
|
91
96
|
|
|
92
97
|
theme_family_for(explicit) || diagram_default_family || DEFAULT_FONT_FAMILY
|
|
93
98
|
end
|
|
@@ -28,10 +28,12 @@ module Ea
|
|
|
28
28
|
MIN_ROLE_X = 3
|
|
29
29
|
|
|
30
30
|
attr_reader :canvas, :model_index, :document, :theme,
|
|
31
|
-
:font_family, :font_size, :font_unit
|
|
31
|
+
:font_family, :font_size, :font_unit,
|
|
32
|
+
:font_weight, :font_style
|
|
32
33
|
|
|
33
34
|
def initialize(canvas:, model_index:, document:, theme:,
|
|
34
|
-
font_family:, font_size:, font_unit
|
|
35
|
+
font_family:, font_size:, font_unit:,
|
|
36
|
+
font_weight: 400, font_style: "normal")
|
|
35
37
|
@canvas = canvas
|
|
36
38
|
@model_index = model_index
|
|
37
39
|
@document = document
|
|
@@ -39,6 +41,8 @@ module Ea
|
|
|
39
41
|
@font_family = font_family
|
|
40
42
|
@font_size = font_size
|
|
41
43
|
@font_unit = font_unit
|
|
44
|
+
@font_weight = font_weight
|
|
45
|
+
@font_style = font_style
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
# Returns an Array of `<text>` SVG strings for one endpoint.
|
|
@@ -49,11 +53,13 @@ module Ea
|
|
|
49
53
|
# connector - the DiagramConnector (for relationship lookup)
|
|
50
54
|
# end_kind - :source or :target
|
|
51
55
|
# heading - [dx, dy] of the first leg leaving the anchor
|
|
56
|
+
# source_bounds - effective drawn source outline, in anchor coordinates
|
|
57
|
+
# bend - first bend of a saved descending vertical stair
|
|
52
58
|
# stored_labels - the connector has stored label boxes at
|
|
53
59
|
# either end; false keeps the opposite-end
|
|
54
60
|
# fallback
|
|
55
61
|
def texts(text_box:, mult_box:, anchor:, connector:, end_kind:,
|
|
56
|
-
tree: false, heading: nil, stored_labels: false)
|
|
62
|
+
tree: false, heading: nil, stored_labels: false, bend: nil, source_bounds: nil)
|
|
57
63
|
from_property, role, mult = role_and_multiplicity(connector, end_kind)
|
|
58
64
|
if role_empty?(role) && mult_empty?(mult)
|
|
59
65
|
other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
|
|
@@ -92,7 +98,8 @@ module Ea
|
|
|
92
98
|
stereo = stereo.nil? || stereo.empty? ? nil : "«#{stereo}»"
|
|
93
99
|
direction = leg_direction(end_kind, heading)
|
|
94
100
|
if direction && !(tree && zero_offset?(text_box, mult_box))
|
|
95
|
-
text_pos =
|
|
101
|
+
text_pos = plain_leftward_source_role(connector, direction, anchor, text_box, role) ||
|
|
102
|
+
plain_vertical_role(connector, direction, anchor, text_box, role) ||
|
|
96
103
|
source_property_seat(connector, direction, anchor, text_box, role) ||
|
|
97
104
|
plain_upward_seat(connector, direction, anchor, text_box, role) ||
|
|
98
105
|
role_seat(direction, anchor, text_box, role) || text_pos
|
|
@@ -100,7 +107,10 @@ module Ea
|
|
|
100
107
|
horizontal_tree_multiplicity(connector, direction, anchor, mult_box, mult) ||
|
|
101
108
|
mult_seat(direction, anchor, mult_box) || mult_pos
|
|
102
109
|
elsif end_kind == :target && !(tree && zero_offset?(text_box, mult_box))
|
|
103
|
-
text_pos =
|
|
110
|
+
text_pos = plain_leftward_target_role(connector, heading, anchor, text_box, role) ||
|
|
111
|
+
target_leftward_property_role(connector, heading, anchor, text_box, role) ||
|
|
112
|
+
target_property_role(connector, heading, anchor, text_box, role) ||
|
|
113
|
+
saved_ray_target_property_role(connector, heading, anchor, text_box, role) ||
|
|
104
114
|
target_diagonal_property_role(connector, heading, anchor, text_box, role) ||
|
|
105
115
|
target_vertical_role(connector, heading, anchor, text_box, role) ||
|
|
106
116
|
target_rightward_role(heading, anchor, text_box, role) || text_pos
|
|
@@ -111,16 +121,112 @@ module Ea
|
|
|
111
121
|
if end_kind == :source
|
|
112
122
|
text_pos = source_top_property_role(connector, heading, anchor, text_box, role) ||
|
|
113
123
|
source_bottom_property_role(connector, heading, anchor, text_box, role) || text_pos
|
|
124
|
+
if external_upward_property_source?(connector, heading, anchor, text_box, source_bounds)
|
|
125
|
+
text_pos = external_upward_property_role(anchor, role) || text_pos
|
|
126
|
+
end
|
|
114
127
|
end
|
|
115
128
|
|
|
116
129
|
text_box = text_box.merge("alignment" => 1) if phantom_property_box?(connector, direction, text_box)
|
|
117
130
|
|
|
131
|
+
if end_kind == :target && !from_property
|
|
132
|
+
bend_positions = target_bend_positions(connector, bend, text_box, mult_box, role)
|
|
133
|
+
text_pos, mult_pos = bend_positions if bend_positions
|
|
134
|
+
end
|
|
135
|
+
if end_kind == :source && !from_property
|
|
136
|
+
stair_positions = source_stair_positions(connector, anchor, text_box, mult_box, role)
|
|
137
|
+
text_pos, mult_pos = stair_positions if stair_positions
|
|
138
|
+
end
|
|
139
|
+
|
|
118
140
|
build_texts(role:, mult:, standalone:,
|
|
119
141
|
text_pos:, mult_pos:, stereo:, text_box:)
|
|
120
142
|
end
|
|
121
143
|
|
|
122
144
|
private
|
|
123
145
|
|
|
146
|
+
def plain_leftward_target_role(connector, heading, anchor, box, role)
|
|
147
|
+
return nil unless heading && heading[0].negative? && heading[1].zero?
|
|
148
|
+
return nil unless plain_target_role?(connector, box, role)
|
|
149
|
+
|
|
150
|
+
return saved_ray_plain_target_position(anchor, box, role) if moved_plain_target_ray?(connector, box)
|
|
151
|
+
|
|
152
|
+
PlainTargetSeat.position(
|
|
153
|
+
connector: connector, anchor: anchor, box: box,
|
|
154
|
+
width: Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def saved_ray_plain_target_position(anchor, box, role)
|
|
159
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
160
|
+
x, y = seat_corner(anchor, box)
|
|
161
|
+
[x - width - SEAT_X_INSET, y - 7] if width
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def moved_plain_target_ray?(connector, box)
|
|
165
|
+
saved_target_ray?(connector) && box.values_at("alignment", "cy") == [1, 16] &&
|
|
166
|
+
!stored_offsets_zero?(box)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def saved_target_ray?(connector)
|
|
170
|
+
connector.connector_type == "Association" &&
|
|
171
|
+
connector.style.to_h.values_at(:mode, :lwidth, :tree, :stored_association_ray) == ["3", "0", nil, true]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Moved Property boxes on saved rays arriving from below use the
|
|
175
|
+
# downward seat, relative to the persisted label corner.
|
|
176
|
+
def saved_ray_target_property_role(connector, heading, anchor, box, role)
|
|
177
|
+
return nil unless downward_saved_property?(connector, heading, box)
|
|
178
|
+
|
|
179
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
180
|
+
return nil unless width
|
|
181
|
+
|
|
182
|
+
x, y = seat_corner(anchor, box)
|
|
183
|
+
inset, baseline = property_role_offsets(:down)
|
|
184
|
+
[x - inset - width, y + baseline]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def downward_saved_property?(connector, heading, box)
|
|
188
|
+
heading && heading[1].positive? && heading[1] > heading[0].abs &&
|
|
189
|
+
saved_target_ray?(connector) && moved_target_property_box?(connector, box)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def plain_target_role?(connector, box, role)
|
|
193
|
+
seat_font? && cambria_box?(box) && !role_empty?(role) &&
|
|
194
|
+
connector_source_dest_stereotype(connector, :target).to_s.empty?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def plain_leftward_source_role(connector, direction, anchor, box, role)
|
|
198
|
+
return nil unless direction == :left && seat_font? && cambria_box?(box) && !role_empty?(role)
|
|
199
|
+
return nil unless connector_source_dest_stereotype(connector, :source).to_s.empty?
|
|
200
|
+
|
|
201
|
+
PlainSourceSeat.position(
|
|
202
|
+
connector: connector, anchor: anchor, box: box,
|
|
203
|
+
width: Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def source_stair_positions(connector, anchor, text_box, mult_box, role)
|
|
208
|
+
return nil unless seat_font? && cambria_box?(text_box) && cambria_box?(mult_box)
|
|
209
|
+
return nil if role_empty?(role)
|
|
210
|
+
|
|
211
|
+
property = property_label?(connector, :source)
|
|
212
|
+
return nil unless property || connector_source_dest_stereotype(connector, :source).to_s.empty?
|
|
213
|
+
|
|
214
|
+
SourceStairSeat.positions(
|
|
215
|
+
connector: connector, anchor: anchor, text_box: text_box, mult_box: mult_box,
|
|
216
|
+
width: Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def target_bend_positions(connector, bend, text_box, mult_box, role)
|
|
221
|
+
return nil unless bend && seat_font? && cambria_box?(text_box) && cambria_box?(mult_box)
|
|
222
|
+
return nil unless connector_source_dest_stereotype(connector, :target).to_s.empty? && !role_empty?(role)
|
|
223
|
+
|
|
224
|
+
TargetBendSeat.positions(
|
|
225
|
+
connector: connector, bend: bend, text_box: text_box, mult_box: mult_box,
|
|
226
|
+
width: Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
124
230
|
# With stored boxes on the connector, an end that carries no
|
|
125
231
|
# data of its own draws only its own stereotype; the data end
|
|
126
232
|
# draws the role and multiplicity; with no data anywhere nothing
|
|
@@ -241,6 +347,43 @@ module Ea
|
|
|
241
347
|
source_property_seat(connector, :up, anchor, box, role)
|
|
242
348
|
end
|
|
243
349
|
|
|
350
|
+
# A persisted origin above the drawn source outline keeps the
|
|
351
|
+
# upward seat's inset and the shared upper baseline, including
|
|
352
|
+
# steep diagonals. Origins clipped onto the face retain -6.
|
|
353
|
+
def external_upward_property_role(anchor, role)
|
|
354
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
355
|
+
return nil unless width
|
|
356
|
+
|
|
357
|
+
inset, = property_role_offsets(:up)
|
|
358
|
+
_, baseline = property_role_offsets(:left)
|
|
359
|
+
[[anchor[0] - inset - width, MIN_ROLE_X].max, anchor[1] + baseline]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def external_upward_property_source?(connector, heading, anchor, box, bounds)
|
|
363
|
+
upward_source_heading?(heading) &&
|
|
364
|
+
zero_source_property_box?(box) && cambria_box?(box) && source_property_cambria?(connector) &&
|
|
365
|
+
external_source_route?(connector, anchor) && above_source_outline?(anchor, bounds)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def upward_source_heading?(heading)
|
|
369
|
+
source_seat?(:source, heading) && heading[1].negative? && heading[1].abs > heading[0].abs
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def external_source_route?(connector, anchor)
|
|
373
|
+
origin = connector.source_anchor
|
|
374
|
+
connector.connector_type == "Association" && connector.waypoints.size == 3 &&
|
|
375
|
+
connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] &&
|
|
376
|
+
connector.label_boxes.to_h.values_at(:llt, :llb).all?(&:nil?) &&
|
|
377
|
+
origin && [origin.x, origin.y] == anchor
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def above_source_outline?(anchor, bounds)
|
|
381
|
+
return false unless bounds&.width&.positive? && bounds.height.positive?
|
|
382
|
+
|
|
383
|
+
anchor[1] < bounds.y &&
|
|
384
|
+
anchor[0].between?(bounds.x, bounds.x + bounds.width)
|
|
385
|
+
end
|
|
386
|
+
|
|
244
387
|
def zero_source_property_box?(box)
|
|
245
388
|
box && box["alignment"]&.zero? && stored_offsets_zero?(box)
|
|
246
389
|
end
|
|
@@ -281,7 +424,7 @@ module Ea
|
|
|
281
424
|
return nil unless direction == :up && cambria_box?(box)
|
|
282
425
|
return nil unless [0, 1].include?(box["alignment"])
|
|
283
426
|
return nil unless connector_source_dest_stereotype(connector, :source).empty?
|
|
284
|
-
return nil unless plain_role_route?(connector.style.to_h)
|
|
427
|
+
return nil unless plain_role_route?(connector.style.to_h, box)
|
|
285
428
|
|
|
286
429
|
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
287
430
|
return nil unless width
|
|
@@ -290,6 +433,30 @@ module Ea
|
|
|
290
433
|
[x - SEAT_X_INSET - width, y - 6]
|
|
291
434
|
end
|
|
292
435
|
|
|
436
|
+
# Moved target Property clusters on horizontal arrivals share
|
|
437
|
+
# the leftward source seat relative to their stored corner.
|
|
438
|
+
def target_leftward_property_role(connector, heading, anchor, box, role)
|
|
439
|
+
return nil unless heading && heading[0].negative? && heading[1].zero?
|
|
440
|
+
return nil unless moved_target_property_box?(connector, box)
|
|
441
|
+
return nil unless horizontal_target_property_route?(connector.style.to_h)
|
|
442
|
+
|
|
443
|
+
width = Ea::Fonts::Metrics.text_length(role, font_size, family: font_family)
|
|
444
|
+
return nil unless width
|
|
445
|
+
|
|
446
|
+
x, y = seat_corner(anchor, box)
|
|
447
|
+
[x - property_role_offsets(:left).first - width, y - 7]
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def moved_target_property_box?(connector, box)
|
|
451
|
+
plain_vertical_box?(box) && box["alignment"] == 1 && [31, 35].include?(box["cy"]) &&
|
|
452
|
+
!stored_offsets_zero?(box) && property_label?(connector, :target)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def horizontal_target_property_route?(style)
|
|
456
|
+
style.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] ||
|
|
457
|
+
(style.values_at(:mode, :lwidth) == %w[3 2] && [nil, "LH"].include?(style[:tree]))
|
|
458
|
+
end
|
|
459
|
+
|
|
293
460
|
# An upward target uses the downward property's right edge and
|
|
294
461
|
# the upward target baseline, both relative to its stored corner.
|
|
295
462
|
def target_property_role(connector, heading, anchor, box, role)
|
|
@@ -369,9 +536,8 @@ module Ea
|
|
|
369
536
|
seat_font? && cambria_box?(box) && [0, 1].include?(box["alignment"])
|
|
370
537
|
end
|
|
371
538
|
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
# width-0 route.
|
|
539
|
+
# Downward target seats require LV width 2; upward target seats
|
|
540
|
+
# also accept LV width 0. Source seats require a plain width-0 route.
|
|
375
541
|
def plain_vertical_connector?(connector, end_kind, direction)
|
|
376
542
|
return false unless connector.connector_type == "Association" &&
|
|
377
543
|
connector_source_dest_stereotype(connector, end_kind).empty?
|
|
@@ -384,12 +550,16 @@ module Ea
|
|
|
384
550
|
|
|
385
551
|
if direction == :target_down
|
|
386
552
|
style[:lwidth] == "2" && style[:tree] == "LV"
|
|
553
|
+
elsif direction == :target_up
|
|
554
|
+
style[:lwidth] == "0" && [nil, "LV"].include?(style[:tree])
|
|
387
555
|
else
|
|
388
556
|
style[:lwidth] == "0" && !style[:tree]
|
|
389
557
|
end
|
|
390
558
|
end
|
|
391
559
|
|
|
392
|
-
def plain_role_route?(style)
|
|
560
|
+
def plain_role_route?(style, box)
|
|
561
|
+
return box["cy"] == 16 if orthogonal_stub_route?(style)
|
|
562
|
+
|
|
393
563
|
%w[1 3].include?(style[:mode]) && %w[0 2].include?(style[:lwidth]) &&
|
|
394
564
|
[nil, "V"].include?(style[:tree])
|
|
395
565
|
end
|
|
@@ -473,6 +643,7 @@ module Ea
|
|
|
473
643
|
|
|
474
644
|
def left_property_route?(style)
|
|
475
645
|
downward_property_route?(style) || mode_two_route?(style) ||
|
|
646
|
+
(style[:overlap_ray] == true && style.values_at(:mode, :lwidth, :tree) == ["2", "0", nil]) ||
|
|
476
647
|
(style[:mode] == "3" && %w[0 2].include?(style[:lwidth]) &&
|
|
477
648
|
[nil, "H", "LV"].include?(style[:tree]))
|
|
478
649
|
end
|
|
@@ -828,6 +999,7 @@ module Ea
|
|
|
828
999
|
y_t = canvas ? canvas.translate_y(y) : y
|
|
829
1000
|
TextRenderer.new(content:, x: x_t, y: y_t,
|
|
830
1001
|
family: font_family, size: font_size,
|
|
1002
|
+
weight: font_weight, style: font_style,
|
|
831
1003
|
size_unit: font_unit,
|
|
832
1004
|
fill: fill_color).to_svg
|
|
833
1005
|
end
|
|
@@ -15,15 +15,17 @@ module Ea
|
|
|
15
15
|
# Stereotype wins when both are present.
|
|
16
16
|
class MidpointLabel
|
|
17
17
|
attr_reader :canvas, :model_index, :font_family, :font_size,
|
|
18
|
-
:font_unit
|
|
18
|
+
:font_unit, :font_weight, :font_style
|
|
19
19
|
|
|
20
20
|
def initialize(canvas:, model_index:, font_family:, font_size:,
|
|
21
|
-
font_unit:)
|
|
21
|
+
font_unit:, font_weight: 400, font_style: "normal")
|
|
22
22
|
@canvas = canvas
|
|
23
23
|
@model_index = model_index
|
|
24
24
|
@font_family = font_family
|
|
25
25
|
@font_size = font_size
|
|
26
26
|
@font_unit = font_unit
|
|
27
|
+
@font_weight = font_weight
|
|
28
|
+
@font_style = font_style
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Returns the SVG `<text>` for the connector's midpoint
|
|
@@ -112,14 +114,15 @@ module Ea
|
|
|
112
114
|
return nil unless stereotype_label(connector)
|
|
113
115
|
|
|
114
116
|
box = connector.label_boxes.to_h[:lmb]
|
|
115
|
-
return nil unless default_name_box?(box)
|
|
117
|
+
return nil unless default_name_box?(box, alignments: [0, 1])
|
|
116
118
|
|
|
117
119
|
stored_position(connector, points, box, LINE_PITCH)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# At 7pt, a centered name's stored corner is 3px below its baseline.
|
|
121
123
|
def stored_name_position(connector, points)
|
|
122
|
-
|
|
124
|
+
seat_points = horizontal_bend_seat(points) || points
|
|
125
|
+
stored_position(connector, seat_points, stored_name_box(connector, points), -3)
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
def stored_position(connector, points, box, baseline)
|
|
@@ -133,21 +136,37 @@ module Ea
|
|
|
133
136
|
[x + offset_x - (width / 2.0).floor, y + offset_y + baseline]
|
|
134
137
|
end
|
|
135
138
|
|
|
136
|
-
def stored_name_box(connector)
|
|
139
|
+
def stored_name_box(connector, points)
|
|
137
140
|
return nil unless connector.is_a?(Ea::Model::DiagramConnector) && measured_name_font?
|
|
138
141
|
return nil if connector.is_substitutable || stereotype_label(connector)
|
|
139
142
|
return nil if midpoint_content(connector).include?(": ")
|
|
140
143
|
|
|
141
144
|
box = connector.label_boxes.to_h[:lmt]
|
|
142
|
-
|
|
145
|
+
alignments = name_box_alignments(points)
|
|
146
|
+
box if default_name_box?(box, alignments: alignments)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def name_box_alignments(points)
|
|
150
|
+
horizontal_bend_seat(points) ? [0, 1] : [1]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A vertical-to-horizontal bend seats its name on the horizontal leg.
|
|
154
|
+
def horizontal_bend_seat(points)
|
|
155
|
+
return nil unless points.size == 3
|
|
156
|
+
|
|
157
|
+
source, bend, target = points
|
|
158
|
+
return nil unless source[0] == bend[0] && source[1] != bend[1] &&
|
|
159
|
+
bend[1] == target[1] && bend[0] != target[0]
|
|
160
|
+
|
|
161
|
+
[bend, target]
|
|
143
162
|
end
|
|
144
163
|
|
|
145
164
|
def measured_name_font?
|
|
146
165
|
font_family.to_s.casecmp("Cambria").zero? && font_size == 7 && font_unit == "pt"
|
|
147
166
|
end
|
|
148
167
|
|
|
149
|
-
def default_name_box?(box)
|
|
150
|
-
return false unless box && box["alignment"]
|
|
168
|
+
def default_name_box?(box, alignments: [1])
|
|
169
|
+
return false unless box && alignments.include?(box["alignment"])
|
|
151
170
|
return false unless box.values_at("ox", "oy").all?
|
|
152
171
|
return false unless [nil, 0, 1].include?(box["direction"]) && [nil, -1].include?(box["color"])
|
|
153
172
|
|
|
@@ -191,6 +210,7 @@ module Ea
|
|
|
191
210
|
y_t = canvas ? canvas.translate_y(y) : y
|
|
192
211
|
TextRenderer.new(content:, x: x_t, y: y_t,
|
|
193
212
|
family: font_family, size: font_size,
|
|
213
|
+
weight: font_weight, style: font_style,
|
|
194
214
|
size_unit: font_unit,
|
|
195
215
|
fill: "#000000").to_svg
|
|
196
216
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
module Label
|
|
7
|
+
# Plain leftward roles end before the stored corner, mirroring
|
|
8
|
+
# the 3px gutter and baseline of the rightward role seat.
|
|
9
|
+
module PlainSourceSeat
|
|
10
|
+
def self.position(connector:, anchor:, box:, width:)
|
|
11
|
+
return nil unless width && eligible_connector?(connector) && moved_role_box?(box)
|
|
12
|
+
|
|
13
|
+
[anchor[0] + box["ox"].to_i - width - 3, anchor[1] + box["oy"].to_i - 7]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.eligible_connector?(connector)
|
|
17
|
+
connector.connector_type == "Aggregation" &&
|
|
18
|
+
connector.style.to_h.values_at(:mode, :lwidth, :tree, :plain_source_seat) == ["3", "2", nil, true]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.moved_role_box?(box)
|
|
22
|
+
box.values_at("alignment", "cy") == [1, 18] &&
|
|
23
|
+
box.values_at("ox", "oy").map(&:to_i) != [0, 0]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
module Label
|
|
7
|
+
# Plain leftward target roles end three pixels before the anchor.
|
|
8
|
+
module PlainTargetSeat
|
|
9
|
+
def self.position(connector:, anchor:, box:, width:)
|
|
10
|
+
return nil unless width && eligible_connector?(connector) && zero_role_box?(box)
|
|
11
|
+
|
|
12
|
+
[anchor[0] - width - 3, anchor[1] - 7]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.eligible_connector?(connector)
|
|
16
|
+
connector.connector_type == "Association" &&
|
|
17
|
+
connector.style.to_h.values_at(:mode, :lwidth, :tree, :plain_target_seat) == ["3", "2", nil, true]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.zero_role_box?(box)
|
|
21
|
+
box.values_at("alignment", "cy") == [1, 18] &&
|
|
22
|
+
box.values_at("ox", "oy").map(&:to_i) == [0, 0]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
module Label
|
|
7
|
+
# Moved source pairs on descending stairs share a baseline 18px
|
|
8
|
+
# below the stored source dock. Property clusters reserve the
|
|
9
|
+
# wider role gutter; multiplicities keep the plain 3px gutter.
|
|
10
|
+
module SourceStairSeat
|
|
11
|
+
def self.positions(connector:, anchor:, text_box:, mult_box:, width:)
|
|
12
|
+
return nil unless width && connector.connector_type == "Aggregation"
|
|
13
|
+
return nil unless connector.style.to_h.values_at(:mode, :lwidth, :tree) == %w[3 0 V]
|
|
14
|
+
return nil unless descending_stair?(connector.waypoints)
|
|
15
|
+
|
|
16
|
+
height, inset = role_geometry(connector)
|
|
17
|
+
return nil unless moved_box?(text_box, height) && moved_box?(mult_box, 18)
|
|
18
|
+
|
|
19
|
+
[position(anchor, text_box, -width - inset), position(anchor, mult_box, 3)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.role_geometry(connector)
|
|
23
|
+
connector.source_stereotype.to_s.strip.casecmp("property").zero? ? [35, 60] : [18, 3]
|
|
24
|
+
end
|
|
25
|
+
private_class_method :role_geometry
|
|
26
|
+
|
|
27
|
+
def self.descending_stair?(waypoints)
|
|
28
|
+
return false unless waypoints.size == 4 && waypoints.all?(&:position)
|
|
29
|
+
|
|
30
|
+
first, bend, corner, last = waypoints.map(&:position)
|
|
31
|
+
descending_leg?(first, bend) && descending_leg?(corner, last) &&
|
|
32
|
+
horizontal_leg?(bend, corner)
|
|
33
|
+
end
|
|
34
|
+
private_class_method :descending_stair?
|
|
35
|
+
|
|
36
|
+
def self.descending_leg?(first, last)
|
|
37
|
+
first.x == last.x && first.y < last.y
|
|
38
|
+
end
|
|
39
|
+
private_class_method :descending_leg?
|
|
40
|
+
|
|
41
|
+
def self.horizontal_leg?(first, last)
|
|
42
|
+
first.y == last.y && first.x != last.x
|
|
43
|
+
end
|
|
44
|
+
private_class_method :horizontal_leg?
|
|
45
|
+
|
|
46
|
+
def self.position(anchor, box, inset)
|
|
47
|
+
[anchor[0] + box["ox"].to_i + inset, anchor[1] + box["oy"].to_i + 18]
|
|
48
|
+
end
|
|
49
|
+
private_class_method :position
|
|
50
|
+
|
|
51
|
+
def self.moved_box?(box, height)
|
|
52
|
+
box["alignment"] == 1 && box["cy"] == height &&
|
|
53
|
+
[box["ox"].to_i, box["oy"].to_i] != [0, 0]
|
|
54
|
+
end
|
|
55
|
+
private_class_method :moved_box?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|