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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.diagrams/01-xmi-extension-elements-parsing.md +67 -0
  3. data/TODO.diagrams/02-diagram-element-rich-model.md +61 -0
  4. data/TODO.diagrams/03-ea-style-svg-emitter.md +70 -0
  5. data/TODO.diagrams/04-connector-routing-from-soid-eoid.md +55 -0
  6. data/TODO.diagrams/05-marker-rendering-per-relationship-type.md +49 -0
  7. data/TODO.diagrams/06-connector-labels.md +49 -0
  8. data/TODO.diagrams/07-canvas-size-and-viewbox.md +39 -0
  9. data/TODO.diagrams/08-visual-regression-harness.md +34 -0
  10. data/TODO.diagrams/09-cli-render-all-diagrams.md +25 -0
  11. data/TODO.diagrams/10-stereotype-color-config.md +29 -0
  12. data/TODO.diagrams/11-elkrb-auto-layout-integration.md +53 -0
  13. data/config/stereotype_colors.yml +15 -0
  14. data/lib/ea/cli/app.rb +7 -1
  15. data/lib/ea/cli/command/svg.rb +50 -6
  16. data/lib/ea/model/diagram_connector.rb +19 -1
  17. data/lib/ea/model/diagram_element.rb +28 -0
  18. data/lib/ea/sources/xmi/annotation_builder.rb +1 -1
  19. data/lib/ea/sources/xmi/diagram_builder.rb +164 -44
  20. data/lib/ea/sources/xmi/extension_elements.rb +74 -0
  21. data/lib/ea/sources/xmi/extension_geometry_parser.rb +107 -0
  22. data/lib/ea/sources/xmi/extension_style_parser.rb +78 -0
  23. data/lib/ea/sources/xmi.rb +3 -0
  24. data/lib/ea/svg/ea_emitter/background.rb +16 -0
  25. data/lib/ea/svg/ea_emitter/canvas.rb +65 -0
  26. data/lib/ea/svg/ea_emitter/connectors.rb +50 -0
  27. data/lib/ea/svg/ea_emitter/document.rb +44 -0
  28. data/lib/ea/svg/ea_emitter/elements.rb +169 -0
  29. data/lib/ea/svg/ea_emitter/labels.rb +64 -0
  30. data/lib/ea/svg/ea_emitter/markers.rb +117 -0
  31. data/lib/ea/svg/ea_emitter.rb +29 -0
  32. data/lib/ea/svg/stereotype_color_resolver.rb +50 -0
  33. data/lib/ea/svg.rb +2 -0
  34. data/lib/ea/version.rb +1 -1
  35. 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.body) }
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 uml:Diagram elements into Ea::Model::Diagram
7
- # instances. Coordinates come from the UML-DI owned_element
8
- # bounds; element references come from the model_element
9
- # attribute.
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
- diagrams = Array(root.model.diagram)
19
- diagrams.map { |d| build_one(d) }
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(diagram)
23
- id = IdNormalizer.from_xmi_id(diagram.id)
24
- ext_diagram = extension_diagram_for(id)
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: ext_diagram&.properties&.name,
28
- package_id: ext_diagram&.model&.package,
29
- diagram_type: ext_diagram&.properties&.type&.split(":")&.last&.downcase,
30
- bounds: bounds_for(diagram),
31
- elements: build_elements(diagram, id),
32
- connectors: [],
33
- annotations: AnnotationBuilder.from_element(diagram, id)
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 extension_diagram_for(diagram_id)
40
- return nil unless root.extension&.diagrams
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
- root.extension.diagrams.diagram.find { |d| d.id == diagram_id }
70
+ Ea::Model::Bounds.new(x: 0, y: 0,
71
+ width: right - left,
72
+ height: bottom - top)
43
73
  end
44
74
 
45
- def bounds_for(diagram)
46
- first_bounds = diagram.owned_element.flat_map(&:bounds).first
47
- return nil unless first_bounds
75
+ def build_elements(placed, owner_id)
76
+ placed.map { |p| build_element(p, owner_id) }
77
+ end
48
78
 
49
- Ea::Model::Bounds.new(
50
- x: first_bounds.x || 0,
51
- y: first_bounds.y || 0,
52
- width: first_bounds.width || 0,
53
- height: first_bounds.height || 0
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 build_elements(diagram, owner_diagram_id)
58
- diagram.owned_element.flat_map do |oe|
59
- oe.bounds.each_with_index.map do |bnd, idx|
60
- build_element(oe, bnd, owner_diagram_id, idx)
61
- end
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 build_element(owned_element, bounds, diagram_id, idx)
66
- Ea::Model::DiagramElement.new(
67
- id: IdNormalizer.synthetic_id(diagram_id, "elem", idx.to_s),
68
- diagram_id: diagram_id,
69
- model_element_ref: IdNormalizer.from_xmi_id(owned_element.model_element),
70
- bounds: Ea::Model::Bounds.new(
71
- x: bounds.x || 0,
72
- y: bounds.y || 0,
73
- width: bounds.width || 0,
74
- height: bounds.height || 0
75
- ),
76
- style: {}
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
@@ -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