ea 0.4.0 → 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 (529) 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/02-polygon-over-rendering.md +29 -0
  36. data/TODO.diagrams/03-text-over-rendering.md +30 -0
  37. data/TODO.diagrams/04-rect-under-rendering.md +33 -0
  38. data/TODO.diagrams/05-canvas-dimension-parity.md +51 -0
  39. data/TODO.diagrams/06-group-layering-parity.md +39 -0
  40. data/TODO.diagrams/07-text-content-fidelity.md +65 -0
  41. data/TODO.diagrams/08-qea-direct-pipeline.md +46 -0
  42. data/TODO.diagrams/12-connector-labels.md +40 -0
  43. data/TODO.diagrams/12-per-entity-svg-layer-ordering.md +24 -0
  44. data/TODO.diagrams/13-marker-polygon-vs-path-dispatch.md +20 -0
  45. data/TODO.diagrams/13-per-text-type-font-sizes.md +33 -0
  46. data/TODO.diagrams/14-center-header-text.md +33 -0
  47. data/TODO.diagrams/14-filter-diagram-frame-elements.md +18 -0
  48. data/TODO.diagrams/15-deep-dead-code-audit.md +47 -0
  49. data/TODO.diagrams/15-per-element-font-from-style.md +24 -0
  50. data/TODO.diagrams/16-canvas-marker-extent.md +30 -0
  51. data/TODO.diagrams/16-document-remaining-parity-gaps.md +91 -0
  52. data/TODO.diagrams/17-italic-abstract-classes.md +28 -0
  53. data/TODO.diagrams/17-remaining-parity-gaps.md +90 -0
  54. data/TODO.diagrams/18-canvas-from-image-bounds.md +20 -0
  55. data/TODO.diagrams/18-property-lookup-index.md +42 -0
  56. data/TODO.diagrams/19-connector-sx-sy-routing.md +27 -0
  57. data/TODO.diagrams/19-replace-respond-to-with-is-a.md +31 -0
  58. data/TODO.diagrams/20-split-labels-emitter.md +56 -0
  59. data/TODO.diagrams/20-visibility-glyph-font-encoding.md +27 -0
  60. data/TODO.diagrams/21-aggregation-navigability-arrow.md +21 -0
  61. data/TODO.diagrams/21-auto-generated-legend-block.md +65 -0
  62. data/TODO.diagrams/22-ghost-element-discriminator.md +57 -0
  63. data/TODO.diagrams/22-style-based-path-grouping.md +28 -0
  64. data/TODO.diagrams/23-marker-polygon-deduplication.md +47 -0
  65. data/TODO.diagrams/23-multiplicity-label-positioning.md +29 -0
  66. data/TODO.diagrams/24-marker-anchor-structural.md +35 -0
  67. data/TODO.diagrams/24-track-marker-anchors-structurally.md +66 -0
  68. data/TODO.diagrams/24-truncate-properties-to-fit-box.md +24 -0
  69. data/TODO.diagrams/26-phantom-general-parent-rendering.md +59 -0
  70. data/TODO.diagrams/27-path-grouping-mode-detection.md +43 -0
  71. data/TODO.diagrams/28-architecture-cleanup-batch.md +47 -0
  72. data/TODO.diagrams/30-decompose-elements-groups-for.md +52 -0
  73. data/TODO.diagrams/32-examples-coverage-batch.md +78 -0
  74. data/TODO.diagrams/36-instance-specification-model.md +42 -0
  75. data/TODO.diagrams/37-element-icon-decorations.md +41 -0
  76. data/TODO.diagrams/38-html-note-and-receptions.md +32 -0
  77. data/TODO.diagrams/39-property-stereotype-discriminator.md +40 -0
  78. data/TODO.diagrams/40-standalone-1-discriminator.md +38 -0
  79. data/TODO.diagrams/41-xmi-to-svg-pipeline.md +113 -0
  80. data/TODO.diagrams/42-close-all-parity-deltas.md +69 -0
  81. data/TODO.diagrams/43-marker-deduplication-strategy.md +54 -0
  82. data/TODO.diagrams/44-element-icon-decorations.md +34 -0
  83. data/TODO.diagrams/45-phantom-parent-class-boxes.md +40 -0
  84. data/TODO.diagrams/46-user-drawn-legend-swatches.md +44 -0
  85. data/TODO.diagrams/47-attribute-line-fidelity.md +72 -0
  86. data/TODO.diagrams/48-spec-coverage-critical-emitter-components.md +45 -0
  87. data/TODO.diagrams/49-spec-coverage-orchestrators.md +25 -0
  88. data/TODO.diagrams/50-examples-parity.md +85 -0
  89. data/TODO.diagrams/51-property-oy-gap-and-remaining-gaps.md +48 -0
  90. data/TODO.diagrams/52-marker-registration-architecture.md +89 -0
  91. data/TODO.diagrams/53-remaining-example-gaps.md +86 -0
  92. data/TODO.diagrams/54-display-name-nested-classes.md +40 -0
  93. data/TODO.diagrams/55-visibility-icon-discriminator.md +58 -0
  94. data/TODO.diagrams/56-package-contents-rendering.md +148 -0
  95. data/TODO.diagrams/57-instance-specification-model.md +100 -0
  96. data/TODO.diagrams/58-domain-model-note-text.md +67 -0
  97. data/TODO.diagrams/59-classes-arrow-discriminator.md +57 -0
  98. data/TODO.diagrams/60-testschema-association-marker.md +40 -0
  99. data/TODO.diagrams/61-legend-block-rendering.md +55 -0
  100. data/TODO.diagrams/61-plateau-parity-gaps.md +59 -0
  101. data/TODO.diagrams/62-ghost-element-rendering.md +56 -0
  102. data/TODO.diagrams/63-qualified-name-wrap.md +36 -0
  103. data/TODO.diagrams/64-property-stereotype-labels.md +22 -0
  104. data/TODO.diagrams/65-path-coordinate-frame.md +62 -0
  105. data/TODO.diagrams/67-path-coordinate-frame.md +62 -0
  106. data/TODO.diagrams/68-inherited-attributes.md +65 -0
  107. data/TODO.diagrams/69-package-anchor-conditional.md +29 -0
  108. data/TODO.diagrams/70-mdg-technology-support.md +68 -0
  109. data/TODO.diagrams/71-off-canvas-parent-ghost.md +45 -0
  110. data/TODO.diagrams/72-basic-attr-divider-paths.md +39 -0
  111. data/TODO.diagrams/73-plateau-polygon-gap.md +69 -0
  112. data/TODO.diagrams/74-plateau-rect-gap.md +22 -0
  113. data/TODO.diagrams/75-plateau-text-gap.md +39 -0
  114. data/TODO.diagrams/76-basic-poly-path-gaps.md +41 -0
  115. data/TODO.diagrams/77-basic-text-overrender.md +40 -0
  116. data/TODO.diagrams/78-headerlines-ocp-refactor.md +65 -0
  117. data/TODO.diagrams/79-architecture-backlog.md +65 -0
  118. data/TODO.diagrams/80-basic-triangle-polygons.md +51 -0
  119. data/TODO.diagrams/81-plateau-path-overrender.md +53 -0
  120. data/TODO.diagrams/82-appearance-canvas-offset.md +54 -0
  121. data/TODO.diagrams/83-3d-diagram-subtitle.md +58 -0
  122. data/TODO.diagrams/84-canvas-offset-mismatches.md +61 -0
  123. data/TODO.diagrams/85-phantom-property-overrender.md +95 -0
  124. data/TODO.diagrams/86-foreign-package-rule.md +62 -0
  125. data/TODO.diagrams/87-internal-tech-encryption.md +80 -0
  126. data/TODO.diagrams/EXAMPLES_STATUS.md +88 -0
  127. data/TODO.diagrams/README.md +69 -0
  128. data/TODO.diagrams/STATUS.md +73 -0
  129. data/TODO.fixup/01-default-grouped-paths.md +34 -0
  130. data/TODO.fixup/02-marker-consolidation-per-connector.md +20 -0
  131. data/TODO.fixup/03-diagram-font-inference.md +28 -0
  132. data/TODO.fixup/04-extract-bounds-calculator.md +25 -0
  133. data/TODO.fixup/05-marker-registry-ocp.md +32 -0
  134. data/TODO.fixup/06-connector-path-bend-routing.md +28 -0
  135. data/TODO.fixup/07-edge-attachment-formula.md +22 -0
  136. data/TODO.fixup/08-visual-diff-harness.md +28 -0
  137. data/TODO.fixup/09-extract-element-compartments.md +34 -0
  138. data/TODO.fixup/10-canvas-width-overshoot-fix.md +20 -0
  139. data/TODO.fixup/11-layer-sequencer-extraction.md +18 -0
  140. data/TODO.fixup/12-path-style-harmony.md +23 -0
  141. data/TODO.fixup/13-datatype-stereotype-fallback.md +28 -0
  142. data/TODO.fixup/14-ea-exe-reverse-engineering-notes.md +56 -0
  143. data/TODO.fixup/15-enumeration-literal-rendering.md +30 -0
  144. data/TODO.fixup/16-operation-compartment-rendering.md +42 -0
  145. data/TODO.fixup/17-path-count-calibration.md +27 -0
  146. data/TODO.fixup/18-polygon-count-calibration.md +32 -0
  147. data/TODO.fixup/19-rect-count-undershoot-investigation.md +40 -0
  148. data/TODO.fixup/20-stereotype-color-from-classifier.md +18 -0
  149. data/TODO.fixup/21-visibility-filter-for-properties.md +24 -0
  150. data/TODO.fixup/22-multiplicity-display-rule-calibration.md +25 -0
  151. data/TODO.fixup/23-diagram-frame-title-element.md +31 -0
  152. data/TODO.fixup/24-note-element-rendering.md +23 -0
  153. data/TODO.fixup/25-ea-exe-disassembly-findings.md +177 -0
  154. data/TODO.fixup/26-svg-renderer-architecture.md +78 -0
  155. data/TODO.fixup/27-svg-element-emitters-disassembly.md +135 -0
  156. data/TODO.fixup/28-geometry-parser-identification.md +57 -0
  157. data/TODO.fixup/29-qea-svg-xmi-triple-comparison.md +185 -0
  158. data/TODO.fixup/30-theme-system-stub.md +46 -0
  159. data/TODO.fixup/31-theme-value-object-registry.md +17 -0
  160. data/TODO.fixup/32-styleex-on-diagram-model.md +22 -0
  161. data/TODO.fixup/33-text-encoding-decimals-rotation.md +29 -0
  162. data/TODO.fixup/34-per-type-fill-via-theme.md +18 -0
  163. data/TODO.fixup/35-stroke-width-per-theme.md +7 -0
  164. data/TODO.fixup/36-qea-rect-coord-normalization.md +24 -0
  165. data/TODO.fixup/37-package-shape-rendering.md +22 -0
  166. data/TODO.fixup/38-note-element-rendering.md +15 -0
  167. data/TODO.fixup/39-styleex-flag-honoring.md +25 -0
  168. data/TODO.fixup/40-connector-dashed-lines.md +15 -0
  169. data/TODO.fixup/41-diagram-frame-showborder-detection.md +23 -0
  170. data/TODO.fixup/42-textrenderer-centralization.md +10 -0
  171. data/TODO.fixup/43-colorresolver-unification.md +24 -0
  172. data/TODO.fixup/44-theme-aware-textrenderer.md +17 -0
  173. data/TODO.fixup/45-theme-aware-fontresolver.md +22 -0
  174. data/TODO.fixup/46-style-parameterized-by-theme.md +21 -0
  175. data/TODO.fixup/47-canvas-frame-inset.md +19 -0
  176. data/TODO.fixup/48-background-rect-format.md +15 -0
  177. data/TODO.fixup/49-all-samples-integration-test.md +17 -0
  178. data/TODO.fixup/50-svg-emitter-architecture-docs.md +33 -0
  179. data/TODO.fixup/51-promote-theme-to-ea-namespace.md +28 -0
  180. data/TODO.fixup/52-theme-definition-integrated-fills.md +26 -0
  181. data/TODO.fixup/53-theme-registry-yaml-loading.md +50 -0
  182. data/TODO.fixup/54-diagram-theme-api.md +50 -0
  183. data/TODO.fixup/55-update-eaemitter-to-ea-theme.md +23 -0
  184. data/TODO.fixup/56-theme-yaml-files.md +65 -0
  185. data/TODO.fixup/57-theme-specs-and-integration.md +27 -0
  186. data/TODO.fixup/58-ea-entry-point-theme-autoload.md +20 -0
  187. data/TODO.fixup/59-thread-theme-through-all-renderers.md +29 -0
  188. data/TODO.fixup/60-cli-theme-option.md +22 -0
  189. data/TODO.fixup/61-theme-validation.md +28 -0
  190. data/TODO.fixup/62-theme-builder-dsl.md +31 -0
  191. data/TODO.fixup/63-theme-integration-rendering-spec.md +24 -0
  192. data/TODO.fixup/64-theme-writeback-styleex.md +21 -0
  193. data/TODO.fixup/65-remove-dead-eaemitter-theme-files.md +30 -0
  194. data/TODO.fixup/66-styleex-writeback.md +22 -0
  195. data/TODO.fixup/67-delete-dead-code.md +28 -0
  196. data/TODO.fixup/68-figure-parity-status.md +82 -0
  197. data/TODO.fixup/69-package-polygon-rendering.md +54 -0
  198. data/TODO.fixup/70-tagged-values-compartment.md +39 -0
  199. data/TODO.fixup/71-auto-detect-theme-from-xmi.md +37 -0
  200. data/TODO.fixup/72-sample-exports-as-regression-specs.md +37 -0
  201. data/TODO.fixup/73-delete-dead-transformations.md +14 -0
  202. data/TODO.fixup/74-delete-dead-diagram-renderers.md +18 -0
  203. data/TODO.fixup/75-visibility-legend-icons.md +23 -0
  204. data/TODO.fixup/76-plateau-text-undershoot.md +27 -0
  205. data/TODO.fixup/77-canvas-frame-margins.md +28 -0
  206. data/TODO.fixup/78-note-element-rendering.md +30 -0
  207. data/TODO.fixup/79-operation-compartment-rendering.md +27 -0
  208. data/TODO.fixup/80-tagged-value-stereotype-visibility.md +37 -0
  209. data/TODO.fixup/81-figure-parity-current-state.md +63 -0
  210. data/config/qea_schema.yml +356 -0
  211. data/config/themes/119.yml +98 -0
  212. data/config/themes/default.yml +14 -0
  213. data/examples/exports/20251010_current_plateau_v5/UML_EA.DTD +6856 -0
  214. data/examples/exports/20251010_current_plateau_v5/model.xml +96709 -0
  215. data/examples/exports/ArcGISWorkspace_template/UML_EA.DTD +6856 -0
  216. data/examples/exports/ArcGISWorkspace_template/model.xml +1427 -0
  217. data/examples/exports/UmlModel_template/UML_EA.DTD +6856 -0
  218. data/examples/exports/UmlModel_template/model.xml +23 -0
  219. data/examples/exports/basic/UML_EA.DTD +6856 -0
  220. data/examples/exports/basic/model.xml +8039 -0
  221. data/examples/exports/simple/UML_EA.DTD +6856 -0
  222. data/examples/exports/simple/model.xml +585 -0
  223. data/examples/exports/simple_example/UML_EA.DTD +6856 -0
  224. data/examples/exports/simple_example/model.xml +23 -0
  225. data/examples/exports/test/UML_EA.DTD +6856 -0
  226. data/examples/exports/test/model.xml +1638 -0
  227. data/examples/qea/basic.qea +0 -0
  228. data/frontend/README.md +51 -0
  229. data/frontend/dist/app.iife.js +24 -0
  230. data/frontend/dist/style.css +1 -0
  231. data/frontend/index.html +26 -0
  232. data/frontend/package-lock.json +2359 -0
  233. data/frontend/package.json +26 -0
  234. data/frontend/src/App.vue +59 -0
  235. data/frontend/src/app.ts +8 -0
  236. data/frontend/src/components/AppHeader.vue +127 -0
  237. data/frontend/src/components/AppSidebar.vue +119 -0
  238. data/frontend/src/components/ClassDetails.vue +247 -0
  239. data/frontend/src/components/DiagramView.vue +79 -0
  240. data/frontend/src/components/PackageDetails.vue +92 -0
  241. data/frontend/src/components/PackageTreeNode.vue +64 -0
  242. data/frontend/src/components/SearchResults.vue +48 -0
  243. data/frontend/src/components/WelcomeView.vue +87 -0
  244. data/frontend/src/shims-vue.d.ts +5 -0
  245. data/frontend/src/stores/dataStore.ts +79 -0
  246. data/frontend/src/stores/uiStore.ts +167 -0
  247. data/frontend/src/styles/main.css +1566 -0
  248. data/frontend/src/types.ts +221 -0
  249. data/frontend/tests/e2e/__screenshots__/new-dark-mode.png +0 -0
  250. data/frontend/tests/e2e/__screenshots__/new-package-details.png +0 -0
  251. data/frontend/tests/e2e/__screenshots__/new-search-modal.png +0 -0
  252. data/frontend/tests/e2e/__screenshots__/new-sidebar-collapsed.png +0 -0
  253. data/frontend/tests/e2e/__screenshots__/new-welcome.png +0 -0
  254. data/frontend/tests/e2e/render.spec.ts +188 -0
  255. data/frontend/tsconfig.json +20 -0
  256. data/frontend/vite.config.ts +26 -0
  257. data/frontend/vitest.config.ts +8 -0
  258. data/lib/ea/cli/app.rb +57 -2
  259. data/lib/ea/cli/command/diagrams.rb +37 -28
  260. data/lib/ea/cli/command/diff.rb +46 -0
  261. data/lib/ea/cli/command/export.rb +80 -0
  262. data/lib/ea/cli/command/info.rb +75 -0
  263. data/lib/ea/cli/command/lint.rb +48 -0
  264. data/lib/ea/cli/command/mdg.rb +98 -0
  265. data/lib/ea/cli/command/query.rb +34 -0
  266. data/lib/ea/cli/command/render.rb +135 -0
  267. data/lib/ea/cli/command/svg.rb +8 -15
  268. data/lib/ea/cli/command.rb +7 -0
  269. data/lib/ea/diagram/display_config.rb +184 -0
  270. data/lib/ea/diagram.rb +4 -41
  271. data/lib/ea/diff/change.rb +30 -0
  272. data/lib/ea/diff/comparator.rb +82 -0
  273. data/lib/ea/diff/html_reporter.rb +91 -0
  274. data/lib/ea/diff.rb +15 -0
  275. data/lib/ea/export/json/generator.rb +42 -0
  276. data/lib/ea/export/json.rb +12 -0
  277. data/lib/ea/export/json_schema/generator.rb +77 -0
  278. data/lib/ea/export/json_schema.rb +12 -0
  279. data/lib/ea/export/plantuml/generator.rb +73 -0
  280. data/lib/ea/export/plantuml.rb +12 -0
  281. data/lib/ea/export/xmi/generator.rb +77 -0
  282. data/lib/ea/export/xmi.rb +11 -0
  283. data/lib/ea/export/xsd/class_mapping.rb +65 -0
  284. data/lib/ea/export/xsd/generator.rb +142 -0
  285. data/lib/ea/export/xsd/namespace_registry.rb +82 -0
  286. data/lib/ea/export/xsd.rb +19 -0
  287. data/lib/ea/export.rb +14 -0
  288. data/lib/ea/image/emf_renderer.rb +42 -0
  289. data/lib/ea/image.rb +12 -0
  290. data/lib/ea/lint/engine.rb +30 -0
  291. data/lib/ea/lint/lint_rule.rb +38 -0
  292. data/lib/ea/lint/offense.rb +18 -0
  293. data/lib/ea/lint/rules/cyclic_generalization.rb +62 -0
  294. data/lib/ea/lint/rules/duplicate_name.rb +28 -0
  295. data/lib/ea/lint/rules/missing_stereotype.rb +35 -0
  296. data/lib/ea/lint/rules/naming_convention.rb +44 -0
  297. data/lib/ea/lint/rules/orphan_element.rb +28 -0
  298. data/lib/ea/lint/rules.rb +14 -0
  299. data/lib/ea/lint.rb +13 -0
  300. data/lib/ea/mdg/document.rb +120 -0
  301. data/lib/ea/mdg/loader.rb +118 -0
  302. data/lib/ea/mdg/registry.rb +98 -0
  303. data/lib/ea/mdg/stereotype_alias_registry.rb +77 -0
  304. data/lib/ea/mdg/xml/applies_to.rb +18 -0
  305. data/lib/ea/mdg/xml/apply.rb +18 -0
  306. data/lib/ea/mdg/xml/content.rb +18 -0
  307. data/lib/ea/mdg/xml/documentation.rb +24 -0
  308. data/lib/ea/mdg/xml/stereotype.rb +28 -0
  309. data/lib/ea/mdg/xml/stereotypes.rb +18 -0
  310. data/lib/ea/mdg/xml/tag.rb +28 -0
  311. data/lib/ea/mdg/xml/tagged_values.rb +18 -0
  312. data/lib/ea/mdg/xml/technology.rb +28 -0
  313. data/lib/ea/mdg/xml/uml_profile.rb +22 -0
  314. data/lib/ea/mdg/xml/uml_profiles.rb +18 -0
  315. data/lib/ea/mdg/xml.rb +24 -0
  316. data/lib/ea/mdg.rb +25 -0
  317. data/lib/ea/model/classifier.rb +4 -0
  318. data/lib/ea/model/constraint.rb +24 -0
  319. data/lib/ea/model/diagram.rb +85 -0
  320. data/lib/ea/model/diagram_connector.rb +34 -0
  321. data/lib/ea/model/diagram_element.rb +16 -0
  322. data/lib/ea/model/document.rb +47 -2
  323. data/lib/ea/model/ghost_label.rb +32 -0
  324. data/lib/ea/model/instance_specification.rb +34 -0
  325. data/lib/ea/model/legend.rb +43 -0
  326. data/lib/ea/model/legend_item.rb +26 -0
  327. data/lib/ea/model/note.rb +28 -0
  328. data/lib/ea/model/operation.rb +2 -0
  329. data/lib/ea/model/property.rb +2 -0
  330. data/lib/ea/model/relationship.rb +2 -0
  331. data/lib/ea/model/signal.rb +14 -0
  332. data/lib/ea/model/slot.rb +19 -0
  333. data/lib/ea/model.rb +8 -0
  334. data/lib/ea/ocl/evaluator.rb +94 -0
  335. data/lib/ea/ocl/nodes.rb +30 -0
  336. data/lib/ea/ocl/parser.rb +133 -0
  337. data/lib/ea/ocl/unsupported_error.rb +7 -0
  338. data/lib/ea/ocl.rb +13 -0
  339. data/lib/ea/qea/factory/document_builder.rb +24 -10
  340. data/lib/ea/qea/factory/generalization_builder.rb +9 -11
  341. data/lib/ea/qea/infrastructure/table_reader.rb +9 -0
  342. data/lib/ea/qea/models/ea_author.rb +35 -0
  343. data/lib/ea/qea/models/ea_glossary.rb +34 -0
  344. data/lib/ea/qea/models/ea_image.rb +53 -0
  345. data/lib/ea/qea/models/ea_implement.rb +32 -0
  346. data/lib/ea/qea/models/ea_list.rb +42 -0
  347. data/lib/ea/qea/models/ea_object_effort.rb +34 -0
  348. data/lib/ea/qea/models/ea_object_problem.rb +37 -0
  349. data/lib/ea/qea/models/ea_object_require.rb +34 -0
  350. data/lib/ea/qea/models/ea_object_resource.rb +34 -0
  351. data/lib/ea/qea/models/ea_object_risk.rb +34 -0
  352. data/lib/ea/qea/models/ea_object_scenario.rb +34 -0
  353. data/lib/ea/qea/models/ea_object_test.rb +35 -0
  354. data/lib/ea/qea/models/ea_object_trx.rb +34 -0
  355. data/lib/ea/qea/models/ea_operation.rb +7 -0
  356. data/lib/ea/qea/models/ea_palette.rb +30 -0
  357. data/lib/ea/qea/models/ea_palette_item.rb +36 -0
  358. data/lib/ea/qea/models/ea_phase.rb +40 -0
  359. data/lib/ea/qea/models/ea_role_constraint.rb +32 -0
  360. data/lib/ea/qea/models/ea_secrypt.rb +34 -0
  361. data/lib/ea/qea/models/ea_version.rb +39 -0
  362. data/lib/ea/qea/models/ea_xref.rb +11 -1
  363. data/lib/ea/qea/models.rb +19 -0
  364. data/lib/ea/qea/services/database_loader.rb +22 -1
  365. data/lib/ea/query/builder.rb +108 -0
  366. data/lib/ea/query.rb +9 -0
  367. data/lib/ea/render/image_converter.rb +101 -0
  368. data/lib/ea/render.rb +10 -0
  369. data/lib/ea/shapescript/parser.rb +208 -0
  370. data/lib/ea/shapescript/renderer.rb +74 -0
  371. data/lib/ea/shapescript/shape.rb +34 -0
  372. data/lib/ea/shapescript.rb +24 -0
  373. data/lib/ea/sources/qea/adapter.rb +22 -5
  374. data/lib/ea/sources/qea/classifier_builder.rb +184 -7
  375. data/lib/ea/sources/qea/diagram_builder.rb +352 -24
  376. data/lib/ea/sources/qea/id_normalizer.rb +16 -1
  377. data/lib/ea/sources/qea/instance_builder.rb +113 -0
  378. data/lib/ea/sources/qea/legend_builder.rb +137 -0
  379. data/lib/ea/sources/qea/note_builder.rb +58 -0
  380. data/lib/ea/sources/qea/object_classifier_map.rb +6 -1
  381. data/lib/ea/sources/qea/operation_builder.rb +1 -0
  382. data/lib/ea/sources/qea/package_builder.rb +16 -0
  383. data/lib/ea/sources/qea/relationship_builder.rb +21 -5
  384. data/lib/ea/sources/qea/tagged_value_builder.rb +28 -9
  385. data/lib/ea/sources/qea/xref/legend_definition.rb +35 -0
  386. data/lib/ea/sources/qea/xref/parser.rb +150 -0
  387. data/lib/ea/sources/qea/xref/property_definition.rb +29 -0
  388. data/lib/ea/sources/qea/xref/record.rb +41 -0
  389. data/lib/ea/sources/qea/xref/stereotype_application.rb +37 -0
  390. data/lib/ea/sources/qea/xref.rb +27 -0
  391. data/lib/ea/sources/qea.rb +4 -0
  392. data/lib/ea/sources/xmi/adapter.rb +80 -2
  393. data/lib/ea/sources/xmi/classifier_builder.rb +10 -1
  394. data/lib/ea/sources/xmi/diagram_builder.rb +95 -18
  395. data/lib/ea/sources/xmi/extension_geometry_parser.rb +19 -2
  396. data/lib/ea/sources/xmi/extension_style_parser.rb +15 -2
  397. data/lib/ea/sources/xmi/note_builder.rb +46 -0
  398. data/lib/ea/sources/xmi/property_builder.rb +9 -0
  399. data/lib/ea/sources/xmi/stereotype_builder.rb +54 -0
  400. data/lib/ea/sources/xmi/tag_builder.rb +45 -0
  401. data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +87 -0
  402. data/lib/ea/sources/xmi.rb +4 -0
  403. data/lib/ea/spa/output/sharded_multi_file_strategy.rb +18 -3
  404. data/lib/ea/spa/output/single_file_strategy.rb +8 -2
  405. data/lib/ea/spa/output/strategy.rb +34 -0
  406. data/lib/ea/svg/connector_router.rb +134 -0
  407. data/lib/ea/svg/ea_emitter/background.rb +2 -3
  408. data/lib/ea/svg/ea_emitter/bounds_calculator.rb +142 -0
  409. data/lib/ea/svg/ea_emitter/canvas.rb +56 -33
  410. data/lib/ea/svg/ea_emitter/color_resolver.rb +56 -0
  411. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +28 -0
  412. data/lib/ea/svg/ea_emitter/compartment/constraints.rb +30 -0
  413. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +86 -0
  414. data/lib/ea/svg/ea_emitter/compartment/header.rb +32 -0
  415. data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +57 -0
  416. data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +96 -0
  417. data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +67 -0
  418. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +52 -0
  419. data/lib/ea/svg/ea_emitter/compartment/note_text.rb +102 -0
  420. data/lib/ea/svg/ea_emitter/compartment/operations.rb +38 -0
  421. data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +72 -0
  422. data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +136 -0
  423. data/lib/ea/svg/ea_emitter/compartment/shape.rb +82 -0
  424. data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +30 -0
  425. data/lib/ea/svg/ea_emitter/compartment.rb +58 -0
  426. data/lib/ea/svg/ea_emitter/connectors.rb +46 -12
  427. data/lib/ea/svg/ea_emitter/diagram_frame.rb +143 -0
  428. data/lib/ea/svg/ea_emitter/document.rb +15 -24
  429. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +117 -0
  430. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +119 -0
  431. data/lib/ea/svg/ea_emitter/element/bcol_decoder.rb +29 -0
  432. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +113 -0
  433. data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +53 -0
  434. data/lib/ea/svg/ea_emitter/element/divider_renderer.rb +17 -0
  435. data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +34 -0
  436. data/lib/ea/svg/ea_emitter/element/filter.rb +44 -0
  437. data/lib/ea/svg/ea_emitter/element/hand_draw_shape_renderer.rb +77 -0
  438. data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +43 -0
  439. data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +27 -0
  440. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +76 -0
  441. data/lib/ea/svg/ea_emitter/element/header_line_provider/parent_ghost.rb +25 -0
  442. data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +68 -0
  443. data/lib/ea/svg/ea_emitter/element/header_line_provider.rb +19 -0
  444. data/lib/ea/svg/ea_emitter/element/header_lines.rb +27 -0
  445. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +48 -0
  446. data/lib/ea/svg/ea_emitter/element/icon_renderer.rb +109 -0
  447. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +177 -0
  448. data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +112 -0
  449. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +100 -0
  450. data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +181 -0
  451. data/lib/ea/svg/ea_emitter/element/row_icon_renderer.rb +156 -0
  452. data/lib/ea/svg/ea_emitter/element/shape_renderer.rb +17 -0
  453. data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +82 -0
  454. data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +50 -0
  455. data/lib/ea/svg/ea_emitter/element/text_escape.rb +20 -0
  456. data/lib/ea/svg/ea_emitter/element.rb +37 -0
  457. data/lib/ea/svg/ea_emitter/elements.rb +283 -95
  458. data/lib/ea/svg/ea_emitter/font_resolver.rb +111 -0
  459. data/lib/ea/svg/ea_emitter/ghost_labels.rb +66 -0
  460. data/lib/ea/svg/ea_emitter/label/end_label.rb +266 -0
  461. data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +101 -0
  462. data/lib/ea/svg/ea_emitter/label/registry.rb +50 -0
  463. data/lib/ea/svg/ea_emitter/label.rb +23 -0
  464. data/lib/ea/svg/ea_emitter/labels.rb +93 -27
  465. data/lib/ea/svg/ea_emitter/layer.rb +20 -0
  466. data/lib/ea/svg/ea_emitter/layer_sequencer.rb +93 -0
  467. data/lib/ea/svg/ea_emitter/marker/arrow_path.rb +52 -0
  468. data/lib/ea/svg/ea_emitter/marker/diamond.rb +57 -0
  469. data/lib/ea/svg/ea_emitter/marker/kind.rb +35 -0
  470. data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +27 -0
  471. data/lib/ea/svg/ea_emitter/marker/package_import.rb +39 -0
  472. data/lib/ea/svg/ea_emitter/marker/plus.rb +40 -0
  473. data/lib/ea/svg/ea_emitter/marker/registry.rb +42 -0
  474. data/lib/ea/svg/ea_emitter/marker.rb +37 -0
  475. data/lib/ea/svg/ea_emitter/markers.rb +351 -64
  476. data/lib/ea/svg/ea_emitter/render_context.rb +52 -0
  477. data/lib/ea/svg/ea_emitter/style.rb +15 -0
  478. data/lib/ea/svg/ea_emitter/text_renderer.rb +92 -0
  479. data/lib/ea/svg/ea_emitter.rb +33 -11
  480. data/lib/ea/svg/parity/checker.rb +105 -0
  481. data/lib/ea/svg/parity/source.rb +59 -0
  482. data/lib/ea/svg/parity/suite.rb +113 -0
  483. data/lib/ea/svg/parity.rb +14 -0
  484. data/lib/ea/svg.rb +2 -15
  485. data/lib/ea/theme/definition.rb +176 -0
  486. data/lib/ea/theme/loader.rb +125 -0
  487. data/lib/ea/theme/registry.rb +96 -0
  488. data/lib/ea/theme.rb +37 -0
  489. data/lib/ea/transformations.rb +5 -88
  490. data/lib/ea/version.rb +4 -1
  491. data/lib/ea.rb +14 -4
  492. data/lib/tasks/svg_bench.rake +87 -0
  493. data/lutaml-uml-0.5.1.gem +0 -0
  494. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes-list.png +0 -0
  495. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes.png +0 -0
  496. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-2-gradients.png +0 -0
  497. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-3-colors.png +0 -0
  498. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance-fonts.png +0 -0
  499. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance.png +0 -0
  500. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-5-behavior.png +0 -0
  501. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-6-sequence.png +0 -0
  502. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram.png +0 -0
  503. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-2-objects.png +0 -0
  504. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-3-links.png +0 -0
  505. metadata +449 -26
  506. data/lib/ea/diagram/configuration.rb +0 -379
  507. data/lib/ea/diagram/element_renderers/base_renderer.rb +0 -77
  508. data/lib/ea/diagram/element_renderers/class_renderer.rb +0 -323
  509. data/lib/ea/diagram/element_renderers/connector_renderer.rb +0 -41
  510. data/lib/ea/diagram/element_renderers/package_renderer.rb +0 -61
  511. data/lib/ea/diagram/element_renderers.rb +0 -43
  512. data/lib/ea/diagram/extractor.rb +0 -566
  513. data/lib/ea/diagram/layout_engine.rb +0 -170
  514. data/lib/ea/diagram/path_builder.rb +0 -202
  515. data/lib/ea/diagram/style_parser.rb +0 -42
  516. data/lib/ea/diagram/style_resolver.rb +0 -291
  517. data/lib/ea/diagram/svg_renderer.rb +0 -274
  518. data/lib/ea/diagram/util.rb +0 -73
  519. data/lib/ea/svg/bounds_calculator.rb +0 -74
  520. data/lib/ea/svg/connector_path.rb +0 -138
  521. data/lib/ea/svg/element_box.rb +0 -256
  522. data/lib/ea/svg/renderer.rb +0 -77
  523. data/lib/ea/svg/style_resolver.rb +0 -72
  524. data/lib/ea/transformations/configuration.rb +0 -333
  525. data/lib/ea/transformations/format_registry.rb +0 -366
  526. data/lib/ea/transformations/parsers/base_parser.rb +0 -482
  527. data/lib/ea/transformations/parsers/qea_parser.rb +0 -401
  528. data/lib/ea/transformations/parsers/xmi_parser.rb +0 -243
  529. data/lib/ea/transformations/transformation_engine.rb +0 -390
