ea 0.4.0 → 0.5.1

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 (548) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/Rakefile +3 -0
  4. data/TODO.complete/01-t-xref-parsing.md +91 -0
  5. data/TODO.complete/02-per-label-geometry.md +50 -0
  6. data/TODO.complete/03-object-properties-constraints.md +57 -0
  7. data/TODO.complete/04-operation-params.md +37 -0
  8. data/TODO.complete/05-pdata-flag-matrix.md +52 -0
  9. data/TODO.complete/06-emf-image-rendering.md +34 -0
  10. data/TODO.complete/07-auxiliary-tables.md +47 -0
  11. data/TODO.complete/10-mdg-stereotype-registry.md +40 -0
  12. data/TODO.complete/11-gml-xsd-generation.md +42 -0
  13. data/TODO.complete/13-shapescript-interpreter.md +28 -0
  14. data/TODO.complete/20-cli-diff.md +46 -0
  15. data/TODO.complete/21-cli-render.md +26 -0
  16. data/TODO.complete/22-cli-export.md +29 -0
  17. data/TODO.complete/23-cli-mdg.md +23 -0
  18. data/TODO.complete/30-headerlines-ocp-refactor.md +72 -0
  19. data/TODO.complete/31-public-send-cleanup.md +61 -0
  20. data/TODO.complete/32-spec-coverage.md +33 -0
  21. data/TODO.complete/33-blocked-parity-gaps.md +41 -0
  22. data/TODO.complete/36-stereotype-icon-wiring.md +32 -0
  23. data/TODO.complete/37-cli-lint.md +32 -0
  24. data/TODO.complete/38-cli-query.md +26 -0
  25. data/TODO.complete/39-cli-info.md +23 -0
  26. data/TODO.complete/40-ocl-evaluator.md +31 -0
  27. data/TODO.complete/41-round-trip-spec.md +18 -0
  28. data/TODO.complete/42-html-diff-report.md +18 -0
  29. data/TODO.complete/43-json-schema-export.md +20 -0
  30. data/TODO.complete/44-remaining-qea-tables.md +40 -0
  31. data/TODO.complete/45-shapescript-extensions.md +25 -0
  32. data/TODO.complete/46-xsd-consolidation.md +20 -0
  33. data/TODO.complete/47-headerlines-legacy-removal.md +20 -0
  34. data/TODO.complete/51-remove-respond-to.md +25 -0
  35. data/TODO.complete/52-xsd-decouple-fixtures.md +23 -0
  36. data/TODO.complete/53-xmi-public-send-cleanup.md +26 -0
  37. data/TODO.complete/54-wire-stereotype-icon-renderer.md +20 -0
  38. data/TODO.complete/55-wire-shapescript.md +20 -0
  39. data/TODO.complete/56-wire-emf-renderer.md +27 -0
  40. data/TODO.complete/57-wire-ocl-evaluator.md +23 -0
  41. data/TODO.complete/58-diff-modifications.md +21 -0
  42. data/TODO.complete/59-delete-toy-xmi-export.md +30 -0
  43. data/TODO.complete/60-curate-json-export.md +20 -0
  44. data/TODO.complete/61-plantuml-relationships.md +22 -0
  45. data/TODO.complete/62-changelog.md +17 -0
  46. data/TODO.complete/63-dependabot-fix.md +18 -0
  47. data/TODO.complete/STATUS.md +85 -0
  48. data/TODO.diagrams/01-svg-measurement-harness.md +31 -0
  49. data/TODO.diagrams/02-polygon-over-rendering.md +29 -0
  50. data/TODO.diagrams/03-text-over-rendering.md +30 -0
  51. data/TODO.diagrams/04-rect-under-rendering.md +33 -0
  52. data/TODO.diagrams/05-canvas-dimension-parity.md +51 -0
  53. data/TODO.diagrams/06-group-layering-parity.md +39 -0
  54. data/TODO.diagrams/07-text-content-fidelity.md +65 -0
  55. data/TODO.diagrams/08-qea-direct-pipeline.md +46 -0
  56. data/TODO.diagrams/12-connector-labels.md +40 -0
  57. data/TODO.diagrams/12-per-entity-svg-layer-ordering.md +24 -0
  58. data/TODO.diagrams/13-marker-polygon-vs-path-dispatch.md +20 -0
  59. data/TODO.diagrams/13-per-text-type-font-sizes.md +33 -0
  60. data/TODO.diagrams/14-center-header-text.md +33 -0
  61. data/TODO.diagrams/14-filter-diagram-frame-elements.md +18 -0
  62. data/TODO.diagrams/15-deep-dead-code-audit.md +47 -0
  63. data/TODO.diagrams/15-per-element-font-from-style.md +24 -0
  64. data/TODO.diagrams/16-canvas-marker-extent.md +30 -0
  65. data/TODO.diagrams/16-document-remaining-parity-gaps.md +91 -0
  66. data/TODO.diagrams/17-italic-abstract-classes.md +28 -0
  67. data/TODO.diagrams/17-remaining-parity-gaps.md +90 -0
  68. data/TODO.diagrams/18-canvas-from-image-bounds.md +20 -0
  69. data/TODO.diagrams/18-property-lookup-index.md +42 -0
  70. data/TODO.diagrams/19-connector-sx-sy-routing.md +27 -0
  71. data/TODO.diagrams/19-replace-respond-to-with-is-a.md +31 -0
  72. data/TODO.diagrams/20-split-labels-emitter.md +56 -0
  73. data/TODO.diagrams/20-visibility-glyph-font-encoding.md +27 -0
  74. data/TODO.diagrams/21-aggregation-navigability-arrow.md +21 -0
  75. data/TODO.diagrams/21-auto-generated-legend-block.md +65 -0
  76. data/TODO.diagrams/22-ghost-element-discriminator.md +57 -0
  77. data/TODO.diagrams/22-style-based-path-grouping.md +28 -0
  78. data/TODO.diagrams/23-marker-polygon-deduplication.md +47 -0
  79. data/TODO.diagrams/23-multiplicity-label-positioning.md +29 -0
  80. data/TODO.diagrams/24-marker-anchor-structural.md +35 -0
  81. data/TODO.diagrams/24-track-marker-anchors-structurally.md +66 -0
  82. data/TODO.diagrams/24-truncate-properties-to-fit-box.md +24 -0
  83. data/TODO.diagrams/26-phantom-general-parent-rendering.md +59 -0
  84. data/TODO.diagrams/27-path-grouping-mode-detection.md +43 -0
  85. data/TODO.diagrams/28-architecture-cleanup-batch.md +47 -0
  86. data/TODO.diagrams/30-decompose-elements-groups-for.md +52 -0
  87. data/TODO.diagrams/32-examples-coverage-batch.md +78 -0
  88. data/TODO.diagrams/36-instance-specification-model.md +42 -0
  89. data/TODO.diagrams/37-element-icon-decorations.md +41 -0
  90. data/TODO.diagrams/38-html-note-and-receptions.md +32 -0
  91. data/TODO.diagrams/39-property-stereotype-discriminator.md +40 -0
  92. data/TODO.diagrams/40-standalone-1-discriminator.md +38 -0
  93. data/TODO.diagrams/41-xmi-to-svg-pipeline.md +113 -0
  94. data/TODO.diagrams/42-close-all-parity-deltas.md +69 -0
  95. data/TODO.diagrams/43-marker-deduplication-strategy.md +54 -0
  96. data/TODO.diagrams/44-element-icon-decorations.md +34 -0
  97. data/TODO.diagrams/45-phantom-parent-class-boxes.md +40 -0
  98. data/TODO.diagrams/46-user-drawn-legend-swatches.md +44 -0
  99. data/TODO.diagrams/47-attribute-line-fidelity.md +72 -0
  100. data/TODO.diagrams/48-spec-coverage-critical-emitter-components.md +45 -0
  101. data/TODO.diagrams/49-spec-coverage-orchestrators.md +25 -0
  102. data/TODO.diagrams/50-examples-parity.md +85 -0
  103. data/TODO.diagrams/51-property-oy-gap-and-remaining-gaps.md +48 -0
  104. data/TODO.diagrams/52-marker-registration-architecture.md +89 -0
  105. data/TODO.diagrams/53-remaining-example-gaps.md +86 -0
  106. data/TODO.diagrams/54-display-name-nested-classes.md +40 -0
  107. data/TODO.diagrams/55-visibility-icon-discriminator.md +58 -0
  108. data/TODO.diagrams/56-package-contents-rendering.md +148 -0
  109. data/TODO.diagrams/57-instance-specification-model.md +100 -0
  110. data/TODO.diagrams/58-domain-model-note-text.md +67 -0
  111. data/TODO.diagrams/59-classes-arrow-discriminator.md +57 -0
  112. data/TODO.diagrams/60-testschema-association-marker.md +40 -0
  113. data/TODO.diagrams/61-legend-block-rendering.md +55 -0
  114. data/TODO.diagrams/61-plateau-parity-gaps.md +59 -0
  115. data/TODO.diagrams/62-ghost-element-rendering.md +56 -0
  116. data/TODO.diagrams/63-qualified-name-wrap.md +36 -0
  117. data/TODO.diagrams/64-property-stereotype-labels.md +22 -0
  118. data/TODO.diagrams/65-path-coordinate-frame.md +62 -0
  119. data/TODO.diagrams/67-path-coordinate-frame.md +62 -0
  120. data/TODO.diagrams/68-inherited-attributes.md +65 -0
  121. data/TODO.diagrams/69-package-anchor-conditional.md +29 -0
  122. data/TODO.diagrams/70-mdg-technology-support.md +68 -0
  123. data/TODO.diagrams/71-off-canvas-parent-ghost.md +45 -0
  124. data/TODO.diagrams/72-basic-attr-divider-paths.md +39 -0
  125. data/TODO.diagrams/73-plateau-polygon-gap.md +69 -0
  126. data/TODO.diagrams/74-plateau-rect-gap.md +22 -0
  127. data/TODO.diagrams/75-plateau-text-gap.md +39 -0
  128. data/TODO.diagrams/76-basic-poly-path-gaps.md +41 -0
  129. data/TODO.diagrams/77-basic-text-overrender.md +40 -0
  130. data/TODO.diagrams/78-headerlines-ocp-refactor.md +65 -0
  131. data/TODO.diagrams/79-architecture-backlog.md +65 -0
  132. data/TODO.diagrams/80-basic-triangle-polygons.md +51 -0
  133. data/TODO.diagrams/81-plateau-path-overrender.md +53 -0
  134. data/TODO.diagrams/82-appearance-canvas-offset.md +54 -0
  135. data/TODO.diagrams/83-3d-diagram-subtitle.md +58 -0
  136. data/TODO.diagrams/84-canvas-offset-mismatches.md +61 -0
  137. data/TODO.diagrams/85-phantom-property-overrender.md +95 -0
  138. data/TODO.diagrams/86-foreign-package-rule.md +62 -0
  139. data/TODO.diagrams/87-internal-tech-encryption.md +80 -0
  140. data/TODO.diagrams/EXAMPLES_STATUS.md +88 -0
  141. data/TODO.diagrams/README.md +69 -0
  142. data/TODO.diagrams/STATUS.md +73 -0
  143. data/TODO.fixup/01-default-grouped-paths.md +34 -0
  144. data/TODO.fixup/02-marker-consolidation-per-connector.md +20 -0
  145. data/TODO.fixup/03-diagram-font-inference.md +28 -0
  146. data/TODO.fixup/04-extract-bounds-calculator.md +25 -0
  147. data/TODO.fixup/05-marker-registry-ocp.md +32 -0
  148. data/TODO.fixup/06-connector-path-bend-routing.md +28 -0
  149. data/TODO.fixup/07-edge-attachment-formula.md +22 -0
  150. data/TODO.fixup/08-visual-diff-harness.md +28 -0
  151. data/TODO.fixup/09-extract-element-compartments.md +34 -0
  152. data/TODO.fixup/10-canvas-width-overshoot-fix.md +20 -0
  153. data/TODO.fixup/11-layer-sequencer-extraction.md +18 -0
  154. data/TODO.fixup/12-path-style-harmony.md +23 -0
  155. data/TODO.fixup/13-datatype-stereotype-fallback.md +28 -0
  156. data/TODO.fixup/14-ea-exe-reverse-engineering-notes.md +56 -0
  157. data/TODO.fixup/15-enumeration-literal-rendering.md +30 -0
  158. data/TODO.fixup/16-operation-compartment-rendering.md +42 -0
  159. data/TODO.fixup/17-path-count-calibration.md +27 -0
  160. data/TODO.fixup/18-polygon-count-calibration.md +32 -0
  161. data/TODO.fixup/19-rect-count-undershoot-investigation.md +40 -0
  162. data/TODO.fixup/20-stereotype-color-from-classifier.md +18 -0
  163. data/TODO.fixup/21-visibility-filter-for-properties.md +24 -0
  164. data/TODO.fixup/22-multiplicity-display-rule-calibration.md +25 -0
  165. data/TODO.fixup/23-diagram-frame-title-element.md +31 -0
  166. data/TODO.fixup/24-note-element-rendering.md +23 -0
  167. data/TODO.fixup/25-ea-exe-disassembly-findings.md +177 -0
  168. data/TODO.fixup/26-svg-renderer-architecture.md +78 -0
  169. data/TODO.fixup/27-svg-element-emitters-disassembly.md +135 -0
  170. data/TODO.fixup/28-geometry-parser-identification.md +57 -0
  171. data/TODO.fixup/29-qea-svg-xmi-triple-comparison.md +185 -0
  172. data/TODO.fixup/30-theme-system-stub.md +46 -0
  173. data/TODO.fixup/31-theme-value-object-registry.md +17 -0
  174. data/TODO.fixup/32-styleex-on-diagram-model.md +22 -0
  175. data/TODO.fixup/33-text-encoding-decimals-rotation.md +29 -0
  176. data/TODO.fixup/34-per-type-fill-via-theme.md +18 -0
  177. data/TODO.fixup/35-stroke-width-per-theme.md +7 -0
  178. data/TODO.fixup/36-qea-rect-coord-normalization.md +24 -0
  179. data/TODO.fixup/37-package-shape-rendering.md +22 -0
  180. data/TODO.fixup/38-note-element-rendering.md +15 -0
  181. data/TODO.fixup/39-styleex-flag-honoring.md +25 -0
  182. data/TODO.fixup/40-connector-dashed-lines.md +15 -0
  183. data/TODO.fixup/41-diagram-frame-showborder-detection.md +23 -0
  184. data/TODO.fixup/42-textrenderer-centralization.md +10 -0
  185. data/TODO.fixup/43-colorresolver-unification.md +24 -0
  186. data/TODO.fixup/44-theme-aware-textrenderer.md +17 -0
  187. data/TODO.fixup/45-theme-aware-fontresolver.md +22 -0
  188. data/TODO.fixup/46-style-parameterized-by-theme.md +21 -0
  189. data/TODO.fixup/47-canvas-frame-inset.md +19 -0
  190. data/TODO.fixup/48-background-rect-format.md +15 -0
  191. data/TODO.fixup/49-all-samples-integration-test.md +17 -0
  192. data/TODO.fixup/50-svg-emitter-architecture-docs.md +33 -0
  193. data/TODO.fixup/51-promote-theme-to-ea-namespace.md +28 -0
  194. data/TODO.fixup/52-theme-definition-integrated-fills.md +26 -0
  195. data/TODO.fixup/53-theme-registry-yaml-loading.md +50 -0
  196. data/TODO.fixup/54-diagram-theme-api.md +50 -0
  197. data/TODO.fixup/55-update-eaemitter-to-ea-theme.md +23 -0
  198. data/TODO.fixup/56-theme-yaml-files.md +65 -0
  199. data/TODO.fixup/57-theme-specs-and-integration.md +27 -0
  200. data/TODO.fixup/58-ea-entry-point-theme-autoload.md +20 -0
  201. data/TODO.fixup/59-thread-theme-through-all-renderers.md +29 -0
  202. data/TODO.fixup/60-cli-theme-option.md +22 -0
  203. data/TODO.fixup/61-theme-validation.md +28 -0
  204. data/TODO.fixup/62-theme-builder-dsl.md +31 -0
  205. data/TODO.fixup/63-theme-integration-rendering-spec.md +24 -0
  206. data/TODO.fixup/64-theme-writeback-styleex.md +21 -0
  207. data/TODO.fixup/65-remove-dead-eaemitter-theme-files.md +30 -0
  208. data/TODO.fixup/66-styleex-writeback.md +22 -0
  209. data/TODO.fixup/67-delete-dead-code.md +28 -0
  210. data/TODO.fixup/68-figure-parity-status.md +82 -0
  211. data/TODO.fixup/69-package-polygon-rendering.md +54 -0
  212. data/TODO.fixup/70-tagged-values-compartment.md +39 -0
  213. data/TODO.fixup/71-auto-detect-theme-from-xmi.md +37 -0
  214. data/TODO.fixup/72-sample-exports-as-regression-specs.md +37 -0
  215. data/TODO.fixup/73-delete-dead-transformations.md +14 -0
  216. data/TODO.fixup/74-delete-dead-diagram-renderers.md +18 -0
  217. data/TODO.fixup/75-visibility-legend-icons.md +23 -0
  218. data/TODO.fixup/76-plateau-text-undershoot.md +27 -0
  219. data/TODO.fixup/77-canvas-frame-margins.md +28 -0
  220. data/TODO.fixup/78-note-element-rendering.md +30 -0
  221. data/TODO.fixup/79-operation-compartment-rendering.md +27 -0
  222. data/TODO.fixup/80-tagged-value-stereotype-visibility.md +37 -0
  223. data/TODO.fixup/81-figure-parity-current-state.md +63 -0
  224. data/config/qea_schema.yml +356 -0
  225. data/config/themes/119.yml +98 -0
  226. data/config/themes/default.yml +14 -0
  227. data/examples/exports/20251010_current_plateau_v5/UML_EA.DTD +6856 -0
  228. data/examples/exports/20251010_current_plateau_v5/model.xml +96709 -0
  229. data/examples/exports/ArcGISWorkspace_template/UML_EA.DTD +6856 -0
  230. data/examples/exports/ArcGISWorkspace_template/model.xml +1427 -0
  231. data/examples/exports/UmlModel_template/UML_EA.DTD +6856 -0
  232. data/examples/exports/UmlModel_template/model.xml +23 -0
  233. data/examples/exports/basic/UML_EA.DTD +6856 -0
  234. data/examples/exports/basic/model.xml +8039 -0
  235. data/examples/exports/simple/UML_EA.DTD +6856 -0
  236. data/examples/exports/simple/model.xml +585 -0
  237. data/examples/exports/simple_example/UML_EA.DTD +6856 -0
  238. data/examples/exports/simple_example/model.xml +23 -0
  239. data/examples/exports/test/UML_EA.DTD +6856 -0
  240. data/examples/exports/test/model.xml +1638 -0
  241. data/examples/qea/basic.qea +0 -0
  242. data/frontend/README.md +51 -0
  243. data/frontend/dist/app.iife.js +24 -0
  244. data/frontend/dist/style.css +1 -0
  245. data/frontend/index.html +26 -0
  246. data/frontend/package-lock.json +2359 -0
  247. data/frontend/package.json +26 -0
  248. data/frontend/src/App.vue +59 -0
  249. data/frontend/src/app.ts +8 -0
  250. data/frontend/src/components/AppHeader.vue +127 -0
  251. data/frontend/src/components/AppSidebar.vue +119 -0
  252. data/frontend/src/components/ClassDetails.vue +247 -0
  253. data/frontend/src/components/DiagramView.vue +79 -0
  254. data/frontend/src/components/PackageDetails.vue +92 -0
  255. data/frontend/src/components/PackageTreeNode.vue +64 -0
  256. data/frontend/src/components/SearchResults.vue +48 -0
  257. data/frontend/src/components/WelcomeView.vue +87 -0
  258. data/frontend/src/shims-vue.d.ts +5 -0
  259. data/frontend/src/stores/dataStore.ts +79 -0
  260. data/frontend/src/stores/uiStore.ts +167 -0
  261. data/frontend/src/styles/main.css +1566 -0
  262. data/frontend/src/types.ts +221 -0
  263. data/frontend/tests/e2e/__screenshots__/new-dark-mode.png +0 -0
  264. data/frontend/tests/e2e/__screenshots__/new-package-details.png +0 -0
  265. data/frontend/tests/e2e/__screenshots__/new-search-modal.png +0 -0
  266. data/frontend/tests/e2e/__screenshots__/new-sidebar-collapsed.png +0 -0
  267. data/frontend/tests/e2e/__screenshots__/new-welcome.png +0 -0
  268. data/frontend/tests/e2e/render.spec.ts +188 -0
  269. data/frontend/tsconfig.json +20 -0
  270. data/frontend/vite.config.ts +26 -0
  271. data/frontend/vitest.config.ts +8 -0
  272. data/lib/ea/cli/app.rb +57 -2
  273. data/lib/ea/cli/command/diagrams.rb +37 -28
  274. data/lib/ea/cli/command/diff.rb +46 -0
  275. data/lib/ea/cli/command/export.rb +101 -0
  276. data/lib/ea/cli/command/info.rb +75 -0
  277. data/lib/ea/cli/command/lint.rb +48 -0
  278. data/lib/ea/cli/command/mdg.rb +98 -0
  279. data/lib/ea/cli/command/query.rb +34 -0
  280. data/lib/ea/cli/command/render.rb +135 -0
  281. data/lib/ea/cli/command/svg.rb +8 -15
  282. data/lib/ea/cli/command.rb +7 -0
  283. data/lib/ea/diagram/display_config.rb +184 -0
  284. data/lib/ea/diagram.rb +4 -41
  285. data/lib/ea/diff/change.rb +30 -0
  286. data/lib/ea/diff/comparator.rb +109 -0
  287. data/lib/ea/diff/html_reporter.rb +91 -0
  288. data/lib/ea/diff.rb +15 -0
  289. data/lib/ea/export/json/generator.rb +118 -0
  290. data/lib/ea/export/json.rb +12 -0
  291. data/lib/ea/export/json_schema/generator.rb +77 -0
  292. data/lib/ea/export/json_schema.rb +12 -0
  293. data/lib/ea/export/plantuml/generator.rb +140 -0
  294. data/lib/ea/export/plantuml.rb +12 -0
  295. data/lib/ea/export/xsd/class_mapping.rb +65 -0
  296. data/lib/ea/export/xsd/generator.rb +137 -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 +13 -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 +40 -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 +34 -0
  333. data/lib/ea/model/diagram_element.rb +16 -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/qea/validation/database/ocl_constraint_validator.rb +65 -0
  378. data/lib/ea/qea/validation/database.rb +2 -0
  379. data/lib/ea/qea/validation/validation_engine.rb +2 -0
  380. data/lib/ea/qea/validation.rb +2 -0
  381. data/lib/ea/query/builder.rb +127 -0
  382. data/lib/ea/query.rb +9 -0
  383. data/lib/ea/render/image_converter.rb +101 -0
  384. data/lib/ea/render.rb +10 -0
  385. data/lib/ea/shapescript/parser.rb +208 -0
  386. data/lib/ea/shapescript/renderer.rb +74 -0
  387. data/lib/ea/shapescript/shape.rb +34 -0
  388. data/lib/ea/shapescript.rb +24 -0
  389. data/lib/ea/sources/qea/adapter.rb +22 -5
  390. data/lib/ea/sources/qea/classifier_builder.rb +184 -7
  391. data/lib/ea/sources/qea/diagram_builder.rb +352 -24
  392. data/lib/ea/sources/qea/id_normalizer.rb +16 -1
  393. data/lib/ea/sources/qea/instance_builder.rb +113 -0
  394. data/lib/ea/sources/qea/legend_builder.rb +137 -0
  395. data/lib/ea/sources/qea/note_builder.rb +58 -0
  396. data/lib/ea/sources/qea/object_classifier_map.rb +6 -1
  397. data/lib/ea/sources/qea/operation_builder.rb +1 -0
  398. data/lib/ea/sources/qea/package_builder.rb +16 -0
  399. data/lib/ea/sources/qea/relationship_builder.rb +21 -5
  400. data/lib/ea/sources/qea/tagged_value_builder.rb +28 -9
  401. data/lib/ea/sources/qea/xref/legend_definition.rb +35 -0
  402. data/lib/ea/sources/qea/xref/parser.rb +150 -0
  403. data/lib/ea/sources/qea/xref/property_definition.rb +29 -0
  404. data/lib/ea/sources/qea/xref/record.rb +41 -0
  405. data/lib/ea/sources/qea/xref/stereotype_application.rb +37 -0
  406. data/lib/ea/sources/qea/xref.rb +27 -0
  407. data/lib/ea/sources/qea.rb +4 -0
  408. data/lib/ea/sources/xmi/adapter.rb +80 -2
  409. data/lib/ea/sources/xmi/classifier_builder.rb +10 -1
  410. data/lib/ea/sources/xmi/diagram_builder.rb +95 -18
  411. data/lib/ea/sources/xmi/extension_geometry_parser.rb +19 -2
  412. data/lib/ea/sources/xmi/extension_style_parser.rb +15 -2
  413. data/lib/ea/sources/xmi/note_builder.rb +46 -0
  414. data/lib/ea/sources/xmi/property_builder.rb +9 -0
  415. data/lib/ea/sources/xmi/stereotype_builder.rb +54 -0
  416. data/lib/ea/sources/xmi/tag_builder.rb +45 -0
  417. data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +87 -0
  418. data/lib/ea/sources/xmi.rb +4 -0
  419. data/lib/ea/spa/output/sharded_multi_file_strategy.rb +18 -3
  420. data/lib/ea/spa/output/single_file_strategy.rb +8 -2
  421. data/lib/ea/spa/output/strategy.rb +34 -0
  422. data/lib/ea/svg/connector_router.rb +134 -0
  423. data/lib/ea/svg/ea_emitter/background.rb +2 -3
  424. data/lib/ea/svg/ea_emitter/bounds_calculator.rb +142 -0
  425. data/lib/ea/svg/ea_emitter/canvas.rb +56 -33
  426. data/lib/ea/svg/ea_emitter/color_resolver.rb +56 -0
  427. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +28 -0
  428. data/lib/ea/svg/ea_emitter/compartment/constraints.rb +30 -0
  429. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +86 -0
  430. data/lib/ea/svg/ea_emitter/compartment/header.rb +32 -0
  431. data/lib/ea/svg/ea_emitter/compartment/header_divider.rb +57 -0
  432. data/lib/ea/svg/ea_emitter/compartment/instance_slots.rb +96 -0
  433. data/lib/ea/svg/ea_emitter/compartment/instance_underline.rb +67 -0
  434. data/lib/ea/svg/ea_emitter/compartment/note_body.rb +52 -0
  435. data/lib/ea/svg/ea_emitter/compartment/note_text.rb +102 -0
  436. data/lib/ea/svg/ea_emitter/compartment/operations.rb +38 -0
  437. data/lib/ea/svg/ea_emitter/compartment/package_contents.rb +72 -0
  438. data/lib/ea/svg/ea_emitter/compartment/package_from_parent.rb +136 -0
  439. data/lib/ea/svg/ea_emitter/compartment/shape.rb +82 -0
  440. data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +32 -0
  441. data/lib/ea/svg/ea_emitter/compartment/tagged_values.rb +30 -0
  442. data/lib/ea/svg/ea_emitter/compartment.rb +60 -0
  443. data/lib/ea/svg/ea_emitter/connectors.rb +46 -12
  444. data/lib/ea/svg/ea_emitter/diagram_frame.rb +143 -0
  445. data/lib/ea/svg/ea_emitter/document.rb +49 -25
  446. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +117 -0
  447. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +119 -0
  448. data/lib/ea/svg/ea_emitter/element/bcol_decoder.rb +29 -0
  449. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +113 -0
  450. data/lib/ea/svg/ea_emitter/element/constraint_renderer.rb +53 -0
  451. data/lib/ea/svg/ea_emitter/element/divider_renderer.rb +17 -0
  452. data/lib/ea/svg/ea_emitter/element/enumeration_literal_renderer.rb +34 -0
  453. data/lib/ea/svg/ea_emitter/element/filter.rb +44 -0
  454. data/lib/ea/svg/ea_emitter/element/hand_draw_shape_renderer.rb +77 -0
  455. data/lib/ea/svg/ea_emitter/element/header_line_pipeline.rb +43 -0
  456. data/lib/ea/svg/ea_emitter/element/header_line_provider/instance_spec.rb +27 -0
  457. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +76 -0
  458. data/lib/ea/svg/ea_emitter/element/header_line_provider/parent_ghost.rb +25 -0
  459. data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +68 -0
  460. data/lib/ea/svg/ea_emitter/element/header_line_provider.rb +19 -0
  461. data/lib/ea/svg/ea_emitter/element/header_lines.rb +27 -0
  462. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +48 -0
  463. data/lib/ea/svg/ea_emitter/element/icon_renderer.rb +109 -0
  464. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +177 -0
  465. data/lib/ea/svg/ea_emitter/element/note_shape_renderer.rb +112 -0
  466. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +100 -0
  467. data/lib/ea/svg/ea_emitter/element/package_shape_renderer.rb +181 -0
  468. data/lib/ea/svg/ea_emitter/element/row_icon_renderer.rb +156 -0
  469. data/lib/ea/svg/ea_emitter/element/shape_renderer.rb +17 -0
  470. data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +119 -0
  471. data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +50 -0
  472. data/lib/ea/svg/ea_emitter/element/text_escape.rb +20 -0
  473. data/lib/ea/svg/ea_emitter/element.rb +37 -0
  474. data/lib/ea/svg/ea_emitter/elements.rb +283 -95
  475. data/lib/ea/svg/ea_emitter/font_resolver.rb +111 -0
  476. data/lib/ea/svg/ea_emitter/ghost_labels.rb +66 -0
  477. data/lib/ea/svg/ea_emitter/label/end_label.rb +266 -0
  478. data/lib/ea/svg/ea_emitter/label/midpoint_label.rb +101 -0
  479. data/lib/ea/svg/ea_emitter/label/registry.rb +50 -0
  480. data/lib/ea/svg/ea_emitter/label.rb +23 -0
  481. data/lib/ea/svg/ea_emitter/labels.rb +93 -27
  482. data/lib/ea/svg/ea_emitter/layer.rb +20 -0
  483. data/lib/ea/svg/ea_emitter/layer_sequencer.rb +93 -0
  484. data/lib/ea/svg/ea_emitter/marker/arrow_path.rb +52 -0
  485. data/lib/ea/svg/ea_emitter/marker/diamond.rb +57 -0
  486. data/lib/ea/svg/ea_emitter/marker/kind.rb +35 -0
  487. data/lib/ea/svg/ea_emitter/marker/open_triangle.rb +27 -0
  488. data/lib/ea/svg/ea_emitter/marker/package_import.rb +39 -0
  489. data/lib/ea/svg/ea_emitter/marker/plus.rb +40 -0
  490. data/lib/ea/svg/ea_emitter/marker/registry.rb +42 -0
  491. data/lib/ea/svg/ea_emitter/marker.rb +37 -0
  492. data/lib/ea/svg/ea_emitter/markers.rb +351 -64
  493. data/lib/ea/svg/ea_emitter/render_context.rb +52 -0
  494. data/lib/ea/svg/ea_emitter/style.rb +15 -0
  495. data/lib/ea/svg/ea_emitter/text_renderer.rb +92 -0
  496. data/lib/ea/svg/ea_emitter.rb +33 -11
  497. data/lib/ea/svg/parity/checker.rb +105 -0
  498. data/lib/ea/svg/parity/source.rb +59 -0
  499. data/lib/ea/svg/parity/suite.rb +113 -0
  500. data/lib/ea/svg/parity.rb +14 -0
  501. data/lib/ea/svg.rb +2 -15
  502. data/lib/ea/theme/definition.rb +176 -0
  503. data/lib/ea/theme/loader.rb +125 -0
  504. data/lib/ea/theme/registry.rb +96 -0
  505. data/lib/ea/theme.rb +37 -0
  506. data/lib/ea/transformations.rb +5 -88
  507. data/lib/ea/validation/xmi_parity.rb +80 -0
  508. data/lib/ea/validation.rb +12 -0
  509. data/lib/ea/version.rb +4 -1
  510. data/lib/ea.rb +15 -4
  511. data/lib/tasks/svg_bench.rake +87 -0
  512. data/lutaml-uml-0.5.1.gem +0 -0
  513. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes-list.png +0 -0
  514. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-1-themes.png +0 -0
  515. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-2-gradients.png +0 -0
  516. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-3-colors.png +0 -0
  517. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance-fonts.png +0 -0
  518. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-4-appearance.png +0 -0
  519. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-5-behavior.png +0 -0
  520. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram-6-sequence.png +0 -0
  521. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-1-diagram.png +0 -0
  522. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-2-objects.png +0 -0
  523. data/reference-docs/enterprise-architect-appearance-dialogs/appearance-3-links.png +0 -0
  524. metadata +464 -26
  525. data/lib/ea/diagram/configuration.rb +0 -379
  526. data/lib/ea/diagram/element_renderers/base_renderer.rb +0 -77
  527. data/lib/ea/diagram/element_renderers/class_renderer.rb +0 -323
  528. data/lib/ea/diagram/element_renderers/connector_renderer.rb +0 -41
  529. data/lib/ea/diagram/element_renderers/package_renderer.rb +0 -61
  530. data/lib/ea/diagram/element_renderers.rb +0 -43
  531. data/lib/ea/diagram/extractor.rb +0 -566
  532. data/lib/ea/diagram/layout_engine.rb +0 -170
  533. data/lib/ea/diagram/path_builder.rb +0 -202
  534. data/lib/ea/diagram/style_parser.rb +0 -42
  535. data/lib/ea/diagram/style_resolver.rb +0 -291
  536. data/lib/ea/diagram/svg_renderer.rb +0 -274
  537. data/lib/ea/diagram/util.rb +0 -73
  538. data/lib/ea/svg/bounds_calculator.rb +0 -74
  539. data/lib/ea/svg/connector_path.rb +0 -138
  540. data/lib/ea/svg/element_box.rb +0 -256
  541. data/lib/ea/svg/renderer.rb +0 -77
  542. data/lib/ea/svg/style_resolver.rb +0 -72
  543. data/lib/ea/transformations/configuration.rb +0 -333
  544. data/lib/ea/transformations/format_registry.rb +0 -366
  545. data/lib/ea/transformations/parsers/base_parser.rb +0 -482
  546. data/lib/ea/transformations/parsers/qea_parser.rb +0 -401
  547. data/lib/ea/transformations/parsers/xmi_parser.rb +0 -243
  548. data/lib/ea/transformations/transformation_engine.rb +0 -390
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Ea
6
+ module Sources
7
+ module Xmi
8
+ # Reads UMLDI `<ownedElement>` blocks from the XMI source and
9
+ # returns the keyword text EA emitted for each modeled element.
10
+ #
11
+ # The xmi gem's typed parser does not surface umldi: keyword
12
+ # labels, but EA encodes them as
13
+ # `<ownedElement xmi:type="umldi:UMLKeywordLabel" text="Type"/>`.
14
+ # This class pre-parses the raw XML once and indexes by the
15
+ # referenced classifier ID.
16
+ #
17
+ # Returned text is the raw keyword ("Type", "FeatureType",
18
+ # "dataType"). The emitter wraps it in «...» when rendering.
19
+ class UmldiKeywordExtractor
20
+ # Sparx EA 6.5 declares umldi: as the 20161101 namespace on
21
+ # the XMI root. Older EAs use the 20131001 version. Try both.
22
+ UMLDI_NAMESPACES = [
23
+ "http://www.omg.org/spec/UML/20161101/UMLDI",
24
+ "http://www.omg.org/spec/UML/20131001/UMLDI"
25
+ ].freeze
26
+
27
+ attr_reader :xmi_path
28
+
29
+ def initialize(xmi_path)
30
+ @xmi_path = xmi_path
31
+ end
32
+
33
+ # Returns { classifier_id => keyword_text } for every
34
+ # umldi:UMLKeywordLabel found, scoped to the given diagram.
35
+ def keywords_for_diagram(diagram_id)
36
+ return {} unless File.exist?(xmi_path)
37
+
38
+ keywords_by_diagram[diagram_id] || {}
39
+ end
40
+
41
+ private
42
+
43
+ def doc
44
+ @doc ||= Nokogiri::XML(File.read(xmi_path))
45
+ end
46
+
47
+ def keywords_by_diagram
48
+ @keywords_by_diagram ||= begin
49
+ result = {}
50
+ # The Sparx XMI export declares the umldi: namespace
51
+ # but the elements inside Diagram have lost their
52
+ # prefix binding once parsed by Nokogiri — the
53
+ # element name resolves to local-name "ownedElement"
54
+ # and the umldi:UMLClassifierShape type information
55
+ # lives in the literal `type` attribute. We therefore
56
+ # match shape children by the presence of `modelElement`
57
+ # and the keyword label by its `text` attribute.
58
+ doc.xpath("//*[local-name()=\"Diagram\"]").each do |diagram|
59
+ diagram_id = diagram["id"] || diagram["xmi:id"]
60
+ next unless diagram_id
61
+
62
+ keywords = {}
63
+ diagram.xpath("./ownedElement[@modelElement]").each do |shape|
64
+ model_id = shape["modelElement"]
65
+ next unless model_id
66
+
67
+ # Keyword label is the first ownedElement child
68
+ # with a non-empty `text` attribute. The name
69
+ # label follows it.
70
+ shape.xpath("./ownedElement[@text]").each do |child|
71
+ text = child["text"]
72
+ next if text.nil? || text.empty?
73
+ next if text.start_with?("«") # safety
74
+
75
+ keywords[model_id] = text
76
+ break
77
+ end
78
+ end
79
+ result[diagram_id] = keywords unless keywords.empty?
80
+ end
81
+ result
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -22,10 +22,14 @@ module Ea
22
22
  autoload :OperationBuilder, "ea/sources/xmi/operation_builder"
