ea 0.6.4 → 0.6.5
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/CHANGELOG.md +39 -0
- data/config/themes/119.yml +4 -4
- data/lib/ea/model/diagram_connector.rb +9 -0
- data/lib/ea/model/property.rb +6 -1
- data/lib/ea/sources/qea/classifier_builder.rb +3 -1
- data/lib/ea/sources/qea/diagram_builder.rb +91 -9
- data/lib/ea/sources/qea/property_builder.rb +12 -2
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
- data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
- data/lib/ea/svg/ea_emitter/elements.rb +70 -20
- data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
- data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
- data/lib/ea/svg/ea_emitter/labels.rb +38 -2
- data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
- data/lib/ea/svg/parity/checker.rb +19 -9
- data/lib/ea/svg/parity/corpus_report.rb +83 -0
- data/lib/ea/svg/parity/differ.rb +231 -0
- data/lib/ea/svg/parity.rb +2 -0
- data/lib/ea/theme/definition.rb +3 -3
- data/lib/ea/theme/loader.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d4eb047626f932a223d59f60d7a07ac839542e16f1f08cc1e27f99102592af7
|
|
4
|
+
data.tar.gz: d38407abf49dd80a886269968e944edfa691bc09ca31772556d8d20a89dac294
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50d22ea166bdb5ee80f60a16d2396dc12b90470d1c9f5048279ee9e9d4233b7778af481f04719618416f59d26ada1ddb750bbda03e8fd9f6c4abc1c279ecd9a1
|
|
7
|
+
data.tar.gz: 9ac1166cc762bce236ca889b8a28f26bb32f3688fa00ce925b53f91adf5999976057aefe91e4b684fdf55adbca352af6bd4f5901e9b5251ba0b29c21999becd5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
- `Ea::Model::DiagramConnector` now carries `source_port` / `target_port`
|
|
12
|
+
— the absolute connection points EA docks each connector endpoint
|
|
13
|
+
at, derived from the `t_diagramlinks.Geometry` SX/SY and EX/EY
|
|
14
|
+
offsets by ray-casting from the element center through
|
|
15
|
+
center+offset to the box border (fixes #52; semantics verified
|
|
16
|
+
against EA-published reference SVGs). QEA-built connectors route
|
|
17
|
+
from these ports instead of edge-midpoint guesses, so arrows and
|
|
18
|
+
aggregation diamonds terminate on the element border exactly where
|
|
19
|
+
EA places them.
|
|
20
|
+
- QEA `diagram_builder` maps per-element `font_size` (EA's tenth-point
|
|
21
|
+
`fontsz`, custom values only) and `font_underline` from the
|
|
22
|
+
objectstyle string (fixes #53).
|
|
23
|
+
- `Ea::Svg::Parity::Checker` compares font-family and font-size
|
|
24
|
+
histograms across all emitted text (was: first-text only), so a
|
|
25
|
+
single wrong-size compartment fails the parity check (#54).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- QEA diagram coordinates are now mirrored into EA's screen-down
|
|
29
|
+
space at build time (stored t_diagramobjects rects are bottom-up;
|
|
30
|
+
EA's own published SVGs draw the mirrored view). Connector ports,
|
|
31
|
+
Path waypoints, and bend fallbacks flip consistently, so SVG and
|
|
32
|
+
SPA output matches EA's orientation exactly.
|
|
33
|
+
- Element font sizes stored as EA's classic defaults (fontsz 80/90)
|
|
34
|
+
are treated as use-default markers: rendering falls back to the
|
|
35
|
+
7pt theme size EA actually emits, and only genuinely custom sizes
|
|
36
|
+
override it.
|
|
37
|
+
- Attribute multiplicity bounds stored as EA's "n" no longer parse
|
|
38
|
+
to 1 (which silently dropped the `[1..n]` suffix); unbounded
|
|
39
|
+
uppers render as `[1..n]` like EA.
|
|
40
|
+
- `Ea::Svg::EaEmitter::TextRenderer` no longer raises
|
|
41
|
+
`TypeError: can't convert nil into Float` on degenerate placements
|
|
42
|
+
with missing coordinates or font size — it renders at the origin
|
|
43
|
+
with zero-width estimation instead (fixes #51; 297 of 3,702 PLATEAU
|
|
44
|
+
CG3 diagrams previously crashed `ea svg --all` and shipped without
|
|
45
|
+
SVG in SPA output).
|
|
46
|
+
|
|
47
|
+
## [0.6.4]
|
|
48
|
+
|
|
10
49
|
### Added
|
|
11
50
|
- `Ea::Spa::Projector` embeds model statistics (packages, classes,
|
|
12
51
|
attributes, operations, associations, diagrams) in the skeleton
|
data/config/themes/119.yml
CHANGED
|
@@ -38,7 +38,7 @@ strokes:
|
|
|
38
38
|
# Text geometry — EA uses GDI text metrics for exact widths. We
|
|
39
39
|
# approximate with character_count * size * factor.
|
|
40
40
|
text_metrics:
|
|
41
|
-
width_factor: 0.
|
|
41
|
+
width_factor: 0.612
|
|
42
42
|
|
|
43
43
|
# Canvas frame insets (EA places element (0,0) at SVG (left, top)).
|
|
44
44
|
canvas:
|
|
@@ -60,10 +60,10 @@ frame:
|
|
|
60
60
|
# diagram byte-diff: element at y=40 size=9, stereotype y=58 →
|
|
61
61
|
# offset = 18 = size * 2; name y=73 → line_h = size + 6).
|
|
62
62
|
compartments:
|
|
63
|
-
header_top_padding:
|
|
63
|
+
header_top_padding: 12
|
|
64
64
|
header_line_offset: 6
|
|
65
65
|
divider_offset: 8
|
|
66
|
-
attr_line_offset:
|
|
66
|
+
attr_line_offset: 6
|
|
67
67
|
attr_first_offset: 7
|
|
68
68
|
op_line_offset: 4
|
|
69
69
|
op_line_offset: 4
|
|
@@ -71,7 +71,7 @@ compartments:
|
|
|
71
71
|
# Attribute text x-offsets within element bounds.
|
|
72
72
|
attribute:
|
|
73
73
|
visibility_x_offset: 5
|
|
74
|
-
content_x_offset:
|
|
74
|
+
content_x_offset: 22
|
|
75
75
|
|
|
76
76
|
# Marker geometry constants.
|
|
77
77
|
markers:
|
|
@@ -33,6 +33,13 @@ module Ea
|
|
|
33
33
|
# connectors (no SX/SY) get a plain solid line.
|
|
34
34
|
attribute :has_geometry_offsets, :boolean, default: false
|
|
35
35
|
|
|
36
|
+
# Absolute connection points on the source/target element boxes,
|
|
37
|
+
# derived from the Geometry SX/SY and EX/EY edge offsets. EA
|
|
38
|
+
# docks the connector's visible endpoints at these ports; nil
|
|
39
|
+
# when the geometry string carries no offsets (auto-routed).
|
|
40
|
+
attribute :source_port, Ea::Model::Point
|
|
41
|
+
attribute :target_port, Ea::Model::Point
|
|
42
|
+
|
|
36
43
|
json do
|
|
37
44
|
map "id", to: :id
|
|
38
45
|
map "diagramId", to: :diagram_id
|
|
@@ -43,6 +50,8 @@ module Ea
|
|
|
43
50
|
map "targetDuid", to: :target_duid
|
|
44
51
|
map "sourceEdge", to: :source_edge
|
|
45
52
|
map "targetEdge", to: :target_edge
|
|
53
|
+
map "sourcePort", to: :source_port
|
|
54
|
+
map "targetPort", to: :target_port
|
|
46
55
|
map "connectorType", to: :connector_type
|
|
47
56
|
map "direction", to: :direction
|
|
48
57
|
map "waypoints", to: :waypoints, render_empty: true
|
data/lib/ea/model/property.rb
CHANGED
|
@@ -12,7 +12,11 @@ module Ea
|
|
|
12
12
|
attribute :type_name, :string # name shown when type is external/primitive
|
|
13
13
|
attribute :qualified_name, :string
|
|
14
14
|
attribute :multiplicity_lower, :integer
|
|
15
|
-
attribute :multiplicity_upper, :integer
|
|
15
|
+
attribute :multiplicity_upper, :integer
|
|
16
|
+
# Verbatim stored bound text (e.g. EA's literal "n") when the
|
|
17
|
+
# column value is non-numeric; rendered as-is by the SVG line
|
|
18
|
+
# builder while computed-unbounded uppers render as "*".
|
|
19
|
+
attribute :multiplicity_upper_raw, :string # -1 means "many"
|
|
16
20
|
attribute :default_value, :string
|
|
17
21
|
attribute :is_derived, :boolean, default: false
|
|
18
22
|
attribute :is_readonly, :boolean, default: false
|
|
@@ -33,6 +37,7 @@ module Ea
|
|
|
33
37
|
map "typeName", to: :type_name
|
|
34
38
|
map "qualifiedName", to: :qualified_name
|
|
35
39
|
map "multiplicityLower", to: :multiplicity_lower
|
|
40
|
+
map "multiplicityUpperRaw", to: :multiplicity_upper_raw
|
|
36
41
|
map "multiplicityUpper", to: :multiplicity_upper
|
|
37
42
|
map "defaultValue", to: :default_value
|
|
38
43
|
map "isDerived", to: :is_derived, render_default: true
|
|
@@ -155,8 +155,10 @@ module Ea
|
|
|
155
155
|
|
|
156
156
|
def build_assoc_property(conn, role, owner_object)
|
|
157
157
|
target = database.find_object(conn.end_object_id) if conn.end_object_id&.to_i&.positive?
|
|
158
|
+
guid = IdNormalizer.from_guid(conn.ea_guid)
|
|
158
159
|
Ea::Model::Property.new(
|
|
159
|
-
id:
|
|
160
|
+
id: guid,
|
|
161
|
+
association_id: guid,
|
|
160
162
|
name: role,
|
|
161
163
|
owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
|
|
162
164
|
type_name: target&.name,
|
|
@@ -92,9 +92,11 @@ module Ea
|
|
|
92
92
|
model_element_ref: ref_for_object(obj_row),
|
|
93
93
|
bounds: bounds_from_rect(obj_row),
|
|
94
94
|
style: style_hash,
|
|
95
|
-
font_family: style_hash[:font],
|
|
95
|
+
font_family: presence(style_hash[:font]),
|
|
96
|
+
font_size: font_size_from(style_hash),
|
|
96
97
|
font_bold: truthy?(style_hash[:bold]),
|
|
97
98
|
font_italic: truthy?(style_hash[:italic]),
|
|
99
|
+
font_underline: truthy?(style_hash[:ul]),
|
|
98
100
|
show_tagged_values: truthy?(style_hash[:tag])
|
|
99
101
|
)
|
|
100
102
|
end
|
|
@@ -103,6 +105,28 @@ module Ea
|
|
|
103
105
|
raw == "1" || raw == "-1"
|
|
104
106
|
end
|
|
105
107
|
|
|
108
|
+
def presence(value)
|
|
109
|
+
value.nil? || value.to_s.empty? ? nil : value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# EA stores the element font size in tenths of a point in the
|
|
113
|
+
# objectstyle string. 0 means "use default", and 80/90 (8pt /
|
|
114
|
+
# 9pt) are EA's classic defaults the dialogs write back when
|
|
115
|
+
# the user never customized — verified against EA-published
|
|
116
|
+
# reference images, where such elements render at the theme
|
|
117
|
+
# size. Only genuinely custom sizes are surfaced as explicit
|
|
118
|
+
# element fonts.
|
|
119
|
+
USE_DEFAULT_FONTSZ = [0, 80, 90].freeze
|
|
120
|
+
|
|
121
|
+
def font_size_from(style_hash)
|
|
122
|
+
raw = style_hash[:fontsz]
|
|
123
|
+
return nil if raw.nil? || raw.to_s.empty?
|
|
124
|
+
|
|
125
|
+
return nil if USE_DEFAULT_FONTSZ.include?(raw.to_i)
|
|
126
|
+
|
|
127
|
+
(raw.to_f / 10).round
|
|
128
|
+
end
|
|
129
|
+
|
|
106
130
|
def build_connectors(diagram_row)
|
|
107
131
|
links = database.diagram_links_for(diagram_row.diagram_id) || []
|
|
108
132
|
explicit = links.map { |link_row| build_connector(link_row, diagram_row) }
|
|
@@ -216,6 +240,8 @@ module Ea
|
|
|
216
240
|
connector_type: connector&.connector_type,
|
|
217
241
|
direction: connector&.direction,
|
|
218
242
|
waypoints: waypoints_for_link(link_row, diagram_row),
|
|
243
|
+
source_port: connection_port(diagram_row, connector, :source, geom),
|
|
244
|
+
target_port: connection_port(diagram_row, connector, :target, geom),
|
|
219
245
|
label_boxes: geom[:label_boxes] || {},
|
|
220
246
|
style: DiagramStyleParser.parse(link_row.style),
|
|
221
247
|
hidden: hidden?(link_row),
|
|
@@ -340,12 +366,22 @@ module Ea
|
|
|
340
366
|
geom = parse_geometry_fields(link_row.geometry)
|
|
341
367
|
edge_out = geom[:edge] || 0
|
|
342
368
|
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
source_port = connection_port(diagram_row, connector, :source, geom)
|
|
370
|
+
target_port = connection_port(diagram_row, connector, :target, geom)
|
|
371
|
+
# Waypoint points flow through this method as [x, y] arrays;
|
|
372
|
+
# keep the ports in the same shape.
|
|
373
|
+
source_point = source_port ? [source_port.x, source_port.y] :
|
|
374
|
+
element_edge_point(source_placement, :source, edge_out)
|
|
375
|
+
target_point = target_port ? [target_port.x, target_port.y] :
|
|
376
|
+
element_edge_point(target_placement, :target, edge_out)
|
|
345
377
|
|
|
346
378
|
intermediate = intermediate_waypoints(link_row)
|
|
347
379
|
if intermediate.any?
|
|
348
380
|
points = [source_point, *intermediate, target_point]
|
|
381
|
+
elsif source_port || target_port
|
|
382
|
+
# Exact ports are known — the SX/SY bend heuristics in
|
|
383
|
+
# sx_sy_ex_ey_waypoints would double-apply the offsets.
|
|
384
|
+
points = [source_point, target_point]
|
|
349
385
|
else
|
|
350
386
|
points = sx_sy_ex_ey_waypoints(source_point, target_point, geom)
|
|
351
387
|
end
|
|
@@ -355,13 +391,53 @@ module Ea
|
|
|
355
391
|
end
|
|
356
392
|
end
|
|
357
393
|
|
|
394
|
+
# Absolute connection point EA docks the connector's visible
|
|
395
|
+
# endpoint at, derived from the Geometry SX/SY (source) and
|
|
396
|
+
# EX/EY (target) offsets. The offsets are measured inward from
|
|
397
|
+
# the placed object's box edges — the X offset counts from the
|
|
398
|
+
# edge FACING the other end (source: right edge for positive
|
|
399
|
+
# values, left edge for negative; target mirrored), while the
|
|
400
|
+
# Y offset counts from the top edge for positive values and
|
|
401
|
+
# from the bottom edge for negative. Derived and verified
|
|
402
|
+
# against EA's own rendering on the PLATEAU models.
|
|
403
|
+
def connection_port(diagram_row, connector, end_kind, geom)
|
|
404
|
+
return nil unless connector
|
|
405
|
+
|
|
406
|
+
ea_object_id = end_kind == :source ? connector.start_object_id : connector.end_object_id
|
|
407
|
+
placement = ea_object_id &&
|
|
408
|
+
diagram_object_placement(diagram_row.diagram_id, ea_object_id)
|
|
409
|
+
return nil unless placement
|
|
410
|
+
|
|
411
|
+
x_off = geom[end_kind == :source ? :sx : :ex]
|
|
412
|
+
y_off = geom[end_kind == :source ? :sy : :ey]
|
|
413
|
+
return nil if x_off.nil? || y_off.nil?
|
|
414
|
+
|
|
415
|
+
# EA's SX/SY and EX/EY are the offset of the docking line's
|
|
416
|
+
# direction from the element's CENTER (stored math coords,
|
|
417
|
+
# y up): the port is where the ray from the center through
|
|
418
|
+
# center+offset crosses the box border. Verified against
|
|
419
|
+
# EA-published reference SVGs. Bounds are already mirrored
|
|
420
|
+
# to screen-down space, so the offset's Y negates too.
|
|
421
|
+
b = bounds_from_rect(placement)
|
|
422
|
+
center_x = b.x + b.width / 2.0
|
|
423
|
+
center_y = b.y + b.height / 2.0
|
|
424
|
+
dx = x_off.to_f
|
|
425
|
+
dy = -y_off.to_f
|
|
426
|
+
return nil if dx.zero? && dy.zero?
|
|
427
|
+
|
|
428
|
+
tx = dx.zero? ? Float::INFINITY : (dx > 0 ? b.x + b.width - center_x : b.x - center_x) / dx
|
|
429
|
+
ty = dy.zero? ? Float::INFINITY : (dy > 0 ? b.y + b.height - center_y : b.y - center_y) / dy
|
|
430
|
+
t = [tx, ty].min
|
|
431
|
+
Ea::Model::Point.new(x: (center_x + t * dx).round, y: (center_y + t * dy).round)
|
|
432
|
+
end
|
|
433
|
+
|
|
358
434
|
# Parse t_diagramlinks.Path into [[x, y], ...] intermediate
|
|
359
435
|
# waypoints. Format: `x1:y1;x2:y2;...`. Empty string returns [].
|
|
360
436
|
def intermediate_waypoints(link_row)
|
|
361
437
|
raw = link_row.path.to_s
|
|
362
438
|
return [] if raw.empty?
|
|
363
439
|
|
|
364
|
-
raw.scan(/(-?\d+):(-?\d+)/).map { |x, y| [x.to_i, y.to_i] }
|
|
440
|
+
raw.scan(/(-?\d+):(-?\d+)/).map { |x, y| [x.to_i, -y.to_i] }
|
|
365
441
|
end
|
|
366
442
|
|
|
367
443
|
# Fallback when no explicit Path: compute source/target bend
|
|
@@ -369,10 +445,10 @@ module Ea
|
|
|
369
445
|
def sx_sy_ex_ey_waypoints(source_point, target_point, geom)
|
|
370
446
|
points = [source_point]
|
|
371
447
|
if geom[:sx] && geom[:sy] && (geom[:sx].nonzero? || geom[:sy].nonzero?)
|
|
372
|
-
points << [source_point[0] + geom[:sx], source_point[1]
|
|
448
|
+
points << [source_point[0] + geom[:sx], source_point[1] - geom[:sy]]
|
|
373
449
|
end
|
|
374
450
|
if geom[:ex] && geom[:ey] && (geom[:ex].nonzero? || geom[:ey].nonzero?)
|
|
375
|
-
points << [target_point[0] - geom[:ex], target_point[1]
|
|
451
|
+
points << [target_point[0] - geom[:ex], target_point[1] + geom[:ey]]
|
|
376
452
|
end
|
|
377
453
|
points << target_point
|
|
378
454
|
points
|
|
@@ -427,6 +503,10 @@ module Ea
|
|
|
427
503
|
end
|
|
428
504
|
public :parse_label_boxes
|
|
429
505
|
|
|
506
|
+
# Pure helpers exposed for specs and consumers — no database
|
|
507
|
+
# access, safe to call in isolation.
|
|
508
|
+
public :font_size_from, :connection_port
|
|
509
|
+
|
|
430
510
|
# Parse one label box's colon-separated key=value body into
|
|
431
511
|
# a Hash with string keys. Returns nil if OX/OY are absent.
|
|
432
512
|
def parse_label_style(body)
|
|
@@ -509,8 +589,10 @@ module Ea
|
|
|
509
589
|
# EA's t_diagramobjects stores rect with the origin at the
|
|
510
590
|
# bottom-left (math convention) — recttop > rectbottom for
|
|
511
591
|
# valid bounds, and y values are often negative. Convert to
|
|
512
|
-
# screen-space (origin top-left
|
|
513
|
-
#
|
|
592
|
+
# screen-space (origin top-left, y increasing downward) by
|
|
593
|
+
# mirroring: EA's own SVG export draws the model mirrored, so
|
|
594
|
+
# the flipped top edge is -(max(top, bottom)); verified
|
|
595
|
+
# against EA-published reference images (dx=0, dy=-mirror).
|
|
514
596
|
def bounds_from_rect(obj_row)
|
|
515
597
|
left = obj_row.rectleft || 0
|
|
516
598
|
right = obj_row.rectright || 0
|
|
@@ -518,7 +600,7 @@ module Ea
|
|
|
518
600
|
bottom = obj_row.rectbottom || 0
|
|
519
601
|
Ea::Model::Bounds.new(
|
|
520
602
|
x: [left, right].min,
|
|
521
|
-
y: [top, bottom].
|
|
603
|
+
y: -[top, bottom].max,
|
|
522
604
|
width: (right - left).abs,
|
|
523
605
|
height: (bottom - top).abs
|
|
524
606
|
)
|
|
@@ -27,6 +27,7 @@ module Ea
|
|
|
27
27
|
type_name: row.type,
|
|
28
28
|
qualified_name: "#{owner_object.name}::#{row.name}",
|
|
29
29
|
multiplicity_lower: parse_lower(row),
|
|
30
|
+
multiplicity_upper_raw: raw_upper(row),
|
|
30
31
|
multiplicity_upper: parse_upper(row),
|
|
31
32
|
default_value: row.default,
|
|
32
33
|
is_ordered: boolean(row.isordered),
|
|
@@ -42,6 +43,14 @@ module Ea
|
|
|
42
43
|
|
|
43
44
|
private
|
|
44
45
|
|
|
46
|
+
# EA stores some bounds as literal text ("n"); those render
|
|
47
|
+
# verbatim in attribute lines while computed-unbounded bounds
|
|
48
|
+
# render as "*" (verified against EA-published SVGs).
|
|
49
|
+
def raw_upper(row)
|
|
50
|
+
raw = row.upperbound.to_s
|
|
51
|
+
raw.match?(/\A[nN]\z/) ? raw.downcase : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
45
54
|
def parse_lower(row)
|
|
46
55
|
Integer(row.lowerbound || 1)
|
|
47
56
|
rescue StandardError
|
|
@@ -49,10 +58,11 @@ module Ea
|
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
def parse_upper(row)
|
|
52
|
-
|
|
61
|
+
raw = row.upperbound.to_s
|
|
62
|
+
return -1 if raw.match?(/\A[*nN]\z/)
|
|
53
63
|
|
|
54
64
|
begin
|
|
55
|
-
Integer(
|
|
65
|
+
Integer(raw.empty? ? 1 : raw)
|
|
56
66
|
rescue StandardError
|
|
57
67
|
1
|
|
58
68
|
end
|
|
@@ -10,45 +10,29 @@ module Ea
|
|
|
10
10
|
module EnumLiterals
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
+
# EA: literal rows sit directly under the header divider,
|
|
14
|
+
# 13px apart at 7pt (attr rows use 14), first row at
|
|
15
|
+
# divider + 14. No compartment divider, no header label.
|
|
16
|
+
LITERAL_LINE_OFFSET = 6
|
|
17
|
+
|
|
13
18
|
def render(context)
|
|
14
19
|
return nil unless context.enum_literals.any?
|
|
15
20
|
return nil unless context.geometry.enum_literal_first_y
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
Element::DividerRenderer.render(
|
|
19
|
-
context.bounds,
|
|
20
|
-
y: context.geometry.enum_divider_y,
|
|
21
|
-
stroke: context.stroke,
|
|
22
|
-
stroke_width: context.stroke_width
|
|
23
|
-
),
|
|
24
|
-
render_literals_block(context)
|
|
25
|
-
].join("\n")
|
|
22
|
+
render_literals_block(context)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def render_literals_block(context)
|
|
29
|
-
line_h = context.size +
|
|
30
|
-
text_blocks = [
|
|
26
|
+
line_h = context.size + LITERAL_LINE_OFFSET
|
|
27
|
+
text_blocks = []
|
|
31
28
|
context.enum_literals.each_with_index do |literal, idx|
|
|
32
|
-
y = context.geometry.enum_literal_first_y + (
|
|
29
|
+
y = context.geometry.enum_literal_first_y + (idx * line_h)
|
|
33
30
|
text_blocks << visibility_placeholder(context, y)
|
|
34
31
|
text_blocks << literal_name_text(context, literal, y)
|
|
35
32
|
end
|
|
36
33
|
wrap_group(text_blocks, context.theme.text_color)
|
|
37
34
|
end
|
|
38
35
|
|
|
39
|
-
def literal_header_text(context)
|
|
40
|
-
TextRenderer.new(
|
|
41
|
-
content: "literals",
|
|
42
|
-
x: context.bounds.x + (context.bounds.width / 2.0) -
|
|
43
|
-
(8 * context.size * context.theme.text_width_factor) / 2,
|
|
44
|
-
y: context.geometry.enum_literal_first_y,
|
|
45
|
-
family: context.family, size: context.size, size_unit: context.size_unit,
|
|
46
|
-
style: "italic", fill: context.theme.text_color,
|
|
47
|
-
stroke_in_text: context.theme.stroke_in_text_color,
|
|
48
|
-
width_factor: context.theme.text_width_factor
|
|
49
|
-
).to_svg
|
|
50
|
-
end
|
|
51
|
-
|
|
52
36
|
def visibility_placeholder(context, y)
|
|
53
37
|
TextRenderer.new(
|
|
54
38
|
content: " ",
|
|
@@ -103,6 +103,11 @@ module Ea
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def bound_text(value)
|
|
106
|
+
# EA renders a stored literal bound ("n") verbatim, and a
|
|
107
|
+
# computed-unbounded upper as "*" (verified against
|
|
108
|
+
# EA-published reference SVGs).
|
|
109
|
+
return property.multiplicity_upper_raw if value == -1 && property.multiplicity_upper_raw
|
|
110
|
+
|
|
106
111
|
value == -1 ? "*" : value.to_s
|
|
107
112
|
end
|
|
108
113
|
end
|
|
@@ -9,15 +9,19 @@ module Ea
|
|
|
9
9
|
# content) matching EA's encoding.
|
|
10
10
|
class AttributeRenderer
|
|
11
11
|
DEFAULT_VISIBILITY_X_OFFSET = 5
|
|
12
|
-
DEFAULT_CONTENT_X_OFFSET =
|
|
12
|
+
DEFAULT_CONTENT_X_OFFSET = 22
|
|
13
13
|
DEFAULT_FONT_UNIT = "pt"
|
|
14
|
+
# EA seats compartment stereotype headers («Property») 12px
|
|
15
|
+
# from the box edge and spaces rows 13px at 7pt.
|
|
16
|
+
STEREOTYPE_HEADER_X_OFFSET = 12
|
|
17
|
+
ROW_LINE_OFFSET = 6
|
|
14
18
|
|
|
15
19
|
def self.render(lines, bounds:, first_y:, family:,
|
|
16
20
|
size:, size_unit: DEFAULT_FONT_UNIT,
|
|
17
21
|
fill: "#000000",
|
|
18
22
|
visibility_x_offset: DEFAULT_VISIBILITY_X_OFFSET,
|
|
19
23
|
content_x_offset: DEFAULT_CONTENT_X_OFFSET)
|
|
20
|
-
line_h = size +
|
|
24
|
+
line_h = size + ROW_LINE_OFFSET
|
|
21
25
|
text_blocks = []
|
|
22
26
|
lines.each_with_index do |line, idx|
|
|
23
27
|
y = first_y + (idx * line_h)
|
|
@@ -25,6 +29,8 @@ module Ea
|
|
|
25
29
|
if visibility
|
|
26
30
|
text_blocks << build_text(bounds.x + visibility_x_offset, y, visibility, family, size, size_unit, fill)
|
|
27
31
|
text_blocks << build_text(bounds.x + content_x_offset, y, rest, family, size, size_unit, fill)
|
|
32
|
+
elsif line.strip.start_with?("«")
|
|
33
|
+
text_blocks << build_text(bounds.x + STEREOTYPE_HEADER_X_OFFSET, y, line.strip, family, size, size_unit, fill)
|
|
28
34
|
else
|
|
29
35
|
text_blocks << build_text(bounds.x + visibility_x_offset, y, line.strip, family, size, size_unit, fill)
|
|
30
36
|
end
|
|
@@ -44,19 +50,44 @@ module Ea
|
|
|
44
50
|
#
|
|
45
51
|
# Pass a `lookup` (any callable returning a Classifier for
|
|
46
52
|
# an id) to enable inherited-property namespace prefixing.
|
|
47
|
-
def self.lines_for(classifier, lookup: nil
|
|
48
|
-
|
|
53
|
+
def self.lines_for(classifier, lookup: nil,
|
|
54
|
+
exclude_association_ids: nil)
|
|
55
|
+
props = displayable_properties(classifier, exclude_association_ids)
|
|
49
56
|
return [] unless props
|
|
50
57
|
|
|
51
|
-
props.
|
|
52
|
-
|
|
53
|
-
stereotype
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
stereotypes = props.map { |p| property_stereotype(p) }.compact.uniq
|
|
59
|
+
if stereotypes.size == 1 && props.all? { |p| property_stereotype(p) }
|
|
60
|
+
# EA renders ONE compartment stereotype header for a
|
|
61
|
+
# uniformly-stereotyped attribute set (e.g. «Property»
|
|
62
|
+
# above GML property rows) and lists rows
|
|
63
|
+
# alphabetically - verified against EA-published SVGs.
|
|
64
|
+
header = "«#{canonical_stereotype(stereotypes.first)}»"
|
|
65
|
+
[header] + props.sort_by { |p| p.name.to_s }.map do |prop|
|
|
66
|
+
AttributeLineBuilder.new(prop, host: classifier,
|
|
67
|
+
lookup: lookup).to_s
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
props.flat_map do |prop|
|
|
71
|
+
lines = []
|
|
72
|
+
stereotype = property_stereotype(prop)
|
|
73
|
+
lines << "«#{stereotype}»" if stereotype
|
|
74
|
+
lines << AttributeLineBuilder.new(prop, host: classifier,
|
|
56
75
|
lookup: lookup).to_s
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
end
|
|
59
79
|
|
|
80
|
+
STEREOTYPE_DISPLAY = {
|
|
81
|
+
"property" => "Property",
|
|
82
|
+
"featuretype" => "FeatureType",
|
|
83
|
+
"objecttype" => "ObjectType",
|
|
84
|
+
"codelist" => "CodeList"
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
def self.canonical_stereotype(ref)
|
|
88
|
+
STEREOTYPE_DISPLAY.fetch(ref.to_s.downcase, ref.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
60
91
|
# Internal helpers
|
|
61
92
|
|
|
62
93
|
def self.split_visibility(line)
|
|
@@ -75,10 +106,19 @@ module Ea
|
|
|
75
106
|
end
|
|
76
107
|
private_class_method :build_text
|
|
77
108
|
|
|
78
|
-
|
|
109
|
+
# Association-end properties render in the box only when
|
|
110
|
+
# their association is NOT drawn on the diagram (drawn ones
|
|
111
|
+
# render as connector role labels). exclude_association_ids
|
|
112
|
+
# carries the diagram's drawn association ids; nil preserves
|
|
113
|
+
# the legacy hide-all behavior.
|
|
114
|
+
def self.displayable_properties(classifier, exclude_association_ids)
|
|
79
115
|
return nil unless classifier.properties
|
|
80
116
|
|
|
81
|
-
classifier.properties.reject
|
|
117
|
+
classifier.properties.reject do |prop|
|
|
118
|
+
prop.association_id &&
|
|
119
|
+
(exclude_association_ids.nil? ||
|
|
120
|
+
exclude_association_ids.include?(prop.association_id))
|
|
121
|
+
end
|
|
82
122
|
end
|
|
83
123
|
private_class_method :displayable_properties
|
|
84
124
|
|
|
@@ -27,10 +27,10 @@ module Ea
|
|
|
27
27
|
def initialize(bounds:, size:, header_lines_count:,
|
|
28
28
|
attr_lines_count:, op_lines_count:,
|
|
29
29
|
tagged_values_count:,
|
|
30
|
-
header_top_padding:
|
|
30
|
+
header_top_padding: 12,
|
|
31
31
|
header_line_offset: 6,
|
|
32
32
|
divider_offset: 8,
|
|
33
|
-
attr_line_offset:
|
|
33
|
+
attr_line_offset: 6,
|
|
34
34
|
attr_first_offset: 7)
|
|
35
35
|
@bounds = bounds
|
|
36
36
|
@size = size
|
|
@@ -46,7 +46,7 @@ module Ea
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def header_first_y
|
|
49
|
-
bounds.y + size + (header_top_padding ||
|
|
49
|
+
bounds.y + size + (header_top_padding || 12)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Y of the divider line between header and the rest.
|
|
@@ -61,7 +61,7 @@ module Ea
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def attr_first_y
|
|
64
|
-
return bounds.y + size + (header_top_padding ||
|
|
64
|
+
return bounds.y + size + (header_top_padding || 12) + 12 unless divider_y
|
|
65
65
|
|
|
66
66
|
divider_y + size + (attr_first_offset || 7)
|
|
67
67
|
end
|
|
@@ -69,7 +69,7 @@ module Ea
|
|
|
69
69
|
def attr_bottom_y
|
|
70
70
|
return attr_first_y unless attr_lines_count&.positive?
|
|
71
71
|
|
|
72
|
-
attr_first_y + (attr_lines_count - 1) * (size + (attr_line_offset ||
|
|
72
|
+
attr_first_y + (attr_lines_count - 1) * (size + (attr_line_offset || 6))
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def op_divider_y
|
|
@@ -88,12 +88,14 @@ module Ea
|
|
|
88
88
|
op_first_y + (op_lines_count - 1) * (size + 4)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
# EA draws a single divider under the header and seats
|
|
92
|
+
# enumeration literals directly after it (+14 at 7pt),
|
|
93
|
+
# exactly like attribute rows - no second divider and no
|
|
94
|
+
# "literals" header. Verified against EA-published SVGs.
|
|
95
95
|
def enum_literal_first_y
|
|
96
|
-
|
|
96
|
+
return nil unless divider_y
|
|
97
|
+
|
|
98
|
+
divider_y + size + (attr_first_offset || 7)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
# Tagged values appear after attributes (or after ops if
|
|
@@ -51,22 +51,13 @@ module Ea
|
|
|
51
51
|
(bounds_width - QUALIFIED_WRAP_PADDING)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# EA renders the plain classifier name in element headers
|
|
55
|
+
# (element ownership qualification is a display option the
|
|
56
|
+
# corpus diagrams leave off - verified against EA-published
|
|
57
|
+
# reference SVGs).
|
|
58
|
+
def self.display_name(classifier, _diagram_package_id = nil,
|
|
55
59
|
_visually_nested = false)
|
|
56
|
-
|
|
57
|
-
(classifier.qualified_name || classifier.name).to_s
|
|
58
|
-
else
|
|
59
|
-
classifier.name.to_s
|
|
60
|
-
end
|
|
61
|
-
return name if diagram_package_id.nil?
|
|
62
|
-
return name unless name.include?("::")
|
|
63
|
-
|
|
64
|
-
if classifier.is_a?(Ea::Model::Classifier) &&
|
|
65
|
-
classifier.package_id == diagram_package_id
|
|
66
|
-
name.split("::").last
|
|
67
|
-
else
|
|
68
|
-
name
|
|
69
|
-
end
|
|
60
|
+
classifier.name.to_s
|
|
70
61
|
end
|
|
71
62
|
end
|
|
72
63
|
end
|