@@ -3,32 +3,35 @@
3
3
  module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
- # Emits the elements layer: for each DiagramElement on the
7
- # diagram, emits the EA-shape group (filled rect) plus the
8
- # EA-text group (stereotype + name + attributes + operations).
9
- #
10
- # Each element is rendered in z_order (ascending). Output
11
- # follows EA's pattern of layered groups per element:
12
- #
13
- # <g style="fill:COLOR;..."><rect x y w h/></g>
14
- # <g style="fill:#000000;..."><text>...</text></g>
15
- # <g style="..."><path d="M x y L x2 y2"/></g> (divider)
16
- # <g style="..."><text>attr1</text>...</g>
6
+ # Emits the elements layer. Orchestrates per-element rendering
7
+ # by delegating to specialized compartment renderers (shape,
8
+ # header, divider, attribute). Frame-element filtering and
9
+ # stereotype-color fallback also live in dedicated
10
+ # collaborators.
17
11
  class Elements
18
12
  DEFAULT_FILL = "#FFFFFF"
19
13
  DEFAULT_STROKE = "#000000"
20
- DEFAULT_FONT_FAMILY = "Calibri"
21
- DEFAULT_FONT_SIZE = 13
14
+ DEFAULT_STROKE_WIDTH = 2
15
+ DEFAULT_TEXT_COLOR = "#000000"
22
16
 
