ea 0.3.1 → 0.5.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 (544) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -0
  3. data/TODO.complete/01-t-xref-parsing.md +91 -0
  4. data/TODO.complete/02-per-label-geometry.md +50 -0
  5. data/TODO.complete/03-object-properties-constraints.md +57 -0
  6. data/TODO.complete/04-operation-params.md +37 -0
  7. data/TODO.complete/05-pdata-flag-matrix.md +52 -0
  8. data/TODO.complete/06-emf-image-rendering.md +34 -0
  9. data/TODO.complete/07-auxiliary-tables.md +47 -0
  10. data/TODO.complete/10-mdg-stereotype-registry.md +40 -0
  11. data/TODO.complete/11-gml-xsd-generation.md +42 -0
  12. data/TODO.complete/13-shapescript-interpreter.md +28 -0
  13. data/TODO.complete/20-cli-diff.md +46 -0
  14. data/TODO.complete/21-cli-render.md +26 -0
  15. data/TODO.complete/22-cli-export.md +29 -0
  16. data/TODO.complete/23-cli-mdg.md +23 -0
  17. data/TODO.complete/30-headerlines-ocp-refactor.md +72 -0
  18. data/TODO.complete/31-public-send-cleanup.md +61 -0
  19. data/TODO.complete/32-spec-coverage.md +33 -0
  20. data/TODO.complete/33-blocked-parity-gaps.md +41 -0
  21. data/TODO.complete/36-stereotype-icon-wiring.md +32 -0
  22. data/TODO.complete/37-cli-lint.md +32 -0
  23. data/TODO.complete/38-cli-query.md +26 -0
  24. data/TODO.complete/39-cli-info.md +23 -0
  25. data/TODO.complete/40-ocl-evaluator.md +31 -0
  26. data/TODO.complete/41-round-trip-spec.md +18 -0
  27. data/TODO.complete/42-html-diff-report.md +18 -0
  28. data/TODO.complete/43-json-schema-export.md +20 -0
  29. data/TODO.complete/44-remaining-qea-tables.md +40 -0
  30. data/TODO.complete/45-shapescript-extensions.md +25 -0
  31. data/TODO.complete/46-xsd-consolidation.md +20 -0
  32. data/TODO.complete/47-headerlines-legacy-removal.md +20 -0
  33. data/TODO.complete/STATUS.md +109 -0
  34. data/TODO.diagrams/01-svg-measurement-harness.md +31 -0
  35. data/TODO.diagrams/01-xmi-extension-elements-parsing.md +67 -0
  36. data/TODO.diagrams/02-diagram-element-rich-model.md +61 -0
  37. data/TODO.diagrams/02-polygon-over-rendering.md +29 -0
  38. data/TODO.diagrams/03-ea-style-svg-emitter.md +70 -0
  39. data/TODO.diagrams/03-text-over-rendering.md +30 -0
  40. data/TODO.diagrams/04-connector-routing-from-soid-eoid.md +55 -0
  41. data/TODO.diagrams/04-rect-under-rendering.md +33 -0
  42. data/TODO.diagrams/05-canvas-dimension-parity.md +51 -0
  43. data/TODO.diagrams/05-marker-rendering-per-relationship-type.md +49 -0
  44. data/TODO.diagrams/06-connector-labels.md +49 -0
  45. data/TODO.diagrams/06-group-layering-parity.md +39 -0
  46. data/TODO.diagrams/07-canvas-size-and-viewbox.md +39 -0
  47. data/TODO.diagrams/07-text-content-fidelity.md +65 -0
  48. data/TODO.diagrams/08-qea-direct-pipeline.md +46 -0
  49. data/TODO.diagrams/08-visual-regression-harness.md +34 -0
  50. data/TODO.diagrams/09-cli-render-all-diagrams.md +25 -0
  51. data/TODO.diagrams/10-stereotype-color-config.md +29 -0
  52. data/TODO.diagrams/11-elkrb-auto-layout-integration.md +53 -0
  53. data/TODO.diagrams/12-connector-labels.md +40 -0
  54. data/TODO.diagrams/12-per-entity-svg-layer-ordering.md +24 -0
  55. data/TODO.diagrams/13-marker-polygon-vs-path-dispatch.md +20 -0
  56. data/TODO.diagrams/13-per-text-type-font-sizes.md +33 -0
  57. data/TODO.diagrams/14-center-header-text.md +33 -0
  58. data/TODO.diagrams/14-filter-diagram-frame-elements.md +18 -0
  59. data/TODO.diagrams/15-deep-dead-code-audit.md +47 -0
  60. data/TODO.diagrams/15-per-element-font-from-style.md +24 -0
  61. data/TODO.diagrams/16-canvas-marker-extent.md +30 -0
  62. data/TODO.diagrams/16-document-remaining-parity-gaps.md +91 -0
  63. data/TODO.diagrams/17-italic-abstract-classes.md +28 -0
  64. data/TODO.diagrams/17-remaining-parity-gaps.md +90 -0
  65. data/TODO.diagrams/18-canvas-from-image-bounds.md +20 -0
  66. data/TODO.diagrams/18-property-lookup-index.md +42 -0
  67. data/TODO.diagrams/19-connector-sx-sy-routing.md +27 -0
  68. data/TODO.diagrams/19-replace-respond-to-with-is-a.md +31 -0
  69. data/TODO.diagrams/20-split-labels-emitter.md +56 -0
  70. data/TODO.diagrams/20-visibility-glyph-font-encoding.md +27 -0
  71. data/TODO.diagrams/21-aggregation-navigability-arrow.md +21 -0
  72. data/TODO.diagrams/21-auto-generated-legend-block.md +65 -0
  73. data/TODO.diagrams/22-ghost-element-discriminator.md +57 -0
  74. data/TODO.diagrams/22-style-based-path-grouping.md +28 -0
  75. data/TODO.diagrams/23-marker-polygon-deduplication.md +47 -0
  76. data/TODO.diagrams/23-multiplicity-label-positioning.md +29 -0
  77. data/TODO.diagrams/24-marker-anchor-structural.md +35 -0
  78. data/TODO.diagrams/24-track-marker-anchors-structurally.md +66 -0
  79. data/TODO.diagrams/24-truncate-properties-to-fit-box.md +24 -0
  80. data/TODO.diagrams/26-phantom-general-parent-rendering.md +59 -0
  81. data/TODO.diagrams/27-path-grouping-mode-detection.md +43 -0
  82. data/TODO.diagrams/28-architecture-cleanup-batch.md +47 -0
  83. data/TODO.diagrams/30-decompose-elements-groups-for.md +52 -0
  84. data/TODO.diagrams/32-examples-coverage-batch.md +78 -0
  85. data/TODO.diagrams/36-instance-specification-model.md +42 -0
  86. data/TODO.diagrams/37-element-icon-decorations.md +41 -0
  87. data/TODO.diagrams/38-html-note-and-receptions.md +32 -0
  88. data/TODO.diagrams/39-property-stereotype-discriminator.md +40 -0
  89. data/TODO.diagrams/40-standalone-1-discriminator.md +38 -0
  90. data/TODO.diagrams/41-xmi-to-svg-pipeline.md +113 -0
  91. data/TODO.diagrams/42-close-all-parity-deltas.md +69 -0
  92. data/TODO.diagrams/43-marker-deduplication-strategy.md +54 -0
  93. data/TODO.diagrams/44-element-icon-decorations.md +34 -0
  94. data/TODO.diagrams/45-phantom-parent-class-boxes.md +40 -0
  95. data/TODO.diagrams/46-user-drawn-legend-swatches.md +44 -0
  96. data/TODO.diagrams/47-attribute-line-fidelity.md +72 -0
  97. data/TODO.diagrams/48-spec-coverage-critical-emitter-components.md +45 -0
  98. data/TODO.diagrams/49-spec-coverage-orchestrators.md +25 -0
  99. data/TODO.diagrams/50-examples-parity.md +85 -0
  100. data/TODO.diagrams/51-property-oy-gap-and-remaining-gaps.md +48 -0
  101. data/TODO.diagrams/52-marker-registration-architecture.md +89 -0
  102. data/TODO.diagrams/53-remaining-example-gaps.md +86 -0
  103. data/TODO.diagrams/54-display-name-nested-classes.md +40 -0
  104. data/TODO.diagrams/55-visibility-icon-discriminator.md +58 -0
  105. data/TODO.diagrams/56-package-contents-rendering.md +148 -0
  106. data/TODO.diagrams/57-instance-specification-model.md +100 -0
  107. data/TODO.diagrams/58-domain-model-note-text.md +67 -0
  108. data/TODO.diagrams/59-classes-arrow-discriminator.md +57 -0
  109. data/TODO.diagrams/60-testschema-association-marker.md +40 -0
  110. data/TODO.diagrams/61-legend-block-rendering.md +55 -0
  111. data/TODO.diagrams/61-plateau-parity-gaps.md +59 -0
  112. data/TODO.diagrams/62-ghost-element-rendering.md +56 -0
  113. data/TODO.diagrams/63-qualified-name-wrap.md +36 -0
  114. data/TODO.diagrams/64-property-stereotype-labels.md +22 -0
  115. data/TODO.diagrams/65-path-coordinate-frame.md +62 -0
  116. data/TODO.diagrams/67-path-coordinate-frame.md +62 -0
  117. data/TODO.diagrams/68-inherited-attributes.md +65 -0
  118. data/TODO.diagrams/69-package-anchor-conditional.md +29 -0
  119. data/TODO.diagrams/70-mdg-technology-support.md +68 -0
  120. data/TODO.diagrams/71-off-canvas-parent-ghost.md +45 -0
  121. data/TODO.diagrams/72-basic-attr-divider-paths.md +39 -0
  122. data/TODO.diagrams/73-plateau-polygon-gap.md +69 -0
  123. data/TODO.diagrams/74-plateau-rect-gap.md +22 -0
  124. data/TODO.diagrams/75-plateau-text-gap.md +39 -0
  125. data/TODO.diagrams/76-basic-poly-path-gaps.md +41 -0
  126. data/TODO.diagrams/77-basic-text-overrender.md +40 -0
  127. data/TODO.diagrams/78-headerlines-ocp-refactor.md +65 -0
  128. data/TODO.diagrams/79-architecture-backlog.md +65 -0
  129. data/TODO.diagrams/80-basic-triangle-polygons.md +51 -0
  130. data/TODO.diagrams/81-plateau-path-overrender.md +53 -0
  131. data/TODO.diagrams/82-appearance-canvas-offset.md +54 -0
  132. data/TODO.diagrams/83-3d-diagram-subtitle.md +58 -0
  133. data/TODO.diagrams/84-canvas-offset-mismatches.md +61 -0
  134. data/TODO.diagrams/85-phantom-property-overrender.md +95 -0
  135. data/TODO.diagrams/86-foreign-package-rule.md +62 -0
  136. data/TODO.diagrams/87-internal-tech-encryption.md +80 -0
  137. data/TODO.diagrams/EXAMPLES_STATUS.md +88 -0
  138. data/TODO.diagrams/README.md +69 -0
  139. data/TODO.diagrams/STATUS.md +73 -0
  140. data/TODO.fixup/01-default-grouped-paths.md +34 -0
  141. data/TODO.fixup/02-marker-consolidation-per-connector.md +20 -0
  142. data/TODO.fixup/03-diagram-font-inference.md +28 -0
  143. data/TODO.fixup/04-extract-bounds-calculator.md +25 -0
  144. data/TODO.fixup/05-marker-registry-ocp.md +32 -0
  145. data/TODO.fixup/06-connector-path-bend-routing.md +28 -0
  146. data/TODO.fixup/07-edge-attachment-formula.md +22 -0
  147. data/TODO.fixup/08-visual-diff-harness.md +28 -0
  148. data/TODO.fixup/09-extract-element-compartments.md +34 -0
  149. data/TODO.fixup/10-canvas-width-overshoot-fix.md +20 -0
  150. data/TODO.fixup/11-layer-sequencer-extraction.md +18 -0
  151. data/TODO.fixup/12-path-style-harmony.md +23 -0
  152. data/TODO.fixup/13-datatype-stereotype-fallback.md +28 -0
  153. data/TODO.fixup/14-ea-exe-reverse-engineering-notes.md +56 -0
  154. data/TODO.fixup/15-enumeration-literal-rendering.md +30 -0
  155. data/TODO.fixup/16-operation-compartment-rendering.md +42 -0
  156. data/TODO.fixup/17-path-count-calibration.md +27 -0
  157. data/TODO.fixup/18-polygon-count-calibration.md +32 -0
  158. data/TODO.fixup/19-rect-count-undershoot-investigation.md +40 -0
  159. data/TODO.fixup/20-stereotype-color-from-classifier.md +18 -0
  160. data/TODO.fixup/21-visibility-filter-for-properties.md +24 -0
  161. data/TODO.fixup/22-multiplicity-display-rule-calibration.md +25 -0
  162. data/TODO.fixup/23-diagram-frame-title-element.md +31 -0
  163. data/TODO.fixup/24-note-element-rendering.md +23 -0
  164. data/TODO.fixup/25-ea-exe-disassembly-findings.md +177 -0
  165. data/TODO.fixup/26-svg-renderer-architecture.md +78 -0
  166. data/TODO.fixup/27-svg-element-emitters-disassembly.md +135 -0
  167. data/TODO.fixup/28-geometry-parser-identification.md +57 -0
  168. data/TODO.fixup/29-qea-svg-xmi-triple-comparison.md +185 -0
  169. data/TODO.fixup/30-theme-system-stub.md +46 -0
  170. data/TODO.fixup/31-theme-value-object-registry.md +17 -0
  171. data/TODO.fixup/32-styleex-on-diagram-model.md +22 -0
  172. data/TODO.fixup/33-text-encoding-decimals-rotation.md +29 -0
  173. data/TODO.fixup/34-per-type-fill-via-theme.md +18 -0
  174. data/TODO.fixup/35-stroke-width-per-theme.md +7 -0
  175. data/TODO.fixup/36-qea-rect-coord-normalization.md +24 -0
  176. data/TODO.fixup/37-package-shape-rendering.md +22 -0
  177. data/TODO.fixup/38-note-element-rendering.md +15 -0
  178. data/TODO.fixup/39-styleex-flag-honoring.md +25 -0
  179. data/TODO.fixup/40-connector-dashed-lines.md +15 -0
  180. data/TODO.fixup/41-diagram-frame-showborder-detection.md +23 -0
  181. data/TODO.fixup/42-textrenderer-centralization.md +10 -0
  182. data/TODO.fixup/43-colorresolver-unification.md +24 -0
  183. data/TODO.fixup/44-theme-aware-textrenderer.md +17 -0
  184. data/TODO.fixup/45-theme-aware-fontresolver.md +22 -0
  185. data/TODO.fixup/46-style-parameterized-by-theme.md +21 -0
  186. data/TODO.fixup/47-canvas-frame-inset.md +19 -0
  187. data/TODO.fixup/48-background-rect-format.md +15 -0
  188. data/TODO.fixup/49-all-samples-integration-test.md +17 -0
  189. data/TODO.fixup/50-svg-emitter-architecture-docs.md +33 -0
  190. data/TODO.fixup/51-promote-theme-to-ea-namespace.md +28 -0
  191. data/TODO.fixup/52-theme-definition-integrated-fills.md +26 -0
  192. data/TODO.fixup/53-theme-registry-yaml-loading.md +50 -0
  193. data/TODO.fixup/54-diagram-theme-api.md +50 -0
  194. data/TODO.fixup/55-update-eaemitter-to-ea-theme.md +23 -0
  195. data/TODO.fixup/56-theme-yaml-files.md +65 -0
  196. data/TODO.fixup/57-theme-specs-and-integration.md +27 -0
  197. data/TODO.fixup/58-ea-entry-point-theme-autoload.md +20 -0
  198. data/TODO.fixup/59-thread-theme-through-all-renderers.md +29 -0
  199. data/TODO.fixup/60-cli-theme-option.md +22 -0
  200. data/TODO.fixup/61-theme-validation.md +28 -0
  201. data/TODO.fixup/62-theme-builder-dsl.md +31 -0
  202. data/TODO.fixup/63-theme-integration-rendering-spec.md +24 -0
  203. data/TODO.fixup/64-theme-writeback-styleex.md +21 -0
  204. data/TODO.fixup/65-remove-dead-eaemitter-theme-files.md +30 -0
  205. data/TODO.fixup/66-styleex-writeback.md +22 -0
  206. data/TODO.fixup/67-delete-dead-code.md +28 -0
  207. data/TODO.fixup/68-figure-parity-status.md +82 -0
  208. data/TODO.fixup/69-package-polygon-rendering.md +54 -0
  209. data/TODO.fixup/70-tagged-values-compartment.md +39 -0
  210. data/TODO.fixup/71-auto-detect-theme-from-xmi.md +37 -0
  211. data/TODO.fixup/72-sample-exports-as-regression-specs.md +37 -0
  212. data/TODO.fixup/73-delete-dead-transformations.md +14 -0
  213. data/TODO.fixup/74-delete-dead-diagram-renderers.md +18 -0
  214. data/TODO.fixup/75-visibility-legend-icons.md +23 -0
  215. data/TODO.fixup/76-plateau-text-undershoot.md +27 -0
  216. data/TODO.fixup/77-canvas-frame-margins.md +28 -0
  217. data/TODO.fixup/78-note-element-rendering.md +30 -0
  218. data/TODO.fixup/79-operation-compartment-rendering.md +27 -0
  219. data/TODO.fixup/80-tagged-value-stereotype-visibility.md +37 -0
  220. data/TODO.fixup/81-figure-parity-current-state.md +63 -0
  221. data/config/qea_schema.yml +356 -0
  222. data/config/stereotype_colors.yml +15 -0
  223. data/config/themes/119.yml +98 -0
  224. data/config/themes/default.yml +14 -0
  225. data/examples/exports/20251010_current_plateau_v5/UML_EA.DTD +6856 -0
  226. data/examples/exports/20251010_current_plateau_v5/model.xml +96709 -0
  227. data/examples/exports/ArcGISWorkspace_template/UML_EA.DTD +6856 -0
  228. data/examples/exports/ArcGISWorkspace_template/model.xml +1427 -0
  229. data/examples/exports/UmlModel_template/UML_EA.DTD +6856 -0
  230. data/examples/exports/UmlModel_template/model.xml +23 -0
  231. data/examples/exports/basic/UML_EA.DTD +6856 -0
  232. data/examples/exports/basic/model.xml +8039 -0
  233. data/examples/exports/simple/UML_EA.DTD +6856 -0
  234. data/examples/exports/simple/model.xml +585 -0
  235. data/examples/exports/simple_example/UML_EA.DTD +6856 -0
  236. data/examples/exports/simple_example/model.xml +23 -0
  237. data/examples/exports/test/UML_EA.DTD +6856 -0
  238. data/examples/exports/test/model.xml +1638 -0
  239. data/examples/qea/basic.qea +0 -0
  240. data/frontend/README.md +51 -0
  241. data/frontend/dist/app.iife.js +24 -0
  242. data/frontend/dist/style.css +1 -0
  243. data/frontend/index.html +26 -0
  244. data/frontend/package-lock.json +2359 -0
  245. data/frontend/package.json +26 -0
  246. data/frontend/src/App.vue +59 -0
  247. data/frontend/src/app.ts +8 -0
  248. data/frontend/src/components/AppHeader.vue +127 -0
  249. data/frontend/src/components/AppSidebar.vue +119 -0
  250. data/frontend/src/components/ClassDetails.vue +247 -0
  251. data/frontend/src/components/DiagramView.vue +79 -0
  252. data/frontend/src/components/PackageDetails.vue +92 -0
  253. data/frontend/src/components/PackageTreeNode.vue +64 -0
  254. data/frontend/src/components/SearchResults.vue +48 -0
  255. data/frontend/src/components/WelcomeView.vue +87 -0
  256. data/frontend/src/shims-vue.d.ts +5 -0
  257. data/frontend/src/stores/dataStore.ts +79 -0
  258. data/frontend/src/stores/uiStore.ts +167 -0
  259. data/frontend/src/styles/main.css +1566 -0
  260. data/frontend/src/types.ts +221 -0
  261. data/frontend/tests/e2e/__screenshots__/new-dark-mode.png +0 -0
  262. data/frontend/tests/e2e/__screenshots__/new-package-details.png +0 -0
  263. data/frontend/tests/e2e/__screenshots__/new-search-modal.png +0 -0
  264. data/frontend/tests/e2e/__screenshots__/new-sidebar-collapsed.png +0 -0
  265. data/frontend/tests/e2e/__screenshots__/new-welcome.png +0 -0
  266. data/frontend/tests/e2e/render.spec.ts +188 -0
  267. data/frontend/tsconfig.json +20 -0
  268. data/frontend/vite.config.ts +26 -0
  269. data/frontend/vitest.config.ts +8 -0
  270. data/lib/ea/cli/app.rb +62 -1
  271. data/lib/ea/cli/command/diagrams.rb +37 -28
  272. data/lib/ea/cli/command/diff.rb +46 -0
  273. data/lib/ea/cli/command/export.rb +80 -0
  274. data/lib/ea/cli/command/info.rb +75 -0
  275. data/lib/ea/cli/command/lint.rb +48 -0
  276. data/lib/ea/cli/command/mdg.rb +98 -0
  277. data/lib/ea/cli/command/query.rb +34 -0
  278. data/lib/ea/cli/command/render.rb +135 -0
  279. data/lib/ea/cli/command/svg.rb +43 -6
  280. data/lib/ea/cli/command.rb +7 -0
  281. data/lib/ea/diagram/display_config.rb +184 -0
  282. data/lib/ea/diagram.rb +4 -41
  283. data/lib/ea/diff/change.rb +30 -0
  284. data/lib/ea/diff/comparator.rb +82 -0
  285. data/lib/ea/diff/html_reporter.rb +91 -0
  286. data/lib/ea/diff.rb +15 -0
  287. data/lib/ea/export/json/generator.rb +42 -0
  288. data/lib/ea/export/json.rb +12 -0
  289. data/lib/ea/export/json_schema/generator.rb +77 -0
  290. data/lib/ea/export/json_schema.rb +12 -0
  291. data/lib/ea/export/plantuml/generator.rb +73 -0
  292. data/lib/ea/export/plantuml.rb +12 -0
  293. data/lib/ea/export/xmi/generator.rb +77 -0
  294. data/lib/ea/export/xmi.rb +11 -0
  295. data/lib/ea/export/xsd/class_mapping.rb +65 -0
  296. data/lib/ea/export/xsd/generator.rb +142 -0
  297. data/lib/ea/export/xsd/namespace_registry.rb +82 -0
  298. data/lib/ea/export/xsd.rb +19 -0
  299. data/lib/ea/export.rb +14 -0
  300. data/lib/ea/image/emf_renderer.rb +42 -0
  301. data/lib/ea/image.rb +12 -0
  302. data/lib/ea/lint/engine.rb +30 -0
  303. data/lib/ea/lint/lint_rule.rb +38 -0
  304. data/lib/ea/lint/offense.rb +18 -0
  305. data/lib/ea/lint/rules/cyclic_generalization.rb +62 -0
  306. data/lib/ea/lint/rules/duplicate_name.rb +28 -0
  307. data/lib/ea/lint/rules/missing_stereotype.rb +35 -0
  308. data/lib/ea/lint/rules/naming_convention.rb +44 -0
  309. data/lib/ea/lint/rules/orphan_element.rb +28 -0
  310. data/lib/ea/lint/rules.rb +14 -0
  311. data/lib/ea/lint.rb +13 -0
  312. data/lib/ea/mdg/document.rb +120 -0
  313. data/lib/ea/mdg/loader.rb +118 -0
  314. data/lib/ea/mdg/registry.rb +98 -0
  315. data/lib/ea/mdg/stereotype_alias_registry.rb +77 -0
  316. data/lib/ea/mdg/xml/applies_to.rb +18 -0
  317. data/lib/ea/mdg/xml/apply.rb +18 -0
  318. data/lib/ea/mdg/xml/content.rb +18 -0
  319. data/lib/ea/mdg/xml/documentation.rb +24 -0
  320. data/lib/ea/mdg/xml/stereotype.rb +28 -0
  321. data/lib/ea/mdg/xml/stereotypes.rb +18 -0
  322. data/lib/ea/mdg/xml/tag.rb +28 -0
  323. data/lib/ea/mdg/xml/tagged_values.rb +18 -0
  324. data/lib/ea/mdg/xml/technology.rb +28 -0
  325. data/lib/ea/mdg/xml/uml_profile.rb +22 -0
  326. data/lib/ea/mdg/xml/uml_profiles.rb +18 -0
  327. data/lib/ea/mdg/xml.rb +24 -0
  328. data/lib/ea/mdg.rb +25 -0
  329. data/lib/ea/model/classifier.rb +4 -0
  330. data/lib/ea/model/constraint.rb +24 -0
  331. data/lib/ea/model/diagram.rb +85 -0
  332. data/lib/ea/model/diagram_connector.rb +53 -1
  333. data/lib/ea/model/diagram_element.rb +44 -0
  334. data/lib/ea/model/document.rb +47 -2
  335. data/lib/ea/model/ghost_label.rb +32 -0
  336. data/lib/ea/model/instance_specification.rb +34 -0
  337. data/lib/ea/model/legend.rb +43 -0
  338. data/lib/ea/model/legend_item.rb +26 -0
  339. data/lib/ea/model/note.rb +28 -0
  340. data/lib/ea/model/operation.rb +2 -0
  341. data/lib/ea/model/property.rb +2 -0
  342. data/lib/ea/model/relationship.rb +2 -0
  343. data/lib/ea/model/signal.rb +14 -0
  344. data/lib/ea/model/slot.rb +19 -0
  345. data/lib/ea/model.rb +8 -0
  346. data/lib/ea/ocl/evaluator.rb +94 -0
  347. data/lib/ea/ocl/nodes.rb +30 -0
  348. data/lib/ea/ocl/parser.rb +133 -0
  349. data/lib/ea/ocl/unsupported_error.rb +7 -0
  350. data/lib/ea/ocl.rb +13 -0
  351. data/lib/ea/qea/factory/document_builder.rb +24 -10
  352. data/lib/ea/qea/factory/generalization_builder.rb +9 -11
  353. data/lib/ea/qea/infrastructure/table_reader.rb +9 -0
  354. data/lib/ea/qea/models/ea_author.rb +35 -0
  355. data/lib/ea/qea/models/ea_glossary.rb +34 -0
  356. data/lib/ea/qea/models/ea_image.rb +53 -0
  357. data/lib/ea/qea/models/ea_implement.rb +32 -0
  358. data/lib/ea/qea/models/ea_list.rb +42 -0
  359. data/lib/ea/qea/models/ea_object_effort.rb +34 -0
  360. data/lib/ea/qea/models/ea_object_problem.rb +37 -0
  361. data/lib/ea/qea/models/ea_object_require.rb +34 -0
  362. data/lib/ea/qea/models/ea_object_resource.rb +34 -0
  363. data/lib/ea/qea/models/ea_object_risk.rb +34 -0
  364. data/lib/ea/qea/models/ea_object_scenario.rb +34 -0
  365. data/lib/ea/qea/models/ea_object_test.rb +35 -0
  366. data/lib/ea/qea/models/ea_object_trx.rb +34 -0
  367. data/lib/ea/qea/models/ea_operation.rb +7 -0
  368. data/lib/ea/qea/models/ea_palette.rb +30 -0
  369. data/lib/ea/qea/models/ea_palette_item.rb +36 -0
  370. data/lib/ea/qea/models/ea_phase.rb +40 -0
  371. data/lib/ea/qea/models/ea_role_constraint.rb +32 -0
  372. data/lib/ea/qea/models/ea_secrypt.rb +34 -0
  373. data/lib/ea/qea/models/ea_version.rb +39 -0
  374. data/lib/ea/qea/models/ea_xref.rb +11 -1
  375. data/lib/ea/qea/models.rb +19 -0
  376. data/lib/ea/qea/services/database_loader.rb +22 -1
  377. data/lib/ea/query/builder.rb +108 -0
  378. data/lib/ea/query.rb +9 -0
  379. data/lib/ea/render/image_converter.rb +101 -0
  380. data/lib/ea/render.rb +10 -0
  381. data/lib/ea/shapescript/parser.rb +208 -0
  382. data/lib/ea/shapescript/renderer.rb +74 -0
  383. data/lib/ea/shapescript/shape.rb +34 -0
  384. data/lib/ea/shapescript.rb +24 -0
  385. data/lib/ea/sources/qea/adapter.rb +22 -5
  386. data/lib/ea/sources/qea/classifier_builder.rb +184 -7
  387. data/lib/ea/sources/qea/diagram_builder.rb +352 -24
  388. data/lib/ea/sources/qea/id_normalizer.rb +16 -1
  389. data/lib/ea/sources/qea/instance_builder.rb +113 -0
  390. data/lib/ea/sources/qea/legend_builder.rb +137 -0
  391. data/lib/ea/sources/qea/note_builder.rb +58 -0
  392. data/lib/ea/sources/qea/object_classifier_map.rb +6 -1
  393. data/lib/ea/sources/qea/operation_builder.rb +1 -0
  394. data/lib/ea/sources/qea/package_builder.rb +16 -0
  395. data/lib/ea/sources/qea/relationship_builder.rb +21 -5
  396. data/lib/ea/sources/qea/tagged_value_builder.rb +28 -9
  397. data/lib/ea/sources/qea/xref/legend_definition.rb +35 -0
  398. data/lib/ea/sources/qea/xref/parser.rb +150 -0
  399. data/lib/ea/sources/qea/xref/property_definition.rb +29 -0
  400. data/lib/ea/sources/qea/xref/record.rb +41 -0
  401. data/lib/ea/sources/qea/xref/stereotype_application.rb +37 -0
  402. data/lib/ea/sources/qea/xref.rb +27 -0
  403. data/lib/ea/sources/qea.rb +4 -0
  404. data/lib/ea/sources/xmi/adapter.rb +80 -2
  405. data/lib/ea/sources/xmi/annotation_builder.rb +1 -1
  406. data/lib/ea/sources/xmi/classifier_builder.rb +10 -1
  407. data/lib/ea/sources/xmi/diagram_builder.rb +242 -45
  408. data/lib/ea/sources/xmi/extension_elements.rb +74 -0
  409. data/lib/ea/sources/xmi/extension_geometry_parser.rb +124 -0
  410. data/lib/ea/sources/xmi/extension_style_parser.rb +91 -0
  411. data/lib/ea/sources/xmi/note_builder.rb +46 -0
  412. data/lib/ea/sources/xmi/property_builder.rb +9 -0
  413. data/lib/ea/sources/xmi/stereotype_builder.rb +54 -0
  414. data/lib/ea/sources/xmi/tag_builder.rb +45 -0
  415. data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +87 -0
  416. data/lib/ea/sources/xmi.rb +7 -0
  417. data/lib/ea/spa/output/sharded_multi_file_strategy.rb +18 -3
  418. data/lib/ea/spa/output/single_file_strategy.rb +8 -2
  419. data/lib/ea/spa/output/strategy.rb +34 -0
  420. data/lib/ea/svg/connector_router.rb +134 -0
  421. data/lib/ea/svg/ea_emitter/background.rb +15 -0
  422. data/lib/ea/svg/ea_emitter/bounds_calculator.rb +142 -0
  423. data/lib/ea/svg/ea_emitter/canvas.rb +88 -0
  424. data/lib/ea/svg/ea_emitter/color_resolver.rb +56 -0
  425. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +28 -0
  426. data/lib/ea/svg/ea_emitter/compartment/constraints.rb +30 -0
  427. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +86 -0
  428. data/lib/ea/svg/ea_emitter/compartment/header.rb +32 -0
  429. data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +57 -0
  430. data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +96 -0
  431. data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +67 -0
  432. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +52 -0
  433. data/lib/ea/svg/ea_emitter/compartment/note_text.rb +102 -0
  434. data/lib/ea/svg/ea_emitter/compartment/operations.rb +38 -0
  435. data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +72 -0
  436. data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +136 -0
  437. data/lib/ea/svg/ea_emitter/compartment/shape.rb +82 -0
  438. data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +30 -0
  439. data/lib/ea/svg/ea_emitter/compartment.rb +58 -0
  440. data/lib/ea/svg/ea_emitter/connectors.rb +84 -0
  441. data/lib/ea/svg/ea_emitter/diagram_frame.rb +143 -0
  442. data/lib/ea/svg/ea_emitter/document.rb +35 -0
  443. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +117 -0
  444. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +119 -0
  445. data/lib/ea/svg/ea_emitter/element/bcol_decoder.rb +29 -0
  446. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +113 -0
  447. data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +53 -0
  448. data/lib/ea/svg/ea_emitter/element/divider_renderer.rb +17 -0
  449. data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +34 -0
  450. data/lib/ea/svg/ea_emitter/element/filter.rb +44 -0
  451. data/lib/ea/svg/ea_emitter/element/hand_draw_shape_renderer.rb +77 -0
  452. data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +43 -0
  453. data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +27 -0
  454. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +76 -0
  455. data/lib/ea/svg/ea_emitter/element/header_line_provider/parent_ghost.rb +25 -0
  456. data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +68 -0
  457. data/lib/ea/svg/ea_emitter/element/header_line_provider.rb +19 -0
  458. data/lib/ea/svg/ea_emitter/element/header_lines.rb +27 -0
  459. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +48 -0
  460. data/lib/ea/svg/ea_emitter/element/icon_renderer.rb +109 -0
  461. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +177 -0
  462. data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +112 -0
  463. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +100 -0
  464. data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +181 -0
  465. data/lib/ea/svg/ea_emitter/element/row_icon_renderer.rb +156 -0
  466. data/lib/ea/svg/ea_emitter/element/shape_renderer.rb +17 -0
  467. data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +82 -0
  468. data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +50 -0
  469. data/lib/ea/svg/ea_emitter/element/text_escape.rb +20 -0
  470. data/lib/ea/svg/ea_emitter/element.rb +37 -0
  471. data/lib/ea/svg/ea_emitter/elements.rb +357 -0
  472. data/lib/ea/svg/ea_emitter/font_resolver.rb +111 -0
  473. data/lib/ea/svg/ea_emitter/ghost_labels.rb +66 -0
  474. data/lib/ea/svg/ea_emitter/label/end_label.rb +266 -0
  475. data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +101 -0
  476. data/lib/ea/svg/ea_emitter/label/registry.rb +50 -0
  477. data/lib/ea/svg/ea_emitter/label.rb +23 -0
  478. data/lib/ea/svg/ea_emitter/labels.rb +130 -0
  479. data/lib/ea/svg/ea_emitter/layer.rb +20 -0
  480. data/lib/ea/svg/ea_emitter/layer_sequencer.rb +93 -0
  481. data/lib/ea/svg/ea_emitter/marker/arrow_path.rb +52 -0
  482. data/lib/ea/svg/ea_emitter/marker/diamond.rb +57 -0
  483. data/lib/ea/svg/ea_emitter/marker/kind.rb +35 -0
  484. data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +27 -0
  485. data/lib/ea/svg/ea_emitter/marker/package_import.rb +39 -0
  486. data/lib/ea/svg/ea_emitter/marker/plus.rb +40 -0
  487. data/lib/ea/svg/ea_emitter/marker/registry.rb +42 -0
  488. data/lib/ea/svg/ea_emitter/marker.rb +37 -0
  489. data/lib/ea/svg/ea_emitter/markers.rb +404 -0
  490. data/lib/ea/svg/ea_emitter/render_context.rb +52 -0
  491. data/lib/ea/svg/ea_emitter/style.rb +15 -0
  492. data/lib/ea/svg/ea_emitter/text_renderer.rb +92 -0
  493. data/lib/ea/svg/ea_emitter.rb +51 -0
  494. data/lib/ea/svg/parity/checker.rb +105 -0
  495. data/lib/ea/svg/parity/source.rb +59 -0
  496. data/lib/ea/svg/parity/suite.rb +113 -0
  497. data/lib/ea/svg/parity.rb +14 -0
  498. data/lib/ea/svg/stereotype_color_resolver.rb +50 -0
  499. data/lib/ea/svg.rb +4 -15
  500. data/lib/ea/theme/definition.rb +176 -0
  501. data/lib/ea/theme/loader.rb +125 -0
  502. data/lib/ea/theme/registry.rb +96 -0
  503. data/lib/ea/theme.rb +37 -0
  504. data/lib/ea/transformations.rb +5 -88
  505. data/lib/ea/version.rb +4 -1
  506. data/lib/ea.rb +14 -4
  507. data/lib/tasks/svg_bench.rake +87 -0
  508. data/lutaml-uml-0.5.1.gem +0 -0
  509. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes-list.png +0 -0
  510. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes.png +0 -0
  511. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-2-gradients.png +0 -0
  512. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-3-colors.png +0 -0
  513. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance-fonts.png +0 -0
  514. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance.png +0 -0
  515. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-5-behavior.png +0 -0
  516. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-6-sequence.png +0 -0
  517. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram.png +0 -0
  518. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-2-objects.png +0 -0
  519. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-3-links.png +0 -0
  520. metadata +473 -26
  521. data/lib/ea/diagram/configuration.rb +0 -379
  522. data/lib/ea/diagram/element_renderers/base_renderer.rb +0 -77
  523. data/lib/ea/diagram/element_renderers/class_renderer.rb +0 -323
  524. data/lib/ea/diagram/element_renderers/connector_renderer.rb +0 -41
  525. data/lib/ea/diagram/element_renderers/package_renderer.rb +0 -61
  526. data/lib/ea/diagram/element_renderers.rb +0 -43
  527. data/lib/ea/diagram/extractor.rb +0 -566
  528. data/lib/ea/diagram/layout_engine.rb +0 -170
  529. data/lib/ea/diagram/path_builder.rb +0 -202
  530. data/lib/ea/diagram/style_parser.rb +0 -42
  531. data/lib/ea/diagram/style_resolver.rb +0 -291
  532. data/lib/ea/diagram/svg_renderer.rb +0 -274
  533. data/lib/ea/diagram/util.rb +0 -73
  534. data/lib/ea/svg/bounds_calculator.rb +0 -74
  535. data/lib/ea/svg/connector_path.rb +0 -138
  536. data/lib/ea/svg/element_box.rb +0 -256
  537. data/lib/ea/svg/renderer.rb +0 -77
  538. data/lib/ea/svg/style_resolver.rb +0 -72
  539. data/lib/ea/transformations/configuration.rb +0 -333
  540. data/lib/ea/transformations/format_registry.rb +0 -366
  541. data/lib/ea/transformations/parsers/base_parser.rb +0 -482
  542. data/lib/ea/transformations/parsers/qea_parser.rb +0 -401
  543. data/lib/ea/transformations/parsers/xmi_parser.rb +0 -243
  544. data/lib/ea/transformations/transformation_engine.rb +0 -390