23
23
  autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
24
24
  autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
25
+ autoload :TagBuilder, "ea/sources/xmi/tag_builder"
26
+ autoload :NoteBuilder, "ea/sources/xmi/note_builder"
27
+ autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
25
28
  autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
26
29
  autoload :ExtensionElements, "ea/sources/xmi/extension_elements"
27
30
  autoload :ExtensionGeometryParser, "ea/sources/xmi/extension_geometry_parser"
28
31
  autoload :ExtensionStyleParser, "ea/sources/xmi/extension_style_parser"
32
+ autoload :UmldiKeywordExtractor, "ea/sources/xmi/umldi_keyword_extractor"
29
33
  autoload :Adapter, "ea/sources/xmi/adapter"
30
34
  end
31
35
  end
@@ -6,11 +6,14 @@ module Ea
6
6
  module Spa
7
7
  module Output
8
8
  # Sharded layout: directory with skeleton.json, search.json,
9
- # and one JSON file per entity under data/. The frontend
10
- # loads skeleton + search upfront and fetches shards on
11
- # demand. This is the layout for large models.
9
+ # one JSON file per entity under data/, the pre-built SPA
10
+ # bundle (app.js + style.css), and an index.html shell that
11
+ # ties them together. The frontend loads skeleton + search
12
+ # upfront and fetches shards on demand. Suitable for large
13
+ # models that won't fit comfortably in a single HTML file.
12
14
  class ShardedMultiFileStrategy < Strategy