23
- attr_reader :diagram, :model_index
17
+ attr_reader :diagram, :model_index, :canvas, :document
24
18
 
25
- def initialize(diagram, model_index:)
19
+ def initialize(diagram, model_index:, canvas: nil, document: nil)
26
20
  @diagram = diagram
27
21
  @model_index = model_index
22
+ @canvas = canvas
23
+ @document = document
28
24
  end
29
25
 
30
26
  def render
31
- ordered_elements.map { |e| render_one(e) }.join("\n")
27
+ groups.join("\n")
28
+ end
29
+
30
+ # Returns an Array of per-entity `<g>...</g>` strings.
31
+ # Each element contributes up to 4 groups (shape, header,
32
+ # divider, attrs) in EA's per-entity layer order.
33
+ def groups
34
+ ordered_elements.flat_map { |e| groups_for(e) }
32
35
  end
33
36
 
34
37
  private
@@ -37,131 +40,316 @@ module Ea
37
40
  (diagram.elements || []).sort_by { |e| e.z_order || 0 }
38
41
  end
39
42
 
40
- def render_one(element)
41
- bounds = element.image_bounds || element.bounds
42
- return "" unless bounds
43
+ def groups_for(element)
44
+ return [] if element_filter.skip?(element)
45
+
46
+ context = build_context(element)
47
+ return [] unless context
48
+
49
+ Compartment.render_all(context).compact
50
+ end
51
+
52
+ # Build the RenderContext for one element. Returns nil when
53
+ # the element has no usable bounds (skip rendering).
54
+ def build_context(element)
55
+ raw_bounds = element.bounds || element.image_bounds
56
+ return nil unless raw_bounds
57
+
58
+ bounds = translate_bounds(raw_bounds)
59
+ model_element = model_element_for(element)
60
+ classifier = classifier_for(element)
61
+ size = font_resolver.size_for(element)
62
+ family = font_resolver.family_for(element)
63
+ size_unit = font_resolver.size_unit_for(element)
64
+ parent_name = off_canvas_parent_name_for(classifier)
65
+
66
+ header_lines = classifier ? Element::HeaderLines.for(classifier,
67
+ diagram_package_id: diagram.package_id,
68
+ visually_nested: visually_nested?(element),
69
+ umldi_keyword: element.umldi_keyword,
70
+ bounds_width: raw_bounds&.width,
71
+ font_size: size,
72
+ off_canvas_parent_name: parent_name) : []
73
+ is_classifier = classifier.is_a?(Ea::Model::Classifier)
74
+ attr_lines = (is_classifier && show_attributes?) ? Element::AttributeRenderer.lines_for(classifier, lookup: attribute_lookup) : []
75
+ op_lines = (is_classifier && show_operations?) ? Element::OperationRenderer.lines_for(classifier) : []
76
+ geometry = CompartmentGeometry.new(bounds: bounds, size: size,
77
+ header_lines_count: header_lines.size,
78
+ attr_lines_count: attr_lines.size,
79
+ op_lines_count: op_lines.size,
80
+ tagged_values_count: tagged_values_for(classifier).size,
81
+ header_top_padding: theme.compartments.header_top_padding,
82
+ header_line_offset: theme.compartments.header_line_offset,
83
+ divider_offset: theme.compartments.divider_offset,
84
+ attr_line_offset: theme.compartments.attr_line_offset,
85
+ attr_first_offset: theme.compartments.attr_first_offset)
86
+ RenderContext.new(
87
+ element: element,
88
+ bounds: bounds,
89
+ model_element: model_element,
90
+ classifier: classifier,
91
+ fill: resolve_fill(element, classifier),
92
+ stroke: resolve_stroke,
93
+ stroke_width: resolve_stroke_width,
94
+ text_fill: theme.text_color,
95
+ family: family, size: size, size_unit: size_unit,
96
+ header_lines: header_lines,
97
+ attr_lines: attr_lines,
98
+ op_lines: op_lines,
99
+ enum_literals: enum_literals_for(classifier),
100
+ tagged_values: element.show_tagged_values ? tagged_values_for(classifier) : [],
101
+ constraints: constraints_for(classifier),
102
+ package_content_lines: package_content_lines_for(model_element),
103
+ geometry: geometry,
104
+ theme: theme,
105
+ canvas: canvas,
106
+ diagram: diagram,
107
+ model_index: model_index,
108
+ off_canvas_parent_name: parent_name
109
+ )
110
+ end
43
111
 