@@ -8,10 +8,11 @@ module Ea
8
8
  # ObjectClassifierMap for dispatch (OCP — new classifier
9
9
  # kinds need no edit here).
10
10
  class ClassifierBuilder
11
- attr_reader :database
11
+ attr_reader :database, :mdg_registry
12
12
 
13
- def initialize(database)
13
+ def initialize(database, mdg_registry: nil)
14
14
  @database = database
15
+ @mdg_registry = mdg_registry
15
16
  end
16
17
 
17
18
  def build_all
@@ -23,12 +24,53 @@ module Ea
23
24
  end
24
25
 
25
26
  def build_one(object)
26
- klass = ObjectClassifierMap.class_for(object.object_type)
27
+ klass = classifier_class_for(object)
27
28
  args = common_args(object)
28
29
  args.merge!(enumeration_args(object)) if klass == Ea::Model::Enumeration
30
+ args.delete(:properties) if klass == Ea::Model::Enumeration
29
31
  klass.new(**args)
30
32
  end
31
33
 
34
+ # EA's `t_object.Object_Type` selects the concrete classifier
35
+ # subclass, but a Class with the "enumeration" stereotype is
36
+ # rendered as an Enumeration (literals compartment, no
37
+ # visibility marker). Promote the model class when the
38
+ # stereotype says so.
39
+ def classifier_class_for(object)
40
+ base = ObjectClassifierMap.class_for(object.object_type)
41
+ return base unless base == Ea::Model::Klass
42
+ return Ea::Model::Enumeration if enumeration_stereotype?(object)
43
+
44
+ base
45
+ end
46
+
47
+ def enumeration_stereotype?(object)
48
+ stereotype = object.stereotype.to_s
49
+ !stereotype.empty? && stereotype.downcase == "enumeration"
50
+ end
51
+
52
+ # Resolves the classifier's full attribute list. Combines:
53
+ # 1. Own properties from t_attribute
54
+ # 2. Inherited properties from MDG-defined ancestor classes,
55
+ # when an mdg_registry is provided AND the classifier's
56
+ # applied stereotype maps to an MDG class.
57
+ #
58
+ # Public so integration tests can drive the merge directly
59
+ # without stubbing the entire build_one pipeline.
60
+ #
61
+ # When no mdg_registry is wired in, this returns just the
62
+ # own properties — preserving prior behavior.
63
+ def properties_for(object)
64
+ own = PropertyBuilder.new(database).build_all_for(object)
65
+ return own unless mdg_registry
66
+
67
+ inherited = mdg_inherited_properties_for(object)
68
+ return own if inherited.empty?
69
+
70
+ own_names = own.to_set { |p| p.name.to_s }
71
+ own + inherited.reject { |p| own_names.include?(p.name.to_s) }
72
+ end
73
+
32
74
  private