13
15
  def render(projector)
16
+ assert_frontend_bundle!
14
17
  FileUtils.mkdir_p(output_path)
15
18
  FileUtils.mkdir_p(File.join(output_path, "data"))
16
19
 
@@ -19,6 +22,7 @@ module Ea
19
22
  write_json(File.join(output_path, "search.json"),
20
23
  projector.search_index)
21
24
  write_shards(projector)
25
+ write_assets
22
26
  write_index_html(skeleton)
23
27
 
24
28
  output_path
@@ -42,6 +46,15 @@ module Ea
42
46
  end
43
47
  end
44
48
 
49
+ # Copy the pre-built JS + CSS bundles next to index.html so
50
+ # the shell can <script src="app.js"> them. Assets are always
51
+ # the same bytes for every generation, so a plain file copy
52
+ # is both simplest and fastest.
53
+ def write_assets
54
+ FileUtils.cp(FRONTEND_APP_JS, File.join(output_path, "app.js"))
55
+ FileUtils.cp(FRONTEND_STYLE_CSS, File.join(output_path, "style.css"))
56
+ end
57
+
45
58
  def write_index_html(skeleton)
46
59
  title = skeleton.view_extras&.dig("ui", "title") ||
47
60
  skeleton.metadata&.fetch("title", nil) ||
