ea 0.6.140 → 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 +44 -16
- 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/diagram_builder.rb +21 -5
- data/lib/ea/sources/xmi/extension_style_parser.rb +1 -1
- data/lib/ea/sources/xmi/stored_route/reversed_routes.rb +10 -26
- 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 +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 +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/content_extent.rb +11 -2
- data/lib/ea/svg/ea_emitter/diagram_frame.rb +7 -0
- data/lib/ea/svg/ea_emitter/document.rb +21 -5
- 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 +11 -9
- data/lib/ea/svg/ea_emitter/font_resolver.rb +3 -1
- data/lib/ea/svg/ea_emitter/label/end_label.rb +72 -10
- data/lib/ea/svg/ea_emitter/labels.rb +10 -3
- data/lib/ea/svg/ea_emitter/layer_sequencer.rb +14 -7
- 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 +3 -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
|
@@ -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)
|
|
@@ -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.
|
|
@@ -197,9 +197,11 @@ module Ea
|
|
|
197
197
|
|
|
198
198
|
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
199
199
|
# Index elements by DUID so we can resolve SOID/EOID.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
# A DUID shared by several placements names none of them (QEA
|
|
201
|
+
# placement_order_reversed? needs a unique match too).
|
|
202
|
+
by_duid = placed_elements.select { |pe| pe.style.duid }.group_by { |pe| pe.style.duid }
|
|
203
|
+
.select { |_duid, placements| placements.one? }
|
|
204
|
+
.transform_values(&:first)
|
|
203
205
|
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
204
206
|
|
|
205
207
|
placed_connectors.each_with_index.map do |placed, index|
|
|
@@ -368,14 +370,28 @@ module Ea
|
|
|
368
370
|
# The saved SOID/EOID can name the model connector's ends in reverse
|
|
369
371
|
# order; the model connector decides which placement is the source.
|
|
370
372
|
def placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
named = [by_duid[style.soid], by_duid[style.eoid]]
|
|
374
|
+
source = named[0] || sole_placement(by_subject, edge_source)
|
|
375
|
+
target = named[1] || sole_placement(by_subject, edge_target)
|
|
376
|
+
if source.equal?(target) && named.one?
|
|
377
|
+
source, target = subject_placements(by_subject, named, edge_source, edge_target)
|
|
378
|
+
end
|
|
373
379
|
return [source, target] unless source && target &&
|
|
374
380
|
reversed_ends?(source, target, edge_source, edge_target)
|
|
375
381
|
|
|
376
382
|
[target, source]
|
|
377
383
|
end
|
|
378
384
|
|
|
385
|
+
# One stale DUID falls back to the relationship's own end, which can
|
|
386
|
+
# be the very placement the other DUID named; two distinct ends never
|
|
387
|
+
# share a box, so each end then takes its own subject's placement. A
|
|
388
|
+
# link whose SOID and EOID are one DUID is a genuine self link.
|
|
389
|
+
def subject_placements(by_subject, named, edge_source, edge_target)
|
|
390
|
+
[edge_source, edge_target].zip(named).map do |subject, placement|
|
|
391
|
+
sole_placement(by_subject, subject) || (placement if guid_key(placement&.subject) == guid_key(subject))
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
379
395
|
def reversed_ends?(source, target, edge_source, edge_target)
|
|
380
396
|
guid_key(edge_source) != guid_key(edge_target) &&
|
|
381
397
|
guid_key(source.subject) == guid_key(edge_target) &&
|
|
@@ -18,21 +18,17 @@ module Ea
|
|
|
18
18
|
return {} unless reversed_tree_candidate?(model, link, bounds, offsets)
|
|
19
19
|
|
|
20
20
|
start, finish = bounds.values_at(:source, :target)
|
|
21
|
-
|
|
22
|
-
return {} unless
|
|
21
|
+
route = reversed_tree_points(start, finish, ray_geometry(offsets, edge), stored_bus_levels(link))
|
|
22
|
+
return {} unless route
|
|
23
23
|
|
|
24
|
-
|
|
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)
|
|
24
|
+
reversed_tree_fields(model, route[0], route[1])
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
def reversed_tree_fields(model,
|
|
27
|
+
def reversed_tree_fields(model, points, beside)
|
|
32
28
|
style = model.style.to_h.except(:umldi_route, :reversed_ends)
|
|
33
29
|
style[:left_seated_source] = true if beside
|
|
34
|
-
ends = [point_for(first), point_for(last)]
|
|
35
|
-
{ waypoints: points_to_waypoints(
|
|
30
|
+
ends = [point_for(points.first), point_for(points.last)]
|
|
31
|
+
{ waypoints: points_to_waypoints(points),
|
|
36
32
|
source_anchor: ends[0], target_anchor: ends[1], source_port: ends[0], target_port: ends[1],
|
|
37
33
|
style: style }
|
|
38
34
|
end
|
|
@@ -49,26 +45,14 @@ module Ea
|
|
|
49
45
|
def reversed_corner_metadata(model, link, bounds, offsets, edge)
|
|
50
46
|
return {} unless reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ends = origins.zip(boxes).map { |origin, box| corner_exit(origin, bend, box) }
|
|
56
|
-
ends.all? ? corner_fields(origins, bend, ends) : {}
|
|
57
|
-
end
|
|
48
|
+
origins, points = reversed_corner_points(bounds.values_at(:source, :target), ray_geometry(offsets, edge),
|
|
49
|
+
intermediate_waypoints(link).first)
|
|
50
|
+
return {} unless points
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
{ waypoints: points_to_waypoints([ends[0], bend, ends[1]].map { |point| [point.x, point.y] }),
|
|
52
|
+
{ waypoints: points_to_waypoints(points.map { |point| [point.x, point.y] }),
|
|
61
53
|
source_anchor: origins[0], target_anchor: origins[1] }
|
|
62
54
|
end
|
|
63
55
|
|
|
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
56
|
def reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
73
57
|
model.connector_type == "Association" && model.style[:reversed_ends] && edge == 2 &&
|
|
74
58
|
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] &&
|
data/lib/ea/spa/generator.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Ea
|
|
|
17
17
|
attr_reader :document, :mode, :output_path, :shard_url_for, :configuration
|
|
18
18
|
|
|
19
19
|
def initialize(document, output:, mode: nil, shard_url_for: nil,
|
|
20
|
-
configuration: nil)
|
|
20
|
+
configuration: nil, frame: false)
|
|
21
|
+
@frame = frame
|
|
21
22
|
@document = document
|
|
22
23
|
@output_path = output
|
|
23
24
|
@mode = (mode || DEFAULT_MODE).to_sym
|
|
@@ -32,7 +33,8 @@ module Ea
|
|
|
32
33
|
def projector
|
|
33
34
|
@projector ||= Projector.new(document,
|
|
34
35
|
shard_url_for: shard_url_for,
|
|
35
|
-
configuration: configuration
|
|
36
|
+
configuration: configuration,
|
|
37
|
+
frame: @frame)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def strategy_class
|
data/lib/ea/spa/projector.rb
CHANGED
|
@@ -15,8 +15,9 @@ module Ea
|
|
|
15
15
|
class Projector
|
|
16
16
|
attr_reader :document, :shard_url_for, :configuration
|
|
17
17
|
|
|
18
|
-
def initialize(document, shard_url_for: nil, configuration: nil)
|
|
18
|
+
def initialize(document, shard_url_for: nil, configuration: nil, frame: false)
|
|
19
19
|
@document = document
|
|
20
|
+
@frame = frame
|
|
20
21
|
@shard_url_for = shard_url_for || default_shard_url
|
|
21
22
|
@configuration = configuration
|
|
22
23
|
end
|
|
@@ -288,7 +289,8 @@ module Ea
|
|
|
288
289
|
payload["svg"] = Ea::Svg::EaEmitter::Document.new(
|
|
289
290
|
diagram,
|
|
290
291
|
model_index: document.index_by_id,
|
|
291
|
-
document: document
|
|
292
|
+
document: document,
|
|
293
|
+
frame: @frame
|
|
292
294
|
).render
|
|
293
295
|
payload
|
|
294
296
|
rescue StandardError => e
|
|
@@ -12,7 +12,7 @@ module Ea
|
|
|
12
12
|
# the 7pt default): 31 for fontsz 100, 30 for 90, 28 for the
|
|
13
13
|
# default. A plain Note, and a Package that draws no "(from
|
|
14
14
|
# Parent)" subtitle, keep the bare 10 margin, as does a classifier
|
|
15
|
-
# of the diagram's own package. Misses: 2 outliers.
|
|
15
|
+
# or instance of the diagram's own package. Misses: 2 outliers.
|
|
16
16
|
class BottomMargin
|
|
17
17
|
BARE = 10
|
|
18
18
|
LINE_ALLOWANCE = 21
|
|
@@ -41,7 +41,7 @@ module Ea
|
|
|
41
41
|
model = model_index && model_index[element.model_element_ref]
|
|
42
42
|
return home_text_margin(element) if home_text?(model, diagram)
|
|
43
43
|
return BARE if plain_note?(model) || bare_package?(model, diagram) ||
|
|
44
|
-
|
|
44
|
+
home_element?(model, diagram)
|
|
45
45
|
|
|
46
46
|
LINE_ALLOWANCE + points(element)
|
|
47
47
|
end
|
|
@@ -60,17 +60,21 @@ module Ea
|
|
|
60
60
|
model.is_a?(Ea::Model::Note) && model.note_type != "Text"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# on the 7 PLATEAU diagrams
|
|
67
|
-
# diagram's package (all natively
|
|
68
|
-
# foreign (all 28..31).
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
# An element of the diagram's own package (a classifier, nested
|
|
64
|
+
# in an owner class or not, or an instance) draws no "(from
|
|
65
|
+
# Package)" row under its name, so EA keeps only the bare margin
|
|
66
|
+
# below it. EMPIRICAL FIT, checked on: the 7 PLATEAU diagrams
|
|
67
|
+
# whose lowest class is in the diagram's package (all natively
|
|
68
|
+
# 10) against the 84 foreign ones (all 28..31); basic.qea
|
|
69
|
+
# 3B8BD087 and D14AA320 (lowest an own-package "Object B"
|
|
70
|
+
# instance) and 372F7F7B, 4FC1D352, 6D0349D5 (lowest a Klass
|
|
71
|
+
# nested in "Class A.x" / "Class B" of the own package), all
|
|
72
|
+
# natively 10; and the foreign basic instances, natively 28.
|
|
73
|
+
def self.home_element?(model, diagram)
|
|
74
|
+
(model.is_a?(Ea::Model::Classifier) ||
|
|
75
|
+
model.is_a?(Ea::Model::InstanceSpecification)) &&
|
|
71
76
|
!diagram.package_id.to_s.empty? &&
|
|
72
|
-
model.package_id == diagram.package_id
|
|
73
|
-
model.nested_parent_name.to_s.empty?
|
|
77
|
+
model.package_id == diagram.package_id
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# A Text element of the diagram's own package keeps 13 + its
|