33
75
 
34
76
  def each_classifier_object
@@ -49,18 +91,123 @@ module Ea
49
91
  id: IdNormalizer.from_guid(object.ea_guid),
50
92
  name: object.name,
51
93
  package_id: package_id_for(object),
94
+ package_name: package_name_for(object),
52
95
  qualified_name: qualified_name_for(object),
53
96
  is_abstract: abstract?(object),
54
97
  visibility: visibility_from_scope(object.scope),
55
- properties: PropertyBuilder.new(database).build_all_for(object),
98
+ properties: properties_for(object),
56
99
  operations: OperationBuilder.new(database).build_all_for(object),
57
100
  stereotype_refs: stereotype_refs_for(object),
58
- tagged_values: TaggedValueBuilder.new(database).for_object(object.ea_guid),
101
+ tagged_values: TaggedValueBuilder.new(database)
102
+ .for_object(object.ea_guid,
103
+ ea_object_id: object.ea_object_id),
104
+ constraints: constraints_for(object),
59
105
  annotations: AnnotationBuilder.from_note(object.note, object.ea_guid,
60
106
  kind: "documentation")
61
107
  }
62
108
  end
63
109
 
110
+ # Looks up the classifier's applied stereotype's MDG class
111
+ # via the registry and returns its inherited properties.
112
+ # Returns [] when no stereotype or no matching MDG class.
113
+ def mdg_inherited_properties_for(object)
114
+ stereotype = stereotype_target_name(object)
115
+ return [] if stereotype.nil? || stereotype.empty?
116
+
117
+ classifier = mdg_registry.find_classifier_by_name(stereotype)
118
+ return [] unless classifier
119
+
120
+ mdg_registry.inherited_properties_for(classifier.id).map do |entry|
121
+ build_property_from_mdg_entry(entry)
122
+ end
123
+ end
124
+
125
+ # EA's stereotype field can carry either the simple name
126
+ # ("Type") or the FQName ("GML::Type"). We extract the
127
+ # class name from either form for MDG lookup.
128
+ def stereotype_target_name(object)
129
+ raw = object.stereotype.to_s
130
+ return nil if raw.empty?
131
+
132
+ # FQName format: "MDGName::ClassName" — take the part
133
+ # after the last "::". EA stores the simple form more
134
+ # often than the FQName.
135
+ raw.split("::").last
136
+ end
137
+
138
+ # EA renders association SourceRole names as virtual
139
+ # "property" attributes on the SOURCE classifier. When a
140
+ # connector has SourceRole="lod1ImplicitRepresentation",
141
+ # EA shows "+lod1ImplicitRepresentation" inside the source
142
+ # element's attribute compartment with «property»
143
+ # stereotype.
144
+ def association_role_properties_for(object)
145
+ connectors = database.collections[:connectors] || []
146
+ connectors.filter_map do |conn|
147
+ next nil unless conn.start_object_id == object.ea_object_id
148
+
149
+ role = conn.sourcerole.to_s
150
+ next nil if role.empty?
151
+
152
+ build_assoc_property(conn, role, object)
153
+ end
154
+ end
155
+
156
+ def build_assoc_property(conn, role, owner_object)
157
+ target = database.find_object(conn.end_object_id) if conn.end_object_id&.to_i&.positive?
158
+ Ea::Model::Property.new(
159
+ id: IdNormalizer.from_guid(conn.ea_guid),
160
+ name: role,
161
+ owner_id: IdNormalizer.from_guid(owner_object.ea_guid),
162
+ type_name: target&.name,
163
+ qualified_name: "#{owner_object.name}::#{role}",
164
+ multiplicity_lower: parse_cardinality(conn.sourcecard, :lower),
165
+ multiplicity_upper: parse_cardinality(conn.sourcecard, :upper),
166
+ visibility: "public",
167
+ aggregation: "none",
168
+ stereotype_refs: ["property"],
169
+ tagged_values: [],
170
+ annotations: []
171
+ )
172
+ end
173
+
174
+ def parse_cardinality(card_str, bound)
175
+ return nil if card_str.nil? || card_str.to_s.empty?
176
+
177
+ s = card_str.to_s
178
+ parts = s.split("..")
179
+ val = bound == :lower ? parts.first : (parts.size > 1 ? parts.last : parts.first)
180
+ val = val.to_s.strip
181
+ return -1 if val == "*"
182
+
183
+ Integer(val)
184
+ rescue ArgumentError
185
+ nil
186
+ end
187
+
188
+ def build_property_from_mdg_entry(entry)
189
+ Ea::Model::Property.new(
190
+ id: "mdg:#{entry.name}",
191
+ name: entry.name,
192
+ visibility: entry.visibility || "public",
193
+ multiplicity_lower: entry.multiplicity_lower,
194
+ multiplicity_upper: entry.multiplicity_upper
195
+ )
196
+ end
197
+
198
+ def constraints_for(object)
199
+ rows = database.constraints_for_object(object.ea_object_id) || []
200
+ rows.map do |row|
201
+ Ea::Model::Constraint.new(
202
+ id: IdNormalizer.from_guid("{#{row.constraint_id}}"),
203
+ name: row.constraint.to_s,
204
+ kind: row.constraint_type.to_s,
205
+ body: row.notes.to_s,
206
+ status: row.status.to_s
207
+ )
208
+ end
209
+ end
210
+
64
211
  def enumeration_args(object)