@@ -53,6 +66,7 @@ module Ea
53
66
  <meta charset="UTF-8">
54
67
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
68
  <title>#{title}</title>
69
+ <link rel="stylesheet" href="style.css">
56
70
  </head>
57
71
  <body>
58
72
  <div id="app"></div>
@@ -61,6 +75,7 @@ module Ea
61
75
  window.__SPA_SEARCH_URL__ = 'search.json';
62
76
  window.__SPA_SHARD_BASE__ = 'data/';
63
77
  </script>
78
+ <script src="app.js" defer></script>
64
79
  </body>
65
80
  </html>
66
81
  HTML
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "json"
4
5
 
5
6
  module Ea
6
7
  module Spa
7
8
  module Output
8
- # Embeds skeleton + search + every shard in one HTML file
9
- # (data inlined as a JSON blob). Suitable for small models
9
+ # Embeds skeleton + search + every shard AND the pre-built SPA
10
+ # bundle (JS + CSS) in one HTML file. Suitable for small models
10
11
  # only; not appropriate when shard count or total payload is
11
12
  # large.
12
13
  class SingleFileStrategy < Strategy
13
14
  def render(projector)
15
+ assert_frontend_bundle!
14
16
  FileUtils.mkdir_p(File.dirname(output_path))
15
17
 
