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
|
@@ -12,6 +12,7 @@ module Ea
|
|
|
12
12
|
autoload :ConnectorMetadata, "ea/sources/xmi/diagram_builder/connector_metadata"
|
|
13
13
|
autoload :AssociationClassMetadata, "ea/sources/xmi/diagram_builder/association_class_metadata"
|
|
14
14
|
autoload :FontSettings, "ea/sources/xmi/diagram_builder/font_settings"
|
|
15
|
+
autoload :ElementBorders, "ea/sources/xmi/diagram_builder/element_borders"
|
|
15
16
|
autoload :StereotypeProfiles, "ea/sources/xmi/diagram_builder/stereotype_profiles"
|
|
16
17
|
autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
17
18
|
|
|
@@ -135,7 +136,7 @@ module Ea
|
|
|
135
136
|
image_bounds: (build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom) unless bounds),
|
|
136
137
|
background_color: style.background_color,
|
|
137
138
|
line_color: style.line_color,
|
|
138
|
-
line_width: style.
|
|
139
|
+
line_width: placed_line_width(style, placed.subject),
|
|
139
140
|
font_family: font[:family],
|
|
140
141
|
font_size: font[:size],
|
|
141
142
|
font_size_unit: "pt",
|
|
@@ -155,6 +156,17 @@ module Ea
|
|
|
155
156
|
|
|
156
157
|
# XMI preserves UML member order in ownedAttribute. Carry that
|
|
157
158
|
# provenance to the renderer so it does not replace it with a name sort.
|
|
159
|
+
# LWth on the placement, else the model element's saved border width.
|
|
160
|
+
# A negative LWth (-1) inherits; zero requests the one-pixel cosmetic
|
|
161
|
+
# pen, as in the QEA source. Checked on 01661E1B (LWth=-1 over
|
|
162
|
+
# BorderWidth=2) and 1A10EB6D (LWth=0, native stroke-width 1).
|
|
163
|
+
def placed_line_width(style, subject)
|
|
164
|
+
own = style.line_width
|
|
165
|
+
return [own, 1].max if own && own >= 0
|
|
166
|
+
|
|
167
|
+
(@element_borders ||= ElementBorders.new(root)).width_for(subject)
|
|
168
|
+
end
|
|
169
|
+
|
|
158
170
|
def element_style(subject)
|
|
159
171
|
style = (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(subject)
|
|
160
172
|
return style unless source_ordered_classifiers.include?(subject)
|
|
@@ -179,15 +191,17 @@ module Ea
|
|
|
179
191
|
return nil unless left && top && right && bottom
|
|
180
192
|
|
|
181
193
|
Ea::Model::Bounds.new(x: left, y: top,
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
width: right - left,
|
|
195
|
+
height: bottom - top)
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
187
199
|
# Index elements by DUID so we can resolve SOID/EOID.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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)
|
|
191
205
|
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
192
206
|
|
|
193
207
|
placed_connectors.each_with_index.map do |placed, index|
|
|
@@ -245,7 +259,7 @@ module Ea
|
|
|
245
259
|
waypoints: route[:waypoints],
|
|
246
260
|
style: route[:style],
|
|
247
261
|
line_color: style.color,
|
|
248
|
-
line_width:
|
|
262
|
+
line_width: ext_meta[:line_width],
|
|
249
263
|
hidden: style.hidden,
|
|
250
264
|
label_boxes: connector_label_boxes(placed, ext_meta[:ea_type],
|
|
251
265
|
options.fetch(:context) ? nil : owner_id)
|
|
@@ -257,7 +271,10 @@ module Ea
|
|
|
257
271
|
connector.label_boxes)
|
|
258
272
|
end
|
|
259
273
|
mark_stored_ray(connector, placed, source, target) unless options.fetch(:context)
|
|
260
|
-
|
|
274
|
+
unless options.fetch(:context)
|
|
275
|
+
reversed = reversed_placement?(style, by_duid, edge_source, edge_target)
|
|
276
|
+
mark_association_route(connector, placed, source, target, reversed)
|
|
277
|
+
end
|
|
261
278
|
connector
|
|
262
279
|
end
|
|
263
280
|
|
|
@@ -293,9 +310,20 @@ module Ea
|
|
|
293
310
|
StoredRoute.new.public_send(stage, connector, link: link, bounds: bounds, offsets: offsets, edge: geom.edge)
|
|
294
311
|
end
|
|
295
312
|
|
|
296
|
-
def mark_association_route(connector, placed, source, target)
|
|
313
|
+
def mark_association_route(connector, placed, source, target, reversed)
|
|
314
|
+
connector.style[:reversed_ends] = true if reversed
|
|
297
315
|
metadata = stored_ray_metadata(connector, placed, source, target, stage: :association_route)
|
|
298
316
|
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
317
|
+
connector.style.delete(:reversed_ends)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Mirrors QEA placement_order_reversed?: SOID and EOID must both name
|
|
321
|
+
# placements on this diagram and be crossed against the relationship's
|
|
322
|
+
# ends. A stale DUID resolves to nothing and is not a reversal.
|
|
323
|
+
def reversed_placement?(style, by_duid, edge_source, edge_target)
|
|
324
|
+
source = by_duid[style.soid]
|
|
325
|
+
target = by_duid[style.eoid]
|
|
326
|
+
source && target && reversed_ends?(source, target, edge_source, edge_target)
|
|
299
327
|
end
|
|
300
328
|
|
|
301
329
|
def mark_plain_source_seat(connector, placed, context)
|
|
@@ -342,14 +370,28 @@ module Ea
|
|
|
342
370
|
# The saved SOID/EOID can name the model connector's ends in reverse
|
|
343
371
|
# order; the model connector decides which placement is the source.
|
|
344
372
|
def placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
347
379
|
return [source, target] unless source && target &&
|
|
348
380
|
reversed_ends?(source, target, edge_source, edge_target)
|
|
349
381
|
|
|
350
382
|
[target, source]
|
|
351
383
|
end
|
|
352
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
|
+
|
|
353
395
|
def reversed_ends?(source, target, edge_source, edge_target)
|
|
354
396
|
guid_key(edge_source) != guid_key(edge_target) &&
|
|
355
397
|
guid_key(source.subject) == guid_key(edge_target) &&
|
|
@@ -358,12 +400,13 @@ module Ea
|
|
|
358
400
|
|
|
359
401
|
def connector_meta(subject_id)
|
|
360
402
|
connector = connector_index[subject_id]
|
|
361
|
-
return { ea_type: nil, direction: nil } unless connector
|
|
403
|
+
return { ea_type: nil, direction: nil, line_width: nil } unless connector
|
|
362
404
|
|
|
363
405
|
props = connector.properties
|
|
364
406
|
{
|
|
365
407
|
ea_type: props&.ea_type,
|
|
366
|
-
direction: props&.direction
|
|
408
|
+
direction: props&.direction,
|
|
409
|
+
line_width: connector.appearance&.linewidth
|
|
367
410
|
}.merge(connector_metadata.for(connector))
|
|
368
411
|
end
|
|
369
412
|
|
|
@@ -515,8 +558,8 @@ module Ea
|
|
|
515
558
|
return nil unless g.left && g.top && g.right && g.bottom
|
|
516
559
|
|
|
517
560
|
Ea::Model::Bounds.new(x: g.left, y: g.top,
|
|
518
|
-
|
|
519
|
-
|
|
561
|
+
width: g.right - g.left,
|
|
562
|
+
height: g.bottom - g.top)
|
|
520
563
|
end
|
|
521
564
|
|
|
522
565
|
def edge_point(placed, edge_code, _end_kind)
|
|
@@ -40,7 +40,8 @@ module Ea
|
|
|
40
40
|
name: ext_element.name,
|
|
41
41
|
body: ext_element.properties&.documentation,
|
|
42
42
|
note_type: note_type,
|
|
43
|
-
legend: legend_for(ext_element, note_type)
|
|
43
|
+
legend: legend_for(ext_element, note_type),
|
|
44
|
+
package_id: IdNormalizer.from_xmi_id(ext_element.model&.package)
|
|
44
45
|
)
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class StoredRoute < Qea::DiagramBuilder
|
|
7
|
+
# QEA's reversed-end Association rules for XMI. A connector is reversed
|
|
8
|
+
# when its SOID names a placement other than the relationship's source
|
|
9
|
+
# (builder flag :reversed_ends). Geometry helpers come from
|
|
10
|
+
# Qea::DiagramBuilder::Routing, so both sources share one rule.
|
|
11
|
+
module ReversedRoutes
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# A width-0 vertical-tree Association takes the bus 50px past the End
|
|
15
|
+
# box's facing edge with legs at the stored origins (an empirical fit,
|
|
16
|
+
# see Routing#reversed_tree_route; checked on 5038EEE3).
|
|
17
|
+
def reversed_tree_metadata(model, link, bounds, offsets, edge)
|
|
18
|
+
return {} unless reversed_tree_candidate?(model, link, bounds, offsets)
|
|
19
|
+
|
|
20
|
+
start, finish = bounds.values_at(:source, :target)
|
|
21
|
+
route = reversed_tree_points(start, finish, ray_geometry(offsets, edge), stored_bus_levels(link))
|
|
22
|
+
return {} unless route
|
|
23
|
+
|
|
24
|
+
reversed_tree_fields(model, route[0], route[1])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reversed_tree_fields(model, points, beside)
|
|
28
|
+
style = model.style.to_h.except(:umldi_route, :reversed_ends)
|
|
29
|
+
style[:left_seated_source] = true if beside
|
|
30
|
+
ends = [point_for(points.first), point_for(points.last)]
|
|
31
|
+
{ waypoints: points_to_waypoints(points),
|
|
32
|
+
source_anchor: ends[0], target_anchor: ends[1], source_port: ends[0], target_port: ends[1],
|
|
33
|
+
style: style }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reversed_tree_candidate?(model, link, bounds, offsets)
|
|
37
|
+
model.connector_type == "Association" && model.style[:reversed_ends] &&
|
|
38
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == %w[3 0 V] &&
|
|
39
|
+
ray_inputs?(bounds, offsets) && stored_bus_levels(link)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A thin Mode 3 EDGE 2 Association whose single saved corner follows
|
|
43
|
+
# the opposite placement order is clipped from each leg's stored
|
|
44
|
+
# origin, as QEA's reversed_stored_corner does (5038EEE3).
|
|
45
|
+
def reversed_corner_metadata(model, link, bounds, offsets, edge)
|
|
46
|
+
return {} unless reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
47
|
+
|
|
48
|
+
origins, points = reversed_corner_points(bounds.values_at(:source, :target), ray_geometry(offsets, edge),
|
|
49
|
+
intermediate_waypoints(link).first)
|
|
50
|
+
return {} unless points
|
|
51
|
+
|
|
52
|
+
{ waypoints: points_to_waypoints(points.map { |point| [point.x, point.y] }),
|
|
53
|
+
source_anchor: origins[0], target_anchor: origins[1] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reversed_corner_candidate?(model, link, bounds, offsets, edge)
|
|
57
|
+
model.connector_type == "Association" && model.style[:reversed_ends] && edge == 2 &&
|
|
58
|
+
model.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "0", nil] &&
|
|
59
|
+
ray_inputs?(bounds, offsets) && intermediate_waypoints(link).size == 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# QEA seats the source labels of a reversed vertical lateral tree
|
|
63
|
+
# ("LV") as if its source leg left to the left (5038EEE3 connector 207).
|
|
64
|
+
def reversed_lateral_tree_metadata(model, _link, _bounds, _offsets, _edge)
|
|
65
|
+
return {} unless model.connector_type == "Association" && model.style[:reversed_ends] &&
|
|
66
|
+
model.style[:tree] == "LV"
|
|
67
|
+
|
|
68
|
+
{ style: model.style.to_h.except(:reversed_ends).merge(left_seated_source: true) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def points_to_waypoints(points)
|
|
72
|
+
points.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -9,6 +9,9 @@ module Ea
|
|
|
9
9
|
# boxes). Returns nil where QEA would fall back to bare SX/SY deltas,
|
|
10
10
|
# which the generic router keeps handling.
|
|
11
11
|
class StoredRoute < Qea::DiagramBuilder
|
|
12
|
+
autoload :ReversedRoutes, "ea/sources/xmi/stored_route/reversed_routes"
|
|
13
|
+
include ReversedRoutes
|
|
14
|
+
|
|
12
15
|
Link = Struct.new(:style, :path, :geometry)
|
|
13
16
|
Connector = Struct.new(:connector_type)
|
|
14
17
|
|
|
@@ -29,6 +32,9 @@ module Ea
|
|
|
29
32
|
# Saved rays retain origins and computed boundary ports, matching QEA.
|
|
30
33
|
def ray_metadata(model, link:, bounds:, offsets:, edge:)
|
|
31
34
|
if model.connector_type == "Generalization"
|
|
35
|
+
overlap = mode2_overlap_metadata(model, link, bounds, offsets, edge)
|
|
36
|
+
return overlap unless overlap.empty?
|
|
37
|
+
|
|
32
38
|
return generalization_ray_metadata(model, link, bounds, offsets, edge)
|
|
33
39
|
end
|
|
34
40
|
|
|
@@ -49,11 +55,10 @@ module Ea
|
|
|
49
55
|
# UMLDI fallback. A single stored corner and an empty-path stored ray
|
|
50
56
|
# carry mathematical origins that the generic facing-edge route loses.
|
|
51
57
|
def association_route(model, link:, bounds:, offsets:, edge:)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return partial unless partial.empty?
|
|
58
|
+
ROUTE_STAGES.each do |stage|
|
|
59
|
+
found = method(stage).call(model, link, bounds, offsets, edge)
|
|
60
|
+
return found unless found.empty?
|
|
61
|
+
end
|
|
57
62
|
|
|
58
63
|
return {} unless stored_association_candidate?(model, bounds)
|
|
59
64
|
|
|
@@ -67,6 +72,69 @@ module Ea
|
|
|
67
72
|
|
|
68
73
|
private
|
|
69
74
|
|
|
75
|
+
ROUTE_STAGES = %i[umldi_association_metadata shared_overlap_metadata
|
|
76
|
+
source_row_metadata auto_port_elbow_metadata reversed_tree_metadata
|
|
77
|
+
reversed_corner_metadata reversed_lateral_tree_metadata partial_port_metadata].freeze
|
|
78
|
+
|
|
79
|
+
# Overlap rules need both end boxes and every end offset.
|
|
80
|
+
def ray_inputs?(bounds, offsets)
|
|
81
|
+
bounds.values.all? && offsets.values.flatten.none?(&:nil?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A Mode 2 Generalization whose stored Path is a horizontal jog is EA's
|
|
85
|
+
# auto-route: QEA replaces the stored bends with one vertical ray, and
|
|
86
|
+
# the same QEA rule decides it here.
|
|
87
|
+
def mode2_overlap_metadata(model, link, bounds, offsets, edge)
|
|
88
|
+
return {} if link.path.to_s.empty? || !ray_inputs?(bounds, offsets)
|
|
89
|
+
|
|
90
|
+
candidate = association_model(model, bounds, offsets)
|
|
91
|
+
initial = candidate.waypoints
|
|
92
|
+
mode2_overlap_ray(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
93
|
+
return {} if candidate.waypoints.equal?(initial)
|
|
94
|
+
|
|
95
|
+
{ waypoints: candidate.waypoints, source_anchor: candidate.source_anchor,
|
|
96
|
+
target_anchor: candidate.target_anchor, has_geometry_offsets: true }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A stored (non-UMLDI) Association through the shared overlap row takes
|
|
100
|
+
# QEA's overlap origins and flush ports, decided by the same QEA rule.
|
|
101
|
+
def shared_overlap_metadata(model, link, bounds, offsets, edge)
|
|
102
|
+
return {} if model.style[:umldi_route] || !ray_inputs?(bounds, offsets)
|
|
103
|
+
|
|
104
|
+
candidate = association_model(model, bounds, offsets)
|
|
105
|
+
geom = ray_geometry(offsets, edge)
|
|
106
|
+
association_stored_ray(candidate, link, bounds, geom, false) if association_ray_style?(model.style.to_h)
|
|
107
|
+
overlap_ray_origins(candidate, link, bounds, geom, false)
|
|
108
|
+
return {} unless candidate.style[:overlap_ray]
|
|
109
|
+
|
|
110
|
+
association_metadata(candidate)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A Mode 2 Association with unequal end offsets and no (or a stale) Path
|
|
114
|
+
# rides the source row, as QEA's source_row_association_ray decides.
|
|
115
|
+
def source_row_metadata(model, link, bounds, offsets, edge)
|
|
116
|
+
return {} if model.style[:umldi_route] || !ray_inputs?(bounds, offsets)
|
|
117
|
+
|
|
118
|
+
candidate = association_model(model, bounds, offsets)
|
|
119
|
+
source_row_association_ray(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
120
|
+
return {} unless candidate.style[:overlap_ray]
|
|
121
|
+
|
|
122
|
+
association_metadata(candidate)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A Mode 2 Association with uniform offsets, EDGE 4 and no Path keeps its
|
|
126
|
+
# physical ports and takes QEA's elbow, even when UMLDI saved a straight
|
|
127
|
+
# route (30D62231, 934F4502).
|
|
128
|
+
def auto_port_elbow_metadata(model, link, bounds, offsets, edge)
|
|
129
|
+
return {} unless ray_inputs?(bounds, offsets)
|
|
130
|
+
|
|
131
|
+
candidate = association_model(model, bounds, offsets)
|
|
132
|
+
auto_port_elbow(candidate, link, bounds, ray_geometry(offsets, edge), false)
|
|
133
|
+
return {} unless candidate.style[:auto_port_elbow]
|
|
134
|
+
|
|
135
|
+
association_metadata(candidate)
|
|
136
|
+
end
|
|
137
|
+
|
|
70
138
|
def umldi_association_metadata(model, link, bounds, offsets, edge)
|
|
71
139
|
overlap = umldi_overlap_metadata(model, link, bounds, offsets, edge)
|
|
72
140
|
return overlap unless overlap.empty?
|
|
@@ -99,7 +167,9 @@ module Ea
|
|
|
99
167
|
return {} unless umldi_overlap_candidate?(model, bounds, offsets)
|
|
100
168
|
|
|
101
169
|
candidate = association_model(model, bounds, offsets)
|
|
102
|
-
|
|
170
|
+
geom = ray_geometry(offsets, edge)
|
|
171
|
+
association_stored_ray(candidate, link, bounds, geom, false) if association_ray_style?(model.style.to_h)
|
|
172
|
+
overlap_ray_origins(candidate, link, bounds, geom, false)
|
|
103
173
|
return {} unless candidate.style[:overlap_ray]
|
|
104
174
|
|
|
105
175
|
{ source_anchor: candidate.source_anchor, target_anchor: candidate.target_anchor,
|
|
@@ -162,6 +232,13 @@ module Ea
|
|
|
162
232
|
y: box.y + box.height / 2 + offset[1].to_i)
|
|
163
233
|
end
|
|
164
234
|
|
|
235
|
+
# XMI has no QEA database from which to infer a wider element outline,
|
|
236
|
+
# so the saved connector width stands. A border-aware variant changed no
|
|
237
|
+
# connector in the PLATEAU census.
|
|
238
|
+
def association_outline_width(model, _link, _bounds)
|
|
239
|
+
model.style[:lwidth]
|
|
240
|
+
end
|
|
241
|
+
|
|
165
242
|
def stored_ray_boxes(_link, bounds)
|
|
166
243
|
bounds.values_at(:source, :target)
|
|
167
244
|
end
|
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
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Space EA leaves below the lowest element of an unframed export.
|
|
7
|
+
#
|
|
8
|
+
# EMPIRICAL FIT (no stored selector was found; measured on the 187
|
|
9
|
+
# PLATEAU QEA diagrams whose native SVG is available, 172 match):
|
|
10
|
+
# below the lowest element the canvas keeps 21 + its font size in
|
|
11
|
+
# points (the objectstyle fontsz is in tenths of a point, 0 meaning
|
|
12
|
+
# the 7pt default): 31 for fontsz 100, 30 for 90, 28 for the
|
|
13
|
+
# default. A plain Note, and a Package that draws no "(from
|
|
14
|
+
# Parent)" subtitle, keep the bare 10 margin, as does a classifier
|
|
15
|
+
# or instance of the diagram's own package. Misses: 2 outliers.
|
|
16
|
+
class BottomMargin
|
|
17
|
+
BARE = 10
|
|
18
|
+
LINE_ALLOWANCE = 21
|
|
19
|
+
HOME_TEXT_ALLOWANCE = 13
|
|
20
|
+
DEFAULT_POINTS = 7
|
|
21
|
+
|
|
22
|
+
def self.for(diagram, model_index)
|
|
23
|
+
lowest = lowest_elements(diagram)
|
|
24
|
+
return CanvasAnchor::BOTTOM_MARGIN if lowest.empty?
|
|
25
|
+
|
|
26
|
+
lowest.map { |element| of(element, diagram, model_index) }.max
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.lowest_elements(diagram)
|
|
30
|
+
placed = (diagram.elements || []).select { |e| e.bounds || e.image_bounds }
|
|
31
|
+
bottom = placed.map { |e| edge(e) }.max
|
|
32
|
+
placed.select { |e| edge(e) == bottom }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.edge(element)
|
|
36
|
+
bounds = element.bounds || element.image_bounds
|
|
37
|
+
bounds.y + bounds.height
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.of(element, diagram, model_index)
|
|
41
|
+
model = model_index && model_index[element.model_element_ref]
|
|
42
|
+
return home_text_margin(element) if home_text?(model, diagram)
|
|
43
|
+
return BARE if plain_note?(model) || bare_package?(model, diagram) ||
|
|
44
|
+
home_element?(model, diagram)
|
|
45
|
+
|
|
46
|
+
LINE_ALLOWANCE + points(element)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# QEA keeps the raw objectstyle fontsz; XMI keeps only the GDI
|
|
50
|
+
# pixel height it was rendered at (tenths * 96 / 720, floored).
|
|
51
|
+
def self.points(element)
|
|
52
|
+
tenths = element.style && element.style[:fontsz].to_i
|
|
53
|
+
return tenths / 10 if tenths.to_i.positive?
|
|
54
|
+
|
|
55
|
+
pixels = element.font_logical_height.to_i.abs
|
|
56
|
+
pixels.positive? ? (pixels * 0.75).round : DEFAULT_POINTS
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.plain_note?(model)
|
|
60
|
+
model.is_a?(Ea::Model::Note) && model.note_type != "Text"
|
|
61
|
+
end
|
|
62
|
+
|
|
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)) &&
|
|
76
|
+
!diagram.package_id.to_s.empty? &&
|
|
77
|
+
model.package_id == diagram.package_id
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A Text element of the diagram's own package keeps 13 + its
|
|
81
|
+
# font size in points (22 at 9pt) instead of the foreign 21 + size.
|
|
82
|
+
# EMPIRICAL FIT on a single sample (PLATEAU 2E98EA19, fontsz 90,
|
|
83
|
+
# natively 22); the 8 foreign Text cases (28..30) are unaffected.
|
|
84
|
+
def self.home_text?(model, diagram)
|
|
85
|
+
model.is_a?(Ea::Model::Note) && model.note_type == "Text" &&
|
|
86
|
+
!diagram.package_id.to_s.empty? &&
|
|
87
|
+
model.package_id == diagram.package_id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.home_text_margin(element)
|
|
91
|
+
HOME_TEXT_ALLOWANCE + points(element)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.bare_package?(model, diagram)
|
|
95
|
+
return false unless model.is_a?(Ea::Model::Package)
|
|
96
|
+
|
|
97
|
+
parent = model.parent_id
|
|
98
|
+
!(diagram.show_foreign && !parent.to_s.empty? &&
|
|
99
|
+
parent != diagram.package_id)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -33,12 +33,19 @@ module Ea
|
|
|
33
33
|
|
|
34
34
|
attr_reader :diagram, :model_index
|
|
35
35
|
|
|
36
|
+
# Where the leftmost model x lands: 30 + min_x capped at 35. A
|
|
37
|
+
# negative min_x keeps the 30 of min_x 0 (the corpus has none),
|
|
38
|
+
# so content left of the origin is not drawn off the canvas.
|
|
39
|
+
def self.inset_left_for(min_x)
|
|
40
|
+
[[INSET_LEFT - 5 + min_x, INSET_LEFT - 5].max, INSET_LEFT].min
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def initialize(diagram, model_index: nil)
|
|
37
44
|
@diagram = diagram
|
|
38
45
|
@model_index = model_index
|
|
39
46
|
end
|
|
40
47
|
|
|
41
|
-
attr_reader :effective_inset_left
|
|
48
|
+
attr_reader :effective_inset_left, :max_x, :max_y
|
|
42
49
|
|
|
43
50
|
def compute
|
|
44
51
|
points = element_points
|
|
@@ -48,10 +55,12 @@ module Ea
|
|
|
48
55
|
ys = points.map(&:last)
|
|
49
56
|
min_x = xs.min || 0
|
|
50
57
|
min_y = ys.min || 0
|
|
58
|
+
@max_x = xs.max
|
|
59
|
+
@max_y = ys.max
|
|
51
60
|
# Left inset scales from 30 up to the 35 cap with the
|
|
52
61
|
# stored leftmost x: leftmost drawn x = 30 + min_x capped
|
|
53
62
|
# at 35 (corpus: min_x 0 -> 30, 2 -> 32, 4 -> 34, >=5 -> 35).
|
|
54
|
-
@effective_inset_left =
|
|
63
|
+
@effective_inset_left = self.class.inset_left_for(min_x)
|
|
55
64
|
[
|
|
56
65
|
min_x,
|
|
57
66
|
min_y,
|
|
@@ -60,19 +69,7 @@ module Ea
|
|
|
60
69
|
]
|
|
61
70
|
end
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Element x-extent: logical bounds only (matches EA's canvas
|
|
66
|
-
# left/right exactly).
|
|
67
|
-
# Element y-extent: union of logical + image_bounds (image
|
|
68
|
-
# extends below bounds for shadow / image padding), plus the
|
|
69
|
-
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
70
|
-
# below the body and its text descent adds ~4 more, reserving
|
|
71
|
-
# 19px of canvas below subtitle-bearing packages (the +19
|
|
72
|
-
# height family: 05DF5000 Main, Metadata All, package
|
|
73
|
-
# Clients/Suppliers contexts).
|
|
74
|
-
SUBTITLE_CANVAS_EXTRA = 19
|
|
75
|
-
|
|
72
|
+
# Canvas points reserved below "(from Parent)" subtitles.
|
|
76
73
|
def subtitle_points
|
|
77
74
|
pts = []
|
|
78
75
|
(diagram.elements || []).each do |e|
|
|
@@ -86,6 +83,19 @@ module Ea
|
|
|
86
83
|
pts
|
|
87
84
|
end
|
|
88
85
|
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Element x-extent: logical bounds only (matches EA's canvas
|
|
89
|
+
# left/right exactly).
|
|
90
|
+
# Element y-extent: union of logical + image_bounds (image
|
|
91
|
+
# extends below bounds for shadow / image padding), plus the
|
|
92
|
+
# "(from Parent)" subtitle extent: EA draws the subtitle 15px
|
|
93
|
+
# below the body and its text descent adds ~4 more, reserving
|
|
94
|
+
# 19px of canvas below subtitle-bearing packages (the +19
|
|
95
|
+
# height family: 05DF5000 Main, Metadata All, package
|
|
96
|
+
# Clients/Suppliers contexts).
|
|
97
|
+
SUBTITLE_CANVAS_EXTRA = 19
|
|
98
|
+
|
|
89
99
|
def element_points
|
|
90
100
|
pts = []
|
|
91
101
|
(diagram.elements || []).each do |e|
|