65
212
  literals = enumeration_literals_for(object)
66
213
  { literals: literals }
@@ -89,10 +236,40 @@ module Ea
89
236
  IdNormalizer.from_guid(pkg.ea_guid)
90
237
  end
91
238
 
239
+ def package_name_for(object)
240
+ pkg = database.find_package(object.package_id)
241
+ pkg&.name
242
+ end
243
+
244
+ # EA renders classifier names with a qualifier prefix using
245
+ # "::" — package prefix for top-level classes, parent-class
246
+ # prefix for nested classes. The QEA t_object.Name may
247
+ # already include a ":" prefix (e.g. "gml:CodeType"); use
248
+ # it verbatim.
92
249
  def qualified_name_for(object)
250
+ name = object.name.to_s
251
+ return name if name.include?(":")
252
+ return name if name.empty?
253
+
254
+ parent_name = nested_parent_name(object)
255
+ return "#{parent_name}::#{name}" if parent_name
256
+
93
257
  pkg = database.find_package(object.package_id)
94
- pkg_prefix = pkg&.xmlpath || pkg&.name
95
- pkg_prefix ? "#{pkg_prefix}::#{object.name}" : object.name
258
+ pkg_name = pkg&.name
259
+ pkg_name ? "#{pkg_name}::#{name}" : name
260
+ end
261
+
262
+ # When t_object.ParentID points at a Class (not a Package),
263
+ # the object is a nested classifier. EA qualifies its name
264
+ # with the parent class's name, not the package name.
265
+ def nested_parent_name(object)
266
+ parent_id_value = object.parentid
267
+ return nil unless parent_id_value && parent_id_value.to_i.positive?
268
+
269
+ parent = database.find_object(parent_id_value.to_i)
270
+ return nil unless parent&.object_type == "Class"
271
+
272
+ parent.name.to_s
96
273
  end