16
18
  skeleton = projector.skeleton
@@ -33,6 +35,8 @@ module Ea
33
35
  title = payload.dig(:viewExtras, "ui", "title") ||
34
36
  payload[:metadata]&.fetch("title", nil) ||
35
37
  "Ea::Spa"
38
+ css = frontend_style_css || ""
39
+ js = frontend_app_js || ""
36
40
  <<~HTML
37
41
  <!DOCTYPE html>
38
42
  <html lang="en">
@@ -40,12 +44,14 @@ module Ea
40
44
  <meta charset="UTF-8">
41
45
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
46
  <title>#{title}</title>
47
+ <style>#{css}</style>
43
48
  </head>
44
49
  <body>
45
50
  <div id="app"></div>
46
51
  <script>
47
52
  window.__SPA_DATA__ = #{payload.to_json};
48
53
  </script>
54
+ <script>#{js}</script>
49
55
  </body>
50
56
  </html>
51
57
  HTML
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  module Ea
4
6
  module Spa
5
7
  module Output
@@ -13,6 +15,14 @@ module Ea
13
15
  class Strategy
14
16
  SIZE_THRESHOLD_BYTES = 2 * 1024 * 1024 # 2 MB
15
17
 
18
+ # Path to the pre-built Vue IIFE bundle shipped with the gem.
19
+ # Built from frontend/ via `npm run build`; committed so the
20
+ # gem never needs npm at install time. Resolved lazily so a
21
+ # missing bundle raises a clear error pointing at frontend/.
22
+ FRONTEND_DIST_DIR = File.expand_path("../../../../frontend/dist", __dir__)
23
+ FRONTEND_APP_JS = File.join(FRONTEND_DIST_DIR, "app.iife.js")
24
+ FRONTEND_STYLE_CSS = File.join(FRONTEND_DIST_DIR, "style.css")
25
+
16
26
  attr_reader :output_path