44
- classifier = model_element_for(element)
45
- fill = color_from_ea(element.background_color) || fill_for_classifier(classifier)
46
- stroke = color_from_ea(element.line_color) || DEFAULT_STROKE
47
- stroke_width = element.line_width || 2
112
+ def enum_literals_for(classifier)
113
+ return [] unless classifier.is_a?(Ea::Model::Enumeration)
48
114
 
49
- parts = []
50
- parts << render_shape_group(bounds, fill, stroke, stroke_width)
51
- parts << render_header_text(element, bounds, classifier)
52
- parts << render_divider(bounds, stroke, stroke_width) if classifier
53
- parts << render_attribute_text(element, bounds, classifier) if classifier
54
- parts.compact.join("\n")
115
+ classifier.literals || []
55
116
  end
56
117
 
57
- def render_shape_group(bounds, fill, stroke, stroke_width)
58
- %(<g style="stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#{fill};fill-opacity:1.00; stroke:#{stroke}; stroke-opacity:1.00">\n <rect x="#{bounds.x}" y="#{bounds.y}" width="#{bounds.width}" height="#{bounds.height}" rx="0.00" shape-rendering="auto" />\n</g>)
118
+ def tagged_values_for(classifier)
119
+ return [] unless classifier.is_a?(Ea::Model::Classifier)
120
+
121
+ classifier.tagged_values || []
59
122
  end