97
274
 
98
275
  def abstract?(object)
@@ -26,6 +26,12 @@ module Ea
26
26
  package_id: package_id_for(diagram_row),
27
27
  diagram_type: diagram_row.diagram_type,
28
28
  bounds: bounds_for(diagram_row),
29
+ style: diagram_row.pdata,
30
+ style_ex: diagram_row.styleex,
31
+ show_package_contents: package_contents_enabled?(diagram_row),
32
+ hand_draw: hand_draw_enabled?(diagram_row),
33
+ show_notes: show_notes_enabled?(diagram_row),
34
+ show_parents: show_parents_enabled?(diagram_row),
29
35
  elements: build_elements(diagram_row),
30
36
  connectors: build_connectors(diagram_row),
31
37
  annotations: AnnotationBuilder.from_note(diagram_row.notes,
@@ -34,6 +40,42 @@ module Ea
34
40
  )
35
41
  end
36
42
 
43
+ # EA encodes "show package contents" as a non-zero integer in
44
+ # t_diagram.ShowPackageContents (1 in basic.qea, 255 in
45
+ # test.qea). When true, the package body lists its child
46
+ # classifiers/sub-packages as "+ Name" rows.
47
+ def package_contents_enabled?(diagram_row)
48
+ return false if diagram_row.showpackagecontents.nil?
49
+
50
+ diagram_row.showpackagecontents.to_i.nonzero?
51
+ end
52
+
53
+ # HandDraw=1 in t_diagram.StyleEx triggers EA's wavy
54
+ # hand-drawn border style for every element on the diagram.
55
+ def hand_draw_enabled?(diagram_row)
56
+ style_ex = diagram_row.styleex.to_s
57
+ match = style_ex.match(/HandDraw=(\d)/)
58
+ match && match[1] == "1"
59
+ end
60
+
61
+ # ShowNotes=1 in t_diagram.StyleEx renders the classifier's
62
+ # documentation note as wrapped text in the element body.
63
+ def show_notes_enabled?(diagram_row)
64
+ style_ex = diagram_row.styleex.to_s
65
+ match = style_ex.match(/ShowNotes=(\d)/)
66
+ match && match[1] == "1"
67
+ end
68
+
69
+ # HideParents=1 in t_diagram.PDATA suppresses EA's "off-canvas
70
+ # parent class" ghost line — the italic parent name that EA
71
+ # normally prepends to a placed element's header when its
72
+ # generalization parent is not placed on the diagram.
73
+ def show_parents_enabled?(diagram_row)
74
+ pdata = diagram_row.pdata.to_s
75
+ match = pdata.match(/HideParents=(\d)/)
76
+ match ? match[1] != "1" : true
77
+ end
78
+
37
79
  private
