ea 0.3.1 → 0.4.0
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/TODO.diagrams/01-xmi-extension-elements-parsing.md +67 -0
- data/TODO.diagrams/02-diagram-element-rich-model.md +61 -0
- data/TODO.diagrams/03-ea-style-svg-emitter.md +70 -0
- data/TODO.diagrams/04-connector-routing-from-soid-eoid.md +55 -0
- data/TODO.diagrams/05-marker-rendering-per-relationship-type.md +49 -0
- data/TODO.diagrams/06-connector-labels.md +49 -0
- data/TODO.diagrams/07-canvas-size-and-viewbox.md +39 -0
- data/TODO.diagrams/08-visual-regression-harness.md +34 -0
- data/TODO.diagrams/09-cli-render-all-diagrams.md +25 -0
- data/TODO.diagrams/10-stereotype-color-config.md +29 -0
- data/TODO.diagrams/11-elkrb-auto-layout-integration.md +53 -0
- data/config/stereotype_colors.yml +15 -0
- data/lib/ea/cli/app.rb +7 -1
- data/lib/ea/cli/command/svg.rb +50 -6
- data/lib/ea/model/diagram_connector.rb +19 -1
- data/lib/ea/model/diagram_element.rb +28 -0
- data/lib/ea/sources/xmi/annotation_builder.rb +1 -1
- data/lib/ea/sources/xmi/diagram_builder.rb +164 -44
- data/lib/ea/sources/xmi/extension_elements.rb +74 -0
- data/lib/ea/sources/xmi/extension_geometry_parser.rb +107 -0
- data/lib/ea/sources/xmi/extension_style_parser.rb +78 -0
- data/lib/ea/sources/xmi.rb +3 -0
- data/lib/ea/svg/ea_emitter/background.rb +16 -0
- data/lib/ea/svg/ea_emitter/canvas.rb +65 -0
- data/lib/ea/svg/ea_emitter/connectors.rb +50 -0
- data/lib/ea/svg/ea_emitter/document.rb +44 -0
- data/lib/ea/svg/ea_emitter/elements.rb +169 -0
- data/lib/ea/svg/ea_emitter/labels.rb +64 -0
- data/lib/ea/svg/ea_emitter/markers.rb +117 -0
- data/lib/ea/svg/ea_emitter.rb +29 -0
- data/lib/ea/svg/stereotype_color_resolver.rb +50 -0
- data/lib/ea/svg.rb +2 -0
- data/lib/ea/version.rb +1 -1
- metadata +26 -2
|
@@ -5,15 +5,25 @@ module Ea
|
|
|
5
5
|
# A connector drawn on a diagram between two DiagramElements.
|
|
6
6
|
# References the underlying Relationship (by id) and the source
|
|
7
7
|
# and target DiagramElements (by id). Carries ordered waypoints
|
|
8
|
-
# for routing
|
|
8
|
+
# for routing plus the EA EDGE codes (1=top, 2=right, 3=bottom,
|
|
9
|
+
# 4=left, 5..8=diagonal variants) which tell the renderer where
|
|
10
|
+
# to anchor the line on each end.
|
|
9
11
|
class DiagramConnector < Base
|
|
10
12
|
attribute :diagram_id, :string
|
|
11
13
|
attribute :relationship_ref, :string
|
|
12
14
|
attribute :source_element_ref, :string
|
|
13
15
|
attribute :target_element_ref, :string
|
|
16
|
+
attribute :source_duid, :string
|
|
17
|
+
attribute :target_duid, :string
|
|
18
|
+
attribute :source_edge, :integer
|
|
19
|
+
attribute :target_edge, :integer
|
|
14
20
|
attribute :waypoints, Waypoint, collection: true, initialize_empty: true
|
|
15
21
|
attribute :label, :string
|
|
16
22
|
attribute :style, :hash, default: -> { {} }
|
|
23
|
+
attribute :line_color, :integer
|
|
24
|
+
attribute :line_width, :integer
|
|
25
|
+
attribute :hidden, :boolean
|
|
26
|
+
attribute :label_boxes, :hash, default: -> { {} }
|
|
17
27
|
|
|
18
28
|
json do
|
|
19
29
|
map "id", to: :id
|
|
@@ -21,9 +31,17 @@ module Ea
|
|
|
21
31
|
map "relationshipRef", to: :relationship_ref
|
|
22
32
|
map "sourceElementRef", to: :source_element_ref
|
|
23
33
|
map "targetElementRef", to: :target_element_ref
|
|
34
|
+
map "sourceDuid", to: :source_duid
|
|
35
|
+
map "targetDuid", to: :target_duid
|
|
36
|
+
map "sourceEdge", to: :source_edge
|
|
37
|
+
map "targetEdge", to: :target_edge
|
|
24
38
|
map "waypoints", to: :waypoints, render_empty: true
|
|
25
39
|
map "label", to: :label
|
|
26
40
|
map "style", to: :style
|
|
41
|
+
map "lineColor", to: :line_color
|
|
42
|
+
map "lineWidth", to: :line_width
|
|
43
|
+
map "hidden", to: :hidden, render_default: true
|
|
44
|
+
map "labelBoxes", to: :label_boxes
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
end
|
|
@@ -5,18 +5,46 @@ module Ea
|
|
|
5
5
|
# A single placed element on a diagram. References the model
|
|
6
6
|
# element it visualizes (by id), carries pixel bounds and a
|
|
7
7
|
# parsed style map (fill color, line color, font, etc.).
|
|
8
|
+
#
|
|
9
|
+
# Carries two distinct bounds:
|
|
10
|
+
# - `bounds` — logical rect (EA's Left/Top/Right/Bottom)
|
|
11
|
+
# - `image_bounds` — visual rect including padding (EA's
|
|
12
|
+
# imgL/imgT/imgR/imgB); used by EA's SVG renderer for the
|
|
13
|
+
# actual `<rect>` width/height
|
|
8
14
|
class DiagramElement < Base
|
|
9
15
|
attribute :diagram_id, :string
|
|
10
16
|
attribute :model_element_ref, :string # id of Classifier/Package/etc.
|
|
11
17
|
attribute :bounds, Bounds
|
|
18
|
+
attribute :image_bounds, Bounds
|
|
12
19
|
attribute :style, :hash, default: -> { {} } # parsed style fields
|
|
20
|
+
attribute :background_color, :integer # EA BGR integer
|
|
21
|
+
attribute :line_color, :integer
|
|
22
|
+
attribute :line_width, :integer
|
|
23
|
+
attribute :font_family, :string
|
|
24
|
+
attribute :font_size, :integer
|
|
25
|
+
attribute :font_bold, :boolean
|
|
26
|
+
attribute :font_italic, :boolean
|
|
27
|
+
attribute :font_underline, :boolean
|
|
28
|
+
attribute :z_order, :integer # seqno
|
|
29
|
+
attribute :duid, :string # EA's per-placement DUID for connector resolution
|
|
13
30
|
|
|
14
31
|
json do
|
|
15
32
|
map "id", to: :id
|
|
16
33
|
map "diagramId", to: :diagram_id
|
|
17
34
|
map "modelElementRef", to: :model_element_ref
|
|
18
35
|
map "bounds", to: :bounds
|
|
36
|
+
map "imageBounds", to: :image_bounds
|
|
19
37
|
map "style", to: :style
|
|
38
|
+
map "backgroundColor", to: :background_color
|
|
39
|
+
map "lineColor", to: :line_color
|
|
40
|
+
map "lineWidth", to: :line_width
|
|
41
|
+
map "fontFamily", to: :font_family
|
|
42
|
+
map "fontSize", to: :font_size
|
|
43
|
+
map "fontBold", to: :font_bold, render_default: true
|
|
44
|
+
map "fontItalic", to: :font_italic, render_default: true
|
|
45
|
+
map "fontUnderline", to: :font_underline, render_default: true
|
|
46
|
+
map "zOrder", to: :z_order
|
|
47
|
+
map "duid", to: :duid
|
|
20
48
|
end
|
|
21
49
|
end
|
|
22
50
|
end
|
|
@@ -23,7 +23,7 @@ module Ea
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def self.from_comments(comments, owner_id, kind: "documentation")
|
|
26
|
-
Array(comments).map { |c| new(owner_id).build(kind, c.
|
|
26
|
+
Array(comments).map { |c| new(owner_id).build(kind, c.body_element || c.body_attribute) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def self.comments_on(element)
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module Ea
|
|
4
4
|
module Sources
|
|
5
5
|
module Xmi
|
|
6
|
-
# Translates XMI
|
|
7
|
-
# instances.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Translates EA diagrams from the XMI source into Ea::Model
|
|
7
|
+
# instances. Uses the rich placement data inside the
|
|
8
|
+
# <xmi:Extension> block (Left/Top/Right/Bottom, BCol, font,
|
|
9
|
+
# SOID/EOID, EDGE, label boxes) when present. Falls back to
|
|
10
|
+
# the UML-DI owned_element bounds otherwise.
|
|
10
11
|
class DiagramBuilder
|
|
11
12
|
attr_reader :root
|
|
12
13
|
|
|
@@ -15,67 +16,186 @@ module Ea
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def build_all
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
ext_diagrams = extension_diagrams
|
|
20
|
+
return [] if ext_diagrams.empty?
|
|
21
|
+
|
|
22
|
+
ext_diagrams.map { |d| build_one(d) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The XMI stores diagrams in two places:
|
|
26
|
+
# - root.model.diagram — UML-DI shape (often just one or none)
|
|
27
|
+
# - root.extension.diagrams.diagram — the rich EA extension
|
|
28
|
+
# block with placement/style/connector data (the source of
|
|
29
|
+
# truth for rendering).
|
|
30
|
+
# We use the extension block when present; fall back to UML-DI
|
|
31
|
+
# otherwise.
|
|
32
|
+
def extension_diagrams
|
|
33
|
+
ext = root.extension
|
|
34
|
+
return [] unless ext&.diagrams
|
|
35
|
+
|
|
36
|
+
ext.diagrams.diagram.to_a
|
|
20
37
|
end
|
|
21
38
|
|
|
22
|
-
def build_one(
|
|
23
|
-
id = IdNormalizer.from_xmi_id(
|
|
24
|
-
|
|
39
|
+
def build_one(ext_diagram)
|
|
40
|
+
id = IdNormalizer.from_xmi_id(ext_diagram.id)
|
|
41
|
+
ext_elements = ExtensionElements.new(ext_diagram)
|
|
42
|
+
placed_elements = ext_elements.placed_elements
|
|
43
|
+
placed_connectors = ext_elements.placed_connectors
|
|
44
|
+
props = ext_diagram.properties
|
|
45
|
+
|
|
25
46
|
Ea::Model::Diagram.new(
|
|
26
47
|
id: id,
|
|
27
|
-
name:
|
|
28
|
-
package_id: ext_diagram
|
|
29
|
-
diagram_type:
|
|
30
|
-
bounds:
|
|
31
|
-
elements: build_elements(
|
|
32
|
-
connectors:
|
|
33
|
-
|
|
48
|
+
name: props&.name || ext_diagram.id,
|
|
49
|
+
package_id: ext_diagram.model&.package,
|
|
50
|
+
diagram_type: props&.type&.split(":")&.last&.downcase,
|
|
51
|
+
bounds: canvas_bounds(placed_elements),
|
|
52
|
+
elements: build_elements(placed_elements, id),
|
|
53
|
+
connectors: build_connectors(placed_connectors,
|
|
54
|
+
placed_elements, id),
|
|
55
|
+
annotations: []
|
|
34
56
|
)
|
|
35
57
|
end
|
|
36
58
|
|
|
37
59
|
private
|
|
38
60
|
|
|
39
|
-
def
|
|
40
|
-
return nil
|
|
61
|
+
def canvas_bounds(placed_elements)
|
|
62
|
+
return nil if placed_elements.empty?
|
|
63
|
+
|
|
64
|
+
left = placed_elements.map { |e| e.geometry.img_left || e.geometry.left }.compact.min
|
|
65
|
+
top = placed_elements.map { |e| e.geometry.img_top || e.geometry.top }.compact.min
|
|
66
|
+
right = placed_elements.map { |e| e.geometry.img_right || e.geometry.right }.compact.max
|
|
67
|
+
bottom = placed_elements.map { |e| e.geometry.img_bottom || e.geometry.bottom }.compact.max
|
|
68
|
+
return nil unless left && top && right && bottom
|
|
41
69
|
|
|
42
|
-
|
|
70
|
+
Ea::Model::Bounds.new(x: 0, y: 0,
|
|
71
|
+
width: right - left,
|
|
72
|
+
height: bottom - top)
|
|
43
73
|
end
|
|
44
74
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
def build_elements(placed, owner_id)
|
|
76
|
+
placed.map { |p| build_element(p, owner_id) }
|
|
77
|
+
end
|
|
48
78
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
def build_element(placed, owner_id)
|
|
80
|
+
geom = placed.geometry
|
|
81
|
+
style = placed.style
|
|
82
|
+
Ea::Model::DiagramElement.new(
|
|
83
|
+
id: IdNormalizer.synthetic_id(owner_id, "elem", placed.subject),
|
|
84
|
+
diagram_id: owner_id,
|
|
85
|
+
model_element_ref: placed.subject,
|
|
86
|
+
bounds: build_bounds(geom.left, geom.top, geom.right, geom.bottom),
|
|
87
|
+
image_bounds: build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom),
|
|
88
|
+
background_color: style.background_color,
|
|
89
|
+
line_color: style.line_color,
|
|
90
|
+
line_width: style.line_width,
|
|
91
|
+
font_family: style.font_family,
|
|
92
|
+
font_size: style.font_size,
|
|
93
|
+
font_bold: style.bold,
|
|
94
|
+
font_italic: style.italic,
|
|
95
|
+
font_underline: style.underline,
|
|
96
|
+
z_order: placed.seqno,
|
|
97
|
+
duid: style.duid,
|
|
98
|
+
style: {}
|
|
54
99
|
)
|
|
55
100
|
end
|
|
56
101
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
def build_bounds(left, top, right, bottom)
|
|
103
|
+
return nil unless left && top && right && bottom
|
|
104
|
+
|
|
105
|
+
Ea::Model::Bounds.new(x: left, y: top,
|
|
106
|
+
width: right - left,
|
|
107
|
+
height: bottom - top)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_connectors(placed_connectors, placed_elements, owner_id)
|
|
111
|
+
# Index elements by DUID so we can resolve SOID/EOID.
|
|
112
|
+
by_duid = placed_elements.each_with_object({}) do |pe, acc|
|
|
113
|
+
acc[pe.style.duid] = pe if pe.style.duid
|
|
62
114
|
end
|
|
115
|
+
|
|
116
|
+
placed_connectors.map { |p| build_connector(p, by_duid, owner_id) }
|
|
63
117
|
end
|
|
64
118
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
119
|
+
def build_connector(placed, by_duid, owner_id)
|
|
120
|
+
geom = placed.geometry
|
|
121
|
+
style = placed.style
|
|
122
|
+
source = by_duid[style.soid]
|
|
123
|
+
target = by_duid[style.eoid]
|
|
124
|
+
waypoints = compute_waypoints(geom, source, target)
|
|
125
|
+
Ea::Model::DiagramConnector.new(
|
|
126
|
+
id: IdNormalizer.synthetic_id(owner_id, "conn", placed.subject),
|
|
127
|
+
diagram_id: owner_id,
|
|
128
|
+
relationship_ref: placed.subject,
|
|
129
|
+
source_duid: style.soid,
|
|
130
|
+
target_duid: style.eoid,
|
|
131
|
+
source_element_ref: source && synthetic_element_id(owner_id, source),
|
|
132
|
+
target_element_ref: target && synthetic_element_id(owner_id, target),
|
|
133
|
+
source_edge: geom.edge,
|
|
134
|
+
target_edge: geom.edge,
|
|
135
|
+
waypoints: waypoints,
|
|
136
|
+
style: {},
|
|
137
|
+
line_color: style.color,
|
|
138
|
+
line_width: style.line_width,
|
|
139
|
+
hidden: style.hidden,
|
|
140
|
+
label_boxes: serialize_label_boxes(geom.label_boxes)
|
|
77
141
|
)
|
|
78
142
|
end
|
|
143
|
+
|
|
144
|
+
def synthetic_element_id(owner_id, placed)
|
|
145
|
+
IdNormalizer.synthetic_id(owner_id, "elem", placed.subject)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def compute_waypoints(geom, source, target)
|
|
149
|
+
src_pt = edge_point(source, geom.edge, :source)
|
|
150
|
+
tgt_pt = edge_point(target, geom.edge, :target)
|
|
151
|
+
return [] unless src_pt && tgt_pt
|
|
152
|
+
|
|
153
|
+
pts = [src_pt]
|
|
154
|
+
if geom.sx && geom.sy && (geom.sx.nonzero? || geom.sy.nonzero?)
|
|
155
|
+
pts << [src_pt[0] + geom.sx, src_pt[1] + geom.sy]
|
|
156
|
+
end
|
|
157
|
+
if geom.ex && geom.ey && (geom.ex.nonzero? || geom.ey.nonzero?)
|
|
158
|
+
pts << [tgt_pt[0] - geom.ex, tgt_pt[1] - geom.ey]
|
|
159
|
+
end
|
|
160
|
+
pts << tgt_pt
|
|
161
|
+
pts.map do |x, y|
|
|
162
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def edge_point(placed, edge_code, _end_kind)
|
|
167
|
+
return nil unless placed
|
|
168
|
+
|
|
169
|
+
geom = placed.geometry
|
|
170
|
+
left = geom.left
|
|
171
|
+
top = geom.top
|
|
172
|
+
right = geom.right
|
|
173
|
+
bottom = geom.bottom
|
|
174
|
+
return nil unless left && top && right && bottom
|
|
175
|
+
|
|
176
|
+
case edge_code
|
|
177
|
+
when 1 then [(left + right) / 2, top] # top
|
|
178
|
+
when 2 then [right, (top + bottom) / 2] # right
|
|
179
|
+
when 3 then [(left + right) / 2, bottom] # bottom
|
|
180
|
+
when 4 then [left, (top + bottom) / 2] # left
|
|
181
|
+
else [(left + right) / 2, (top + bottom) / 2]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def serialize_label_boxes(label_boxes)
|
|
186
|
+
return {} unless label_boxes
|
|
187
|
+
|
|
188
|
+
label_boxes.transform_values do |box|
|
|
189
|
+
{
|
|
190
|
+
"cx" => box.cx, "cy" => box.cy,
|
|
191
|
+
"ox" => box.ox, "oy" => box.oy,
|
|
192
|
+
"bold" => box.bold,
|
|
193
|
+
"italic" => box.italic,
|
|
194
|
+
"underline" => box.underline,
|
|
195
|
+
"align" => box.align
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
79
199
|
end
|
|
80
200
|
end
|
|
81
201
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Walks the <xmi:Extension>/<elements> block for a single
|
|
7
|
+
# EA diagram. Returns two arrays: placed elements and placed
|
|
8
|
+
# connectors, each carrying the parsed geometry + style data.
|
|
9
|
+
#
|
|
10
|
+
# The xmi gem exposes the raw rows via Sparx::Diagram::Elements.
|
|
11
|
+
# This class adds EA-aware parsing on top.
|
|
12
|
+
class ExtensionElements
|
|
13
|
+
attr_reader :diagram
|
|
14
|
+
|
|
15
|
+
def initialize(diagram)
|
|
16
|
+
@diagram = diagram
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def placed_elements
|
|
20
|
+
rows.filter_map { |row| build_placed_element(row) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def placed_connectors
|
|
24
|
+
rows.filter_map { |row| build_placed_connector(row) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def rows
|
|
30
|
+
elements_collection = diagram&.elements
|
|
31
|
+
return [] unless elements_collection
|
|
32
|
+
|
|
33
|
+
elements_collection.element.to_a
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_placed_element(row)
|
|
37
|
+
return nil unless row.subject
|
|
38
|
+
|
|
39
|
+
placement = ExtensionGeometryParser.parse(row.geometry)
|
|
40
|
+
return nil unless placement.left || placement.img_left
|
|
41
|
+
|
|
42
|
+
style = ExtensionStyleParser.parse(row.style)
|
|
43
|
+
PlacedElement.new(
|
|
44
|
+
subject: row.subject,
|
|
45
|
+
seqno: row.seqno,
|
|
46
|
+
geometry: placement,
|
|
47
|
+
style: style
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_placed_connector(row)
|
|
52
|
+
return nil unless row.subject
|
|
53
|
+
|
|
54
|
+
placement = ExtensionGeometryParser.parse(row.geometry)
|
|
55
|
+
style = ExtensionStyleParser.parse(row.style)
|
|
56
|
+
# Connectors carry SOID/EOID in their style; geometry has
|
|
57
|
+
# only bend routing.
|
|
58
|
+
return nil unless style.soid && style.eoid
|
|
59
|
+
|
|
60
|
+
PlacedConnector.new(
|
|
61
|
+
subject: row.subject,
|
|
62
|
+
geometry: placement,
|
|
63
|
+
style: style
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Value types — the diagram builder translates these into
|
|
68
|
+
# Ea::Model::DiagramElement / DiagramConnector.
|
|
69
|
+
PlacedElement = Struct.new(:subject, :seqno, :geometry, :style, keyword_init: true)
|
|
70
|
+
PlacedConnector = Struct.new(:subject, :geometry, :style, keyword_init: true)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Parses EA's packed geometry string from
|
|
7
|
+
# <xmi:Extension>/<elements>/<element>/@geometry. Returns a
|
|
8
|
+
# typed Placement struct with separate fields for placement
|
|
9
|
+
# and bend routing. Label boxes decoded into a Hash keyed by
|
|
10
|
+
# EA's role names (:llb, :llt, :lrt, :lrb).
|
|
11
|
+
module ExtensionGeometryParser
|
|
12
|
+
LABEL_BOX_KEYS = %i[llb llt lrt lrb lmt lmb irhs ilhs].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def parse(geometry_string)
|
|
17
|
+
return Placement.new if geometry_string.nil? || geometry_string.empty?
|
|
18
|
+
|
|
19
|
+
kv = split_pairs(geometry_string)
|
|
20
|
+
Placement.new(
|
|
21
|
+
left: int(kv["Left"]),
|
|
22
|
+
top: int(kv["Top"]),
|
|
23
|
+
right: int(kv["Right"]),
|
|
24
|
+
bottom: int(kv["Bottom"]),
|
|
25
|
+
img_left: int(kv["imgL"]),
|
|
26
|
+
img_top: int(kv["imgT"]),
|
|
27
|
+
img_right: int(kv["imgR"]),
|
|
28
|
+
img_bottom: int(kv["imgB"]),
|
|
29
|
+
sx: int(kv["SX"]),
|
|
30
|
+
sy: int(kv["SY"]),
|
|
31
|
+
ex: int(kv["EX"]),
|
|
32
|
+
ey: int(kv["EY"]),
|
|
33
|
+
edge: int(kv["EDGE"]),
|
|
34
|
+
label_boxes: extract_label_boxes(geometry_string),
|
|
35
|
+
path: kv["Path"]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def split_pairs(str)
|
|
40
|
+
str.split(";").filter_map do |pair|
|
|
41
|
+
next nil unless pair.include?("=")
|
|
42
|
+
next nil if pair.start_with?("$") # $LLB= etc handled separately
|
|
43
|
+
|
|
44
|
+
key, value = pair.split("=", 2)
|
|
45
|
+
[key, value] if key && value
|
|
46
|
+
end.to_h
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def int(value)
|
|
50
|
+
return nil if value.nil? || value.empty?
|
|
51
|
+
|
|
52
|
+
Integer(value)
|
|
53
|
+
rescue ArgumentError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_label_boxes(geometry_string)
|
|
58
|
+
geometry_string.to_s.scan(/\$?([A-Z]{3})=((?:[A-Za-z]{1,4}=-?\d+:)+)/).each_with_object({}) do |(key, body), acc|
|
|
59
|
+
sym = key.downcase.to_sym
|
|
60
|
+
next unless LABEL_BOX_KEYS.include?(sym)
|
|
61
|
+
next if body.nil? || body.empty?
|
|
62
|
+
|
|
63
|
+
acc[sym] = parse_label_body(body)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_label_body(body)
|
|
68
|
+
pairs = body.to_s.split(":").filter_map do |pair|
|
|
69
|
+
next nil unless pair.include?("=")
|
|
70
|
+
|
|
71
|
+
k, v = pair.split("=", 2)
|
|
72
|
+
[k, int(v)] if k && v
|
|
73
|
+
end
|
|
74
|
+
hash = pairs.to_h
|
|
75
|
+
LabelBox.new(
|
|
76
|
+
cx: hash["CX"],
|
|
77
|
+
cy: hash["CY"],
|
|
78
|
+
ox: hash["OX"],
|
|
79
|
+
oy: hash["OY"],
|
|
80
|
+
bold: hash["BLD"] == 1,
|
|
81
|
+
italic: hash["ITA"] == 1,
|
|
82
|
+
underline: hash["UND"] == 1,
|
|
83
|
+
hidden: hash["HDN"] == 1,
|
|
84
|
+
align: hash["ALN"],
|
|
85
|
+
direction: hash["DIR"],
|
|
86
|
+
rotation: hash["ROT"]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Placement = Struct.new(
|
|
91
|
+
:left, :top, :right, :bottom,
|
|
92
|
+
:img_left, :img_top, :img_right, :img_bottom,
|
|
93
|
+
:sx, :sy, :ex, :ey, :edge,
|
|
94
|
+
:label_boxes, :path,
|
|
95
|
+
keyword_init: true
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
LabelBox = Struct.new(
|
|
99
|
+
:cx, :cy, :ox, :oy,
|
|
100
|
+
:bold, :italic, :underline, :hidden,
|
|
101
|
+
:align, :direction, :rotation,
|
|
102
|
+
keyword_init: true
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Parses EA's packed style string from
|
|
7
|
+
# <xmi:Extension>/<elements>/<element>/@style. Returns a typed
|
|
8
|
+
# Style struct so renderers consume typed fields.
|
|
9
|
+
module ExtensionStyleParser
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def parse(style_string)
|
|
13
|
+
return Style.new if style_string.nil? || style_string.empty?
|
|
14
|
+
|
|
15
|
+
kv = split_pairs(style_string)
|
|
16
|
+
Style.new(
|
|
17
|
+
background_color: int(kv["BCol"]),
|
|
18
|
+
line_color: int(kv["LCol"]),
|
|
19
|
+
line_width: int(kv["LWth"] || kv["LWidth"]),
|
|
20
|
+
font_family: kv["font"],
|
|
21
|
+
font_size: scale_font_size(kv["fontsz"]),
|
|
22
|
+
bold: truthy?(kv["bold"]),
|
|
23
|
+
italic: truthy?(kv["italic"]),
|
|
24
|
+
underline: truthy?(kv["ul"]),
|
|
25
|
+
black: truthy?(kv["black"]),
|
|
26
|
+
hide_icon: truthy?(kv["HideIcon"]),
|
|
27
|
+
duid: kv["DUID"],
|
|
28
|
+
soid: kv["SOID"],
|
|
29
|
+
eoid: kv["EOID"],
|
|
30
|
+
mode: int(kv["Mode"]),
|
|
31
|
+
color: int(kv["Color"]),
|
|
32
|
+
hidden: truthy?(kv["Hidden"]),
|
|
33
|
+
raw: style_string
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def split_pairs(str)
|
|
38
|
+
str.split(";").filter_map do |pair|
|
|
39
|
+
next nil unless pair.include?("=")
|
|
40
|
+
|
|
41
|
+
key, value = pair.split("=", 2)
|
|
42
|
+
[key, value] if key && value && !value.empty?
|
|
43
|
+
end.to_h
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def int(value)
|
|
47
|
+
return nil if value.nil? || value.empty?
|
|
48
|
+
|
|
49
|
+
Integer(value)
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def truthy?(value)
|
|
55
|
+
value == "1" || value == "true" || value == "-1"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def scale_font_size(value)
|
|
59
|
+
return 13 if value.nil? || value.empty?
|
|
60
|
+
|
|
61
|
+
pct = int(value)
|
|
62
|
+
return 13 if pct.nil?
|
|
63
|
+
|
|
64
|
+
(pct * 13 / 100).round
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Style = Struct.new(
|
|
68
|
+
:background_color, :line_color, :line_width,
|
|
69
|
+
:font_family, :font_size,
|
|
70
|
+
:bold, :italic, :underline, :black, :hide_icon,
|
|
71
|
+
:duid, :soid, :eoid, :mode, :color, :hidden,
|
|
72
|
+
:raw,
|
|
73
|
+
keyword_init: true
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/ea/sources/xmi.rb
CHANGED
|
@@ -23,6 +23,9 @@ module Ea
|
|
|
23
23
|
autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
|
|
24
24
|
autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
|
|
25
25
|
autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
|
|
26
|
+
autoload :ExtensionElements, "ea/sources/xmi/extension_elements"
|
|
27
|
+
autoload :ExtensionGeometryParser, "ea/sources/xmi/extension_geometry_parser"
|
|
28
|
+
autoload :ExtensionStyleParser, "ea/sources/xmi/extension_style_parser"
|
|
26
29
|
autoload :Adapter, "ea/sources/xmi/adapter"
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Svg
|
|
5
|
+
module EaEmitter
|
|
6
|
+
# Emits the background layer: white rect over the full canvas.
|
|
7
|
+
module Background
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def render(canvas)
|
|
11
|
+
%(<g style="fill:#FFFFFF;fill-opacity:1.00;">\n <rect x="#{canvas.min_x}" y="#{canvas.min_y}" width="#{canvas.width}" height="#{canvas.height}" shape-rendering="auto"/>\n</g>)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|