ea 0.6.140 → 0.6.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/themes/default.yml +7 -0
- 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 +44 -16
- data/lib/ea/model/diagram.rb +6 -0
- data/lib/ea/qea/services/database_loader.rb +4 -1
- data/lib/ea/sources/qea/diagram_builder/routing.rb +64 -22
- data/lib/ea/sources/qea/diagram_builder.rb +4 -1
- data/lib/ea/sources/xmi/adapter.rb +20 -0
- data/lib/ea/sources/xmi/connector_index.rb +30 -0
- data/lib/ea/sources/xmi/constraint_builder.rb +81 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +22 -11
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/extension_types.rb +9 -2
- data/lib/ea/sources/xmi/relationship_builder.rb +17 -11
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +10 -26
- data/lib/ea/sources/xmi.rb +2 -0
- 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 +16 -12
- data/lib/ea/svg/ea_emitter/bounds_calculator.rb +22 -14
- data/lib/ea/svg/ea_emitter/canvas.rb +43 -5
- data/lib/ea/svg/ea_emitter/canvas_anchor.rb +12 -4
- 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 +8 -1
- data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +6 -1
- data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +6 -1
- data/lib/ea/svg/ea_emitter/compartment/marker.rb +5 -0
- data/lib/ea/svg/ea_emitter/compartment/note_body.rb +7 -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 +12 -1
- data/lib/ea/svg/ea_emitter/compartment/shape.rb +14 -1
- 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 +18 -4
- data/lib/ea/svg/ea_emitter/connector_redock.rb +13 -6
- data/lib/ea/svg/ea_emitter/content_extent.rb +11 -2
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +22 -6
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +6 -1
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +9 -0
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +7 -0
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +18 -14
- data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +1 -8
- data/lib/ea/svg/ea_emitter/elements.rb +36 -12
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +113 -19
- data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +66 -0
- data/lib/ea/svg/ea_emitter/label/plain_source_seat.rb +25 -2
- data/lib/ea/svg/ea_emitter/label/plain_target_seat.rb +64 -1
- data/lib/ea/svg/ea_emitter/labels.rb +64 -35
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +14 -7
- data/lib/ea/svg/ea_emitter/markers.rb +1 -1
- data/lib/ea/svg/ea_emitter/paint_order.rb +5 -4
- data/lib/ea/svg/ea_emitter/pen.rb +19 -0
- data/lib/ea/svg/ea_emitter/render_context.rb +1 -1
- data/lib/ea/svg/ea_emitter/text_renderer.rb +24 -8
- data/lib/ea/svg/ea_emitter.rb +1 -0
- data/lib/ea/svg/parity/source.rb +1 -1
- data/lib/ea/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b51e9399fd02dbf1d891731d7a6cdcf754b1778efdbd3e3946479a02aa0761a2
|
|
4
|
+
data.tar.gz: 96c4a49164b75a78ac1a165bdd24f4a1a13e9e8199f84e1577fd111b7498b332
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19e20b995ba1bcad98fb01f30b8cad1eec339994c57782f543e9b0014fe08f8bf4e2176ce4f5bea790e07c08bbe26f5745ee5609ec36ac1f1669e02e40f27973
|
|
7
|
+
data.tar.gz: ee051963f75748a2b32fb6021078146c76687d539e3f76cb52fd76281ef7abf6adeb355bfe48d528aa39244c400f5ea2a80064519f166c19098c3b5ecfef5dac
|
data/config/themes/default.yml
CHANGED
|
@@ -4,6 +4,13 @@ font:
|
|
|
4
4
|
family:
|
|
5
5
|
size:
|
|
6
6
|
unit: px
|
|
7
|
+
# Unthemed diagrams draw connector labels in Cambria 7pt, like the
|
|
8
|
+
# themed ones (test B58D1A53 native; 0 of 220 published SVGs carry the
|
|
9
|
+
# former Yu Gothic UI 13px fallback).
|
|
10
|
+
label_font:
|
|
11
|
+
family: Cambria
|
|
12
|
+
size: 7
|
|
13
|
+
unit: pt
|
|
7
14
|
text:
|
|
8
15
|
color: "#000000"
|
|
9
16
|
weight_normal: 400
|
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
|
@@ -105,6 +105,7 @@ module Ea
|
|
|
105
105
|
# Source sizes 90 and 100 both export as 9pt but retain
|
|
106
106
|
# distinct 12px and 13px logical heights. Apply that same
|
|
107
107
|
# source-height delta to the existing compartment spacing.
|
|
108
|
+
logical_height = source_logical_height(logical_height)
|
|
108
109
|
adjustment = if logical_height && unit == "pt" &&
|
|
109
110
|
GDI_HEIGHT_FAMILIES.include?(resolve_family(family))
|
|
110
111
|
logical_height - gdi_logical_height(size)
|
|
@@ -114,6 +115,13 @@ module Ea
|
|
|
114
115
|
size + offset + adjustment
|
|
115
116
|
end
|
|
116
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
|
+
|
|
117
125
|
def gdi_logical_height(size)
|
|
118
126
|
(size.to_f * 10 / 7).round
|
|
119
127
|
end
|
|
@@ -152,11 +160,17 @@ module Ea
|
|
|
152
160
|
# textLength is the SUM of those per-glyph integers - corpus
|
|
153
161
|
# fitted to 97-100% exact on 10k Carlito texts (bold 7pt
|
|
154
162
|
# 1666/1671). The legacy sum-then-round model matched only 27%.
|
|
155
|
-
|
|
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)
|
|
156
168
|
return nil unless supported_family?(family)
|
|
157
169
|
return nil if text.nil? || text.empty?
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
if resolve_family(family) == "Cambria"
|
|
172
|
+
return cambria_text_length(text, size_pt, weight, style, logical_height, legend_title: legend_title)
|
|
173
|
+
end
|
|
160
174
|
|
|
161
175
|
advance_text_length(text, size_pt, family, weight, style, logical_height)
|
|
162
176
|
end
|
|
@@ -164,7 +178,7 @@ module Ea
|
|
|
164
178
|
def advance_text_length(text, size_pt, family, weight, style, logical_height)
|
|
165
179
|
resolved = resolve_family(family)
|
|
166
180
|
if GDI_HEIGHT_FAMILIES.include?(resolved)
|
|
167
|
-
height = logical_height || gdi_logical_height(size_pt)
|
|
181
|
+
height = source_logical_height(logical_height) || gdi_logical_height(size_pt)
|
|
168
182
|
table = table_for(family, weight, style)
|
|
169
183
|
fallback = cjk_fallback_widths(resolved, height, weight, style)
|
|
170
184
|
measured_latin = measured_carlito_widths(resolved, height, weight, style)
|
|
@@ -206,8 +220,16 @@ module Ea
|
|
|
206
220
|
end
|
|
207
221
|
private_class_method :measured_carlito_widths
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
211
233
|
return nil unless ["", "0", "400", "normal", "700", "bold"].include?(weight.to_s)
|
|
212
234
|
return nil unless ["", "normal", "italic"].include?(style.to_s)
|
|
213
235
|
|
|
@@ -223,17 +245,23 @@ module Ea
|
|
|
223
245
|
private_class_method :cambria_text_length
|
|
224
246
|
|
|
225
247
|
# 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
|
|
227
|
-
# measured height
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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)
|
|
237
265
|
widths = cjk_fallback_widths("Cambria", height, bold ? 700 : 400, style)
|
|
238
266
|
advances = text.each_char.map { |char| widths[char] }
|
|
239
267
|
advances.sum unless advances.include?(nil)
|
data/lib/ea/model/diagram.rb
CHANGED
|
@@ -43,6 +43,12 @@ module Ea
|
|
|
43
43
|
map "annotations", to: :annotations, render_empty: true
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# True for an object diagram. QEA stores "Object", the XMI adapter
|
|
47
|
+
# yields lowercase "object"; compare case-insensitively.
|
|
48
|
+
def object_diagram?
|
|
49
|
+
diagram_type.to_s.casecmp?("Object")
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
# Parses style_ex into a Hash of flag => value. Returns empty
|
|
47
53
|
# Hash when style_ex is nil/empty.
|
|
48
54
|
#
|
|
@@ -102,7 +102,6 @@ module Ea
|
|
|
102
102
|
# @raise [Errno::ENOENT] if QEA file not found
|
|
103
103
|
# @raise [SQLite3::Exception] if database access fails
|
|
104
104
|
def load
|
|
105
|
-
# Connect but don't use with_connection - we need to keep it open
|
|
106
105
|
@connection.connect unless @connection.connected?
|
|
107
106
|
db = @connection.connection
|
|
108
107
|
|
|
@@ -117,6 +116,10 @@ module Ea
|
|
|
117
116
|
end
|
|
118
117
|
|
|
119
118
|
database.freeze
|
|
119
|
+
ensure
|
|
120
|
+
# All tables are in memory; nothing reads the connection after
|
|
121
|
+
# load, and an open handle locks the file on Windows.
|
|
122
|
+
@connection.close
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
# Load a single table
|
|
@@ -7,6 +7,10 @@ module Ea
|
|
|
7
7
|
# Internal geometry for choosing the facing edges of stored routes.
|
|
8
8
|
module Routing
|
|
9
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
|
|
10
14
|
|
|
11
15
|
private
|
|
12
16
|
|
|
@@ -19,19 +23,26 @@ module Ea
|
|
|
19
23
|
connector = database.find_connector(link_row.connectorid)
|
|
20
24
|
return model unless connector && reversed_corner_link?(link_row, diagram_row, connector)
|
|
21
25
|
|
|
22
|
-
|
|
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)
|
|
23
39
|
origins = [stored_ray_origin(boxes[0], geom[:sx], geom[:sy]),
|
|
24
40
|
stored_ray_origin(boxes[1], geom[:ex], geom[:ey])]
|
|
25
|
-
x, y
|
|
26
|
-
bend = Ea::Model::Point.new(x: x, y: y)
|
|
41
|
+
bend = Ea::Model::Point.new(x: corner[0], y: corner[1])
|
|
27
42
|
ends = origins.zip(boxes).map do |origin, box|
|
|
28
43
|
inside_box?(origin, box) ? diagonal_corner_exit(origin, bend, box) : origin
|
|
29
44
|
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
model.waypoints = [ends[0], bend, ends[1]].map { |point| Ea::Model::Waypoint.new(position: point) }
|
|
33
|
-
model.source_anchor, model.target_anchor = origins
|
|
34
|
-
model
|
|
45
|
+
[origins, [ends[0], bend, ends[1]]] if ends.all?
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
# EMPIRICAL FIT (decision a), not a source-backed rule. A width-0
|
|
@@ -48,22 +59,37 @@ module Ea
|
|
|
48
59
|
return model if context || !reversed_tree_link?(model, link_row, diagram_row, geom)
|
|
49
60
|
|
|
50
61
|
start, finish = stored_ray_boxes(link_row, diagram_row)
|
|
51
|
-
|
|
52
|
-
return model unless
|
|
62
|
+
route = reversed_tree_points(start, finish, geom, stored_bus_levels(link_row))
|
|
63
|
+
return model unless route
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
model.waypoints = [first, [first[0], bus], [last[0], bus], last].map do |x, y|
|
|
65
|
+
points, beside = route
|
|
66
|
+
model.waypoints = points.map do |x, y|
|
|
57
67
|
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
58
68
|
end
|
|
59
69
|
model.source_anchor = model.source_port = model.waypoints.first.position
|
|
60
70
|
model.target_anchor = model.target_port = model.waypoints.last.position
|
|
61
71
|
# EMPIRICAL FIT: a leg that starts beside its box (5038EEE3
|
|
62
72
|
# connector 212) seats its source labels like a leftward leg.
|
|
63
|
-
model.style = model.style.to_h.merge(left_seated_source: true) if
|
|
73
|
+
model.style = model.style.to_h.merge(left_seated_source: true) if beside
|
|
64
74
|
model
|
|
65
75
|
end
|
|
66
76
|
|
|
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
|
+
|
|
67
93
|
# EMPIRICAL FIT (decision a). EA seats the source labels of a
|
|
68
94
|
# vertical lateral tree ("LV") whose SOID/EOID name the ends in
|
|
69
95
|
# reverse as if the source leg left to the left, whichever way the
|
|
@@ -92,25 +118,37 @@ module Ea
|
|
|
92
118
|
|
|
93
119
|
def stored_bus_levels(link_row)
|
|
94
120
|
bends = intermediate_waypoints(link_row)
|
|
95
|
-
bends.size == 2 && bends.first[1] == bends.last[1]
|
|
121
|
+
bends.first[1] if bends.size == 2 && bends.first[1] == bends.last[1]
|
|
96
122
|
end
|
|
97
123
|
|
|
98
124
|
# [bus height, Start facing edge, End facing edge] for vertically
|
|
99
125
|
# separated boxes: the bus sits 50px past the End box's edge and
|
|
100
126
|
# must stay inside the gap. Nil otherwise.
|
|
101
|
-
|
|
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)
|
|
102
135
|
if finish.y + finish.height < start.y
|
|
103
136
|
bus = finish.y + finish.height + REVERSED_TREE_BUS_OFFSET
|
|
104
|
-
|
|
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)
|
|
105
140
|
elsif start.y + start.height < finish.y
|
|
106
141
|
bus = finish.y - REVERSED_TREE_BUS_OFFSET
|
|
107
|
-
|
|
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)
|
|
108
145
|
end
|
|
109
146
|
end
|
|
110
147
|
|
|
111
148
|
# 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.
|
|
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.
|
|
114
152
|
#
|
|
115
153
|
# EMPIRICAL FIT (decision a): the leg's x is pulled toward the box
|
|
116
154
|
# centre by |offset_y| / (vertical centre distance to the other box).
|
|
@@ -124,8 +162,12 @@ module Ea
|
|
|
124
162
|
pull = span.positive? ? 1 - [offset_y.to_i.abs / span, 1].min : 1
|
|
125
163
|
pulled = (offset_x.to_i.abs * pull).ceil * (offset_x.to_i.negative? ? -1 : 1)
|
|
126
164
|
origin = stored_ray_origin(box, pulled, offset_y)
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
129
171
|
end
|
|
130
172
|
|
|
131
173
|
# Path follows SOID/EOID placement order, independently of the
|
|
@@ -202,7 +202,10 @@ module Ea
|
|
|
202
202
|
end
|
|
203
203
|
|
|
204
204
|
def build_connectors(diagram_row)
|
|
205
|
-
|
|
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) }
|
|
206
209
|
context = ContextDiagramSupport.context?(diagram_row)
|
|
207
210
|
# EA regenerates context diagrams at draw time; their
|
|
208
211
|
# Hidden link rows are stale and never render.
|
|
@@ -37,6 +37,7 @@ module Ea
|
|
|
37
37
|
)
|
|
38
38
|
mark_connector_proxies(doc)
|
|
39
39
|
apply_tagged_values(doc)
|
|
40
|
+
apply_constraints(doc)
|
|
40
41
|
apply_receptions(doc)
|
|
41
42
|
apply_stereotypes(doc)
|
|
42
43
|
resolve_type_names(doc)
|
|
@@ -93,6 +94,17 @@ module Ea
|
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
# Attach the extension's element constraints to their classifiers,
|
|
98
|
+
# as the QEA adapter does from t_objectconstraint.
|
|
99
|
+
def apply_constraints(doc)
|
|
100
|
+
grouped = ConstraintBuilder.new(xmi_path).grouped_by_element
|
|
101
|
+
return if grouped.empty?
|
|
102
|
+
|
|
103
|
+
doc.classifiers.each do |c|
|
|
104
|
+
c.constraints = grouped[c.id] if c.id && grouped[c.id]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
96
108
|
# Attach tagged values parsed from the <xmi:Extension><tags>
|
|
97
109
|
# block to their owning classifiers and packages.
|
|
98
110
|
def apply_tagged_values(doc)
|
|
@@ -130,6 +142,7 @@ module Ea
|
|
|
130
142
|
(classifier.properties || []).each do |prop|
|
|
131
143
|
prop.type_name = types.attribute_type(prop.id) ||
|
|
132
144
|
qualified_by_id[prop.type_ref] || prop.type_name
|
|
145
|
+
apply_attribute_stereotype(prop, types)
|
|
133
146
|
end
|
|
134
147
|
(classifier.operations || []).each do |op|
|
|
135
148
|
resolve_operation_types(op, qualified_by_id, types)
|
|
@@ -137,6 +150,13 @@ module Ea
|
|
|
137
150
|
end
|
|
138
151
|
end
|
|
139
152
|
|
|
153
|
+
def apply_attribute_stereotype(prop, types)
|
|
154
|
+
stereotype = types.attribute_stereotype(prop.id)
|
|
155
|
+
return if stereotype.nil? || prop.stereotype_refs.to_a.any?
|
|
156
|
+
|
|
157
|
+
prop.stereotype_refs = [stereotype]
|
|
158
|
+
end
|
|
159
|
+
|
|
140
160
|
def resolve_operation_types(operation, qualified_by_id, types)
|
|
141
161
|
operation.return_type_name = types.return_type(operation.id) ||
|
|
142
162
|
qualified_by_id[operation.return_type_name] ||
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Indexes the extension's `<connector>` nodes by `xmi:idref`. EA never
|
|
7
|
+
# repeats `<source>`, `<target>`, `<properties>` or `<xrefs>` inside one
|
|
8
|
+
# connector; if a hand-edited file does, the parse yields an Array where
|
|
9
|
+
# a single node is read, so the first occurrence is kept.
|
|
10
|
+
module ConnectorIndex
|
|
11
|
+
SINGLE_NODES = %i[source target properties xrefs].freeze
|
|
12
|
+
|
|
13
|
+
def self.build(root)
|
|
14
|
+
connectors = root.extension&.connectors&.connector || []
|
|
15
|
+
connectors.each_with_object({}) do |connector, index|
|
|
16
|
+
next unless connector.idref
|
|
17
|
+
|
|
18
|
+
SINGLE_NODES.each { |name| keep_first(connector, name) }
|
|
19
|
+
index[connector.idref] = connector
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.keep_first(connector, name)
|
|
24
|
+
value = connector.public_send(name)
|
|
25
|
+
connector.public_send(:"#{name}=", value.first) if value.is_a?(Array)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
# Extracts element constraints from the `<xmi:Extension><elements>`
|
|
9
|
+
# block (`<element><constraints><constraint/></constraints>`), the
|
|
10
|
+
# XMI image of t_objectconstraint. The xmi gem does not expose them,
|
|
11
|
+
# so the file is streamed once and grouped by owning element id.
|
|
12
|
+
class ConstraintBuilder
|
|
13
|
+
attr_reader :xmi_path
|
|
14
|
+
|
|
15
|
+
def initialize(xmi_path)
|
|
16
|
+
@xmi_path = xmi_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns Hash{element_id => Array<Ea::Model::Constraint>}
|
|
20
|
+
def grouped_by_element
|
|
21
|
+
return {} if xmi_path.nil?
|
|
22
|
+
|
|
23
|
+
@grouped_by_element ||= scan
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def scan
|
|
29
|
+
@path = []
|
|
30
|
+
@owner = nil
|
|
31
|
+
@grouped = {}
|
|
32
|
+
@seen_owners = {}
|
|
33
|
+
@blocks_seen = {}
|
|
34
|
+
@skip_block = false
|
|
35
|
+
File.open(xmi_path) { |io| Nokogiri::XML::Reader(io).each { |node| visit(node) } }
|
|
36
|
+
@grouped
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit(node)
|
|
40
|
+
case node.node_type
|
|
41
|
+
when Nokogiri::XML::Reader::TYPE_END_ELEMENT
|
|
42
|
+
@owner = nil if @path.pop == "element"
|
|
43
|
+
when Nokogiri::XML::Reader::TYPE_ELEMENT
|
|
44
|
+
enter(node)
|
|
45
|
+
@path.push(node.local_name) unless node.self_closing?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enter(node)
|
|
50
|
+
if node.local_name == "element" && @path.last == "elements" && !node.self_closing?
|
|
51
|
+
@owner = first_block_owner(node.attribute("xmi:idref"))
|
|
52
|
+
elsif node.local_name == "constraints" && @owner && @path.last == "element"
|
|
53
|
+
@skip_block = @blocks_seen.key?(@owner)
|
|
54
|
+
@blocks_seen[@owner] = true
|
|
55
|
+
elsif node.local_name == "constraint" && @owner && !@skip_block && @path.last(2) == %w[element constraints]
|
|
56
|
+
(@grouped[@owner] ||= []) << constraint(node)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A repeated `<element xmi:idref="X">` or `<constraints>` block would
|
|
61
|
+
# append X's constraints again, so only the first of each is read.
|
|
62
|
+
def first_block_owner(idref)
|
|
63
|
+
return nil if @seen_owners.key?(idref)
|
|
64
|
+
|
|
65
|
+
@seen_owners[idref] = true
|
|
66
|
+
idref
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def constraint(node)
|
|
70
|
+
Ea::Model::Constraint.new(
|
|
71
|
+
id: IdNormalizer.synthetic_id(@owner, "constraint", @grouped[@owner].to_a.size.to_s),
|
|
72
|
+
name: node.attribute("name").to_s,
|
|
73
|
+
kind: node.attribute("type").to_s,
|
|
74
|
+
body: node.attribute("description").to_s,
|
|
75
|
+
status: node.attribute("status").to_s
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|