38
80
 
39
81
  def build_elements(diagram_row)
@@ -42,41 +84,245 @@ module Ea
42
84
  end
43
85
 
44
86
  def build_element(obj_row, diagram_row)
87
+ style_hash = DiagramStyleParser.parse(obj_row.objectstyle)
45
88
  Ea::Model::DiagramElement.new(
46
89
  id: IdNormalizer.synthetic("de", diagram_row.diagram_id,
47
90
  obj_row.instance_id),
48
91
  diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
49
92
  model_element_ref: ref_for_object(obj_row),
50
93
  bounds: bounds_from_rect(obj_row),
51
- style: DiagramStyleParser.parse(obj_row.objectstyle)
94
+ style: style_hash,
95
+ font_family: style_hash[:font],
96
+ font_bold: truthy?(style_hash[:bold]),
97
+ font_italic: truthy?(style_hash[:italic]),
98
+ show_tagged_values: truthy?(style_hash[:tag])
52
99
  )
53
100
  end
54
101
 
102
+ def truthy?(raw)
103
+ raw == "1" || raw == "-1"
104
+ end
105
+
55
106
  def build_connectors(diagram_row)
56
107
  links = database.diagram_links_for(diagram_row.diagram_id) || []
57
- links.map { |link_row| build_connector(link_row, diagram_row) }
108
+ explicit = links.map { |link_row| build_connector(link_row, diagram_row) }
109
+ phantom = phantom_connectors(diagram_row, explicit)
110
+ explicit + phantom
111
+ end
112
+
113
+ # EA renders PHANTOM CONNECTORS for associations that exist
114
+ # in t_connector between two elements BOTH placed on the
115
+ # current diagram, even when no t_diagramlinks entry exists
116
+ # for that connector on this diagram. The phantom connector
117
+ # renders as a straight line between the two element edges,
118
+ # with SourceRole labels and appropriate markers.
119
+ #
120
+ # Only Association and Aggregation types produce phantom
121
+ # connectors. Generalization, Dependency, and Package types
122
+ # do NOT get phantom rendering.
123
+ PHANTOM_CONNECTOR_TYPES = %w[Association Aggregation].freeze
124
+
125
+ def phantom_connectors(diagram_row, explicit_connectors)
126
+ placed_ids = placed_object_ids_on(diagram_row.diagram_id)
127
+ explicit_conn_ids = explicit_connectors.map(&:relationship_ref).compact.to_set
128
+
129
+ all_connectors = database.collections[:connectors] || []
130
+ all_connectors.filter_map.with_index do |conn, idx|
131
+ next nil unless PHANTOM_CONNECTOR_TYPES.include?(conn.connector_type)
132
+ next nil unless placed_ids.include?(conn.start_object_id.to_i)
133
+ next nil unless placed_ids.include?(conn.end_object_id.to_i)
134
+
135
+ ref = ref_for_raw_connector(conn)
136
+ next nil if explicit_conn_ids.include?(ref)
137
+
138
+ build_phantom_connector(conn, diagram_row, idx)
139
+ end
140
+ end
141
+
142
+ def ref_for_raw_connector(conn)
143
+ IdNormalizer.from_guid(conn.ea_guid)
144
+ end
145
+
146
+ # Default label geometry for phantom connectors. EA renders
147
+ # SourceRole + «property» + multiplicity at default offsets
148
+ # from the source endpoint when no explicit geometry exists.
149
+ DEFAULT_LABEL_BOXES = {
150
+ llb: { "ox" => 0, "oy" => 0 },
151
+ llt: { "ox" => 0, "oy" => 0 }
152
+ }.freeze
153
+
154
+ def build_phantom_connector(conn, diagram_row, idx)
155
+ source_placement = diagram_object_placement(diagram_row.diagram_id,
156
+ conn.start_object_id)
157
+ target_placement = diagram_object_placement(diagram_row.diagram_id,
158
+ conn.end_object_id)
159
+ return nil unless source_placement && target_placement
160
+
161
+ source_bounds = bounds_from_rect(source_placement)
162
+ target_bounds = bounds_from_rect(target_placement)
163
+ source_point = bounds_edge_point(source_bounds, target_bounds)
164
+ target_point = bounds_edge_point(target_bounds, source_bounds)
165
+
166
+ waypoints = [
167
+ Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: source_point[0], y: source_point[1])),
168
+ Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: target_point[0], y: target_point[1]))
169
+ ]
170
+
171
+ Ea::Model::DiagramConnector.new(
172
+ id: IdNormalizer.synthetic("pc", diagram_row.diagram_id, idx),
173
+ diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
174
+ relationship_ref: ref_for_raw_connector(conn),
175
+ connector_type: conn.connector_type,
176
+ direction: conn.direction,
177
+ waypoints: waypoints,
178
+ label_boxes: DEFAULT_LABEL_BOXES,
179
+ style: {},
180
+ hidden: false,
181
+ has_geometry_offsets: false
182
+ )
183
+ end
184
+
185
+ # Computes the edge attachment point on source bounds that
186
+ # faces the target bounds. Uses the center-to-center direction
187
+ # to pick the nearest edge intersection.
188
+ def bounds_edge_point(bounds, other_bounds)
189
+ cx = bounds.x + bounds.width / 2
190
+ cy = bounds.y + bounds.height / 2
191
+ ocx = other_bounds.x + other_bounds.width / 2
192
+ ocy = other_bounds.y + other_bounds.height / 2
193
+
194
+ dx = ocx - cx
195
+ dy = ocy - cy
196
+
197
+ if dx.abs * bounds.height > dy.abs * bounds.width
198
+ x = dx > 0 ? bounds.x + bounds.width : bounds.x
199
+ y = cy + dy * (x - cx).abs / dx.abs rescue cy
200
+ [x, y]
201
+ else
202
+ y = dy > 0 ? bounds.y + bounds.height : bounds.y
203
+ x = cx + dx * (y - cy).abs / dy.abs rescue cx
204
+ [x, y]
205
+ end
58
206
  end
