ea 0.6.138 → 0.6.140
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/metrics.rb +22 -2
- data/lib/ea/model/note.rb +2 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +140 -2
- data/lib/ea/sources/qea/diagram_builder.rb +10 -0
- data/lib/ea/sources/qea/note_builder.rb +7 -1
- data/lib/ea/sources/xmi/diagram_builder/element_borders.rb +27 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +40 -11
- data/lib/ea/sources/xmi/note_builder.rb +2 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +94 -0
- data/lib/ea/sources/xmi/stored_route.rb +121 -8
- data/lib/ea/svg/ea_emitter/bottom_margin.rb +100 -0
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +3 -1
- data/lib/ea/svg/ea_emitter/canvas.rb +37 -9
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +124 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +5 -1
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +23 -4
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +17 -5
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +113 -41
- data/lib/ea/svg/ea_emitter/connectors.rb +67 -17
- data/lib/ea/svg/ea_emitter/content_extent.rb +95 -0
- data/lib/ea/svg/ea_emitter/document.rb +56 -27
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +14 -6
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +64 -32
- data/lib/ea/svg/ea_emitter/elements.rb +21 -4
- data/lib/ea/svg/ea_emitter/label/end_label.rb +64 -6
- data/lib/ea/svg/ea_emitter/labels.rb +6 -0
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +40 -26
- data/lib/ea/svg/ea_emitter/markers.rb +22 -10
- data/lib/ea/svg/ea_emitter/paint_order.rb +29 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +14 -3
- data/lib/ea/svg/ea_emitter/segment_geometry.rb +123 -0
- data/lib/ea/svg/ea_emitter.rb +6 -0
- data/lib/ea/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9db50e16de6c9f9faa698a872686600da15ba3eef0787de0b18b0ea56eca0394
|
|
4
|
+
data.tar.gz: b537f2b76e1fdb3a5a6dcfd1581db9f651279c5aff4e35882144da2ace23cf26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a685c1155369f6800fbfd294feed39ea2b450f482245c30be940c18bb31098466be6dbb0f0e6ca28e90502798ceaf10c8a5bbac0f0739b619b1cc0cd69696cb
|
|
7
|
+
data.tar.gz: 570de8eaf44e482c0330cc96f17eb2ab89e26a75542c613617b53468040b45dfec9ce597b308cd50b068d0a78e20e415a9cda9e897622f1cc1b1ab2a4e110baa
|
data/lib/ea/fonts/metrics.rb
CHANGED
|
@@ -59,6 +59,8 @@ module Ea
|
|
|
59
59
|
File.expand_path("cjk_fallback_widths.json", __dir__)
|
|
60
60
|
)).fetch("widths").freeze
|
|
61
61
|
CJK_FALLBACK_FAMILIES = %w[Carlito Cambria].freeze
|
|
62
|
+
# Logical height from which a bold Cambria CJK run uses regular advances.
|
|
63
|
+
CAMBRIA_REGULAR_BOLD_MIN_HEIGHT = 17
|
|
62
64
|
# Carlito ASCII advances measured on Windows per "height:weight:style";
|
|
63
65
|
# they take precedence over the quantized design advances.
|
|
64
66
|
CARLITO_GDI_WIDTHS = JSON.parse(File.read(
|
|
@@ -71,7 +73,7 @@ module Ea
|
|
|
71
73
|
)).fetch("advance").freeze
|
|
72
74
|
private_constant :CAMBRIA_7_REGULAR_WIDTHS, :CAMBRIA_7_STYLED_WIDTHS,
|
|
73
75
|
:CAMBRIA_7_GUILLEMET_PAIR_WIDTH, :VERTICAL_METRICS,
|
|
74
|
-
:CJK_FALLBACK_WIDTHS, :CJK_FALLBACK_FAMILIES,
|
|
76
|
+
:CJK_FALLBACK_WIDTHS, :CJK_FALLBACK_FAMILIES, :CAMBRIA_REGULAR_BOLD_MIN_HEIGHT,
|
|
75
77
|
:CARLITO_GDI_WIDTHS, :SOURCE_SANS_PRO_ADVANCES
|
|
76
78
|
|
|
77
79
|
# Advance used for codepoints missing from the tables.
|
|
@@ -205,7 +207,7 @@ module Ea
|
|
|
205
207
|
private_class_method :measured_carlito_widths
|
|
206
208
|
|
|
207
209
|
def cambria_text_length(text, size_pt, weight, style)
|
|
208
|
-
return
|
|
210
|
+
return cambria_cjk_only_length(text, size_pt, weight, style) unless size_pt.to_f.to_r == 7
|
|
209
211
|
return nil unless ["", "0", "400", "normal", "700", "bold"].include?(weight.to_s)
|
|
210
212
|
return nil unless ["", "normal", "italic"].include?(style.to_s)
|
|
211
213
|
|
|
@@ -220,6 +222,24 @@ module Ea
|
|
|
220
222
|
end
|
|
221
223
|
private_class_method :cambria_text_length
|
|
222
224
|
|
|
225
|
+
# Cambria has no Latin table outside 7pt, but a run made only of
|
|
226
|
+
# characters in the measured MS UI Gothic table is exact at any
|
|
227
|
+
# measured height. EMPIRICAL FIT (decision a): EA draws a bold
|
|
228
|
+
# Cambria CJK run at logical height >= 17 with the regular-weight
|
|
229
|
+
# advances (no synthetic-bold pixel), although the Windows table
|
|
230
|
+
# records one. Checked on the 116 native legend titles "凡例"
|
|
231
|
+
# (Cambria bold 12pt, H17): native textLength 34 = 2 x 17, where
|
|
232
|
+
# the 17:700 entry would give 36. Below H17 bold keeps the bold
|
|
233
|
+
# table (native 7pt bold "都市計画データ" = 70).
|
|
234
|
+
def cambria_cjk_only_length(text, size_pt, weight, style)
|
|
235
|
+
height = gdi_logical_height(size_pt)
|
|
236
|
+
bold = bold?(weight) && height < CAMBRIA_REGULAR_BOLD_MIN_HEIGHT
|
|
237
|
+
widths = cjk_fallback_widths("Cambria", height, bold ? 700 : 400, style)
|
|
238
|
+
advances = text.each_char.map { |char| widths[char] }
|
|
239
|
+
advances.sum unless advances.include?(nil)
|
|
240
|
+
end
|
|
241
|
+
private_class_method :cambria_cjk_only_length
|
|
242
|
+
|
|
223
243
|
def cambria_cjk_widths(weight, style)
|
|
224
244
|
cjk_fallback_widths("Cambria", gdi_logical_height(7), weight, style)
|
|
225
245
|
end
|
data/lib/ea/model/note.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Ea
|
|
|
15
15
|
attribute :body, :string
|
|
16
16
|
attribute :note_type, :string # "Note" | "Text" | "File"
|
|
17
17
|
attribute :legend, Legend
|
|
18
|
+
attribute :package_id, :string # containing package
|
|
18
19
|
|
|
19
20
|
json do
|
|
20
21
|
map "id", to: :id
|
|
@@ -22,6 +23,7 @@ module Ea
|
|
|
22
23
|
map "body", to: :body
|
|
23
24
|
map "noteType", to: :note_type
|
|
24
25
|
map "legend", to: :legend
|
|
26
|
+
map "packageId", to: :package_id
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
|
@@ -6,6 +6,8 @@ module Ea
|
|
|
6
6
|
class DiagramBuilder
|
|
7
7
|
# Internal geometry for choosing the facing edges of stored routes.
|
|
8
8
|
module Routing
|
|
9
|
+
REVERSED_TREE_BUS_OFFSET = 50
|
|
10
|
+
|
|
9
11
|
private
|
|
10
12
|
|
|
11
13
|
# Offsets stay attached to semantic Start/End IDs even when the
|
|
@@ -32,6 +34,100 @@ module Ea
|
|
|
32
34
|
model
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# EMPIRICAL FIT (decision a), not a source-backed rule. A width-0
|
|
38
|
+
# Mode 3 vertical-tree Association whose saved SOID/EOID name the
|
|
39
|
+
# relationship's ends in reverse keeps a Path (bus height and
|
|
40
|
+
# bend order) that EA no longer draws: its native export routes
|
|
41
|
+
# from the semantic Start box to the End box, with the bus 50px
|
|
42
|
+
# beyond the End box's facing edge (the distance every stored tree
|
|
43
|
+
# bus in the corpus keeps from its End box, 600+ links), the Start
|
|
44
|
+
# leg at center + SX and the End leg at center + EX. Checked on
|
|
45
|
+
# plateau diagram 5038EEE3 connectors 208, 209 and 212 (exact);
|
|
46
|
+
# 211 and 902 follow the leg pull documented on reversed_tree_leg.
|
|
47
|
+
def reversed_tree_route(model, link_row, diagram_row, geom, context)
|
|
48
|
+
return model if context || !reversed_tree_link?(model, link_row, diagram_row, geom)
|
|
49
|
+
|
|
50
|
+
start, finish = stored_ray_boxes(link_row, diagram_row)
|
|
51
|
+
bus, start_edge, finish_edge = reversed_tree_levels(start, finish)
|
|
52
|
+
return model unless bus
|
|
53
|
+
|
|
54
|
+
first = reversed_tree_leg(start, geom[:sx], geom[:sy], start_edge, finish)
|
|
55
|
+
last = reversed_tree_leg(finish, geom[:ex], geom[:ey], finish_edge, start)
|
|
56
|
+
model.waypoints = [first, [first[0], bus], [last[0], bus], last].map do |x, y|
|
|
57
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
58
|
+
end
|
|
59
|
+
model.source_anchor = model.source_port = model.waypoints.first.position
|
|
60
|
+
model.target_anchor = model.target_port = model.waypoints.last.position
|
|
61
|
+
# EMPIRICAL FIT: a leg that starts beside its box (5038EEE3
|
|
62
|
+
# connector 212) seats its source labels like a leftward leg.
|
|
63
|
+
model.style = model.style.to_h.merge(left_seated_source: true) if first[1] != start_edge
|
|
64
|
+
model
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# EMPIRICAL FIT (decision a). EA seats the source labels of a
|
|
68
|
+
# vertical lateral tree ("LV") whose SOID/EOID name the ends in
|
|
69
|
+
# reverse as if the source leg left to the left, whichever way the
|
|
70
|
+
# route leaves the box. Measured on the only such link in the
|
|
71
|
+
# plateau corpus, 5038EEE3 connector 207 (+lod1MultiSurface).
|
|
72
|
+
def mark_reversed_lateral_tree(model, link_row, diagram_row, context)
|
|
73
|
+
return model if context
|
|
74
|
+
|
|
75
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
76
|
+
return model unless style[:tree] == "LV" && model.connector_type == "Association"
|
|
77
|
+
return model unless placement_order_reversed?(style, diagram_row,
|
|
78
|
+
database.find_connector(link_row.connectorid))
|
|
79
|
+
|
|
80
|
+
model.style = model.style.to_h.merge(left_seated_source: true)
|
|
81
|
+
model
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reversed_tree_link?(model, link_row, diagram_row, geom)
|
|
85
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
86
|
+
model.connector_type == "Association" &&
|
|
87
|
+
style.values_at(:mode, :lwidth, :tree) == %w[3 0 V] &&
|
|
88
|
+
geom.values_at(:sx, :sy, :ex, :ey).all? &&
|
|
89
|
+
stored_bus_levels(link_row) &&
|
|
90
|
+
placement_order_reversed?(style, diagram_row, database.find_connector(link_row.connectorid))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def stored_bus_levels(link_row)
|
|
94
|
+
bends = intermediate_waypoints(link_row)
|
|
95
|
+
bends.size == 2 && bends.first[1] == bends.last[1]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# [bus height, Start facing edge, End facing edge] for vertically
|
|
99
|
+
# separated boxes: the bus sits 50px past the End box's edge and
|
|
100
|
+
# must stay inside the gap. Nil otherwise.
|
|
101
|
+
def reversed_tree_levels(start, finish)
|
|
102
|
+
if finish.y + finish.height < start.y
|
|
103
|
+
bus = finish.y + finish.height + REVERSED_TREE_BUS_OFFSET
|
|
104
|
+
[bus, start.y, finish.y + finish.height] if bus < start.y
|
|
105
|
+
elsif start.y + start.height < finish.y
|
|
106
|
+
bus = finish.y - REVERSED_TREE_BUS_OFFSET
|
|
107
|
+
[bus, start.y + start.height, finish.y] if bus > start.y + start.height
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A leg runs vertically at the stored origin's x. It starts on the
|
|
112
|
+
# facing edge, unless the origin lies beside the box (outside its
|
|
113
|
+
# x-span but level with it), where the origin itself is kept.
|
|
114
|
+
#
|
|
115
|
+
# EMPIRICAL FIT (decision a): the leg's x is pulled toward the box
|
|
116
|
+
# centre by |offset_y| / (vertical centre distance to the other box).
|
|
117
|
+
# Checked on 5038EEE3 connectors 211 (SX 41, SY -125: native 13px
|
|
118
|
+
# left of centre+SX), 902 (SX 64, SY -13: 2px) and 212, 208, 209
|
|
119
|
+
# (SY within 5: the rounded-up pull leaves centre + SX unchanged);
|
|
120
|
+
# the offset is rounded away from zero, as all five require.
|
|
121
|
+
# Derived from these five SX/SY pairs, not from EA documentation.
|
|
122
|
+
def reversed_tree_leg(box, offset_x, offset_y, edge_y, other)
|
|
123
|
+
span = ((other.y + other.height / 2.0) - (box.y + box.height / 2.0)).abs
|
|
124
|
+
pull = span.positive? ? 1 - [offset_y.to_i.abs / span, 1].min : 1
|
|
125
|
+
pulled = (offset_x.to_i.abs * pull).ceil * (offset_x.to_i.negative? ? -1 : 1)
|
|
126
|
+
origin = stored_ray_origin(box, pulled, offset_y)
|
|
127
|
+
beside = !origin.x.between?(box.x, box.x + box.width) && origin.y.between?(box.y, box.y + box.height)
|
|
128
|
+
[origin.x, beside ? origin.y : edge_y]
|
|
129
|
+
end
|
|
130
|
+
|
|
35
131
|
# Path follows SOID/EOID placement order, independently of the
|
|
36
132
|
# relationship's Start/End IDs. Only a complete, unique DUID pair
|
|
37
133
|
# can reverse the authored single-corner route.
|
|
@@ -42,6 +138,10 @@ module Ea
|
|
|
42
138
|
parse_geometry_fields(link_row.geometry)[:edge] == 2 &&
|
|
43
139
|
intermediate_waypoints(link_row).size == 1
|
|
44
140
|
|
|
141
|
+
placement_order_reversed?(style, diagram_row, connector)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def placement_order_reversed?(style, diagram_row, connector)
|
|
45
145
|
ids = style.values_at(:soid, :eoid)
|
|
46
146
|
return false if ids.any? { |id| id.to_s.empty? } || ids.uniq.size != 2
|
|
47
147
|
|
|
@@ -695,8 +795,7 @@ module Ea
|
|
|
695
795
|
return nil
|
|
696
796
|
end
|
|
697
797
|
|
|
698
|
-
first =
|
|
699
|
-
last = outline_dock(corners[-1], corners[-2], target)
|
|
798
|
+
first, last = stair_docks(link_row, corners, source, target)
|
|
700
799
|
return nil unless distinct_stair_docks?(corners, first, last)
|
|
701
800
|
|
|
702
801
|
[first, *corners, last]
|
|
@@ -728,6 +827,45 @@ module Ea
|
|
|
728
827
|
:horizontal if from[1] == to[1] && from[0] != to[0]
|
|
729
828
|
end
|
|
730
829
|
|
|
830
|
+
def stair_docks(link_row, corners, source, target)
|
|
831
|
+
geom = parse_geometry_fields(link_row.geometry)
|
|
832
|
+
[stair_dock(corners[0], corners[1], source, geom.values_at(:sx, :sy)),
|
|
833
|
+
stair_dock(corners[-1], corners[-2], target, geom.values_at(:ex, :ey))]
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
# A corner level with its box docks on the outline the leg points at.
|
|
837
|
+
# When the stored origin sits off that row, EA aims the last leg at
|
|
838
|
+
# the origin instead and the dock slants onto the same outline
|
|
839
|
+
# (30D62231 connector 601: bend row 430, origin row 425, dock row 428).
|
|
840
|
+
def stair_dock(corner, neighbour, box, offsets)
|
|
841
|
+
dock = outline_dock(corner, neighbour, box)
|
|
842
|
+
return dock unless dock && offsets.none?(&:nil?)
|
|
843
|
+
|
|
844
|
+
origin = stored_ray_origin(box, *offsets)
|
|
845
|
+
return dock unless inside_box?(origin, grown_bounds(box, 1))
|
|
846
|
+
|
|
847
|
+
slanted_dock([origin.x, origin.y], corner, dock) || dock
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Where the line from the stored origin to the corner meets the
|
|
851
|
+
# outline row/column of the orthogonal dock; the other axis floors.
|
|
852
|
+
def slanted_dock(origin, corner, dock)
|
|
853
|
+
axis = dock[0] == corner[0] ? 1 : 0
|
|
854
|
+
other = intercept(origin, corner, axis, dock[axis])
|
|
855
|
+
return unless other
|
|
856
|
+
|
|
857
|
+
axis.zero? ? [dock[0], other] : [other, dock[1]]
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def intercept(origin, corner, axis, face)
|
|
861
|
+
run = corner[axis] - origin[axis]
|
|
862
|
+
fraction = Rational(face - origin[axis], run) unless run.zero?
|
|
863
|
+
return unless fraction&.between?(0, 1)
|
|
864
|
+
|
|
865
|
+
other = 1 - axis
|
|
866
|
+
(origin[other] + fraction * (corner[other] - origin[other])).floor
|
|
867
|
+
end
|
|
868
|
+
|
|
731
869
|
# The dock runs perpendicular to the corner's own leg.
|
|
732
870
|
def outline_dock(corner, neighbour, box)
|
|
733
871
|
if corner[1] == neighbour[1]
|
|
@@ -128,6 +128,7 @@ module Ea
|
|
|
128
128
|
@current_obj_row = obj_row
|
|
129
129
|
style_hash = DiagramStyleParser.parse(obj_row.objectstyle)
|
|
130
130
|
style_hash[:stored_attribute_order] = true
|
|
131
|
+
style_hash["connector_proxy"] = true if connector_proxy?(obj_row)
|
|
131
132
|
fqname = stereotype_fqname_for(obj_row)
|
|
132
133
|
style_hash[:stereotype_fqname] = fqname if fqname
|
|
133
134
|
font_style = effective_font_style(obj_row, style_hash)
|
|
@@ -154,6 +155,12 @@ module Ea
|
|
|
154
155
|
)
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
# EA anchors association-class links on ProxyConnector stubs and
|
|
159
|
+
# never draws them (the XMI source flags them the same way).
|
|
160
|
+
def connector_proxy?(obj_row)
|
|
161
|
+
database.find_object(obj_row.ea_object_id)&.object_type == "ProxyConnector"
|
|
162
|
+
end
|
|
163
|
+
|
|
157
164
|
# Negative widths inherit; QEA zero widths request a one-pixel cosmetic pen.
|
|
158
165
|
def effective_line_width(obj_row, style)
|
|
159
166
|
object = database.find_object(obj_row.ea_object_id)
|
|
@@ -475,6 +482,7 @@ module Ea
|
|
|
475
482
|
style: context ? { direct: true } :
|
|
476
483
|
DiagramStyleParser.parse(link_row.style),
|
|
477
484
|
hidden: hidden?(link_row),
|
|
485
|
+
line_width: connector&.isbold,
|
|
478
486
|
ghost_labels: ghost_labels_for(connector, diagram_row, link_row),
|
|
479
487
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
480
488
|
is_substitutable: substitutable_connector?(connector)
|
|
@@ -494,6 +502,8 @@ module Ea
|
|
|
494
502
|
model = mark_default_square_ray(model, link_row, geom, context)
|
|
495
503
|
model = restore_stored_square_ports(model, link_row, diagram_row, geom, context)
|
|
496
504
|
model = reversed_stored_corner(model, link_row, diagram_row, geom, context)
|
|
505
|
+
model = reversed_tree_route(model, link_row, diagram_row, geom, context)
|
|
506
|
+
model = mark_reversed_lateral_tree(model, link_row, diagram_row, context)
|
|
497
507
|
mark_association_class(model, connector, diagram_row)
|
|
498
508
|
end
|
|
499
509
|
|
|
@@ -38,10 +38,16 @@ module Ea
|
|
|
38
38
|
name: obj.name,
|
|
39
39
|
body: obj.note,
|
|
40
40
|
note_type: obj.object_type,
|
|
41
|
-
legend: legend_for(obj)
|
|
41
|
+
legend: legend_for(obj),
|
|
42
|
+
package_id: package_id_for(obj)
|
|
42
43
|
)
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
def package_id_for(obj)
|
|
47
|
+
pkg = database.find_package(obj.package_id)
|
|
48
|
+
pkg && IdNormalizer.from_guid(pkg.ea_guid)
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
def note_type?(object_type)
|
|
46
52
|
object_type == NOTE_OBJECT_TYPE || object_type == TEXT_OBJECT_TYPE
|
|
47
53
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class DiagramBuilder
|
|
7
|
+
# An element placed without its own LWth takes the border width saved on
|
|
8
|
+
# the model element (<style appearance="...BorderWidth=n;...">), the same
|
|
9
|
+
# t_object.BorderWidth fallback the QEA source applies. Checked on
|
|
10
|
+
# AB281AC1, where the missing width left the Association 1px long.
|
|
11
|
+
class ElementBorders
|
|
12
|
+
def initialize(root)
|
|
13
|
+
@elements = root.extension&.elements&.element.to_a.to_h { |element| [element.idref, element] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def width_for(subject)
|
|
17
|
+
appearance = @elements[subject]&.style&.appearance.to_s
|
|
18
|
+
raw = ExtensionStyleParser.split_pairs(appearance)["BorderWidth"]
|
|
19
|
+
return nil if raw.nil? || raw.to_i.negative?
|
|
20
|
+
|
|
21
|
+
[raw.to_i, 1].max
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -12,6 +12,7 @@ module Ea
|
|
|
12
12
|
autoload :ConnectorMetadata, "ea/sources/xmi/diagram_builder/connector_metadata"
|
|
13
13
|
autoload :AssociationClassMetadata, "ea/sources/xmi/diagram_builder/association_class_metadata"
|
|
14
14
|
autoload :FontSettings, "ea/sources/xmi/diagram_builder/font_settings"
|
|
15
|
+
autoload :ElementBorders, "ea/sources/xmi/diagram_builder/element_borders"
|
|
15
16
|
autoload :StereotypeProfiles, "ea/sources/xmi/diagram_builder/stereotype_profiles"
|
|
16
17
|
autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
17
18
|
|
|
@@ -135,7 +136,7 @@ module Ea
|
|
|
135
136
|
image_bounds: (build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom) unless bounds),
|
|
136
137
|
background_color: style.background_color,
|
|
137
138
|
line_color: style.line_color,
|
|
138
|
-
line_width: style.
|
|
139
|
+
line_width: placed_line_width(style, placed.subject),
|
|
139
140
|
font_family: font[:family],
|
|
140
141
|
font_size: font[:size],
|
|
141
142
|
font_size_unit: "pt",
|
|
@@ -155,6 +156,17 @@ module Ea
|
|
|
155
156
|
|
|
156
157
|
# XMI preserves UML member order in ownedAttribute. Carry that
|
|
157
158
|
# provenance to the renderer so it does not replace it with a name sort.
|
|
159
|
+
# LWth on the placement, else the model element's saved border width.
|
|
160
|
+
# A negative LWth (-1) inherits; zero requests the one-pixel cosmetic
|
|
161
|
+
# pen, as in the QEA source. Checked on 01661E1B (LWth=-1 over
|
|
162
|
+
# BorderWidth=2) and 1A10EB6D (LWth=0, native stroke-width 1).
|
|
163
|
+
def placed_line_width(style, subject)
|
|
164
|
+
own = style.line_width
|
|
165
|
+
return [own, 1].max if own && own >= 0
|
|
166
|
+
|
|
167
|
+
(@element_borders ||= ElementBorders.new(root)).width_for(subject)
|
|
168
|
+
end
|
|
169
|
+
|
|
158
170
|
def element_style(subject)
|
|
159
171
|
style = (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(subject)
|
|
160
172
|
return style unless source_ordered_classifiers.include?(subject)
|
|
@@ -179,8 +191,8 @@ module Ea
|
|
|
179
191
|
return nil unless left && top && right && bottom
|
|
180
192
|
|
|
181
193
|
Ea::Model::Bounds.new(x: left, y: top,
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
width: right - left,
|
|
195
|
+
height: bottom - top)
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
@@ -245,7 +257,7 @@ module Ea
|
|
|
245
257
|
waypoints: route[:waypoints],
|
|
246
258
|
style: route[:style],
|
|
247
259
|
line_color: style.color,
|
|
248
|
-
line_width:
|
|
260
|
+
line_width: ext_meta[:line_width],
|
|
249
261
|
hidden: style.hidden,
|
|
250
262
|
label_boxes: connector_label_boxes(placed, ext_meta[:ea_type],
|
|
251
263
|
options.fetch(:context) ? nil : owner_id)
|
|
@@ -257,7 +269,10 @@ module Ea
|
|
|
257
269
|
connector.label_boxes)
|
|
258
270
|
end
|
|
259
271
|
mark_stored_ray(connector, placed, source, target) unless options.fetch(:context)
|
|
260
|
-
|
|
272
|
+
unless options.fetch(:context)
|
|
273
|
+
reversed = reversed_placement?(style, by_duid, edge_source, edge_target)
|
|
274
|
+
mark_association_route(connector, placed, source, target, reversed)
|
|
275
|
+
end
|
|
261
276
|
connector
|
|
262
277
|
end
|
|
263
278
|
|
|
@@ -280,7 +295,9 @@ module Ea
|
|
|
280
295
|
|
|
281
296
|
metadata = stored_ray_metadata(connector, placed, source, target)
|
|
282
297
|
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
283
|
-
|
|
298
|
+
return if metadata.empty? || connector.connector_type == "Generalization"
|
|
299
|
+
|
|
300
|
+
connector.style[:association_ray_outline] = true
|
|
284
301
|
end
|
|
285
302
|
|
|
286
303
|
def stored_ray_metadata(connector, placed, source, target, stage: :ray_metadata)
|
|
@@ -291,9 +308,20 @@ module Ea
|
|
|
291
308
|
StoredRoute.new.public_send(stage, connector, link: link, bounds: bounds, offsets: offsets, edge: geom.edge)
|
|
292
309
|
end
|
|
293
310
|
|
|
294
|
-
def mark_association_route(connector, placed, source, target)
|
|
311
|
+
def mark_association_route(connector, placed, source, target, reversed)
|
|
312
|
+
connector.style[:reversed_ends] = true if reversed
|
|
295
313
|
metadata = stored_ray_metadata(connector, placed, source, target, stage: :association_route)
|
|
296
314
|
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
315
|
+
connector.style.delete(:reversed_ends)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Mirrors QEA placement_order_reversed?: SOID and EOID must both name
|
|
319
|
+
# placements on this diagram and be crossed against the relationship's
|
|
320
|
+
# ends. A stale DUID resolves to nothing and is not a reversal.
|
|
321
|
+
def reversed_placement?(style, by_duid, edge_source, edge_target)
|
|
322
|
+
source = by_duid[style.soid]
|
|
323
|
+
target = by_duid[style.eoid]
|
|
324
|
+
source && target && reversed_ends?(source, target, edge_source, edge_target)
|
|
297
325
|
end
|
|
298
326
|
|
|
299
327
|
def mark_plain_source_seat(connector, placed, context)
|
|
@@ -356,12 +384,13 @@ module Ea
|
|
|
356
384
|
|
|
357
385
|
def connector_meta(subject_id)
|
|
358
386
|
connector = connector_index[subject_id]
|
|
359
|
-
return { ea_type: nil, direction: nil } unless connector
|
|
387
|
+
return { ea_type: nil, direction: nil, line_width: nil } unless connector
|
|
360
388
|
|
|
361
389
|
props = connector.properties
|
|
362
390
|
{
|
|
363
391
|
ea_type: props&.ea_type,
|
|
364
|
-
direction: props&.direction
|
|
392
|
+
direction: props&.direction,
|
|
393
|
+
line_width: connector.appearance&.linewidth
|
|
365
394
|
}.merge(connector_metadata.for(connector))
|
|
366
395
|
end
|
|
367
396
|
|
|
@@ -513,8 +542,8 @@ module Ea
|
|
|
513
542
|
return nil unless g.left && g.top && g.right && g.bottom
|
|
514
543
|
|
|
515
544
|
Ea::Model::Bounds.new(x: g.left, y: g.top,
|
|
516
|
-
|
|
517
|
-
|
|
545
|
+
width: g.right - g.left,
|
|
546
|
+
height: g.bottom - g.top)
|
|
518
547
|
end
|
|
519
548
|
|
|
520
549
|
def edge_point(placed, edge_code, _end_kind)
|
|
@@ -40,7 +40,8 @@ module Ea
|
|
|
40
40
|
name: ext_element.name,
|
|
41
41
|
body: ext_element.properties&.documentation,
|
|
42
42
|
note_type: note_type,
|
|
43
|
-
legend: legend_for(ext_element, note_type)
|
|
43
|
+
legend: legend_for(ext_element, note_type),
|
|
44
|
+
package_id: IdNormalizer.from_xmi_id(ext_element.model&.package)
|
|
44
45
|
)
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class StoredRoute < Qea::DiagramBuilder
|
|
7
|
+
# QEA's reversed-end Association rules for XMI. A connector is reversed
|
|
8
|
+
# when its SOID names a placement other than the relationship's source
|
|
9
|
+
# (builder flag :reversed_ends). Geometry helpers come from
|
|
10
|
+
# Qea::DiagramBuilder::Routing, so both sources share one rule.
|
|
11
|
+
module ReversedRoutes
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# A width-0 vertical-tree Association takes the bus 50px past the End
|
|
15
|
+
# box's facing edge with legs at the stored origins (an empirical fit,
|
|
16
|
+
# see Routing#reversed_tree_route; checked on 5038EEE3).
|
|
17
|
+
def reversed_tree_metadata(model, link, bounds, offsets, edge)
|
|
18
|
+
return {} unless reversed_tree_candidate?(model, link, bounds, offsets)
|
|
19
|
+
|
|
20
|
+
start, finish = bounds.values_at(:source, :target)
|
|
21
|
+
bus, start_edge, finish_edge = reversed_tree_levels(start, finish)
|
|
22
|
+
return {} unless bus
|
|
23
|
+
|
|
24
|
+
geom = ray_geometry(offsets, edge)
|
|
25
|
+
first = reversed_tree_leg(start, geom[:sx], geom[:sy], start_edge, finish)
|
|
26
|
+
last = reversed_tree_leg(finish, geom[:ex], geom[:ey], finish_edge, start)
|
|
27
|
+
|
|
28
|
+
reversed_tree_fields(model, first, last, bus, first[1] != start_edge)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reversed_tree_fields(model, first, last, bus, beside)
|
|
32
|
+
style = model.style.to_h.except(:umldi_route, :reversed_ends)
|
|
33
|
+
style[:left_seated_source] = true if beside
|
|
34
|
+
ends = [point_for(first), point_for(last)]
|
|
35
|
+
{ waypoints: points_to_waypoints([first, [first[0], bus], [last[0], bus], last]),
|
|
36
|
+
source_anchor: ends[0], target_anchor: ends[1], source_port: ends[0], target_port: ends[1],
|
|
37
|
+
style: style }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reversed_tree_candidate?(model, link, bounds, offsets)
|
|
41
|
+
model.connector_type == "Association" && model.style[:reversed_ends] &&
|
|
42
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == %w[3 0 V] &&
|
|
43
|
+
ray_inputs?(bounds, offsets) && stored_bus_levels(link)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A thin Mode 3 EDGE 2 Association whose single saved corner follows
|
|
47
|
+
# the opposite placement order is clipped from each leg's stored
|
|
48
|
+
# origin, as QEA's reversed_stored_corner does (5038EEE3).
|
|
49
|
+
def reversed_corner_metadata(model, link, bounds, offsets, edge)
|
|
50
|
+
return {} unless reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
51
|
+
|
|
52
|
+
boxes = bounds.values_at(:source, :target)
|
|
53
|
+
origins = corner_origins(boxes, ray_geometry(offsets, edge))
|
|
54
|
+
bend = Ea::Model::Point.new(**%i[x y].zip(intermediate_waypoints(link).first).to_h)
|
|
55
|
+
ends = origins.zip(boxes).map { |origin, box| corner_exit(origin, bend, box) }
|
|
56
|
+
ends.all? ? corner_fields(origins, bend, ends) : {}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def corner_fields(origins, bend, ends)
|
|
60
|
+
{ waypoints: points_to_waypoints([ends[0], bend, ends[1]].map { |point| [point.x, point.y] }),
|
|
61
|
+
source_anchor: origins[0], target_anchor: origins[1] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def corner_origins(boxes, geom)
|
|
65
|
+
[stored_ray_origin(boxes[0], geom[:sx], geom[:sy]), stored_ray_origin(boxes[1], geom[:ex], geom[:ey])]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def corner_exit(origin, bend, box)
|
|
69
|
+
inside_box?(origin, box) ? diagonal_corner_exit(origin, bend, box) : origin
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
73
|
+
model.connector_type == "Association" && model.style[:reversed_ends] && edge == 2 &&
|
|
74
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] &&
|
|
75
|
+
ray_inputs?(bounds, offsets) && intermediate_waypoints(link).size == 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# QEA seats the source labels of a reversed vertical lateral tree
|
|
79
|
+
# ("LV") as if its source leg left to the left (5038EEE3 connector 207).
|
|
80
|
+
def reversed_lateral_tree_metadata(model, _link, _bounds, _offsets, _edge)
|
|
81
|
+
return {} unless model.connector_type == "Association" && model.style[:reversed_ends] &&
|
|
82
|
+
model.style[:tree] == "LV"
|
|
83
|
+
|
|
84
|
+
{ style: model.style.to_h.except(:reversed_ends).merge(left_seated_source: true) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def points_to_waypoints(points)
|
|
88
|
+
points.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|