17
27
 
18
28
  def initialize(output_path)
@@ -24,6 +34,30 @@ module Ea
24
34
  "#{self.class} must implement #render"
25
35
  end
26
36
 
37
+ protected
38
+
39
+ # The pre-built SPA application source as a String, for
40
+ # embedding directly into a single-file output. Returns nil
41
+ # if the bundle has not been built (e.g. running from a
42
+ # source checkout before `npm run build`).
43
+ def frontend_app_js
44
+ return nil unless File.exist?(FRONTEND_APP_JS)
45
+ File.read(FRONTEND_APP_JS)
46
+ end
47
+
48
+ def frontend_style_css
49
+ return nil unless File.exist?(FRONTEND_STYLE_CSS)
50
+ File.read(FRONTEND_STYLE_CSS)
51
+ end
52
+
53
+ def assert_frontend_bundle!
54
+ unless File.exist?(FRONTEND_APP_JS) && File.exist?(FRONTEND_STYLE_CSS)
55
+ raise Ea::Error,
56
+ "SPA frontend bundle not found under #{FRONTEND_DIST_DIR}. " \
57
+ "Run `cd frontend && npm install && npm run build`."
58
+ end
59
+ end
60
+
27
61
  private
28
62
 
29
63
  def write_json(path, data)
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ class ConnectorRouter
6
+ # Empirically determined offset EA adds to top-edge
7
+ # attachment points: connector starts ~9 px below the
8
+ # element's logical top edge (matches header height).
9
+ EDGE_TOP_OFFSET = 9
10
+
11
+ attr_reader :source_bounds, :target_bounds, :edge_code,
12
+ :source_delta, :target_delta, :bend_path
13
+
14
+ def initialize(source_bounds:, target_bounds:,
15
+ edge_code: nil,
16
+ source_delta: [0, 0], target_delta: [0, 0],
17
+ bend_path: nil)
18
+ @source_bounds = source_bounds
19
+ @target_bounds = target_bounds
20
+ @edge_code = edge_code
21
+ @source_delta = source_delta
22
+ @target_delta = target_delta
23
+ @bend_path = bend_path || []
24
+ end
25
+
26
+ def waypoints
27
+ src_pt = source_point
28
+ tgt_pt = target_point
29
+ return [] unless src_pt && tgt_pt
30
+
31
+ # Path= values from EA are ABSOLUTE pixel positions on the
32
+ # diagram canvas (verified against reference SVGs). Use them
33
+ # as-is — do not offset by src_pt.
34
+ return [src_pt, *bend_path, tgt_pt] if bend_path.any?
35
+
36
+ points = [src_pt]
37
+ bend = bend_point(src_pt, tgt_pt)
38
+ points << bend if bend
39
+ points << tgt_pt
40
+ points
41
+ end
42
+
43
+ private
44
+
45
+ def source_point
46
+ return nil unless source_bounds
47
+ pt = if @edge_code && @edge_code != 0
48
+ edge_point(source_bounds, @edge_code)
49
+ else
50
+ primary_edge(source_bounds, target_bounds)
51
+ end
52
+ return nil unless pt
53
+
54
+ offset_by_delta(pt, source_delta)
55
+ end
56
+
57
+ def target_point
58
+ return nil unless target_bounds
59
+ pt = if @edge_code && @edge_code != 0
60
+ opposite_edge_point(source_bounds, target_bounds, @edge_code)
61
+ else
62
+ primary_edge(target_bounds, source_bounds)
63
+ end
64
+ return nil unless pt
65
+
66
+ offset_by_delta(pt, target_delta)
67
+ end
68
+
69
+ def offset_by_delta(point, delta)
70
+ return point if delta.nil?
71
+
72
+ [point[0] + (delta[0] || 0), point[1] + (delta[1] || 0)]
73
+ end
74
+
75
+ def edge_point(bounds, code)
76
+ case code
77
+ when 1 then [center_x(bounds), bounds.y + EDGE_TOP_OFFSET]
78
+ when 2 then [bounds.x + bounds.width, center_y(bounds)]
79
+ when 3 then [center_x(bounds), bounds.y + bounds.height]
80
+ when 4 then [bounds.x, center_y(bounds)]
81
+ else [center_x(bounds), center_y(bounds)]
82
+ end
83
+ end
84
+
85
+ def opposite_edge_point(source, target, code)
86
+ case code
87
+ when 1 then [center_x(target), target.y + target.height]
88
+ when 2 then [target.x, center_y(target)]
89
+ when 3 then [center_x(target), target.y]
90
+ when 4 then [target.x + target.width, center_y(target)]
91
+ else [center_x(target), center_y(target)]
92
+ end
93
+ end
94
+
95
+ def primary_edge(bounds, other)
96
+ return nil unless bounds && other
97
+
98
+ dx = center_x(bounds) - center_x(other)
99
+ dy = center_y(bounds) - center_y(other)
100
+
101
+ if dx.abs > dy.abs
102
+ dx.positive? ? [bounds.x, center_y(bounds)] : [bounds.x + bounds.width, center_y(bounds)]
103
+ else
104
+ dy.positive? ? [center_x(bounds), bounds.y] : [center_x(bounds), bounds.y + bounds.height]
105
+ end
106
+ end
107
+
108
+ def bend_point(src, tgt)
109
+ return nil if src == tgt
110
+ return nil if src[0] == tgt[0] || src[1] == tgt[1]
111
+
112
+ if horizontal_exit?
113
+ [tgt[0], src[1]]
114
+ else
115
+ [src[0], tgt[1]]
116
+ end
117
+ end
118
+
119
+ def horizontal_exit?
120
+ return false unless source_bounds && @edge_code
121
+
122
+ @edge_code == 2 || @edge_code == 4
123
+ end
124
+
125
+ def center_x(bounds)
126
+ bounds.x + (bounds.width / 2.0)
127
+ end
128
+
129
+ def center_y(bounds)
130
+ bounds.y + (bounds.height / 2.0)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -1,14 +1,13 @@
1
- # frozen_string_literal: true
1
+ # frozen_string_string: true
2
2
 