60
123
 
61
- def render_header_text(element, bounds, classifier)
62
- return "" unless classifier
124
+ def constraints_for(classifier)
125
+ return [] unless classifier.is_a?(Ea::Model::Classifier)
126
+
127
+ classifier.constraints || []
128
+ end
63
129
 
64
- lines = header_lines(classifier)
65
- return "" if lines.empty?
130
+ # EA renders the off-canvas parent classifier's name as an
131
+ # italic line at the top of a placed element's header when
132
+ # ALL of these hold:
133
+ #
134
+ # 1. The diagram's HideParents flag is 0 (the default).
135
+ # HideParents=1 suppresses the ghost line entirely.
136
+ # 2. The parent (general) of a Generalization is NOT placed
137
+ # on the current diagram.
138
+ # 3. No other element from the parent's package is placed
139
+ # on the diagram — EA treats the parent's package as
140
+ # "represented" and omits the ghost as redundant.
141
+ def off_canvas_parent_name_for(classifier)
142
+ return nil unless classifier.is_a?(Ea::Model::Classifier)
143
+ return nil unless document
144
+ return nil unless diagram.show_parents
145
+
146
+ gen = parent_generalization_for(classifier)
147
+ return nil unless gen
148
+
149
+ parent = model_index[gen.general_id]
150
+ return nil unless parent
151
+ return nil if placed_on_diagram?(gen.general_id)
152
+ return nil if parent_package_represented_on_diagram?(parent)
153
+
154
+ name = parent.name.to_s
155
+ name.empty? ? nil : name
156
+ end
66
157
 