59
207
 
60
208
  def build_connector(link_row, diagram_row)
209
+ connector = database.find_connector(link_row.connectorid)
210
+ geom = parse_geometry_fields(link_row.geometry)
61
211
  Ea::Model::DiagramConnector.new(
62
212
  id: IdNormalizer.synthetic("dc", diagram_row.diagram_id,
63
213
  link_row.instance_id),
64
214
  diagram_id: IdNormalizer.from_guid(diagram_row.ea_guid),
65
215
  relationship_ref: ref_for_connector(link_row),
216
+ connector_type: connector&.connector_type,
217
+ direction: connector&.direction,
66
218
  waypoints: waypoints_for_link(link_row, diagram_row),
67
- style: DiagramStyleParser.parse(link_row.style)
219
+ label_boxes: geom[:label_boxes] || {},
220
+ style: DiagramStyleParser.parse(link_row.style),
221
+ hidden: hidden?(link_row),
222
+ ghost_labels: ghost_labels_for(connector, diagram_row, link_row),
223
+ has_geometry_offsets: geometry_has_offsets?(link_row.geometry)
68
224
  )
69
225
  end
70
226
 
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.
227
+ # EA's t_diagramlinks.Geometry may carry explicit SX/SY/EX/EY
228
+ # offset fields (even zeros) OR omit them entirely. The
229
+ # presence of SX= gates the «import» package_anchor marker:
230
+ # basic.qea Package Imports has SX=0;SY=0 marker renders;
231
+ # plateau Urban Planning ADE2 omits SX/SY no marker.
232
+ def geometry_has_offsets?(geometry_str)
233
+ return false if geometry_str.nil? || geometry_str.empty?
234
+
235
+ geometry_str.to_s.include?("SX=")
236
+ end
237
+
238
+ # EA's t_diagramlinks.Hidden is 1 when the user has hidden
239
+ # the connector on this diagram (right-click → Hide). Hidden
240
+ # connectors are absent from the reference SVG output.
241
+ def hidden?(link_row)
242
+ link_row.hidden.to_i.nonzero?
243
+ end
244
+
245
+ # Builds GhostLabel entries for connector endpoints that
246
+ # reference objects NOT placed on the current diagram. EA
247
+ # renders the off-canvas classifier's name as italic text
248
+ # near the connector's on-canvas endpoint so the reader
249
+ # knows what the connector attaches to outside the diagram.
250
+ def ghost_labels_for(connector, diagram_row, link_row)
251
+ return [] unless connector
252
+
253
+ placed_ids = placed_object_ids_on(diagram_row.diagram_id)
254
+ ghosts = []
255
+ %i[source target].each do |end_kind|
256
+ ea_object_id = end_kind == :source ? connector.start_object_id : connector.end_object_id
257
+ next unless ea_object_id&.to_i&.positive?
258
+ next if placed_ids.include?(ea_object_id.to_i)
259
+
260
+ obj = database.find_object(ea_object_id.to_i)
261
+ next unless obj
262
+ next if obj.name.to_s.strip.empty?
263
+
264
+ anchor_point = ghost_anchor(end_kind, connector, link_row, diagram_row)
265
+ next unless anchor_point
266
+
267
+ ghosts << Ea::Model::GhostLabel.new(
268
+ id: IdNormalizer.synthetic("ghost", link_row.instance_id, ea_object_id.to_i),
269
+ name: obj.name.to_s,
270
+ end_kind: end_kind.to_s,
271
+ anchor_x: anchor_point[0],
272
+ anchor_y: anchor_point[1]
273
+ )
274
+ end
275
+ ghosts
276
+ end
277
+
278
+ # Returns the Array of EA object IDs that have a placement
279
+ # (t_diagramobjects row) on the given diagram.
280
+ def placed_object_ids_on(diagram_id)
281
+ objects = database.diagram_objects_for(diagram_id) || []
282
+ objects.map(&:ea_object_id)
283
+ end
284
+
285
+ # Ghost label anchors at the on-canvas end of the connector.
286
+ # When the source is off-canvas, anchor at the target's
287
+ # first waypoint; vice versa for target off-canvas.
288
+ def ghost_anchor(end_kind, connector, link_row, diagram_row)
289
+ placement = if end_kind == :source
290
+ target_placement_for(connector, diagram_row)
291
+ else
292
+ source_placement_for(connector, diagram_row)
293
+ end
294
+ return nil unless placement
295
+
296
+ # Use the on-canvas end's edge center as the anchor.
297
+ b = bounds_from_rect(placement)
298
+ # Offset to the right of the element so the label sits
299
+ # outside the on-canvas element. EA's actual offset is
300
+ # roughly +12px in x and -8px in y from the element's
301
+ # right-center edge.
302
+ [b.x + b.width + 4, b.y + (b.height / 2) - 8]
303
+ end
304
+
305
+ def source_placement_for(connector, diagram_row)
306
+ diagram_object_placement(diagram_row.diagram_id, connector.start_object_id)
307
+ end
308
+
309
+ def target_placement_for(connector, diagram_row)
310
+ diagram_object_placement(diagram_row.diagram_id, connector.end_object_id)
311
+ end
312
+
313
+ # EA stores connector routing in two places:
314
+ #
315
+ # - t_diagramlinks.Path: explicit intermediate waypoints as
316
+ # `x:y;x:y;...` in the element-rect coordinate frame.
317
+ # Present when the user has manually routed the connector
318
+ # or when EA's auto-router produced multi-bend paths.
319
+ # - t_diagramlinks.Geometry SX/SY/EX/EY: delta fields that
320
+ # describe the source-side and target-side bend offsets
321
+ # relative to the edge attachment points.
322
+ #
323
+ # Path takes precedence when present — it's the authoritative
324
+ # routing. SX/SY/EX/EY are fallback for auto-routed direct
325
+ # connectors with at most one bend on each end.
80
326
  #