3
3
  module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
- # Emits the background layer: white rect over the full canvas.
7
6
  module Background
8
7
  module_function
9
8
 
10
9
  def render(canvas)
11
- %(<g style="fill:#FFFFFF;fill-opacity:1.00;">\n <rect x="#{canvas.min_x}" y="#{canvas.min_y}" width="#{canvas.width}" height="#{canvas.height}" shape-rendering="auto"/>\n</g>)
10
+ %(<g style="fill:#FFFFFF;fill-opacity:1.00;">\n <rect x="0" y="0" width="#{canvas.width}" height="#{canvas.height}" shape-rendering="auto"/>\n</g>)
12
11
  end
13
12
  end
14
13
  end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ # Computes the (min_x, min_y, width, height) tuple for a
7
+ # Diagram's canvas. Encapsulates the union rules so Canvas
8
+ # stays a small value-object with just coordinate translation
9
+ # and formatting concerns.
10
+ #
11
+ # Sources contributing to the bounds:
12
+ # - ElementBounds: logical x-extent, logical+image y-extent
13
+ # - ConnectorBounds: waypoint positions
14
+ # - MarkerExtent: MARKER_EXTENT padding around end points
15
+ # - PackageTabExtent: extra space above Package elements for
16
+ # the tab polygon (which sits above the
17
+ # element's logical top).
18
+ #
19
+ # Each source is a method returning an Array<[x, y]> points.
20
+ # Composing new sources = adding a method, no modification of
21
+ # existing ones (OCP).
22
+ class BoundsCalculator
23
+ MARKER_EXTENT = 15
24
+ PACKAGE_TAB_HEIGHT = 20
25
+ # EA's canvas includes non-uniform frame insets around the
26
+ # element content. Reverse-engineered from reference SVG
27
+ # byte-diff: maintenance diagram has element 169x80 at
28
+ # logical (0,0), ref canvas is 254x177 with element at
29
+ # (35, 40). So:
30
+ # canvas_width = element_width + INSET_LEFT + INSET_RIGHT
31
+ # canvas_height = element_height + INSET_TOP + INSET_BOTTOM
32
+ # The top inset accommodates the frame tab + label space
33
+ # above the element.
34
+ INSET_LEFT = 35
35
+ INSET_RIGHT = 50
36
+ INSET_TOP = 40
37
+ INSET_BOTTOM = 57
38
+
39
+ attr_reader :diagram, :model_index
40
+
41
+ def initialize(diagram, model_index: nil)
42
+ @diagram = diagram
43
+ @model_index = model_index
44
+ end
45
+
46
+ def compute
47
+ points = []
48
+ points.concat(element_points)
49
+ points.concat(connector_points)
50
+ points.concat(marker_points)
51
+ points.concat(package_tab_points)
52
+ return [0, 0, 1, 1] if points.empty?
53
+
54
+ xs = points.map(&:first)
55
+ ys = points.map(&:last)
56
+ min_x = xs.min || 0
57
+ min_y = ys.min || 0
58
+ [
59
+ min_x,
60
+ min_y,
61
+ (xs.max - min_x) + INSET_LEFT + INSET_RIGHT,
62
+ (ys.max - min_y) + INSET_TOP + INSET_BOTTOM
63
+ ]
64
+ end
65
+
66
+ private
67
+
68
+ # Element x-extent: logical bounds only (matches EA's canvas
69
+ # left/right exactly).
70
+ # Element y-extent: union of logical + image_bounds (image
71
+ # extends below bounds for shadow / image padding).
72
+ def element_points
73
+ pts = []
74
+ (diagram.elements || []).each do |e|
75
+ primary = e.bounds || e.image_bounds
76
+ if primary
77
+ pts << [primary.x, primary.y]
78
+ pts << [primary.x + primary.width, primary.y + primary.height]
79
+ end
80
+ if e.bounds && e.image_bounds
81
+ ib = e.image_bounds
82
+ pts << [ib.x, ib.y]
83
+ pts << [ib.x + ib.width, ib.y + ib.height]
84
+ end
85
+ end
86
+ pts
87
+ end
88
+
89
+ # Package elements render with a tab polygon ABOVE the
90
+ # element's logical top edge (height PACKAGE_TAB_HEIGHT).
91
+ # Include those points so the canvas reserves space.
92
+ def package_tab_points
93
+ return [] if model_index.nil?
94
+
95
+ pts = []
96
+ (diagram.elements || []).each do |e|
97
+ ref = e.model_element_ref
98
+ next unless ref
99
+
100
+ candidate = model_index[ref]
101
+ next unless candidate.is_a?(Ea::Model::Package)
102
+
103
+ primary = e.bounds || e.image_bounds
104
+ next unless primary
105
+
106
+ pts << [primary.x, primary.y - PACKAGE_TAB_HEIGHT]
107
+ pts << [primary.x + primary.width, primary.y]
108
+ end
109
+ pts
110
+ end
111
+
112
+ def connector_points
113
+ pts = []
114
+ (diagram.connectors || []).each do |c|
115
+ (c.waypoints || []).each do |wp|
116
+ next unless wp.position
117
+
118
+ pts << [wp.position.x, wp.position.y]
119
+ end
120
+ end
121
+ pts
122
+ end
123
+
124
+ def marker_points
125
+ pts = []
126
+ (diagram.connectors || []).each do |c|
127
+ waypoints = (c.waypoints || []).map(&:position).compact
128
+ next unless waypoints.size >= 2
129
+
130
+ source = waypoints.first
131
+ target = waypoints.last
132
+ pts << [source.x - MARKER_EXTENT, source.y - MARKER_EXTENT]
133
+ pts << [source.x + MARKER_EXTENT, source.y + MARKER_EXTENT]
134
+ pts << [target.x - MARKER_EXTENT, target.y - MARKER_EXTENT]
135
+ pts << [target.x + MARKER_EXTENT, target.y + MARKER_EXTENT]
136
+ end
137
+ pts
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -3,44 +3,47 @@
3
3
  module Ea
