ea 0.3.0 → 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/qea/diagram_builder.rb +103 -19
- 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/spa/configuration.rb +17 -3
- data/lib/ea/spa/projector.rb +6 -1
- data/lib/ea/svg/connector_path.rb +109 -17
- 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/element_box.rb +202 -17
- data/lib/ea/svg/renderer.rb +8 -1
- 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
|
|
@@ -63,11 +63,113 @@ module Ea
|
|
|
63
63
|
link_row.instance_id),
|
|
64
64
|
diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
|
|
65
65
|
relationship_ref: ref_for_connector(link_row),
|
|
66
|
-
waypoints:
|
|
66
|
+
waypoints: waypoints_for_link(link_row, diagram_row),
|
|
67
67
|
style: DiagramStyleParser.parse(link_row.style)
|
|
68
68
|
)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# EA's t_diagramlinks.Geometry field stores connector routing
|
|
72
|
+
# in a packed format. The leading "X1,Y1,X2,Y2" pair (when
|
|
73
|
+
# present) is in a coordinate frame that does NOT match the
|
|
74
|
+
# element rect frame (elements use RectTop < RectBottom with
|
|
75
|
+
# negative y; geometry uses positive y). Rather than trust
|
|
76
|
+
# the stored absolute coords, we compute the polyline from
|
|
77
|
+
# the actual placed-element bounds + the SX/SY/EX/EY delta
|
|
78
|
+
# fields, which describe the bend routing relative to the
|
|
79
|
+
# element edges.
|
|
80
|
+
#
|
|
81
|
+
# If we can't resolve the source or target element placement
|
|
82
|
+
# for this diagram, we emit no waypoints (the connector will
|
|
83
|
+
# be invisible) — better than drawing garbage lines.
|
|
84
|
+
def waypoints_for_link(link_row, diagram_row)
|
|
85
|
+
connector = database.find_connector(link_row.connectorid)
|
|
86
|
+
return [] unless connector
|
|
87
|
+
|
|
88
|
+
source_placement = diagram_object_placement(diagram_row.diagram_id,
|
|
89
|
+
connector.start_object_id)
|
|
90
|
+
target_placement = diagram_object_placement(diagram_row.diagram_id,
|
|
91
|
+
connector.end_object_id)
|
|
92
|
+
return [] unless source_placement && target_placement
|
|
93
|
+
|
|
94
|
+
geom = parse_geometry_fields(link_row.geometry)
|
|
95
|
+
edge_out = geom[:edge] || 0
|
|
96
|
+
|
|
97
|
+
source_point = element_edge_point(source_placement, :source, edge_out)
|
|
98
|
+
target_point = element_edge_point(target_placement, :target, edge_out)
|
|
99
|
+
points = [source_point]
|
|
100
|
+
# SX/SY: delta from source point to first bend.
|
|
101
|
+
if geom[:sx] && geom[:sy] && (geom[:sx].nonzero? || geom[:sy].nonzero?)
|
|
102
|
+
points << [source_point[0] + geom[:sx], source_point[1] + geom[:sy]]
|
|
103
|
+
end
|
|
104
|
+
# EX/EY: delta from second bend to target point. Reverse
|
|
105
|
+
# to get from target back to the bend.
|
|
106
|
+
if geom[:ex] && geom[:ey] && (geom[:ex].nonzero? || geom[:ey].nonzero?)
|
|
107
|
+
points << [target_point[0] - geom[:ex], target_point[1] - geom[:ey]]
|
|
108
|
+
end
|
|
109
|
+
points << target_point
|
|
110
|
+
|
|
111
|
+
points.map do |x, y|
|
|
112
|
+
Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parse the SX, SY, EX, EY, EDGE key=value pairs out of the
|
|
117
|
+
# geometry string. Each is `KEY=<int>;`. We deliberately do
|
|
118
|
+
# NOT extract the leading X1,Y1,X2,Y2 numbers because those
|
|
119
|
+
# are in a different coordinate frame (see note above).
|
|
120
|
+
def parse_geometry_fields(geometry)
|
|
121
|
+
return {} if geometry.nil? || geometry.empty?
|
|
122
|
+
|
|
123
|
+
s = geometry.to_s
|
|
124
|
+
{
|
|
125
|
+
sx: pick_int(s, /SX=(-?\d+)/),
|
|
126
|
+
sy: pick_int(s, /SY=(-?\d+)/),
|
|
127
|
+
ex: pick_int(s, /EX=(-?\d+)/),
|
|
128
|
+
ey: pick_int(s, /EY=(-?\d+)/),
|
|
129
|
+
edge: pick_int(s, /EDGE=(-?\d+)/)
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pick_int(str, pattern)
|
|
134
|
+
match = str.match(pattern)
|
|
135
|
+
return nil unless match
|
|
136
|
+
|
|
137
|
+
Integer(match[1])
|
|
138
|
+
rescue ArgumentError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Find where a given EA object is placed on this diagram.
|
|
143
|
+
def diagram_object_placement(diagram_id, ea_object_id)
|
|
144
|
+
objects = database.diagram_objects_for(diagram_id) || []
|
|
145
|
+
objects.find { |o| o.ea_object_id == ea_object_id }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Compute the connection point on an element's edge for the
|
|
149
|
+
# given side. EA's `EDGE` field on t_diagramlinks tells us
|
|
150
|
+
# which edge the connector attaches to:
|
|
151
|
+
# 1 = top (source), 2 = right, 3 = bottom, 4 = left,
|
|
152
|
+
# plus 5/6/7/8 for diagonals (treated as the cardinal here).
|
|
153
|
+
# We use the center of the chosen edge as the connection point.
|
|
154
|
+
def element_edge_point(placement, end_kind, edge_code)
|
|
155
|
+
b = bounds_from_rect(placement)
|
|
156
|
+
case effective_edge(edge_code, end_kind)
|
|
157
|
+
when :top then [b.x + b.width / 2, b.y]
|
|
158
|
+
when :right then [b.x + b.width, b.y + b.height / 2]
|
|
159
|
+
when :bottom then [b.x + b.width / 2, b.y + b.height]
|
|
160
|
+
when :left then [b.x, b.y + b.height / 2]
|
|
161
|
+
else [b.x + b.width / 2, b.y + b.height / 2]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def effective_edge(edge_code, end_kind)
|
|
166
|
+
mapping = {
|
|
167
|
+
1 => :top, 2 => :right, 3 => :bottom, 4 => :left,
|
|
168
|
+
5 => :top, 6 => :right, 7 => :bottom, 8 => :left
|
|
169
|
+
}
|
|
170
|
+
mapping[edge_code.to_i] || :center
|
|
171
|
+
end
|
|
172
|
+
|
|
71
173
|
def package_id_for(diagram_row)
|
|
72
174
|
pkg = database.find_package(diagram_row.package_id)
|
|
73
175
|
return nil unless pkg
|
|
@@ -106,24 +208,6 @@ module Ea
|
|
|
106
208
|
|
|
107
209
|
IdNormalizer.from_guid(conn.ea_guid)
|
|
108
210
|
end
|
|
109
|
-
|
|
110
|
-
# EA stores connector geometry as a packed string of x,y
|
|
111
|
-
# pairs separated by semicolons and pipes. Conservative
|
|
112
|
-
# parse: extract digit pairs, take first/last as endpoints.
|
|
113
|
-
def parse_waypoints(geometry)
|
|
114
|
-
return [] if geometry.nil? || geometry.empty?
|
|
115
|
-
|
|
116
|
-
numbers = geometry.to_s.scan(/-?\d+/).map(&:to_i)
|
|
117
|
-
return [] if numbers.size < 4
|
|
118
|
-
|
|
119
|
-
numbers.each_slice(2).filter_map do |xy|
|
|
120
|
-
next nil if xy.size < 2
|
|
121
|
-
|
|
122
|
-
Ea::Model::Waypoint.new(
|
|
123
|
-
position: Ea::Model::Point.new(x: xy[0], y: xy[1])
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
211
|
end
|
|
128
212
|
end
|
|
129
213
|
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
|