ea 0.6.139 → 0.6.141
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/cli/app.rb +10 -0
- data/lib/ea/cli/command/diagrams.rb +4 -1
- data/lib/ea/cli/command/render.rb +8 -1
- data/lib/ea/cli/command/spa.rb +2 -1
- data/lib/ea/cli/command/svg.rb +9 -6
- data/lib/ea/fonts/metrics.rb +54 -6
- data/lib/ea/model/note.rb +2 -0
- data/lib/ea/sources/qea/diagram_builder/routing.rb +188 -8
- data/lib/ea/sources/qea/diagram_builder.rb +14 -1
- 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 +58 -15
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/note_builder.rb +2 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +78 -0
- data/lib/ea/sources/xmi/stored_route.rb +83 -6
- data/lib/ea/spa/generator.rb +4 -2
- data/lib/ea/spa/projector.rb +4 -2
- data/lib/ea/svg/ea_emitter/bottom_margin.rb +104 -0
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +25 -15
- data/lib/ea/svg/ea_emitter/canvas.rb +80 -14
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +132 -0
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/header.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/marker.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +11 -2
- data/lib/ea/svg/ea_emitter/compartment/note_text.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/operations.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +34 -4
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +30 -5
- data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +6 -0
- data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment.rb +13 -0
- data/lib/ea/svg/ea_emitter/connector_bridges.rb +117 -29
- data/lib/ea/svg/ea_emitter/connectors.rb +67 -17
- data/lib/ea/svg/ea_emitter/content_extent.rb +104 -0
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +76 -31
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +22 -10
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +57 -32
- data/lib/ea/svg/ea_emitter/elements.rb +24 -5
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +134 -14
- data/lib/ea/svg/ea_emitter/labels.rb +16 -3
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +54 -33
- data/lib/ea/svg/ea_emitter/markers.rb +22 -10
- data/lib/ea/svg/ea_emitter/paint_order.rb +30 -0
- data/lib/ea/svg/ea_emitter/pen.rb +19 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +14 -3
- data/lib/ea/svg/ea_emitter/text_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter.rb +5 -0
- data/lib/ea/svg/parity/source.rb +1 -1
- data/lib/ea/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f4c5df621df87a4df4467c9cbe157f70ea49d0463a892ed7e57ada3550b1937
|
|
4
|
+
data.tar.gz: 69fdddf140a6553c565287ce099bfa7f3d70d6c981502c559368c2e9860e70db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5d5331f2954387a2ff2e9a93981964421958bac715ca436630b7aca0cefd00107973219d571a01e162692f6037cc695e52f94f04ab8808e899f1d08bd9fdba7
|
|
7
|
+
data.tar.gz: 6c63841163568dc3b95d840ed14f4d18fd95e7bfc51bf111a8d40b32ae0aec920cf530a99012690f642c6ef4709e6df6adca3917e634f61a5088891fbf99d14c
|
data/lib/ea/cli/app.rb
CHANGED
|
@@ -41,6 +41,8 @@ module Ea
|
|
|
41
41
|
"Diagram operations: list FILE | extract NAME FILE"
|
|
42
42
|
option :format, type: :string, default: "table"
|
|
43
43
|
option :output, **OUTPUT_OPTION, desc: "Output path (extract only)"
|
|
44
|
+
option :frame, type: :boolean, default: false,
|
|
45
|
+
desc: "Draw EA's diagram frame (border, tab, label); default is unframed"
|
|
44
46
|
def diagrams(action, file = nil, name = nil)
|
|
45
47
|
Command::Diagrams
|
|
46
48
|
.new(action: action, file: file, name: name, **symbolize(options))
|
|
@@ -81,6 +83,8 @@ module Ea
|
|
|
81
83
|
desc: "Path to SPA YAML config (overrides model metadata: title, description, etc.)"
|
|
82
84
|
option :mode, type: :string, default: "single_file",
|
|
83
85
|
desc: "Output mode: single_file | sharded"
|
|
86
|
+
option :frame, type: :boolean, default: false,
|
|
87
|
+
desc: "Draw EA's diagram frame (border, tab, label); default is unframed"
|
|
84
88
|
def spa(file)
|
|
85
89
|
Command::Spa.new(file: file, **symbolize(options)).call
|
|
86
90
|
end
|
|
@@ -92,7 +96,10 @@ module Ea
|
|
|
92
96
|
desc: "Render every diagram in the source file (NAME ignored)"
|
|
93
97
|
option :output_dir, type: :string,
|
|
94
98
|
desc: "Directory for --all output (default: <basename>.svgs/)"
|
|
99
|
+
option :frame, type: :boolean, default: false,
|
|
100
|
+
desc: "Draw EA's diagram frame (border, tab, label); default is unframed"
|
|
95
101
|
def svg(name = nil, file = nil)
|
|
102
|
+
name, file = nil, name if options[:all] && file.nil?
|
|
96
103
|
Command::Svg.new(name: name, file: file, **symbolize(options)).call
|
|
97
104
|
end
|
|
98
105
|
|
|
@@ -113,7 +120,10 @@ module Ea
|
|
|
113
120
|
desc: "Output format: svg | png | pdf"
|
|
114
121
|
option :output_dir, type: :string,
|
|
115
122
|
desc: "Directory for --all output (default: <basename>.renders/)"
|
|
123
|
+
option :frame, type: :boolean, default: false,
|
|
124
|
+
desc: "Draw EA's diagram frame (border, tab, label); default is unframed"
|
|
116
125
|
def render(name = nil, file = nil)
|
|
126
|
+
name, file = nil, name if options[:all] && file.nil?
|
|
117
127
|
Command::Render.new(name: name, file: file, **symbolize(options)).call
|
|
118
128
|
end
|
|
119
129
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module Ea
|
|
4
6
|
module Cli
|
|
5
7
|
module Command
|
|
@@ -51,7 +53,8 @@ module Ea
|
|
|
51
53
|
|
|
52
54
|
svg = Ea::Svg::EaEmitter::Document.new(diagram,
|
|
53
55
|
model_index: document.index_by_id,
|
|
54
|
-
document: document
|
|
56
|
+
document: document,
|
|
57
|
+
frame: options[:frame] ? true : false).render
|
|
55
58
|
path = options[:output] || begin
|
|
56
59
|
base = File.basename(file_path, ".*")
|
|
57
60
|
dir = File.dirname(file_path)
|
|
@@ -22,6 +22,12 @@ module Ea
|
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
+
# `--format` names the image format here (svg|png|pdf), not the
|
|
26
|
+
# table/json output formatter, so the result listing is a table.
|
|
27
|
+
def formatter
|
|
28
|
+
Ea::Cli::Output.instance_for(:table)
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
def render_one
|
|
26
32
|
diagram = find_diagram
|
|
27
33
|
write_diagram(diagram, resolve_output_path(diagram))
|
|
@@ -57,7 +63,8 @@ module Ea
|
|
|
57
63
|
def render_svg(diagram)
|
|
58
64
|
emitter_for_mode.new(diagram,
|
|
59
65
|
model_index: document.index_by_id,
|
|
60
|
-
document: document
|
|
66
|
+
document: document,
|
|
67
|
+
frame: options[:frame] ? true : false).render
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def svg_to_png(svg)
|
data/lib/ea/cli/command/spa.rb
CHANGED
data/lib/ea/cli/command/svg.rb
CHANGED
|
@@ -26,9 +26,7 @@ module Ea
|
|
|
26
26
|
|
|
27
27
|
def render_one
|
|
28
28
|
diagram = find_diagram
|
|
29
|
-
svg =
|
|
30
|
-
model_index: document.index_by_id,
|
|
31
|
-
document: document).render
|
|
29
|
+
svg = render_svg(diagram)
|
|
32
30
|
|
|
33
31
|
output_path = resolve_output_path(diagram)
|
|
34
32
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
@@ -41,9 +39,7 @@ module Ea
|
|
|
41
39
|
out_dir = options[:output_dir] || default_output_dir
|
|
42
40
|
FileUtils.mkdir_p(out_dir)
|
|
43
41
|
paths = document.diagrams.map do |diagram|
|
|
44
|
-
svg =
|
|
45
|
-
model_index: document.index_by_id,
|
|
46
|
-
document: document).render
|
|
42
|
+
svg = render_svg(diagram)
|
|
47
43
|
path = File.join(out_dir, "#{diagram.id}.svg")
|
|
48
44
|
File.write(path, svg)
|
|
49
45
|
path
|
|
@@ -52,6 +48,13 @@ module Ea
|
|
|
52
48
|
formatter.render(paths.map { |p| [p] }, columns: [:written_to])
|
|
53
49
|
end
|
|
54
50
|
|
|
51
|
+
def render_svg(diagram)
|
|
52
|
+
emitter_for_mode.new(diagram,
|
|
53
|
+
model_index: document.index_by_id,
|
|
54
|
+
document: document,
|
|
55
|
+
frame: options[:frame] ? true : false).render
|
|
56
|
+
end
|
|
57
|
+
|
|
55
58
|
def default_output_dir
|
|
56
59
|
base = File.basename(file_path, ".*")
|
|
57
60
|
dir = File.dirname(file_path)
|
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.
|
|
@@ -103,6 +105,7 @@ module Ea
|
|
|
103
105
|
# Source sizes 90 and 100 both export as 9pt but retain
|
|
104
106
|
# distinct 12px and 13px logical heights. Apply that same
|
|
105
107
|
# source-height delta to the existing compartment spacing.
|
|
108
|
+
logical_height = source_logical_height(logical_height)
|
|
106
109
|
adjustment = if logical_height && unit == "pt" &&
|
|
107
110
|
GDI_HEIGHT_FAMILIES.include?(resolve_family(family))
|
|
108
111
|
logical_height - gdi_logical_height(size)
|
|
@@ -112,6 +115,13 @@ module Ea
|
|
|
112
115
|
size + offset + adjustment
|
|
113
116
|
end
|
|
114
117
|
|
|
118
|
+
# A source logical height of 0 or less (QEA floors tiny fontsz to
|
|
119
|
+
# 0) carries no information: treat it as absent.
|
|
120
|
+
def source_logical_height(height)
|
|
121
|
+
height = height.to_i
|
|
122
|
+
height if height.positive?
|
|
123
|
+
end
|
|
124
|
+
|
|
115
125
|
def gdi_logical_height(size)
|
|
116
126
|
(size.to_f * 10 / 7).round
|
|
117
127
|
end
|
|
@@ -150,11 +160,17 @@ module Ea
|
|
|
150
160
|
# textLength is the SUM of those per-glyph integers - corpus
|
|
151
161
|
# fitted to 97-100% exact on 10k Carlito texts (bold 7pt
|
|
152
162
|
# 1666/1671). The legacy sum-then-round model matched only 27%.
|
|
153
|
-
|
|
163
|
+
#
|
|
164
|
+
# `legend_title` marks a diagram-legend title, the only context in
|
|
165
|
+
# which a bold Cambria CJK run takes regular-weight advances.
|
|
166
|
+
def text_length(text, size_pt, family: nil, weight: nil, style: nil, logical_height: nil,
|
|
167
|
+
legend_title: false)
|
|
154
168
|
return nil unless supported_family?(family)
|
|
155
169
|
return nil if text.nil? || text.empty?
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
if resolve_family(family) == "Cambria"
|
|
172
|
+
return cambria_text_length(text, size_pt, weight, style, logical_height, legend_title: legend_title)
|
|
173
|
+
end
|
|
158
174
|
|
|
159
175
|
advance_text_length(text, size_pt, family, weight, style, logical_height)
|
|
160
176
|
end
|
|
@@ -162,7 +178,7 @@ module Ea
|
|
|
162
178
|
def advance_text_length(text, size_pt, family, weight, style, logical_height)
|
|
163
179
|
resolved = resolve_family(family)
|
|
164
180
|
if GDI_HEIGHT_FAMILIES.include?(resolved)
|
|
165
|
-
height = logical_height || gdi_logical_height(size_pt)
|
|
181
|
+
height = source_logical_height(logical_height) || gdi_logical_height(size_pt)
|
|
166
182
|
table = table_for(family, weight, style)
|
|
167
183
|
fallback = cjk_fallback_widths(resolved, height, weight, style)
|
|
168
184
|
measured_latin = measured_carlito_widths(resolved, height, weight, style)
|
|
@@ -204,8 +220,16 @@ module Ea
|
|
|
204
220
|
end
|
|
205
221
|
private_class_method :measured_carlito_widths
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
# True for Cambria outside the 7pt tables, where a run is measured
|
|
224
|
+
# character by character when it is not wholly in the CJK table.
|
|
225
|
+
def piecewise_family?(family, size_pt)
|
|
226
|
+
resolve_family(family) == "Cambria" && size_pt.to_f.to_r != 7
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def cambria_text_length(text, size_pt, weight, style, logical_height = nil, legend_title: false)
|
|
230
|
+
unless size_pt.to_f.to_r == 7
|
|
231
|
+
return cambria_cjk_only_length(text, size_pt, weight, style, logical_height, legend_title: legend_title)
|
|
232
|
+
end
|
|
209
233
|
return nil unless ["", "0", "400", "normal", "700", "bold"].include?(weight.to_s)
|
|
210
234
|
return nil unless ["", "normal", "italic"].include?(style.to_s)
|
|
211
235
|
|
|
@@ -220,6 +244,30 @@ module Ea
|
|
|
220
244
|
end
|
|
221
245
|
private_class_method :cambria_text_length
|
|
222
246
|
|
|
247
|
+
# Cambria has no Latin table outside 7pt, but a run made only of
|
|
248
|
+
# characters in the measured MS UI Gothic table is exact at the
|
|
249
|
+
# measured height (the source logical height when it is positive,
|
|
250
|
+
# else the one derived from the point size). A run with any other
|
|
251
|
+
# character is nil here; TextRenderer.estimate_width then prices
|
|
252
|
+
# the characters one at a time.
|
|
253
|
+
#
|
|
254
|
+
# EMPIRICAL FIT (decision a), legend titles only: EA draws a bold
|
|
255
|
+
# Cambria legend title at logical height >= 17 with the
|
|
256
|
+
# regular-weight advances (no synthetic-bold pixel), although the
|
|
257
|
+
# Windows table records one. Checked on the 116 native legend
|
|
258
|
+
# titles "凡例" (Cambria bold 12pt, H17): native textLength 34 =
|
|
259
|
+
# 2 x 17, where the 17:700 entry would give 36. Every other bold
|
|
260
|
+
# run, and a title below H17, keeps the bold table (native 7pt
|
|
261
|
+
# bold "都市計画データ" = 70).
|
|
262
|
+
def cambria_cjk_only_length(text, size_pt, weight, style, logical_height = nil, legend_title: false)
|
|
263
|
+
height = source_logical_height(logical_height) || gdi_logical_height(size_pt)
|
|
264
|
+
bold = bold?(weight) && !(legend_title && height >= CAMBRIA_REGULAR_BOLD_MIN_HEIGHT)
|
|
265
|
+
widths = cjk_fallback_widths("Cambria", height, bold ? 700 : 400, style)
|
|
266
|
+
advances = text.each_char.map { |char| widths[char] }
|
|
267
|
+
advances.sum unless advances.include?(nil)
|
|
268
|
+
end
|
|
269
|
+
private_class_method :cambria_cjk_only_length
|
|
270
|
+
|
|
223
271
|
def cambria_cjk_widths(weight, style)
|
|
224
272
|
cjk_fallback_widths("Cambria", gdi_logical_height(7), weight, style)
|
|
225
273
|
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,12 @@ 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
|
+
# A leg may start beside its box only this far from it (the one native
|
|
11
|
+
# case, 5038EEE3 connector 212, is 9px away); any other leg that
|
|
12
|
+
# leaves the box's x-span would be detached, so the fit yields.
|
|
13
|
+
REVERSED_TREE_BESIDE_REACH = 10
|
|
14
|
+
|
|
9
15
|
private
|
|
10
16
|
|
|
11
17
|
# Offsets stay attached to semantic Start/End IDs even when the
|
|
@@ -17,21 +23,153 @@ module Ea
|
|
|
17
23
|
connector = database.find_connector(link_row.connectorid)
|
|
18
24
|
return model unless connector && reversed_corner_link?(link_row, diagram_row, connector)
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
origins, points = reversed_corner_points(stored_ray_boxes(link_row, diagram_row), geom,
|
|
27
|
+
intermediate_waypoints(link_row).first)
|
|
28
|
+
return model unless points
|
|
29
|
+
|
|
30
|
+
model.waypoints = points.map { |point| Ea::Model::Waypoint.new(position: point) }
|
|
31
|
+
model.source_anchor, model.target_anchor = origins
|
|
32
|
+
model
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Shared by QEA and XMI: clip each leg of the single saved bend from
|
|
36
|
+
# its stored origin, retaining origins outside their box. Returns
|
|
37
|
+
# [origins, [start, bend, end]], or nil when a leg cannot be clipped.
|
|
38
|
+
def reversed_corner_points(boxes, geom, corner)
|
|
21
39
|
origins = [stored_ray_origin(boxes[0], geom[:sx], geom[:sy]),
|
|
22
40
|
stored_ray_origin(boxes[1], geom[:ex], geom[:ey])]
|
|
23
|
-
x, y
|
|
24
|
-
bend = Ea::Model::Point.new(x: x, y: y)
|
|
41
|
+
bend = Ea::Model::Point.new(x: corner[0], y: corner[1])
|
|
25
42
|
ends = origins.zip(boxes).map do |origin, box|
|
|
26
43
|
inside_box?(origin, box) ? diagonal_corner_exit(origin, bend, box) : origin
|
|
27
44
|
end
|
|
28
|
-
|
|
45
|
+
[origins, [ends[0], bend, ends[1]]] if ends.all?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# EMPIRICAL FIT (decision a), not a source-backed rule. A width-0
|
|
49
|
+
# Mode 3 vertical-tree Association whose saved SOID/EOID name the
|
|
50
|
+
# relationship's ends in reverse keeps a Path (bus height and
|
|
51
|
+
# bend order) that EA no longer draws: its native export routes
|
|
52
|
+
# from the semantic Start box to the End box, with the bus 50px
|
|
53
|
+
# beyond the End box's facing edge (the distance every stored tree
|
|
54
|
+
# bus in the corpus keeps from its End box, 600+ links), the Start
|
|
55
|
+
# leg at center + SX and the End leg at center + EX. Checked on
|
|
56
|
+
# plateau diagram 5038EEE3 connectors 208, 209 and 212 (exact);
|
|
57
|
+
# 211 and 902 follow the leg pull documented on reversed_tree_leg.
|
|
58
|
+
def reversed_tree_route(model, link_row, diagram_row, geom, context)
|
|
59
|
+
return model if context || !reversed_tree_link?(model, link_row, diagram_row, geom)
|
|
60
|
+
|
|
61
|
+
start, finish = stored_ray_boxes(link_row, diagram_row)
|
|
62
|
+
route = reversed_tree_points(start, finish, geom, stored_bus_levels(link_row))
|
|
63
|
+
return model unless route
|
|
64
|
+
|
|
65
|
+
points, beside = route
|
|
66
|
+
model.waypoints = points.map do |x, y|
|
|
67
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
68
|
+
end
|
|
69
|
+
model.source_anchor = model.source_port = model.waypoints.first.position
|
|
70
|
+
model.target_anchor = model.target_port = model.waypoints.last.position
|
|
71
|
+
# EMPIRICAL FIT: a leg that starts beside its box (5038EEE3
|
|
72
|
+
# connector 212) seats its source labels like a leftward leg.
|
|
73
|
+
model.style = model.style.to_h.merge(left_seated_source: true) if beside
|
|
74
|
+
model
|
|
75
|
+
end
|
|
29
76
|
|
|
30
|
-
|
|
31
|
-
|
|
77
|
+
# The reversed-tree route points and whether the Start leg starts
|
|
78
|
+
# beside its box, or nil when the fit does not apply: no usable bus
|
|
79
|
+
# level, a stored bus that is not the discarded one (see
|
|
80
|
+
# reversed_tree_levels), or a leg that cannot stay attached to its box.
|
|
81
|
+
def reversed_tree_points(start, finish, geom, stored_bus)
|
|
82
|
+
bus, start_edge, finish_edge = reversed_tree_levels(start, finish, stored_bus)
|
|
83
|
+
return unless bus
|
|
84
|
+
|
|
85
|
+
first = reversed_tree_leg(start, geom[:sx], geom[:sy], start_edge, finish)
|
|
86
|
+
last = reversed_tree_leg(finish, geom[:ex], geom[:ey], finish_edge, start)
|
|
87
|
+
return unless first && last
|
|
88
|
+
|
|
89
|
+
points = [first, [first[0], bus], [last[0], bus], last].chunk_while { |a, b| a == b }.map(&:first)
|
|
90
|
+
[points, first[1] != start_edge]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# EMPIRICAL FIT (decision a). EA seats the source labels of a
|
|
94
|
+
# vertical lateral tree ("LV") whose SOID/EOID name the ends in
|
|
95
|
+
# reverse as if the source leg left to the left, whichever way the
|
|
96
|
+
# route leaves the box. Measured on the only such link in the
|
|
97
|
+
# plateau corpus, 5038EEE3 connector 207 (+lod1MultiSurface).
|
|
98
|
+
def mark_reversed_lateral_tree(model, link_row, diagram_row, context)
|
|
99
|
+
return model if context
|
|
100
|
+
|
|
101
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
102
|
+
return model unless style[:tree] == "LV" && model.connector_type == "Association"
|
|
103
|
+
return model unless placement_order_reversed?(style, diagram_row,
|
|
104
|
+
database.find_connector(link_row.connectorid))
|
|
105
|
+
|
|
106
|
+
model.style = model.style.to_h.merge(left_seated_source: true)
|
|
32
107
|
model
|
|
33
108
|
end
|
|
34
109
|
|
|
110
|
+
def reversed_tree_link?(model, link_row, diagram_row, geom)
|
|
111
|
+
style = DiagramStyleParser.parse(link_row.style)
|
|
112
|
+
model.connector_type == "Association" &&
|
|
113
|
+
style.values_at(:mode, :lwidth, :tree) == %w[3 0 V] &&
|
|
114
|
+
geom.values_at(:sx, :sy, :ex, :ey).all? &&
|
|
115
|
+
stored_bus_levels(link_row) &&
|
|
116
|
+
placement_order_reversed?(style, diagram_row, database.find_connector(link_row.connectorid))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stored_bus_levels(link_row)
|
|
120
|
+
bends = intermediate_waypoints(link_row)
|
|
121
|
+
bends.first[1] if bends.size == 2 && bends.first[1] == bends.last[1]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# [bus height, Start facing edge, End facing edge] for vertically
|
|
125
|
+
# separated boxes: the bus sits 50px past the End box's edge and
|
|
126
|
+
# must stay inside the gap. Nil otherwise.
|
|
127
|
+
#
|
|
128
|
+
# EMPIRICAL FIT (decision a): a stored bus is replaced only when it
|
|
129
|
+
# is a 50px tree bus off one of the two facing edges, as EA lays
|
|
130
|
+
# them out: 50px past the Start box (the Path follows SOID/EOID
|
|
131
|
+
# order; the QEA y670 = Start edge 720 - 50 of all five 5038EEE3
|
|
132
|
+
# links) or already 50px past the End box (the XMI Path of three
|
|
133
|
+
# of them, y568). A stored bus anywhere else is a real route.
|
|
134
|
+
def reversed_tree_levels(start, finish, stored_bus)
|
|
135
|
+
if finish.y + finish.height < start.y
|
|
136
|
+
bus = finish.y + finish.height + REVERSED_TREE_BUS_OFFSET
|
|
137
|
+
discarded = start.y - REVERSED_TREE_BUS_OFFSET
|
|
138
|
+
edges = [start.y, finish.y + finish.height]
|
|
139
|
+
[bus, *edges] if bus < start.y && [bus, discarded].include?(stored_bus)
|
|
140
|
+
elsif start.y + start.height < finish.y
|
|
141
|
+
bus = finish.y - REVERSED_TREE_BUS_OFFSET
|
|
142
|
+
discarded = start.y + start.height + REVERSED_TREE_BUS_OFFSET
|
|
143
|
+
edges = [start.y + start.height, finish.y]
|
|
144
|
+
[bus, *edges] if bus > edges.first && [bus, discarded].include?(stored_bus)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A leg runs vertically at the stored origin's x. It starts on the
|
|
149
|
+
# facing edge, unless the origin lies beside the box (just outside its
|
|
150
|
+
# x-span but level with it), where the origin itself is kept. Nil
|
|
151
|
+
# for an origin that would leave the leg detached.
|
|
152
|
+
#
|
|
153
|
+
# EMPIRICAL FIT (decision a): the leg's x is pulled toward the box
|
|
154
|
+
# centre by |offset_y| / (vertical centre distance to the other box).
|
|
155
|
+
# Checked on 5038EEE3 connectors 211 (SX 41, SY -125: native 13px
|
|
156
|
+
# left of centre+SX), 902 (SX 64, SY -13: 2px) and 212, 208, 209
|
|
157
|
+
# (SY within 5: the rounded-up pull leaves centre + SX unchanged);
|
|
158
|
+
# the offset is rounded away from zero, as all five require.
|
|
159
|
+
# Derived from these five SX/SY pairs, not from EA documentation.
|
|
160
|
+
def reversed_tree_leg(box, offset_x, offset_y, edge_y, other)
|
|
161
|
+
span = ((other.y + other.height / 2.0) - (box.y + box.height / 2.0)).abs
|
|
162
|
+
pull = span.positive? ? 1 - [offset_y.to_i.abs / span, 1].min : 1
|
|
163
|
+
pulled = (offset_x.to_i.abs * pull).ceil * (offset_x.to_i.negative? ? -1 : 1)
|
|
164
|
+
origin = stored_ray_origin(box, pulled, offset_y)
|
|
165
|
+
return [origin.x, edge_y] if origin.x.between?(box.x, box.x + box.width)
|
|
166
|
+
|
|
167
|
+
reach = REVERSED_TREE_BESIDE_REACH
|
|
168
|
+
beside = origin.y.between?(box.y, box.y + box.height) &&
|
|
169
|
+
origin.x.between?(box.x - reach, box.x + box.width + reach)
|
|
170
|
+
[origin.x, origin.y] if beside
|
|
171
|
+
end
|
|
172
|
+
|
|
35
173
|
# Path follows SOID/EOID placement order, independently of the
|
|
36
174
|
# relationship's Start/End IDs. Only a complete, unique DUID pair
|
|
37
175
|
# can reverse the authored single-corner route.
|
|
@@ -42,6 +180,10 @@ module Ea
|
|
|
42
180
|
parse_geometry_fields(link_row.geometry)[:edge] == 2 &&
|
|
43
181
|
intermediate_waypoints(link_row).size == 1
|
|
44
182
|
|
|
183
|
+
placement_order_reversed?(style, diagram_row, connector)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def placement_order_reversed?(style, diagram_row, connector)
|
|
45
187
|
ids = style.values_at(:soid, :eoid)
|
|
46
188
|
return false if ids.any? { |id| id.to_s.empty? } || ids.uniq.size != 2
|
|
47
189
|
|
|
@@ -695,8 +837,7 @@ module Ea
|
|
|
695
837
|
return nil
|
|
696
838
|
end
|
|
697
839
|
|
|
698
|
-
first =
|
|
699
|
-
last = outline_dock(corners[-1], corners[-2], target)
|
|
840
|
+
first, last = stair_docks(link_row, corners, source, target)
|
|
700
841
|
return nil unless distinct_stair_docks?(corners, first, last)
|
|
701
842
|
|
|
702
843
|
[first, *corners, last]
|
|
@@ -728,6 +869,45 @@ module Ea
|
|
|
728
869
|
:horizontal if from[1] == to[1] && from[0] != to[0]
|
|
729
870
|
end
|
|
730
871
|
|
|
872
|
+
def stair_docks(link_row, corners, source, target)
|
|
873
|
+
geom = parse_geometry_fields(link_row.geometry)
|
|
874
|
+
[stair_dock(corners[0], corners[1], source, geom.values_at(:sx, :sy)),
|
|
875
|
+
stair_dock(corners[-1], corners[-2], target, geom.values_at(:ex, :ey))]
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# A corner level with its box docks on the outline the leg points at.
|
|
879
|
+
# When the stored origin sits off that row, EA aims the last leg at
|
|
880
|
+
# the origin instead and the dock slants onto the same outline
|
|
881
|
+
# (30D62231 connector 601: bend row 430, origin row 425, dock row 428).
|
|
882
|
+
def stair_dock(corner, neighbour, box, offsets)
|
|
883
|
+
dock = outline_dock(corner, neighbour, box)
|
|
884
|
+
return dock unless dock && offsets.none?(&:nil?)
|
|
885
|
+
|
|
886
|
+
origin = stored_ray_origin(box, *offsets)
|
|
887
|
+
return dock unless inside_box?(origin, grown_bounds(box, 1))
|
|
888
|
+
|
|
889
|
+
slanted_dock([origin.x, origin.y], corner, dock) || dock
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Where the line from the stored origin to the corner meets the
|
|
893
|
+
# outline row/column of the orthogonal dock; the other axis floors.
|
|
894
|
+
def slanted_dock(origin, corner, dock)
|
|
895
|
+
axis = dock[0] == corner[0] ? 1 : 0
|
|
896
|
+
other = intercept(origin, corner, axis, dock[axis])
|
|
897
|
+
return unless other
|
|
898
|
+
|
|
899
|
+
axis.zero? ? [dock[0], other] : [other, dock[1]]
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def intercept(origin, corner, axis, face)
|
|
903
|
+
run = corner[axis] - origin[axis]
|
|
904
|
+
fraction = Rational(face - origin[axis], run) unless run.zero?
|
|
905
|
+
return unless fraction&.between?(0, 1)
|
|
906
|
+
|
|
907
|
+
other = 1 - axis
|
|
908
|
+
(origin[other] + fraction * (corner[other] - origin[other])).floor
|
|
909
|
+
end
|
|
910
|
+
|
|
731
911
|
# The dock runs perpendicular to the corner's own leg.
|
|
732
912
|
def outline_dock(corner, neighbour, box)
|
|
733
913
|
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)
|
|
@@ -195,7 +202,10 @@ module Ea
|
|
|
195
202
|
end
|
|
196
203
|
|
|
197
204
|
def build_connectors(diagram_row)
|
|
198
|
-
|
|
205
|
+
# A link row whose connector no longer exists (EA leaves them
|
|
206
|
+
# behind when a connector is deleted) has nothing to draw.
|
|
207
|
+
links = (database.diagram_links_for(diagram_row.diagram_id) || [])
|
|
208
|
+
.select { |link_row| database.find_connector(link_row.connectorid) }
|
|
199
209
|
context = ContextDiagramSupport.context?(diagram_row)
|
|
200
210
|
# EA regenerates context diagrams at draw time; their
|
|
201
211
|
# Hidden link rows are stale and never render.
|
|
@@ -475,6 +485,7 @@ module Ea
|
|
|
475
485
|
style: context ? { direct: true } :
|
|
476
486
|
DiagramStyleParser.parse(link_row.style),
|
|
477
487
|
hidden: hidden?(link_row),
|
|
488
|
+
line_width: connector&.isbold,
|
|
478
489
|
ghost_labels: ghost_labels_for(connector, diagram_row, link_row),
|
|
479
490
|
has_geometry_offsets: geometry_has_offsets?(link_row.geometry),
|
|
480
491
|
is_substitutable: substitutable_connector?(connector)
|
|
@@ -494,6 +505,8 @@ module Ea
|
|
|
494
505
|
model = mark_default_square_ray(model, link_row, geom, context)
|
|
495
506
|
model = restore_stored_square_ports(model, link_row, diagram_row, geom, context)
|
|
496
507
|
model = reversed_stored_corner(model, link_row, diagram_row, geom, context)
|
|
508
|
+
model = reversed_tree_route(model, link_row, diagram_row, geom, context)
|
|
509
|
+
model = mark_reversed_lateral_tree(model, link_row, diagram_row, context)
|
|
497
510
|
mark_association_class(model, connector, diagram_row)
|
|
498
511
|
end
|
|
499
512
|
|
|
@@ -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
|