81
327
  # If we can't resolve the source or target element placement
82
328
  # for this diagram, we emit no waypoints (the connector will
@@ -96,21 +342,40 @@ module Ea
96
342
 
97
343
  source_point = element_edge_point(source_placement, :source, edge_out)
98
344
  target_point = element_edge_point(target_placement, :target, edge_out)
345
+
346
+ intermediate = intermediate_waypoints(link_row)
347
+ if intermediate.any?
348
+ points = [source_point, *intermediate, target_point]
349
+ else
350
+ points = sx_sy_ex_ey_waypoints(source_point, target_point, geom)
351
+ end
352
+
353
+ points.map do |x, y|
354
+ Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y))
355
+ end
356
+ end
357
+
358
+ # Parse t_diagramlinks.Path into [[x, y], ...] intermediate
359
+ # waypoints. Format: `x1:y1;x2:y2;...`. Empty string returns [].
360
+ def intermediate_waypoints(link_row)
361
+ raw = link_row.path.to_s
362
+ return [] if raw.empty?
363
+
364
+ raw.scan(/(-?\d+):(-?\d+)/).map { |x, y| [x.to_i, y.to_i] }
365
+ end
366
+
367
+ # Fallback when no explicit Path: compute source/target bend
368
+ # points from the SX/SY/EX/EY delta fields.
369
+ def sx_sy_ex_ey_waypoints(source_point, target_point, geom)
99
370
  points = [source_point]
100
- # SX/SY: delta from source point to first bend.
101
371
  if geom[:sx] && geom[:sy] && (geom[:sx].nonzero? || geom[:sy].nonzero?)
102
372
  points << [source_point[0] + geom[:sx], source_point[1] + geom[:sy]]
103
373
  end
104
- # EX/EY: delta from second bend to target point. Reverse
105
- # to get from target back to the bend.
106
374
  if geom[:ex] && geom[:ey] && (geom[:ex].nonzero? || geom[:ey].nonzero?)
107
375
  points << [target_point[0] - geom[:ex], target_point[1] - geom[:ey]]
108
376
  end
109
377
  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
378
+ points
114
379
  end
115
380
 
116
381
  # Parse the SX, SY, EX, EY, EDGE key=value pairs out of the
@@ -126,7 +391,61 @@ module Ea
126
391
  sy: pick_int(s, /SY=(-?\d+)/),
127
392
  ex: pick_int(s, /EX=(-?\d+)/),
128
393
  ey: pick_int(s, /EY=(-?\d+)/),
129
- edge: pick_int(s, /EDGE=(-?\d+)/)
394
+ edge: pick_int(s, /EDGE=(-?\d+)/),
395
+ label_boxes: parse_label_boxes(s)
396
+ }
397
+ end
398
+
399
+ # Parse EA's $LLB=, $LLT=, $LRB=, $LRT= label box geometry.
400
+ # Format: $LLB=CX=<int>:CY=<int>:OX=<int>:OY=<int>:HDN=0:BLD=0:
401
+ # ITA=0:UND=0:CLR=-1:ALN=0:DIR=0:ROT=0;
402
+ # Only the FIRST segment carries the leading `$`; subsequent
403
+ # segments use bare `KEY=` after the semicolon separator.
404
+ #
405
+ # Returns Hash keyed by :llb, :llt, :lrt, :lrb (and optional
406
+ # :lmt, :lmb, :irhs, :ilhs). Each value is a Hash with:
407
+ # - "ox", "oy" — label offset from anchor (always set)
408
+ # - "cx", "cy" — cell size for label box
409
+ # - "hidden" — bool, label suppressed when true
410
+ # - "bold", "italic", "underline" — bool font flags
411
+ # - "color" — signed int (VB OLE_COLOR; -1 = default)
412
+ # - "alignment" — 0=left, 1=center, 2=right
413
+ # - "direction" — text direction enum
414
+ # - "rotation" — degrees
415
+ def parse_label_boxes(geom_str)
416
+ boxes = {}
417
+ %i[llb llt lrt lrb lmt lmb irhs ilhs].each do |key|
418
+ match = geom_str.match(/(?:\$|;)\s*#{key.to_s.upcase}=([^;]*)/i)
419
+ next unless match && !match[1].empty?
420
+
421
+ parsed = parse_label_style(match[1])
422
+ next unless parsed
423
+
424
+ boxes[key] = parsed
425
+ end
426
+ boxes
427
+ end
428
+
429
+ # Parse one label box's colon-separated key=value body into
430
+ # a Hash with string keys. Returns nil if OX/OY are absent.
431
+ def parse_label_style(body)
432
+ ox = body.match(/OX=(-?\d+)/)
433
+ oy = body.match(/OY=(-?\d+)/)
434
+ return nil unless ox && oy
435
+
436
+ {
437
+ "ox" => ox[1].to_i,
438
+ "oy" => oy[1].to_i,
439
+ "cx" => pick_int(body, /CX=(-?\d+)/),
440
+ "cy" => pick_int(body, /CY=(-?\d+)/),
441
+ "hidden" => pick_int(body, /HDN=(\d+)/) == 1,
442
+ "bold" => pick_int(body, /BLD=(\d+)/) == 1,
443
+ "italic" => pick_int(body, /ITA=(\d+)/) == 1,
444
+ "underline" => pick_int(body, /UND=(\d+)/) == 1,
445
+ "color" => pick_int(body, /CLR=(-?\d+)/),
446
+ "alignment" => pick_int(body, /ALN=(\d+)/),
447
+ "direction" => pick_int(body, /DIR=(\d+)/),
448
+ "rotation" => pick_int(body, /ROT=(-?\d+)/)
130
449
  }
131
450
  end
132
451
 
@@ -186,12 +505,21 @@ module Ea
186
505
  )
187
506
  end
188
507
 
508
+ # EA's t_diagramobjects stores rect with the origin at the
509
+ # bottom-left (math convention) — recttop > rectbottom for
510
+ # valid bounds, and y values are often negative. Convert to
511
+ # screen-space (origin top-left) by taking min/max and using
512
+ # absolute width/height.
189
513
  def bounds_from_rect(obj_row)
514
+ left = obj_row.rectleft || 0
515
+ right = obj_row.rectright || 0
516
+ top = obj_row.recttop || 0
517
+ bottom = obj_row.rectbottom || 0
190
518
  Ea::Model::Bounds.new(
191
- x: obj_row.rectleft || 0,
192
- y: obj_row.recttop || 0,
193
- width: (obj_row.rectright || 0) - (obj_row.rectleft || 0),
194
- height: (obj_row.rectbottom || 0) - (obj_row.recttop || 0)
519
+ x: [left, right].min,
520
+ y: [top, bottom].min,
521
+ width: (right - left).abs,
522
+ height: (bottom - top).abs
195
523
  )
196
524
  end
197
525