4
4
  module Svg
5
5
  module EaEmitter
6
- # Computes the canvas dimensions for the root <svg> element.
7
- # Union of all element image_bounds (the padded bounds EA
8
- # uses for the visual box), with a 10 px outer margin.
9
- # Emits cm dimensions (px / 28.346 at 72 DPI).
10
- Canvas = Struct.new(:min_x, :min_y, :width, :height, keyword_init: true) do
11
- PX_PER_CM = 28.3464567
6
+ # Canvas carries the diagram's computed pixel bounds plus the
7
+ # coordinate-translation helpers that align element (0,0) with
8
+ # EA's content origin (35, 40). Bound computation itself is
9
+ # delegated to BoundsCalculator (SRP).
10
+ #
11
+ # Promoted from a Struct.new + `do ... end` block to a proper
12
+ # class so the public API is explicit (attr_reader vs implicit
13
+ # struct fields) and behavior is named rather than implicit.
14
+ # Struct definitions with instance methods conflate "data" and
15
+ # "behavior" — a class keeps them separable while the values
16
+ # stay immutable.
17
+ class Canvas
18
+ # EA's diagram canvas places element (0,0) at SVG (35, 40) —
19
+ # the diagram's content origin inset from the canvas top-left
20
+ # corner. Reverse-engineered from reference SVG byte-diff
21
+ # against the maintenance diagram.
22
+ PX_PER_CM = 37.795275591
23
+ FRAME_INSET_LEFT = 35
24
+ FRAME_INSET_TOP = 40
12
25
 
13
- def self.from(diagram)
14
- points = []
15
- (diagram.elements || []).each do |e|
16
- b = e.image_bounds || e.bounds
17
- next unless b
26
+ attr_reader :min_x, :min_y, :width, :height
18
27
 
19
- points << [b.x, b.y]
20
- points << [b.x + b.width, b.y + b.height]
21
- end
22
- (diagram.connectors || []).each do |c|
23
- (c.waypoints || []).each do |wp|
24
- next unless wp.position
25
-
26
- points << [wp.position.x, wp.position.y]
27
- end
28
- end
29
- return new(min_x: 0, min_y: 0, width: 1, height: 1) if points.empty?
28
+ def initialize(min_x:, min_y:, width:, height:)
29
+ @min_x = min_x
30
+ @min_y = min_y
31
+ @width = width
32
+ @height = height
33
+ end
30
34
 
31
- xs = points.map(&:first)
32
- ys = points.map(&:last)
33
- margin = 10
34
- new(
35
- min_x: (xs.min || 0) - margin,
36
- min_y: (ys.min || 0) - margin,
37
- width: (xs.max - xs.min) + (2 * margin),
38
- height: (ys.max - ys.min) + (2 * margin)
39
- )
35
+ # Build a Canvas for a diagram by running the bound
36
+ # calculator. Defaults model_index to nil (no canvas-aware
37
+ # element bounds needed) for callers that only need a
38
+ # frame-size estimate.
39
+ def self.from(diagram, model_index: nil)
40
+ min_x, min_y, width, height =
41
+ BoundsCalculator.new(diagram, model_index: model_index).compute
42
+ new(min_x: min_x, min_y: min_y, width: width, height: height)
40
43
  end
41
44
 
42
45
  def view_box
43
- "#{min_x} #{min_y} #{width} #{height}"
46
+ "0 0 #{width} #{height}"
44
47
  end
45
48
 
46
49
  def width_cm
@@ -51,6 +54,26 @@ module Ea
51
54
  format_cm(height)
52
55
  end
53
56
 
57
+ # Translate a content-space point into SVG-space. EA insets
58
+ # the content area from the canvas top-left by
59
+ # FRAME_INSET_LEFT/TOP.
60
+ def translate_x(x)
61
+ x - min_x + FRAME_INSET_LEFT
62
+ end
63
+
64
+ def translate_y(y)
65
+ y - min_y + FRAME_INSET_TOP
66
+ end
67
+
68
+ # Compact coordinate formatter. Integers lose the decimal
69
+ # point; floats keep up to two digits with trailing zeros
70
+ # stripped (matches EA's emitted numeric format).
71
+ def self.coord(value)
72
+ return value.to_i.to_s if value == value.to_i
73
+
74
+ format("%.2f", value).sub(/\.?0+$/, "")
75
+ end
76
+
54
77
  private
55
78
 
56
79
  def format_cm(px)
@@ -62,4 +85,4 @@ module Ea
62
85
  end
63
86
  end
64
87
  end
65
- end
88
+ end