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
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ module Label
7
+ # Renders the role + «property» + multiplicity cluster at one
8
+ # endpoint of a UML association connector.
9
+ #
10
+ # EA encodes end-labels in two geometry boxes per endpoint:
11
+ #
12
+ # LLT/LRT - "label text" position (role name slot)
13
+ # LLB/LRB - "label box" position (multiplicity slot)
14
+ #
15
+ # Each box carries OX/OY offsets from the connector anchor.
16
+ # The «property» stereotype renders one line below the role
17
+ # name. EA records qualified role names ("pkg::role") for
18
+ # cross-package associations but suppresses label rendering
19
+ # for those — only simple role names render.
20
+ class EndLabel
21
+ PROPERTY_STEREOTYPE = "«property»"
22
+
23
+ attr_reader :canvas, :model_index, :document, :theme,
24
+ :font_family, :font_size, :font_unit
25
+
26
+ def initialize(canvas:, model_index:, document:, theme:,
27
+ font_family:, font_size:, font_unit:)
28
+ @canvas = canvas
29
+ @model_index = model_index
30
+ @document = document
31
+ @theme = theme
32
+ @font_family = font_family
33
+ @font_size = font_size
34
+ @font_unit = font_unit
35
+ end
36
+
37
+ # Returns an Array of `<text>` SVG strings for one endpoint.
38
+ #
39
+ # text_box - LLT or LRT box geometry (role position)
40
+ # mult_box - LLB or LRB box geometry (mult position)
41
+ # anchor - [x, y] of the connector endpoint
42
+ # connector - the DiagramConnector (for relationship lookup)
43
+ # end_kind - :source or :target
44
+ def texts(text_box:, mult_box:, anchor:, connector:, end_kind:)
45
+ from_property, role, mult = role_and_multiplicity(connector, end_kind)
46
+ if role_empty?(role) && mult_empty?(mult)
47
+ other_from_prop, other_role, other_mult = role_and_multiplicity(connector,
48
+ opposite(end_kind))
49
+ role = other_role if role_empty?(role)
50
+ mult ||= other_mult
51
+ from_property ||= other_from_prop
52
+ end
53
+ agg = aggregation_kind_for(connector, end_kind)
54
+ standalone = standalone_multiplicity(role, mult, aggregation: agg)
55
+ return [] if role_empty?(role) && (standalone.nil? || standalone.empty?)
56
+
57
+ text_pos = position_at(text_box, anchor)
58
+ mult_pos = position_at(mult_box, anchor) || text_pos
59
+ show_property = property_label?(connector)
60
+
61
+ build_texts(role:, mult:, standalone:,
62
+ text_pos:, mult_pos:, show_property:)
63
+ end
64
+
65
+ private
66
+
67
+ def build_texts(role:, mult:, standalone:, text_pos:, mult_pos:,
68
+ show_property:)
69
+ if role && !role.empty? && text_pos
70
+ x, y = text_pos
71
+ texts = [text_at(x, y, role)]
72
+ if show_property
73
+ texts << text_at(x, y + property_y_offset, PROPERTY_STEREOTYPE)
74
+ end
75
+ if mult && !mult.empty? && mult_pos
76
+ texts << text_at(*mult_pos, mult)
77
+ end
78
+ texts
79
+ elsif standalone && !standalone.empty? && mult_pos
80
+ [text_at(*mult_pos, standalone)]
81
+ else
82
+ []
83
+ end
84
+ end
85
+
86
+ # EA renders «property» between role name and multiplicity
87
+ # ONLY for directed associations/aggregations — those with
88
+ # t_connector.Direction set to "Source -> Destination" or
89
+ # "Destination -> Source". "Unspecified" / "Bi-Directional"
90
+ # connectors do NOT get the stereotype label.
91
+ #
92
+ # Verified against both QEAs (output by the same EA in the
93
+ # same run):
94
+ # - basic.qea: 4 Associations with Direction="Unspecified"
95
+ # and role+mult → 0 «property» labels.
96
+ # - plateau QEA: aggregations with Direction="Destination
97
+ # -> Source" → «property» rendered (737 instances across
98
+ # 137 diagrams).
99
+ UNDIRECTED_DIRECTIONS = %w[Unspecified Bi-Directional].freeze
100
+
101
+ def property_label?(connector)
102
+ direction = connector.direction.to_s
103
+ return false if direction.empty?
104
+ return false if UNDIRECTED_DIRECTIONS.include?(direction)
105
+
106
+ true
107
+ end
108
+
109
+ def role_empty?(role)
110
+ role.nil? || role.empty?
111
+ end
112
+
113
+ def mult_empty?(mult)
114
+ mult.nil? || mult.empty?
115
+ end
116
+
117
+ def opposite(end_kind)
118
+ end_kind == :source ? :target : :source
119
+ end
120
+
121
+ def role_and_multiplicity(connector, end_kind)
122
+ assoc = association_for(connector)
123
+ return [nil, nil, nil] unless assoc
124
+
125
+ prop = property_at_end(assoc, end_kind)
126
+ return [true, *property_role(prop)] if prop
127
+
128
+ [false, *association_role(assoc, end_kind)]
129
+ end
130
+
131
+ def property_at_end(association, end_kind)
132
+ id = end_kind == :source ? association.source_id : association.target_id
133
+ return nil unless id
134
+
135
+ return document.property_by_id(id) if document
136
+
137
+ fallback_property_lookup(id)
138
+ end
139
+
140
+ # Linear-scan fallback for callers that did not wire in
141
+ # the Document. Production paths go through Document's
142
+ # property_index for O(1) lookup.
143
+ def fallback_property_lookup(id)
144
+ return nil unless model_index
145
+
146
+ model_index.each_value do |obj|
147
+ next unless obj.is_a?(Ea::Model::Classifier)
148
+
149
+ found = (obj.properties || []).find { |p| p.id == id }
150
+ return found if found
151
+ end
152
+ nil
153
+ end
154
+
155
+ def property_role(property)
156
+ visibility = visibility_prefix(property)
157
+ name = property.name.to_s
158
+ role = name.empty? ? nil : "#{visibility}#{name}"
159
+ mult = multiplicity_text(property)
160
+ [role, mult]
161
+ end
162
+
163
+ def association_role(assoc, end_kind)
164
+ name = end_kind == :source ? assoc.source_role_name : assoc.target_role_name
165
+ lower = end_kind == :source ? assoc.source_multiplicity_lower : assoc.target_multiplicity_lower
166
+ upper = end_kind == :source ? assoc.source_multiplicity_upper : assoc.target_multiplicity_upper
167
+ return [nil, multiplicity_string(lower, upper)] if name.to_s.include?("::")
168
+
169
+ role = name.nil? || name.empty? ? nil : "+#{name}"
170
+ mult = multiplicity_string(lower, upper)
171
+ [role, mult]
172
+ end
173
+
174
+ def association_for(connector)
175
+ return nil unless model_index
176
+
177
+ rel = model_index[connector.relationship_ref]
178
+ return nil unless rel.is_a?(Ea::Model::Association)
179
+
180
+ rel
181
+ end
182
+
183
+ # Returns the aggregation kind for the given end (:shared,
184
+ # :composite, or nil). An aggregation marker on the OPPOSITE
185
+ # end means THIS end is the "part" end of an aggregation —
186
+ # EA shows "1" multiplicity for such ends.
187
+ def aggregation_kind_for(connector, end_kind)
188
+ assoc = association_for(connector)
189
+ return nil unless assoc
190
+
191
+ opposite_kind = opposite(end_kind)
192
+ agg = opposite_kind == :source ? assoc.source_aggregation : assoc.target_aggregation
193
+ return nil if agg.nil? || agg == "none"
194
+
195
+ agg.to_sym
196
+ end
197
+
198
+ def position_at(box, anchor)
199
+ return nil unless box
200
+
201
+ offset_x = box["ox"] || box[:ox]
202
+ offset_y = box["oy"] || box[:oy]
203
+ return nil if offset_x.nil? && offset_y.nil?
204
+
205
+ [anchor[0] + offset_x.to_i, anchor[1] + offset_y.to_i]
206
+ end
207
+
208
+ def property_y_offset
209
+ font_size + 6
210
+ end
211
+
212
+ def multiplicity_text(property)
213
+ multiplicity_string(property.multiplicity_lower,
214
+ property.multiplicity_upper)
215
+ end
216
+
217
+ def multiplicity_string(lower, upper)
218
+ return nil if lower.nil? && upper.nil?
219
+ return "1" if lower == 1 && upper == 1
220
+
221
+ upper_str = upper == -1 ? "*" : upper.to_s
222
+ "#{lower || 0}..#{upper_str}"
223
+ end
224
+
225
+ # EA suppresses standalone multiplicity when it is
226
+ # unspecified (nil). Explicit multiplicity renders
227
+ # regardless of aggregation kind. The QEA parser now
228
+ # returns nil for absent SourceCard/DestCard so default
229
+ # "1" is no longer produced — only explicit "1" reaches
230
+ # this method.
231
+ def standalone_multiplicity(role, mult, aggregation: nil)
232
+ return nil if role && !role.empty?
233
+ return nil if mult.nil?
234
+
235
+ mult
236
+ end
237
+
238
+ def visibility_prefix(property)
239
+ case property.visibility
240
+ when "private" then "-"
241
+ when "protected" then "#"
242
+ when "package" then "~"
243
+ else "+"
244
+ end
245
+ end
246
+
247
+ def text_at(x, y, content)
248
+ return nil if content.nil? || content.empty?
249
+
250
+ x_t = canvas ? canvas.translate_x(x) : x
251
+ y_t = canvas ? canvas.translate_y(y) : y
252
+ TextRenderer.new(content:, x: x_t, y: y_t,
253
+ family: font_family, size: font_size,
254
+ size_unit: font_unit,
255
+ fill: fill_color,
256
+ text_length: content.length * 6).to_svg
257
+ end
258
+
259
+ def fill_color
260
+ "#000000"
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ module Label
7
+ # Renders the midpoint label for a connector. EA emits one
8
+ # of two things at the midpoint, but not both:
9
+ #
10
+ # 1. «stereotype» when the relationship has an applied
11
+ # stereotype (e.g. «import», «realize»).
12
+ # 2. The relationship's Name when no stereotype is applied
13
+ # and Name is non-empty (e.g. "Association A").
14
+ #
15
+ # Stereotype wins when both are present.
16
+ class MidpointLabel
17
+ attr_reader :canvas, :model_index, :font_family, :font_size,
18
+ :font_unit
19
+
20
+ def initialize(canvas:, model_index:, font_family:, font_size:,
21
+ font_unit:)
22
+ @canvas = canvas
23
+ @model_index = model_index
24
+ @font_family = font_family
25
+ @font_size = font_size
26
+ @font_unit = font_unit
27
+ end
28
+
29
+ # Returns the SVG `<text>` for the connector's midpoint
30
+ # label, or nil if there is no label to render.
31
+ def text_for(connector, points)
32
+ content = midpoint_content(connector)
33
+ return nil if content.nil?
34
+
35
+ mx, my = midpoint(points)
36
+ render_text(mx, my, content)
37
+ end
38
+
39
+ # Returns the formatted «stereotype» string or nil. Used
40
+ # by Label::Registry to dispatch end-label vs midpoint.
41
+ def stereotype_label(connector)
42
+ rel = relationship_for(connector)
43
+ return nil unless rel
44
+
45
+ stereotype = rel.stereotype
46
+ return nil if stereotype.nil? || stereotype.to_s.empty?
47
+
48
+ "«#{stereotype}»"
49
+ end
50
+
51
+ # Returns the midpoint label content: stereotype if
52
+ # present, else the relationship Name.
53
+ def midpoint_content(connector)
54
+ stereotype = stereotype_label(connector)
55
+ return stereotype if stereotype
56
+
57
+ rel = relationship_for(connector)
58
+ return nil unless rel
59
+
60
+ name = rel.name
61
+ return nil if name.nil? || name.to_s.empty?
62
+
63
+ name.to_s
64
+ end
65
+
66
+ def relationship_for(connector)
67
+ return nil unless model_index
68
+
69
+ rel = model_index[connector.relationship_ref]
70
+ return nil unless rel.is_a?(Ea::Model::Relationship)
71
+
72
+ rel
73
+ end
74
+
75
+ private
76
+
77
+ def midpoint(points)
78
+ mid_idx = points.size / 2
79
+ if points.size.odd?
80
+ points[mid_idx]
81
+ else
82
+ a = points[mid_idx - 1]
83
+ b = points[mid_idx]
84
+ [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
85
+ end
86
+ end
87
+
88
+ def render_text(x, y, content)
89
+ x_t = canvas ? canvas.translate_x(x) : x
90
+ y_t = canvas ? canvas.translate_y(y) : y
91
+ TextRenderer.new(content:, x: x_t, y: y_t,
92
+ family: font_family, size: font_size,
93
+ size_unit: font_unit,
94
+ fill: "#000000",
95
+ text_length: content.length * 6).to_svg
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ module Label
7
+ # Dispatches a connector to the appropriate label renderer(s).
8
+ #
9
+ # Rules:
10
+ # - Any relationship with an applied stereotype renders
11
+ # «stereotype» at the midpoint (Associations included —
12
+ # «import» on a Package connector is a common case).
13
+ # - Associations without a stereotype render EndLabel at
14
+ # each positioned LLT/LRT box.
15
+ # - Other relationship kinds without a stereotype render
16
+ # nothing.
17
+ #
18
+ # Open/closed: a new label kind registers here rather than
19
+ # forcing callers to edit case statements elsewhere.
20
+ class Registry
21
+ attr_reader :model_index
22
+
23
+ def initialize(model_index:)
24
+ @model_index = model_index
25
+ end
26
+
27
+ # Returns the formatted stereotype label when the
28
+ # connector should render at the midpoint, nil otherwise.
29
+ def midpoint?(connector)
30
+ MidpointLabel.new(canvas: nil, model_index: model_index,
31
+ font_family: nil, font_size: nil,
32
+ font_unit: nil).stereotype_label(connector)
33
+ end
34
+
35
+ # Returns true if the connector's relationship is an
36
+ # Association AND has no midpoint stereotype (the end-label
37
+ # path).
38
+ def end_label?(connector)
39
+ association?(connector) && !midpoint?(connector)
40
+ end
41
+
42
+ def association?(connector)
43
+ rel = model_index ? model_index[connector.relationship_ref] : nil
44
+ rel.is_a?(Ea::Model::Association)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Per-connector label renderers. Each renderer handles one
7
+ # visual label kind on a connector:
8
+ #
9
+ # EndLabel - role + «property» + multiplicity at
10
+ # LLT/LRT box positions (UML associations)
11
+ # MidpointLabel - «stereotype» at the connector midpoint
12
+ # (Package / Dependency / Realization)
13
+ #
14
+ # Registry dispatches by relationship kind so new label kinds
15
+ # register rather than editing the orchestrator (OCP).
16
+ module Label
17
+ autoload :EndLabel, "ea/svg/ea_emitter/label/end_label"
18
+ autoload :MidpointLabel, "ea/svg/ea_emitter/label/midpoint_label"
19
+ autoload :Registry, "ea/svg/ea_emitter/label/registry"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Emits the labels layer: connector role/multiplicity end
7
+ # labels plus midpoint stereotype labels for non-association
8
+ # connectors.
9
+ #
10
+ # This class is the orchestrator. The actual label-shaping
11
+ # logic lives in dedicated, MECE collaborators under
12
+ # `Ea::Svg::EaEmitter::Label`:
13
+ #
14
+ # Label::EndLabel - role + «property» + mult at LLT/LRT
15
+ # Label::MidpointLabel - «stereotype» at connector midpoint
16
+ # Label::Registry - dispatch by relationship kind (OCP)
17
+ #
18
+ # Theme is read once and threaded to collaborators so the
19
+ # rendering rules (font family/size, fill color) live in one
20
+ # place.
21
+ class Labels
22
+ DEFAULT_FAMILY = "Yu Gothic UI"
23
+ DEFAULT_SIZE = 13
24
+
25
+ attr_reader :diagram, :canvas, :model_index, :theme, :document
26
+
27
+ def initialize(diagram, canvas: nil, model_index: nil, theme: nil,
28
+ document: nil)
29
+ @diagram = diagram
30
+ @canvas = canvas
31
+ @model_index = model_index
32
+ @theme = theme || Ea::Theme::Registry.default
33
+ @document = document
34
+ end
35
+
36
+ def render
37
+ texts = visible_connectors.flat_map { |c| texts_for(c) }
38
+ return "" if texts.empty?
39
+
40
+ %(<g style="stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00">\n#{texts.join("\n")}\n</g>)
41
+ end
42
+
43
+ private
44
+
45
+ def visible_connectors
46
+ (diagram.connectors || []).select(&:renderable?)
47
+ end
48
+
49
+ def texts_for(connector)
50
+ points = waypoint_pairs(connector)
51
+ return [] if points.size < 2
52
+
53
+ texts = []
54
+
55
+ # Midpoint label: stereotype «import», or relationship
56
+ # Name ("Association A"). Renders at the path midpoint.
57
+ mid_text = midpoint_renderer.text_for(connector, points)
58
+ texts << mid_text if mid_text
59
+
60
+ # End-labels: role name + «property» + multiplicity at
61
+ # positioned LLT/LRT boxes. Only for associations without
62
+ # a midpoint stereotype (stereotyped associations route
63
+ # to midpoint only).
64
+ if registry.end_label?(connector)
65
+ end_label_texts(connector, points).each { |t| texts << t }
66
+ end
67
+
68
+ texts
69
+ end
70
+
71
+ def end_label_texts(connector, points)
72
+ source_pt = points.first
73
+ target_pt = points.last
74
+ boxes = connector.label_boxes || {}
75
+
76
+ texts = []
77
+ end_renderer.texts(text_box: boxes[:llt], mult_box: boxes[:llb],
78
+ anchor: source_pt, connector: connector,
79
+ end_kind: :source).each { |t| texts << t }
80
+ end_renderer.texts(text_box: boxes[:lrt], mult_box: boxes[:lrb],
81
+ anchor: target_pt, connector: connector,
82
+ end_kind: :target).each { |t| texts << t }
83
+ texts
84
+ end
85
+
86
+ def registry
87
+ @registry ||= Label::Registry.new(model_index: model_index)
88
+ end
89
+
90
+ def end_renderer
91
+ @end_renderer ||= Label::EndLabel.new(
92
+ canvas: canvas,
93
+ model_index: model_index,
94
+ document: document,
95
+ theme: theme,
96
+ font_family: label_font_family,
97
+ font_size: label_font_size,
98
+ font_unit: theme.font_size_unit
99
+ )
100
+ end
101
+
102
+ def midpoint_renderer
103
+ @midpoint_renderer ||= Label::MidpointLabel.new(
104
+ canvas: canvas,
105
+ model_index: model_index,
106
+ font_family: label_font_family,
107
+ font_size: label_font_size,
108
+ font_unit: theme.font_size_unit
109
+ )
110
+ end
111
+
112
+ def waypoint_pairs(connector)
113
+ (connector.waypoints || []).filter_map do |w|
114
+ next unless w.position
115
+
116
+ [w.position.x, w.position.y]
117
+ end
118
+ end
119
+
120
+ def label_font_family
121
+ theme.font_family || DEFAULT_FAMILY
122
+ end
123
+
124
+ def label_font_size
125
+ theme.font_size || DEFAULT_SIZE
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # A single `<g>` block ready for inclusion in the SVG output.
7
+ # Carries the style string and the body (one or more child
8
+ # elements: `<path>`, `<polygon>`, `<text>`, `<rect>`).
9
+ #
10
+ # Emitters return Arrays of Layer objects. The Document
11
+ # orchestrator either joins them as-is (per-entity ordering)
12
+ # or buckets them by `style_key` for EA-style grouped output.
13
+ Layer = Struct.new(:style_key, :style, :body, keyword_init: true) do
14
+ def to_svg
15
+ %(<g style="#{style}">\n#{body}\n</g>)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Sequences SVG layer output in EA's canonical order:
7
+ #
8
+ # 1. Background <g>
9
+ # 2. (optional) Diagram frame: border + tab + label
10
+ # 3. Per-element groups (shape, header, divider, attrs)
11
+ # 4. Connector + marker groups (merged by style)
12
+ # 5. Labels <g>
13
+ #
14
+ # Reads diagram.theme once and passes to all child renderers
15
+ # so theme settings (font, colors, stroke width) apply
16
+ # uniformly across the entire SVG output.
17
+ #
18
+ # Document delegates the layer assembly here so the Document
19
+ # class shrinks to SVG envelope construction.
20
+ class LayerSequencer
21
+ DEFAULT_STROKE_WIDTH = 2
22
+
23
+ attr_reader :diagram, :model_index, :canvas, :frame_enabled, :theme,
24
+ :document
25
+
26
+ def initialize(diagram, model_index:, canvas:, frame: false, document: nil)
27
+ @diagram = diagram
28
+ @model_index = model_index
29
+ @canvas = canvas
30
+ @frame_enabled = frame
31
+ @theme = diagram.theme
32
+ @document = document
33
+ end
34
+
35
+ def layers
36
+ layers = [Background.render(canvas)]
37
+ layers += frame_layers if frame_enabled
38
+ layers += element_layers
39
+ layers += connector_layers
40
+ layers << labels_layer
41
+ layers << ghost_labels_layer
42
+ layers
43
+ end
44
+
45
+ private
46
+
47
+ def frame_layers
48
+ DiagramFrame.new(canvas: canvas, theme: theme).layers(diagram).map(&:to_svg)
49
+ end
50
+
51
+ def element_layers
52
+ Elements.new(diagram, model_index: model_index, canvas: canvas,
53
+ document: document).groups
54
+ end
55
+
56
+ def connector_layers
57
+ connector_layers_raw = Connectors.new(diagram, canvas: canvas,
58
+ grouped: true,
59
+ stroke_width: DEFAULT_STROKE_WIDTH).layers
60
+ marker_layers_raw = Markers.new(diagram, model_index: model_index,
61
+ canvas: canvas, grouped: true,
62
+ stroke_width: DEFAULT_STROKE_WIDTH).layers
63
+ merge_layers_by_style(connector_layers_raw + marker_layers_raw).map(&:to_svg)
64
+ end
65
+
66
+ def labels_layer
67
+ Labels.new(diagram, canvas: canvas, model_index: model_index,
68
+ theme: theme, document: document).render
69
+ end
70
+
71
+ def ghost_labels_layer
72
+ GhostLabels.new(diagram, canvas: canvas).render
73
+ end
74
+
75
+ def merge_layers_by_style(layers)
76
+ merged = {}
77
+ layers.each do |layer|
78
+ if merged[layer.style_key]
79
+ merged[layer.style_key] = Layer.new(
80
+ style_key: layer.style_key,
81
+ style: layer.style,
82
+ body: "#{merged[layer.style_key].body}\n #{layer.body}"
83
+ )
84
+ else
85
+ merged[layer.style_key] = layer
86
+ end
87
+ end
88
+ merged.values
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end