67
- family = element.font_family || DEFAULT_FONT_FAMILY
68
- size = element.font_size || DEFAULT_FONT_SIZE
69
- text_blocks = lines.each_with_index.map do |(text, style), idx|
70
- weight = style == :bold ? 700 : 400
71
- font_style = style == :italic ? "italic" : "normal"
72
- y = bounds.y + 17 + (idx * 17)
73
- %( <text x="#{bounds.x + (bounds.width / 2.0)}" y="#{y}" textLength="#{text.length * 7}" style="font-family:#{family}; font-weight:#{weight}; font-style:#{font_style}; font-size:#{size}px; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00 stroke-width:0; white-space: pre;" xml:space="preserve">#{escape(text)}</text>)
158
+ def parent_generalization_for(classifier)
159
+ document.relationships.find do |rel|
160
+ rel.is_a?(Ea::Model::Generalization) &&
161
+ rel.specific_id == classifier.id
74
162
  end
75
- %(<g style="stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00">\n#{text_blocks.join("\n")}\n</g>)
76
163
  end
77
164
 
78
- def render_divider(bounds, stroke, stroke_width)
79
- y = bounds.y + 50
80
- %(<g style="stroke-width:#{stroke_width};stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:0.00; stroke:#{stroke}; stroke-opacity:1.00">\n <path d="M #{bounds.x} #{y} L #{bounds.x + bounds.width} #{y}" shape-rendering="auto"/>\n</g>)
165
+ def placed_on_diagram?(model_element_ref)
166
+ (diagram.elements || []).any? { |e| e.model_element_ref == model_element_ref }
81
167
  end
82
168
 
83
- def render_attribute_text(element, bounds, classifier)
84
- return "" unless classifier.properties || classifier.operations
169
+ # Returns true when any element placed on the current diagram
170
+ # lives in the same package as `parent`. EA suppresses the
171
+ # parent-class ghost line in that case — the parent's package
172
+ # is already "represented" on the diagram, so the off-canvas
173
+ # parent is implicitly visible to the reader.
174
+ def parent_package_represented_on_diagram?(parent)
175
+ parent_pkg = parent.is_a?(Ea::Model::Classifier) ? parent.package_id : nil
176
+ return false unless parent_pkg
177
+
178
+ (diagram.elements || []).any? do |e|
179
+ sibling = model_index[e.model_element_ref]
180
+ sibling.is_a?(Ea::Model::Classifier) && sibling.package_id == parent_pkg
181
+ end
182
+ end
85
183
 
86
- attr_lines = (classifier.properties || []).map do |p|
87
- "+ #{p.name}: #{p.type_name || ''} #{mult_text(p)}"
184
+ # EA renders a package's child classifiers and sub-packages as
185
+ # alphabetical "+ Name" rows in the body compartment, but
186
+ # only when the diagram's t_diagram.ShowPackageContents flag
187
+ # is non-zero. Returns [] when the diagram suppresses
188
+ # package contents or the model element isn't a Package.
189
+ # Each entry is a Struct(name:, kind:) so the renderer can
190
+ # pick the correct per-type icon (Enumeration vs default).
191
+ def package_content_lines_for(model_element)
192
+ return [] unless model_element.is_a?(Ea::Model::Package)
193
+ return [] unless diagram.show_package_contents
194
+
195
+ children = package_children(model_element)
196
+ return [] if children.empty?
197
+
198
+ children.sort_by { |c| c.name.to_s }.map do |c|
199
+ PackageContentRow.new(name: c.name.to_s, kind: row_icon_kind(c))
88
200
  end
89
- return "" if attr_lines.empty?
201
+ end
202
+
203
+ def package_children(package)
204
+ return [] unless model_index
90
205
 
91
- family = element.font_family || DEFAULT_FONT_FAMILY
92
- size = element.font_size || DEFAULT_FONT_SIZE
93
- text_blocks = attr_lines.each_with_index.map do |line, idx|
94
- y = bounds.y + 68 + (idx * 17)
95
- %( <text x="#{bounds.x + 5}" y="#{y}" textLength="#{line.length * 7}" style="font-family:#{family}; font-weight:400; font-style:normal; font-size:#{size}px; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00 stroke-width:0; white-space: pre;" xml:space="preserve">#{escape(line.strip)}</text>)
206
+ model_index.values.select do |obj|
207
+ (obj.is_a?(Ea::Model::Classifier) && obj.package_id == package.id) ||
208
+ (obj.is_a?(Ea::Model::Package) && obj.parent_id == package.id)
96
209
  end
97
- %(<g style="stroke-width:1;stroke-linecap:round;stroke-linejoin:bevel; fill:#000000;fill-opacity:1.00; stroke:#000000; stroke-opacity:0.00">\n#{text_blocks.join("\n")}\n</g>)
98
210
  end
99
211
 
100
- def header_lines(classifier)
101
- lines = []
102
- if classifier.is_a?(Ea::Model::Klass) && classifier.is_abstract
103
- lines << ["_#{classifier.name}", :italic]
104
- elsif classifier.stereotype_refs&.any?
105
- lines << ["«#{classifier.stereotype_refs.first}»", :normal]
106
- lines << [(classifier.qualified_name || classifier.name).to_s, :bold]
212
+ # Discriminator for the per-row icon shape. EA draws a
213
+ # list-style icon for Enumeration children (or classifiers
214
+ # stereotyped "enumeration"), a small 13×9 folder icon for
215
+ # sub-Packages, and a "folded paper" icon for other
216
+ # classifiers.
217
+ def row_icon_kind(child)
218
+ if child.is_a?(Ea::Model::Package)
219
+ :package
220
+ elsif child.is_a?(Ea::Model::Enumeration) ||
221
+ enumeration_stereotype?(child)
222
+ :enumeration
107
223
  else
108
- lines << [(classifier.qualified_name || classifier.name).to_s, :bold]
224
+ :default
109
225
  end
110
- lines
111
226
  end
112
227
 
113
- def mult_text(p)
114
- lower = p.multiplicity_lower
115
- upper = p.multiplicity_upper
116
- return "" if lower.nil? && upper.nil?
117
- return "" if lower == 1 && upper == 1
228
+ def enumeration_stereotype?(child)
229
+ return false unless child.is_a?(Ea::Model::Classifier)
118
230
 
119
- "[#{lower || 0}..#{upper == -1 ? "*" : upper}]"
231
+ refs = child.stereotype_refs
232
+ refs&.any? { |r| r.to_s.downcase == "enumeration" }
120
233
  end
121
234
 
122
- def fill_for_classifier(classifier)
123
- return DEFAULT_FILL unless classifier
235
+ PackageContentRow = Struct.new(:name, :kind, keyword_init: true)
236
+
237
+ # Computes y-coordinates for header/divider/attr compartments
238
+ # given bounds, font size, and line count. See
239
+ # Element::CompartmentGeometry for the coordinate math.
240
+ CompartmentGeometry = Element::CompartmentGeometry
241
+
242
+ # Returns true when this element's bounds are geometrically
243
+ # inside another element's bounds in the same diagram. EA
244
+ # shows qualified names (ParentClass::ChildClass) for
245
+ # visually-nested classes and simple names for separately-
246
+ # placed classes.
247
+ def visually_nested?(element)
248
+ my_bounds = element.bounds || element.image_bounds
249
+ return false unless my_bounds
250
+
251
+ (diagram.elements || []).any? do |other|
252
+ next if other.id == element.id
124
253
 
125
- stereotype = primary_stereotype(classifier)
126
- return DEFAULT_FILL unless stereotype
254
+ other_bounds = other.bounds || other.image_bounds
255
+ next unless other_bounds
127
256
 
128
- stereotype_color_resolver.fill_for(stereotype)
257
+ bounds_contain?(other_bounds, my_bounds)
258
+ end
129
259
  end
130
260
 
131
- def stereotype_color_resolver
132
- @stereotype_color_resolver ||= Ea::Svg::StereotypeColorResolver.new
261
+ def bounds_contain?(outer, inner)
262
+ outer.x <= inner.x &&
263
+ outer.y <= inner.y &&
264
+ outer.x + outer.width >= inner.x + inner.width &&
265
+ outer.y + outer.height >= inner.y + inner.height
133
266
  end
134
267
 
135
- def primary_stereotype(classifier)
136
- refs = classifier.stereotype_refs
137
- return nil if refs.nil? || refs.empty?
268
+ def resolve_fill(element, classifier)
269
+ color_resolver.fill_for(element, classifier)
270
+ end
138
271
 
139
- refs.first.to_s.split("::").last
272
+ def resolve_stroke
273
+ color_resolver.stroke_for(nil)
140
274
  end
141
275
 
142
- def model_element_for(element)
276
+ def resolve_stroke_width
277
+ theme.themed? ? theme.stroke_width : DEFAULT_STROKE_WIDTH
278
+ end
279
+
280
+ def color_resolver
281
+ @color_resolver ||= ColorResolver.new(theme: theme)
282
+ end
283
+
284
+ def theme
285
+ @theme ||= diagram.theme
286
+ end
287
+
288
+ def display_config
289
+ @display_config ||= diagram.display_config
290
+ end
291
+
292
+ def show_attributes?
293
+ display_config.show_attributes?
294
+ end
295
+
296
+ def show_operations?
297
+ display_config.show_operations?
298
+ end
299
+
300
+ # EA only renders a header→content divider when there's
301
+ # actual content below (attributes, operations, literals, or
302
+ # tagged values). Classes with only a header (no displayed
303
+ # features) omit the divider entirely.
304
+ def has_content_below_header?(attr_lines, op_lines, enum_literals, tagged_values = [])
305
+ !attr_lines.empty? || !op_lines.empty? || enum_literals.any? || tagged_values.any?
306
+ end
307
+
308
+ def font_resolver
309
+ @font_resolver ||= FontResolver.new(diagram, theme: theme)
310
+ end
311
+
312
+ def element_filter
313
+ @element_filter ||= Element::Filter.new(model_index: model_index)
314
+ end
315
+
316
+ def classifier_for(element)
143
317
  ref = element.model_element_ref
144
318
  return nil unless ref
145
319
 
146
320
  candidate = model_index[ref]
147
- return nil unless candidate.is_a?(Ea::Model::Classifier)
321
+ return nil unless candidate.is_a?(Ea::Model::Classifier) ||
322
+ candidate.is_a?(Ea::Model::InstanceSpecification)
148
323
 
149
324
  candidate
150
325
  end
151
326
 
152
- def color_from_ea(bgr_int)
153
- return nil if bgr_int.nil?
154
-
155
- r = bgr_int & 0xff
156
- g = (bgr_int >> 8) & 0xff
157
- b = (bgr_int >> 16) & 0xff
158
- format("#%02X%02X%02X", r, g, b)
327
+ # Lookup proc for AttributeLineBuilder: resolves a
328
+ # classifier id to the Classifier instance. Returns nil
329
+ # when not found or not a Classifier.
330
+ def attribute_lookup
331
+ lambda { |id| model_index[id] if model_index && model_index[id].is_a?(Ea::Model::Classifier) }
159
332
  end
160
333
 
161
- def escape(text)
162
- return "" if text.nil?
334
+ def model_element_for(element)
335
+ ref = element.model_element_ref
336
+ return nil unless ref
337
+
338
+ model_index[ref]
339
+ end
163
340
 
164
- text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub("\"", "&quot;")
341
+ def translate_bounds(b)
342
+ return b unless canvas
343
+
344
+ # Reuse the model's Bounds value object rather than an
345
+ # ad-hoc OpenStruct. Keeps the type uniform across the
346
+ # pipeline and avoids require "ostruct".
347
+ Ea::Model::Bounds.new(
348
+ x: canvas.translate_x(b.x),
349
+ y: canvas.translate_y(b.y),
350
+ width: b.width,
351
+ height: b.height
352
+ )
165
353
  end
166
354
  end
167
355
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Resolves the effective font family + size for a DiagramElement,
7
+ # using EA's fallback chain:
8
+ #
9
+ # 1. element.font_family / element.font_size (per-element override)
10
+ # 2. theme.font_family / theme.font_size (when theme is themed)
11
+ # 3. diagram-level default (most common non-nil value across
12
+ # the diagram's elements)
13
+ # 4. locale fallback (Calibri 10)
14
+ #
15
+ # Lives outside the Elements emitter so the resolution rule is
16
+ # defined once (MECE) and reusable by Labels and any future
17
+ # text-emitting layer.
18
+ class FontResolver
19
+ DEFAULT_FONT_FAMILY = "Yu Gothic UI"
20
+ # EA's default element compartment font size (matches
21
+ # reference SVGs which use 9pt for classifier header,
22
+ # attributes, operations, etc. — distinct from theme's
23
+ # diagram-level font size which applies only to frame
24
+ # label and swimlanes).
25
+ DEFAULT_ELEMENT_FONT_SIZE = 9
26
+
27
+ attr_reader :diagram, :theme
28
+
29
+ def initialize(diagram, theme: nil)
30
+ @diagram = diagram
31
+ @theme = theme || Ea::Theme::Registry.default
32
+ end
33
+
34
+ def family_for(element)
35
+ explicit = element&.font_family
36
+ return explicit if explicit && !explicit.empty?
37
+ return theme.font_family if theme.themed? && theme.font_family
38
+
39
+ diagram_default_family || DEFAULT_FONT_FAMILY
40
+ end
41
+
42
+ def size_for(element)
43
+ explicit = element&.font_size
44
+ explicit = nil if explicit&.zero?
45
+ return explicit if explicit
46
+ return diagram_default_size if diagram_default_size
47
+
48
+ DEFAULT_ELEMENT_FONT_SIZE
49
+ end
50
+
51
+ def size_unit_for(_element)
52
+ theme.font_size_unit || "pt"
53
+ end
54
+
55
+ def weight_for(element)
56
+ bold = element&.font_bold ? 700 : nil
57
+ bold || (theme.themed? ? theme.text_weight_normal : 400)
58
+ end
59
+
60
+ def style_for(element)
61
+ element&.font_italic ? "italic" : "normal"
62
+ end
63
+
64
+ private
65
+
66
+ # EA's app-default font depends on the system locale:
67
+ # Japanese Windows → "Yu Gothic UI" 13, English Windows →
68
+ # "Calibri" 10. We can't query EA's runtime, so we infer
69
+ # from the diagram:
70
+ #
71
+ # - If ANY element specifies font=, use the most common.
72
+ # - If NO element specifies font= (all are "use default"),
73
+ # assume the EA English-locale default: Calibri 10.
74
+ def diagram_default_family
75
+ families = element_families
76
+ return FALLBACK_DEFAULT_FAMILY if families.empty?
77
+
78
+ most_common(families)
79
+ end
80
+
81
+ def diagram_default_size
82
+ sizes = element_sizes
83
+ return nil if sizes.empty?
84
+
85
+ most_common(sizes)
86
+ end
87
+
88
+ FALLBACK_DEFAULT_FAMILY = "Calibri"
89
+
90
+ def most_common(values)
91
+ return nil if values.empty?
92
+
93
+ tally = tally_values(values)
94
+ tally.max_by { |_, count| count }.first
95
+ end
96
+
97
+ def element_families
98
+ (diagram.elements || []).map(&:font_family).compact
99
+ end
100
+
101
+ def element_sizes
102
+ (diagram.elements || []).map(&:font_size).compact.reject(&:zero?)
103
+ end
104
+
105
+ def tally_values(values)
106
+ values.each_with_object(Hash.new(0)) { |v, acc| acc[v] += 1 }
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Emits ghost classifier name `<text>` elements for connector
7
+ # endpoints that reference off-canvas classifiers. Each ghost
8
+ # label appears as italic text near the connector's on-canvas
9
+ # endpoint.
10
+ class GhostLabels
11
+ DEFAULT_FAMILY = "Carlito"
12
+ DEFAULT_SIZE = 9
13
+ DEFAULT_UNIT = "pt"
14
+ DEFAULT_FILL = "#000000"
15
+
16
+ attr_reader :diagram, :canvas, :family, :size, :size_unit, :fill
17
+
18
+ def initialize(diagram, canvas: nil, family: DEFAULT_FAMILY,
19
+ size: DEFAULT_SIZE, size_unit: DEFAULT_UNIT,
20
+ fill: DEFAULT_FILL)
21
+ @diagram = diagram
22
+ @canvas = canvas
23
+ @family = family
24
+ @size = size
25
+ @size_unit = size_unit
26
+ @fill = fill
27
+ end
28
+
29
+ def render
30
+ texts = visible_connectors.flat_map do |connector|
31
+ (connector.ghost_labels || []).map { |g| text_for(g) }
32
+ end
33
+ return "" if texts.empty?
34
+
35
+ group_style = "stroke-width:1;stroke-linecap:round;" \
36
+ "stroke-linejoin:bevel; fill:#{fill};" \
37
+ "fill-opacity:1.00; stroke:#000000;" \
38
+ " stroke-opacity:0.00"
39
+ %(<g style="#{group_style}">\n#{texts.join("\n")}\n</g>)
40
+ end
41
+
42
+ private
43
+
44
+ def visible_connectors
45
+ (diagram.connectors || []).select(&:renderable?)
46
+ end
47
+
48
+ def text_for(ghost)
49
+ tx, ty = translate(ghost.anchor_x, ghost.anchor_y)
50
+ TextRenderer.new(
51
+ content: ghost.name.to_s,
52
+ x: tx, y: ty,
53
+ family: family, size: size, size_unit: size_unit,
54
+ style: "italic", fill: fill
55
+ ).to_svg
56
+ end
57
+
58
+ def translate(x, y)
59
+ return [x, y] unless canvas
60
+
61
+ [canvas.translate_x(x), canvas.translate_y(y)]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end