ea 0.1.0 → 0.1.4

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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ # Layout engine for positioning diagram elements
6
+ class LayoutEngine
7
+ include Util
8
+
9
+ DEFAULT_SPACING = 50
10
+ DEFAULT_PADDING = 20
11
+ ELEMENT_WIDTH = 120
12
+ ELEMENT_HEIGHT = 80
13
+
14
+ attr_reader :spacing, :element_width, :element_height
15
+
16
+ def initialize(options = {})
17
+ @spacing = options[:spacing] || DEFAULT_SPACING
18
+ @element_width = options[:element_width] || ELEMENT_WIDTH
19
+ @element_height = options[:element_height] || ELEMENT_HEIGHT
20
+ end
21
+
22
+ def calculate_bounds(diagram_data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
23
+ elements = diagram_data[:elements] || []
24
+ return { x: 0, y: 0, width: 400, height: 300 } if elements.empty?
25
+
26
+ min_x = elements.map { |e| e[:x] || 0 }.min
27
+ min_y = elements.map { |e| e[:y] || 0 }.min
28
+ max_x = elements.map do |e|
29
+ (e[:x] || 0) + element_width_for(e)
30
+ end.max
31
+ max_y = elements.map do |e|
32
+ (e[:y] || 0) + element_height_for(e)
33
+ end.max
34
+
35
+ apply_padding_to_bounds(
36
+ {
37
+ x: min_x,
38
+ y: min_y,
39
+ width: max_x - min_x,
40
+ height: max_y - min_y,
41
+ },
42
+ )
43
+ end
44
+
45
+ def apply_padding_to_bounds(bounds) # rubocop:disable Metrics/AbcSize
46
+ padding_x = [bounds[:width] * 0.05, DEFAULT_PADDING].max
47
+ padding_y = [bounds[:height] * 0.05, DEFAULT_PADDING].max
48
+ {
49
+ x: bounds[:x] - padding_x,
50
+ y: bounds[:y] - padding_y,
51
+ width: bounds[:width] + (padding_x * 2),
52
+ height: bounds[:height] + (padding_y * 2),
53
+ }
54
+ end
55
+
56
+ def apply_layout(elements, connectors = []) # rubocop:disable Metrics/MethodLength
57
+ positioned_elements, unpositioned_elements = elements.partition do |e|
58
+ e[:x] && e[:y]
59
+ end
60
+
61
+ if unpositioned_elements.any?
62
+ positioned_elements += apply_force_directed_layout(
63
+ unpositioned_elements,
64
+ connectors,
65
+ positioned_elements,
66
+ )
67
+ end
68
+
69
+ positioned_elements
70
+ end
71
+
72
+ def calculate_element_position(element, related_elements = []) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
73
+ return element if element[:x] && element[:y]
74
+
75
+ if related_elements.any?
76
+ max_x = related_elements.map do |e|
77
+ (e[:x] || 0) + element_width_for(e)
78
+ end.max
79
+ element[:x] = max_x + spacing
80
+ element[:y] = related_elements.first[:y] || 0
81
+ else
82
+ element[:x] = 0
83
+ element[:y] = 0
84
+ end
85
+
86
+ element
87
+ end
88
+
89
+ def calculate_connector_bounds(connectors) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
90
+ return nil if connectors.empty?
91
+
92
+ valid = connectors.select do |c|
93
+ c[:source_element] && c[:target_element] && c[:geometry]
94
+ end
95
+ return nil if valid.empty?
96
+
97
+ points = valid.flat_map { |conn| connector_endpoints(conn) }
98
+ xs = points.map(&:first)
99
+ ys = points.map(&:last)
100
+
101
+ { min_x: xs.min, max_x: xs.max, min_y: ys.min, max_y: ys.max }
102
+ end
103
+
104
+ def connector_endpoints(conn) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
105
+ src = conn[:source_element]
106
+ tgt = conn[:target_element]
107
+ sx, sy, ex, ey = parse_geometry_offsets(conn[:geometry])
108
+
109
+ src_point = [(src[:x] || 0) + (src[:width] || 120) + sx,
110
+ (src[:y] || 0) + ((src[:height] || 80) / 2) + sy]
111
+ tgt_point = [(tgt[:x] || 0) + ex,
112
+ (tgt[:y] || 0) + ((tgt[:height] || 80) / 2) + ey]
113
+
114
+ [src_point, tgt_point]
115
+ end
116
+
117
+ def element_width_for(element)
118
+ if element[:width]
119
+ return element[:width].zero? ? ELEMENT_WIDTH : element[:width]
120
+ end
121
+
122
+ case element[:type]
123
+ when "class"
124
+ (element[:attributes]&.size.to_i * 10) + ELEMENT_WIDTH
125
+ when "package"
126
+ ELEMENT_WIDTH + 20
127
+ else
128
+ ELEMENT_WIDTH
129
+ end
130
+ end
131
+
132
+ def element_height_for(element)
133
+ if element[:height]
134
+ return element[:height].zero? ? ELEMENT_HEIGHT : element[:height]
135
+ end
136
+
137
+ case element[:type]
138
+ when "class"
139
+ (element[:operations]&.size.to_i * 15) + ELEMENT_HEIGHT
140
+ when "package"
141
+ ELEMENT_HEIGHT - 10
142
+ else
143
+ ELEMENT_HEIGHT
144
+ end
145
+ end
146
+
147
+ def apply_force_directed_layout(elements, _connectors, fixed_elements) # rubocop:disable Metrics/AbcSize,Metrics:MethodLength
148
+ positioned = []
149
+ elements.each_with_index do |element, index|
150
+ cols = Math.sqrt(elements.size).ceil
151
+ row = index / cols
152
+ col = index % cols
153
+
154
+ x = col * (ELEMENT_WIDTH + spacing)
155
+ y = row * (ELEMENT_HEIGHT + spacing)
156
+
157
+ if fixed_elements.any?
158
+ x += fixed_elements.map do |e|
159
+ (e[:x] || 0) + element_width_for(e)
160
+ end.max + (spacing * 2)
161
+ end
162
+
163
+ positioned << element.merge(x: x, y: y)
164
+ end
165
+
166
+ positioned
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ # Path builder for connector rendering
6
+ #
7
+ # This class calculates SVG path data for connectors between
8
+ # diagram elements, supporting various connector types and
9
+ # routing algorithms.
10
+ class PathBuilder
11
+ include Util
12
+
13
+ attr_reader :connector, :source_element, :target_element
14
+
15
+ def initialize(connector, source_element = nil, target_element = nil)
16
+ @connector = connector
17
+ @source_element = source_element
18
+ @target_element = target_element
19
+ end
20
+
21
+ # Build SVG path data for the connector
22
+ # @return [String] SVG path data
23
+ def build_path
24
+ return straight_path if simple_connector?
25
+ return waypoint_path if geometry_has_waypoints?
26
+
27
+ case connector[:routing_type]
28
+ when "orthogonal" then orthogonal_path
29
+ when "bezier" then bezier_path
30
+ else manhattan_path
31
+ end
32
+ end
33
+
34
+
35
+ def simple_connector?
36
+ # Use straight line if both elements have direct coordinates
37
+ connector[:source_x] && connector[:source_y] &&
38
+ connector[:target_x] && connector[:target_y]
39
+ end
40
+
41
+ def straight_path
42
+ x1 = connector[:source_x] || 0
43
+ y1 = connector[:source_y] || 0
44
+ x2 = connector[:target_x] || 100
45
+ y2 = connector[:target_y] || 100
46
+
47
+ "M #{x1},#{y1} L #{x2},#{y2}"
48
+ end
49
+
50
+ def orthogonal_path
51
+ # Right-angle routing
52
+ points = calculate_orthogonal_points
53
+ path_from_points(points)
54
+ end
55
+
56
+ def manhattan_path # rubocop:disable Metrics/MethodLength
57
+ # Manhattan distance routing with one bend
58
+ x1, y1 = source_point
59
+ x2, y2 = target_point
60
+
61
+ # Calculate bend point (midpoint)
62
+ bend_x = (x1 + x2) / 2
63
+ bend_y = (y1 + y2) / 2
64
+
65
+ # Choose bend direction to avoid elements
66
+ if (x2 - x1).abs > (y2 - y1).abs
67
+ # Horizontal bend
68
+ "M #{x1},#{y1} L #{bend_x},#{y1} L #{bend_x},#{y2} L #{x2},#{y2}"
69
+ else
70
+ # Vertical bend
71
+ "M #{x1},#{y1} L #{x1},#{bend_y} L #{x2},#{bend_y} L #{x2},#{y2}"
72
+ end
73
+ end
74
+
75
+ def bezier_path
76
+ # Smooth curved path using Bezier curves
77
+ x1, y1 = source_point
78
+ x2, y2 = target_point
79
+
80
+ # Control points for smooth curve
81
+ cp1x = x1 + ((x2 - x1) * 0.3)
82
+ cp1y = y1
83
+ cp2x = x2 - ((x2 - x1) * 0.3)
84
+ cp2y = y2
85
+
86
+ "M #{x1},#{y1} C #{cp1x},#{cp1y} #{cp2x},#{cp2y} #{x2},#{y2}"
87
+ end
88
+
89
+ def calculate_orthogonal_points # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
90
+ x1, y1 = source_point
91
+ x2, y2 = target_point
92
+
93
+ points = [[x1, y1]]
94
+
95
+ # Determine direction based on relative positions
96
+ if (x2 - x1).abs > (y2 - y1).abs
97
+ # Horizontal first, then vertical
98
+ points << [x1 + ((x2 - x1) / 2), y1]
99
+ points << [x1 + ((x2 - x1) / 2), y2]
100
+ else
101
+ # Vertical first, then horizontal
102
+ points << [x1, y1 + ((y2 - y1) / 2)]
103
+ points << [x2, y1 + ((y2 - y1) / 2)]
104
+ end
105
+
106
+ points << [x2, y2]
107
+ points
108
+ end
109
+
110
+ def path_from_points(points)
111
+ return "" if points.empty?
112
+
113
+ path = "M #{points[0][0]},#{points[0][1]}"
114
+ points[1..].each do |point|
115
+ path += " L #{point[0]},#{point[1]}"
116
+ end
117
+ path
118
+ end
119
+
120
+ def geometry_has_waypoints?
121
+ return false unless connector[:geometry]
122
+
123
+ geometry_data = parse_ea_geometry(connector[:geometry])
124
+ geometry_data&.dig(:waypoints)&.any?
125
+ end
126
+
127
+ def waypoint_path
128
+ geometry_data = parse_ea_geometry(connector[:geometry])
129
+ points = []
130
+
131
+ sp = source_point
132
+ points << sp if sp
133
+
134
+ geometry_data[:waypoints].each do |wp|
135
+ points << [wp[:x], wp[:y]]
136
+ end
137
+
138
+ tp = target_point
139
+ points << tp if tp
140
+
141
+ path_from_points(points)
142
+ end
143
+
144
+ def source_point
145
+ if connector[:source_x] && connector[:source_y]
146
+ [connector[:source_x], connector[:source_y]]
147
+ else
148
+ calculate_element_connection_point(source_element, :source)
149
+ end
150
+ end
151
+
152
+ def target_point
153
+ if connector[:target_x] && connector[:target_y]
154
+ [connector[:target_x], connector[:target_y]]
155
+ else
156
+ calculate_element_connection_point(target_element, :target)
157
+ end
158
+ end
159
+
160
+ def calculate_element_connection_point(element, type) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
161
+ return [0, 0] unless element
162
+
163
+ # Calculate connection point based on element bounds and
164
+ # connector type
165
+ x = element[:x] || 0
166
+ y = element[:y] || 0
167
+ width = element[:width] || 120
168
+ height = element[:height] || 80
169
+
170
+ point = case type
171
+ when :source
172
+ # Connect from right side for outgoing connectors
173
+ [x + width, y + (height / 2)]
174
+ when :target
175
+ # Connect to left side for incoming connectors
176
+ [x, y + (height / 2)]
177
+ else
178
+ [x + (width / 2), y + (height / 2)]
179
+ end
180
+
181
+ return point unless connector[:geometry]
182
+
183
+ # Apply relative offsets if specified
184
+ offsets = parse_geometry_offsets(connector[:geometry])
185
+ apply_offset(point, offsets, type)
186
+ end
187
+
188
+ def apply_offset(point, offsets, type)
189
+ offset_x, offset_y = case type
190
+ when :source
191
+ [offsets[0], offsets[1]]
192
+ when :target
193
+ [offsets[2], offsets[3]]
194
+ else
195
+ [0, 0]
196
+ end
197
+
198
+ [point[0] + offset_x, point[1] + offset_y]
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # StyleParser is a thin color utility. Style orchestration (defaults,
4
+ # EA-parsed overrides, connector-type dispatch) lives in StyleResolver.
5
+ #
6
+ # Historically this class held a parallel style-resolution API
7
+ # (`parse_element_style`, `parse_connector_style`, `get_base_element_style`,
8
+ # `element_specific_style`, `parse_ea_style_string`, `stereotype_style`).
9
+ # That API was unused — callers went through StyleResolver's
10
+ # `resolve_element_style` / `resolve_connector_style` instead. The duplicate
11
+ # API was removed on 2026-06-27 to fix the MECE violation (two style
12
+ # pipelines for the same concern).
13
+ #
14
+ # What remains is `color_from_ea_color`, the BGR-integer → hex color
15
+ # converter that StyleResolver uses when parsing EA style strings.
16
+
17
+ module Ea
18
+ module Diagram
19
+ class StyleParser
20
+ # EA's default fill color (light yellow) used when an EA color integer
21
+ # is zero / unset.
22
+ DEFAULT_FILL_COLOR = "#FFFFCC"
23
+
24
+ # Convert EA color integer (BGR) to hex color string.
25
+ #
26
+ # EA stores colors as BGR integers in DiagramObject.style strings
27
+ # (e.g. "BCol=16764159"). A zero value means "use the EA default".
28
+ #
29
+ # @param ea_color [Integer] EA BGR color value
30
+ # @return [String] Hex color string (e.g. "#FFFFCC")
31
+ def color_from_ea_color(ea_color)
32
+ return DEFAULT_FILL_COLOR if ea_color.zero?
33
+
34
+ b = (ea_color & 0xFF0000) >> 16
35
+ g = (ea_color & 0x00FF00) >> 8
36
+ r = ea_color & 0x0000FF
37
+
38
+ format("#%02X%02X%02X", r, g, b)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ # Resolves styles for diagram elements by merging multiple sources
6
+ #
7
+ # Priority order (highest to lowest):
8
+ # 1. EA Data from DiagramObject.style (BCol, LCol, etc.)
9
+ # 2. User Configuration (YAML)
10
+ # 3. Built-in Defaults
11
+ #
12
+ # This ensures that:
13
+ # - EA's original styling is preserved when present
14
+ # - Users can override defaults via configuration
15
+ # - Sensible defaults are always available
16
+ class StyleResolver
17
+ attr_reader :configuration, :style_parser
18
+
19
+ # Initialize with configuration
20
+ #
21
+ # @param config_path [String, nil] Path to configuration file
22
+ def initialize(config_path = nil)
23
+ @configuration = Configuration.new(config_path)
24
+ @style_parser = StyleParser.new
25
+ end
26
+
27
+ # Resolve complete style for an element
28
+ #
29
+ # @param element [Object] UML element (Class, DataType, etc.)
30
+ # @param diagram_object [Lutaml::Uml::DiagramObject, nil]
31
+ # Diagram placement data
32
+ # @return [Hash] Complete resolved style
33
+ def resolve_element_style(element, diagram_object = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
34
+ style = {}
35
+
36
+ # Start with configuration defaults
37
+ style[:fill] = configuration.style_for(element, "colors.fill")
38
+ style[:stroke] = configuration.style_for(element, "colors.stroke")
39
+ style[:stroke_width] =
40
+ configuration.style_for(element, "box.stroke_width")
41
+ style[:stroke_linecap] =
42
+ configuration.style_for(element, "box.stroke_linecap")
43
+ style[:stroke_linejoin] =
44
+ configuration.style_for(element, "box.stroke_linejoin")
45
+ style[:corner_radius] =
46
+ configuration.style_for(element, "box.corner_radius")
47
+ style[:fill_opacity] =
48
+ configuration.style_for(element, "box.fill_opacity")
49
+ style[:stroke_opacity] =
50
+ configuration.style_for(element, "box.stroke_opacity")
51
+
52
+ # Font configuration
53
+ style[:font_family] =
54
+ configuration.style_for(element, "fonts.class_name.family")
55
+ style[:font_size] =
56
+ configuration.style_for(element, "fonts.class_name.size")
57
+ style[:font_weight] =
58
+ configuration.style_for(element, "fonts.class_name.weight")
59
+ style[:font_style] =
60
+ configuration.style_for(element, "fonts.class_name.style")
61
+
62
+ # Override with EA data if present (highest priority)
63
+ if diagram_object&.style
64
+ ea_style = parse_diagram_object_style(diagram_object.style)
65
+ style.merge!(ea_style)
66
+ end
67
+
68
+ style.compact
69
+ end
70
+
71
+ # Resolve complete style for a connector
72
+ #
73
+ # @param connector [Object] UML connector
74
+ # (Association, Generalization, etc.)
75
+ # @param diagram_link [Lutaml::Uml::DiagramLink, nil]
76
+ # Diagram routing data
77
+ # @return [Hash] Complete resolved style
78
+ def resolve_connector_style(connector, diagram_link = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
79
+ # Determine connector type
80
+ connector_type = determine_connector_type(connector)
81
+
82
+ style = {}
83
+
84
+ # Start with configuration defaults for this connector type
85
+ style[:arrow_type] =
86
+ configuration.connector_style(connector_type, "arrow.type")
87
+ style[:arrow_size] =
88
+ configuration.connector_style(connector_type, "arrow.size")
89
+ style[:stroke] =
90
+ configuration.connector_style(connector_type, "line.stroke")
91
+ style[:stroke_width] =
92
+ configuration.connector_style(connector_type, "line.stroke_width")
93
+ style[:stroke_dasharray] =
94
+ configuration.connector_style(connector_type,
95
+ "line.stroke_dasharray")
96
+ style[:fill] =
97
+ configuration.connector_style(connector_type, "line.fill") || "none"
98
+
99
+ # Override with EA data if present (highest priority)
100
+ if diagram_link&.style
101
+ ea_style = parse_diagram_link_style(diagram_link.style)
102
+ style.merge!(ea_style)
103
+ end
104
+
105
+ style.compact
106
+ end
107
+
108
+ # Resolve fill color specifically
109
+ #
110
+ # @param element [Object] UML element
111
+ # @param diagram_object [Lutaml::Uml::DiagramObject, nil]
112
+ # Diagram placement data
113
+ # @return [String] Resolved fill color
114
+ def resolve_fill_color(element, diagram_object = nil)
115
+ # Priority 1: EA data from DiagramObject.style
116
+ if diagram_object&.style
117
+ ea_style = parse_diagram_object_style(diagram_object.style)
118
+ return ea_style[:fill] if ea_style[:fill]
119
+ end
120
+
121
+ # Priority 2: Configuration (Class > Package > Stereotype > Defaults)
122
+ configuration.style_for(element, "colors.fill")
123
+ end
124
+
125
+ # Resolve stroke color specifically
126
+ #
127
+ # @param element [Object] UML element
128
+ # @param diagram_object [Lutaml::Uml::DiagramObject, nil]
129
+ # Diagram placement data
130
+ # @return [String] Resolved stroke color
131
+ def resolve_stroke_color(element, diagram_object = nil)
132
+ # Priority 1: EA data from DiagramObject.style
133
+ if diagram_object&.style
134
+ ea_style = parse_diagram_object_style(diagram_object.style)
135
+ return ea_style[:stroke] if ea_style[:stroke]
136
+ end
137
+
138
+ # Priority 2: Configuration
139
+ configuration.style_for(element, "colors.stroke")
140
+ end
141
+
142
+ # Resolve font properties
143
+ #
144
+ # @param element [Object] UML element
145
+ # @param context [Symbol] Font context
146
+ # (:class_name, :attribute, :operation, :stereotype)
147
+ # @return [Hash] Font properties (family, size, weight, style)
148
+ def resolve_font(element, context = :class_name)
149
+ {
150
+ family: configuration.style_for(element, "fonts.#{context}.family"),
151
+ size: configuration.style_for(element, "fonts.#{context}.size"),
152
+ weight: configuration.style_for(element, "fonts.#{context}.weight"),
153
+ style: configuration.style_for(element, "fonts.#{context}.style"),
154
+ }.compact
155
+ end
156
+
157
+
158
+ # Parse DiagramObject.style string
159
+ # (EA format: "BCol=16764159;LCol=0;SOID=123")
160
+ #
161
+ # @param style_string [String] EA style string
162
+ # @return [Hash] Parsed style with fill and stroke colors
163
+ def parse_diagram_object_style(style_string) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
164
+ return {} unless style_string
165
+
166
+ style = {}
167
+ pairs = style_string.split(";")
168
+
169
+ pairs.each do |pair|
170
+ key, value = pair.split("=", 2)
171
+ next unless key && value
172
+
173
+ case key.strip
174
+ when "BCol"
175
+ # Background color (BGR integer)
176
+ style[:fill] =
177
+ style_parser.color_from_ea_color(value.to_i)
178
+ when "LCol"
179
+ # Line color (BGR integer)
180
+ style[:stroke] =
181
+ style_parser.color_from_ea_color(value.to_i)
182
+ when "BFol"
183
+ # Bold font (0 or 1)
184
+ style[:font_weight] = value == "1" ? 700 : 400
185
+ when "IFol"
186
+ # Italic font (0 or 1)
187
+ style[:font_style] = value == "1" ? "italic" : "normal"
188
+ when "LWth"
189
+ # Line width
190
+ style[:stroke_width] = value.to_i
191
+ end
192
+ end
193
+
194
+ style
195
+ end
196
+
197
+ # Parse DiagramLink.style string
198
+ #
199
+ # @param style_string [String] EA style string
200
+ # @return [Hash] Parsed style
201
+ def parse_diagram_link_style(style_string) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
202
+ return {} unless style_string
203
+
204
+ style = {}
205
+ pairs = style_string.split(";")
206
+
207
+ pairs.each do |pair|
208
+ key, value = pair.split("=", 2)
209
+ next unless key && value
210
+
211
+ case key.strip
212
+ when "LCol"
213
+ # Line color
214
+ style[:stroke] =
215
+ style_parser.color_from_ea_color(value.to_i)
216
+ when "LWth"
217
+ # Line width
218
+ style[:stroke_width] = value.to_i
219
+ when "LStyle"
220
+ # Line style (0=solid, 1=dash, 2=dot, etc.)
221
+ case value.to_i
222
+ when 1
223
+ style[:stroke_dasharray] = "5,5"
224
+ when 2
225
+ style[:stroke_dasharray] = "2,2"
226
+ end
227
+ end
228
+ end
229
+
230
+ style
231
+ end
232
+
233
+ # Maps UML connector classes to their style type names.
234
+ # New connector types are added here — no method changes needed.
235
+ CONNECTOR_TYPE_MAP = {
236
+ Lutaml::Uml::Generalization => "generalization",
237
+ Lutaml::Uml::Association => "association",
238
+ Lutaml::Uml::Dependency => "dependency",
239
+ Lutaml::Uml::Realization => "realization",
240
+ }.freeze
241
+
242
+ # Association sub-type precedence for style resolution
243
+ ASSOCIATION_SUBTYPE_MAP = {
244
+ "aggregation" => "aggregation",
245
+ "composition" => "composition",
246
+ }.freeze
247
+
248
+ # Determine connector type from connector object
249
+ # @param connector [Object] UML connector
250
+ # @return [String] Connector type name
251
+ def determine_connector_type(connector)
252
+ return "association" unless connector
253
+
254
+ type_name = CONNECTOR_TYPE_MAP[connector.class]
255
+ return type_name if type_name && type_name != "association"
256
+ return determine_association_type(connector) if type_name == "association"
257
+
258
+ "association"
259
+ end
260
+
261
+ # Determine specific association type
262
+ # @param connector [Object] Association connector
263
+ # @return [String] Specific association type
264
+ def determine_association_type(connector)
265
+ return "association" unless connector.is_a?(Lutaml::Uml::Association)
266
+
267
+ [connector.owner_end_type, connector.member_end_type].each do |type|
268
+ resolved = ASSOCIATION_SUBTYPE_MAP[type&.downcase]
269
+ return resolved if resolved
270
+ end
271
+
272
+ "association"
273
+ end
274
+ end
275
+ end
276
+ end