etassia 0.1.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 (727) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +140 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +436 -0
  5. data/exe/etassia +7 -0
  6. data/lib/etassia/cli.rb +187 -0
  7. data/lib/etassia/commands/batch.rb +210 -0
  8. data/lib/etassia/commands/notation_loader.rb +22 -0
  9. data/lib/etassia/commands/render.rb +98 -0
  10. data/lib/etassia/commands/types.rb +33 -0
  11. data/lib/etassia/commands/version.rb +24 -0
  12. data/lib/etassia/corpus_type_names.rb +52 -0
  13. data/lib/etassia/diagram/architecture.rb +108 -0
  14. data/lib/etassia/diagram/base.rb +61 -0
  15. data/lib/etassia/diagram/block.rb +102 -0
  16. data/lib/etassia/diagram/c4.rb +337 -0
  17. data/lib/etassia/diagram/class_diagram.rb +466 -0
  18. data/lib/etassia/diagram/class_namespace.rb +17 -0
  19. data/lib/etassia/diagram/class_note.rb +17 -0
  20. data/lib/etassia/diagram/containment.rb +107 -0
  21. data/lib/etassia/diagram/er_diagram.rb +317 -0
  22. data/lib/etassia/diagram/error.rb +38 -0
  23. data/lib/etassia/diagram/flowchart.rb +419 -0
  24. data/lib/etassia/diagram/flowchart_label_text.rb +63 -0
  25. data/lib/etassia/diagram/gantt.rb +94 -0
  26. data/lib/etassia/diagram/generic_text.rb +30 -0
  27. data/lib/etassia/diagram/git_graph.rb +57 -0
  28. data/lib/etassia/diagram/info.rb +38 -0
  29. data/lib/etassia/diagram/kanban.rb +198 -0
  30. data/lib/etassia/diagram/mindmap.rb +71 -0
  31. data/lib/etassia/diagram/packet.rb +93 -0
  32. data/lib/etassia/diagram/pie.rb +115 -0
  33. data/lib/etassia/diagram/quadrant.rb +135 -0
  34. data/lib/etassia/diagram/radar.rb +65 -0
  35. data/lib/etassia/diagram/requirement.rb +168 -0
  36. data/lib/etassia/diagram/sankey.rb +217 -0
  37. data/lib/etassia/diagram/sequence.rb +299 -0
  38. data/lib/etassia/diagram/sequence_box.rb +18 -0
  39. data/lib/etassia/diagram/sequence_frame.rb +37 -0
  40. data/lib/etassia/diagram/sequence_frame_section.rb +23 -0
  41. data/lib/etassia/diagram/sequence_text.rb +69 -0
  42. data/lib/etassia/diagram/sequence_wrap.rb +23 -0
  43. data/lib/etassia/diagram/state_diagram.rb +282 -0
  44. data/lib/etassia/diagram/timeline.rb +183 -0
  45. data/lib/etassia/diagram/treemap.rb +95 -0
  46. data/lib/etassia/diagram/user_journey.rb +153 -0
  47. data/lib/etassia/diagram/xy_chart.rb +90 -0
  48. data/lib/etassia/diagram.rb +8 -0
  49. data/lib/etassia/diagram_registry.rb +34 -0
  50. data/lib/etassia/engine.rb +334 -0
  51. data/lib/etassia/error/diagram_type_error.rb +10 -0
  52. data/lib/etassia/error/layout_error.rb +12 -0
  53. data/lib/etassia/error/notation_registration_error.rb +10 -0
  54. data/lib/etassia/error/parse_error.rb +11 -0
  55. data/lib/etassia/error/pipeline_error.rb +10 -0
  56. data/lib/etassia/error/render_error.rb +12 -0
  57. data/lib/etassia/error/score_error.rb +12 -0
  58. data/lib/etassia/error.rb +10 -0
  59. data/lib/etassia/error_report.rb +69 -0
  60. data/lib/etassia/ir/data.rb +39 -0
  61. data/lib/etassia/ir/data_value.rb +35 -0
  62. data/lib/etassia/ir/dimension.rb +10 -0
  63. data/lib/etassia/ir/edge.rb +34 -0
  64. data/lib/etassia/ir/graph.rb +28 -0
  65. data/lib/etassia/ir/integrity.rb +80 -0
  66. data/lib/etassia/ir/item.rb +46 -0
  67. data/lib/etassia/ir/model.rb +56 -0
  68. data/lib/etassia/ir/node.rb +10 -0
  69. data/lib/etassia/ir/placement.rb +47 -0
  70. data/lib/etassia/ir/prepositioned.rb +25 -0
  71. data/lib/etassia/ir/prepositioned_item.rb +26 -0
  72. data/lib/etassia/ir/property_set.rb +43 -0
  73. data/lib/etassia/ir/scalar.rb +25 -0
  74. data/lib/etassia/ir/series.rb +10 -0
  75. data/lib/etassia/ir.rb +17 -0
  76. data/lib/etassia/js_math.rb +511 -0
  77. data/lib/etassia/js_number.rb +64 -0
  78. data/lib/etassia/layout/architecture.rb +1472 -0
  79. data/lib/etassia/layout/base.rb +276 -0
  80. data/lib/etassia/layout/block.rb +738 -0
  81. data/lib/etassia/layout/c4.rb +876 -0
  82. data/lib/etassia/layout/c4_boundary_metrics.rb +35 -0
  83. data/lib/etassia/layout/c4_bounds.rb +106 -0
  84. data/lib/etassia/layout/c4_intersection.rb +77 -0
  85. data/lib/etassia/layout/c4_placement.rb +135 -0
  86. data/lib/etassia/layout/c4_shape_metrics.rb +97 -0
  87. data/lib/etassia/layout/c4_shape_outline.rb +67 -0
  88. data/lib/etassia/layout/c4_stereotype.rb +30 -0
  89. data/lib/etassia/layout/c4_text.rb +54 -0
  90. data/lib/etassia/layout/class_cell_placement.rb +148 -0
  91. data/lib/etassia/layout/class_diagram.rb +1116 -0
  92. data/lib/etassia/layout/class_diagram_namespaces.rb +26 -0
  93. data/lib/etassia/layout/class_diagram_notes.rb +26 -0
  94. data/lib/etassia/layout/class_name_advances.rb +31 -0
  95. data/lib/etassia/layout/class_namespace_box.rb +21 -0
  96. data/lib/etassia/layout/class_namespace_nodes.rb +197 -0
  97. data/lib/etassia/layout/class_namespace_placement.rb +71 -0
  98. data/lib/etassia/layout/class_note.rb +44 -0
  99. data/lib/etassia/layout/class_note_nodes.rb +84 -0
  100. data/lib/etassia/layout/elk_notices.rb +48 -0
  101. data/lib/etassia/layout/elk_placement.rb +197 -0
  102. data/lib/etassia/layout/er_diagram.rb +1002 -0
  103. data/lib/etassia/layout/er_diagram_adjacency.rb +20 -0
  104. data/lib/etassia/layout/er_diagram_alignment.rb +72 -0
  105. data/lib/etassia/layout/er_diagram_compaction.rb +85 -0
  106. data/lib/etassia/layout/er_diagram_conflicts.rb +70 -0
  107. data/lib/etassia/layout/er_diagram_coordinates.rb +104 -0
  108. data/lib/etassia/layout/er_diagram_entity_size.rb +82 -0
  109. data/lib/etassia/layout/er_diagram_layering.rb +109 -0
  110. data/lib/etassia/layout/er_diagram_ordering.rb +143 -0
  111. data/lib/etassia/layout/er_diagram_placement.rb +163 -0
  112. data/lib/etassia/layout/er_diagram_placement_node.rb +22 -0
  113. data/lib/etassia/layout/error.rb +133 -0
  114. data/lib/etassia/layout/flowchart/brandes_koepf.rb +208 -0
  115. data/lib/etassia/layout/flowchart/cluster_reach.rb +70 -0
  116. data/lib/etassia/layout/flowchart/component_packing.rb +129 -0
  117. data/lib/etassia/layout/flowchart/directed_placement.rb +428 -0
  118. data/lib/etassia/layout/flowchart/edge_chains.rb +111 -0
  119. data/lib/etassia/layout/flowchart/edge_order.rb +34 -0
  120. data/lib/etassia/layout/flowchart/edge_router.rb +533 -0
  121. data/lib/etassia/layout/flowchart/label_backdrop_geometry.rb +62 -0
  122. data/lib/etassia/layout/flowchart/layer_spacing.rb +85 -0
  123. data/lib/etassia/layout/flowchart/level_edges.rb +74 -0
  124. data/lib/etassia/layout/flowchart/level_frame.rb +47 -0
  125. data/lib/etassia/layout/flowchart/minimum_rank_spacing.rb +74 -0
  126. data/lib/etassia/layout/flowchart/row_centering.rb +229 -0
  127. data/lib/etassia/layout/flowchart.rb +1290 -0
  128. data/lib/etassia/layout/gantt.rb +814 -0
  129. data/lib/etassia/layout/gantt_duration.rb +49 -0
  130. data/lib/etassia/layout/gantt_exclusions.rb +74 -0
  131. data/lib/etassia/layout/gantt_ticks.rb +165 -0
  132. data/lib/etassia/layout/git_graph.rb +1075 -0
  133. data/lib/etassia/layout/grid.rb +165 -0
  134. data/lib/etassia/layout/info.rb +113 -0
  135. data/lib/etassia/layout/kanban.rb +452 -0
  136. data/lib/etassia/layout/kanban_card_text.rb +95 -0
  137. data/lib/etassia/layout/mindmap.rb +425 -0
  138. data/lib/etassia/layout/mindmap_cose.rb +81 -0
  139. data/lib/etassia/layout/mindmap_cose_body.rb +93 -0
  140. data/lib/etassia/layout/mindmap_cose_geometry.rb +190 -0
  141. data/lib/etassia/layout/mindmap_cose_grid.rb +96 -0
  142. data/lib/etassia/layout/mindmap_cose_link.rb +42 -0
  143. data/lib/etassia/layout/mindmap_cose_relaxation.rb +166 -0
  144. data/lib/etassia/layout/mindmap_cose_seed.rb +93 -0
  145. data/lib/etassia/layout/mindmap_cose_wedge.rb +37 -0
  146. data/lib/etassia/layout/mindmap_node_size.rb +130 -0
  147. data/lib/etassia/layout/packet.rb +285 -0
  148. data/lib/etassia/layout/pie.rb +305 -0
  149. data/lib/etassia/layout/pie_legend.rb +64 -0
  150. data/lib/etassia/layout/pie_legend_entry.rb +21 -0
  151. data/lib/etassia/layout/quadrant.rb +402 -0
  152. data/lib/etassia/layout/radar.rb +442 -0
  153. data/lib/etassia/layout/requirement.rb +765 -0
  154. data/lib/etassia/layout/sankey.rb +501 -0
  155. data/lib/etassia/layout/scene.rb +18 -0
  156. data/lib/etassia/layout/sequence/actor_gaps.rb +151 -0
  157. data/lib/etassia/layout/sequence/box_shape.rb +23 -0
  158. data/lib/etassia/layout/sequence/creations.rb +73 -0
  159. data/lib/etassia/layout/sequence/frame_divider.rb +22 -0
  160. data/lib/etassia/layout/sequence/frame_placement.rb +424 -0
  161. data/lib/etassia/layout/sequence/frame_reader.rb +75 -0
  162. data/lib/etassia/layout/sequence/frame_shape.rb +45 -0
  163. data/lib/etassia/layout/sequence/geometry.rb +24 -0
  164. data/lib/etassia/layout/sequence/message_rows.rb +165 -0
  165. data/lib/etassia/layout/sequence/note.rb +16 -0
  166. data/lib/etassia/layout/sequence/note_box.rb +114 -0
  167. data/lib/etassia/layout/sequence/note_placement.rb +223 -0
  168. data/lib/etassia/layout/sequence/text_width.rb +69 -0
  169. data/lib/etassia/layout/sequence/text_wrap.rb +93 -0
  170. data/lib/etassia/layout/sequence.rb +936 -0
  171. data/lib/etassia/layout/state_diagram.rb +1277 -0
  172. data/lib/etassia/layout/timeline.rb +165 -0
  173. data/lib/etassia/layout/timeline_card.rb +142 -0
  174. data/lib/etassia/layout/timeline_flow.rb +164 -0
  175. data/lib/etassia/layout/timeline_line.rb +33 -0
  176. data/lib/etassia/layout/timeline_rounding.rb +19 -0
  177. data/lib/etassia/layout/timeline_text.rb +105 -0
  178. data/lib/etassia/layout/treemap.rb +303 -0
  179. data/lib/etassia/layout/treemap_placement.rb +52 -0
  180. data/lib/etassia/layout/treemap_squarify.rb +108 -0
  181. data/lib/etassia/layout/user_journey.rb +300 -0
  182. data/lib/etassia/layout/user_journey_face.rb +33 -0
  183. data/lib/etassia/layout/user_journey_face_geometry.rb +80 -0
  184. data/lib/etassia/layout/user_journey_geometry.rb +336 -0
  185. data/lib/etassia/layout/user_journey_legend.rb +84 -0
  186. data/lib/etassia/layout/xy_chart.rb +620 -0
  187. data/lib/etassia/layout.rb +19 -0
  188. data/lib/etassia/lint_debt_scoreboard.rb +219 -0
  189. data/lib/etassia/markdown_text/emphasis_simulator.rb +373 -0
  190. data/lib/etassia/markdown_text.rb +393 -0
  191. data/lib/etassia/notation/builtin.rb +39 -0
  192. data/lib/etassia/notation/entry.rb +15 -0
  193. data/lib/etassia/notation/mermaid/class_diagram_config.rb +42 -0
  194. data/lib/etassia/notation/mermaid/git_graph_config.rb +46 -0
  195. data/lib/etassia/notation/mermaid/ir_adapter.rb +27 -0
  196. data/lib/etassia/notation/mermaid/ir_adapters/architecture.rb +196 -0
  197. data/lib/etassia/notation/mermaid/ir_adapters/block.rb +138 -0
  198. data/lib/etassia/notation/mermaid/ir_adapters/c4.rb +258 -0
  199. data/lib/etassia/notation/mermaid/ir_adapters/class_diagram.rb +257 -0
  200. data/lib/etassia/notation/mermaid/ir_adapters/er_diagram.rb +220 -0
  201. data/lib/etassia/notation/mermaid/ir_adapters/error.rb +32 -0
  202. data/lib/etassia/notation/mermaid/ir_adapters/flowchart.rb +208 -0
  203. data/lib/etassia/notation/mermaid/ir_adapters/gantt.rb +236 -0
  204. data/lib/etassia/notation/mermaid/ir_adapters/git_graph.rb +186 -0
  205. data/lib/etassia/notation/mermaid/ir_adapters/info.rb +34 -0
  206. data/lib/etassia/notation/mermaid/ir_adapters/kanban.rb +110 -0
  207. data/lib/etassia/notation/mermaid/ir_adapters/mindmap.rb +76 -0
  208. data/lib/etassia/notation/mermaid/ir_adapters/packet.rb +51 -0
  209. data/lib/etassia/notation/mermaid/ir_adapters/pie.rb +71 -0
  210. data/lib/etassia/notation/mermaid/ir_adapters/quadrant.rb +116 -0
  211. data/lib/etassia/notation/mermaid/ir_adapters/radar.rb +142 -0
  212. data/lib/etassia/notation/mermaid/ir_adapters/requirement.rb +237 -0
  213. data/lib/etassia/notation/mermaid/ir_adapters/sankey.rb +74 -0
  214. data/lib/etassia/notation/mermaid/ir_adapters/sequence.rb +304 -0
  215. data/lib/etassia/notation/mermaid/ir_adapters/state_diagram.rb +225 -0
  216. data/lib/etassia/notation/mermaid/ir_adapters/timeline.rb +93 -0
  217. data/lib/etassia/notation/mermaid/ir_adapters/treemap.rb +143 -0
  218. data/lib/etassia/notation/mermaid/ir_adapters/user_journey.rb +127 -0
  219. data/lib/etassia/notation/mermaid/ir_adapters/xychart.rb +131 -0
  220. data/lib/etassia/notation/mermaid.rb +399 -0
  221. data/lib/etassia/notation/parsed.rb +31 -0
  222. data/lib/etassia/notation/plantuml/activity/arrow_head.rb +45 -0
  223. data/lib/etassia/notation/plantuml/activity/arrow_style.rb +53 -0
  224. data/lib/etassia/notation/plantuml/activity/bar_columns.rb +75 -0
  225. data/lib/etassia/notation/plantuml/activity/box.rb +60 -0
  226. data/lib/etassia/notation/plantuml/activity/builder.rb +77 -0
  227. data/lib/etassia/notation/plantuml/activity/canvas.rb +117 -0
  228. data/lib/etassia/notation/plantuml/activity/column_centres.rb +151 -0
  229. data/lib/etassia/notation/plantuml/activity/columns.rb +224 -0
  230. data/lib/etassia/notation/plantuml/activity/diagram.rb +35 -0
  231. data/lib/etassia/notation/plantuml/activity/edge.rb +35 -0
  232. data/lib/etassia/notation/plantuml/activity/edge_marks.rb +68 -0
  233. data/lib/etassia/notation/plantuml/activity/exits.rb +61 -0
  234. data/lib/etassia/notation/plantuml/activity/frame_marks.rb +116 -0
  235. data/lib/etassia/notation/plantuml/activity/lane_steps.rb +82 -0
  236. data/lib/etassia/notation/plantuml/activity/lane_titles.rb +46 -0
  237. data/lib/etassia/notation/plantuml/activity/layout.rb +50 -0
  238. data/lib/etassia/notation/plantuml/activity/legacy_parser.rb +181 -0
  239. data/lib/etassia/notation/plantuml/activity/legacy_row_gaps.rb +65 -0
  240. data/lib/etassia/notation/plantuml/activity/mark.rb +23 -0
  241. data/lib/etassia/notation/plantuml/activity/marks.rb +62 -0
  242. data/lib/etassia/notation/plantuml/activity/metrics.rb +169 -0
  243. data/lib/etassia/notation/plantuml/activity/new_parser/blocks.rb +166 -0
  244. data/lib/etassia/notation/plantuml/activity/new_parser.rb +177 -0
  245. data/lib/etassia/notation/plantuml/activity/node.rb +50 -0
  246. data/lib/etassia/notation/plantuml/activity/node_marks.rb +174 -0
  247. data/lib/etassia/notation/plantuml/activity/note.rb +27 -0
  248. data/lib/etassia/notation/plantuml/activity/parser.rb +51 -0
  249. data/lib/etassia/notation/plantuml/activity/placement.rb +327 -0
  250. data/lib/etassia/notation/plantuml/activity/ranking.rb +142 -0
  251. data/lib/etassia/notation/plantuml/activity/reach.rb +58 -0
  252. data/lib/etassia/notation/plantuml/activity/renderer.rb +220 -0
  253. data/lib/etassia/notation/plantuml/activity/route.rb +22 -0
  254. data/lib/etassia/notation/plantuml/activity/router.rb +308 -0
  255. data/lib/etassia/notation/plantuml/activity/row_gaps.rb +105 -0
  256. data/lib/etassia/notation/plantuml/activity/row_spread.rb +59 -0
  257. data/lib/etassia/notation/plantuml/activity/scene.rb +20 -0
  258. data/lib/etassia/notation/plantuml/activity/text.rb +36 -0
  259. data/lib/etassia/notation/plantuml/activity.rb +50 -0
  260. data/lib/etassia/notation/plantuml/arrow.rb +135 -0
  261. data/lib/etassia/notation/plantuml/caption.rb +50 -0
  262. data/lib/etassia/notation/plantuml/caption_layout.rb +97 -0
  263. data/lib/etassia/notation/plantuml/caption_rows.rb +67 -0
  264. data/lib/etassia/notation/plantuml/class_header.rb +178 -0
  265. data/lib/etassia/notation/plantuml/class_name.rb +51 -0
  266. data/lib/etassia/notation/plantuml/component.rb +37 -0
  267. data/lib/etassia/notation/plantuml/creole.rb +278 -0
  268. data/lib/etassia/notation/plantuml/creole_icons.rb +33 -0
  269. data/lib/etassia/notation/plantuml/diagram.rb +60 -0
  270. data/lib/etassia/notation/plantuml/diagram_builder.rb +442 -0
  271. data/lib/etassia/notation/plantuml/diagram_name.rb +51 -0
  272. data/lib/etassia/notation/plantuml/directives.rb +33 -0
  273. data/lib/etassia/notation/plantuml/drawing/box.rb +28 -0
  274. data/lib/etassia/notation/plantuml/drawing/face.rb +39 -0
  275. data/lib/etassia/notation/plantuml/drawing/pen.rb +105 -0
  276. data/lib/etassia/notation/plantuml/drawing/renderer.rb +36 -0
  277. data/lib/etassia/notation/plantuml/drawing/scene.rb +28 -0
  278. data/lib/etassia/notation/plantuml/drawing.rb +19 -0
  279. data/lib/etassia/notation/plantuml/ebnf/blocks.rb +272 -0
  280. data/lib/etassia/notation/plantuml/ebnf/layout.rb +117 -0
  281. data/lib/etassia/notation/plantuml/ebnf/model.rb +22 -0
  282. data/lib/etassia/notation/plantuml/ebnf/reader.rb +131 -0
  283. data/lib/etassia/notation/plantuml/ebnf.rb +34 -0
  284. data/lib/etassia/notation/plantuml/end_texts.rb +78 -0
  285. data/lib/etassia/notation/plantuml/entity.rb +54 -0
  286. data/lib/etassia/notation/plantuml/further.rb +38 -0
  287. data/lib/etassia/notation/plantuml/gantt/layout.rb +269 -0
  288. data/lib/etassia/notation/plantuml/gantt/reader.rb +210 -0
  289. data/lib/etassia/notation/plantuml/gantt/renderer.rb +81 -0
  290. data/lib/etassia/notation/plantuml/gantt.rb +34 -0
  291. data/lib/etassia/notation/plantuml/include/blocks.rb +66 -0
  292. data/lib/etassia/notation/plantuml/include/finder.rb +61 -0
  293. data/lib/etassia/notation/plantuml/include.rb +78 -0
  294. data/lib/etassia/notation/plantuml/ir_adapter/annotations.rb +40 -0
  295. data/lib/etassia/notation/plantuml/ir_adapter/links.rb +91 -0
  296. data/lib/etassia/notation/plantuml/ir_adapter/sink.rb +49 -0
  297. data/lib/etassia/notation/plantuml/ir_adapter/structure.rb +86 -0
  298. data/lib/etassia/notation/plantuml/ir_adapter.rb +32 -0
  299. data/lib/etassia/notation/plantuml/ir_reader/annotations.rb +51 -0
  300. data/lib/etassia/notation/plantuml/ir_reader/index.rb +49 -0
  301. data/lib/etassia/notation/plantuml/ir_reader/links.rb +133 -0
  302. data/lib/etassia/notation/plantuml/ir_reader/structure.rb +83 -0
  303. data/lib/etassia/notation/plantuml/ir_reader.rb +42 -0
  304. data/lib/etassia/notation/plantuml/junction.rb +20 -0
  305. data/lib/etassia/notation/plantuml/klass.rb +41 -0
  306. data/lib/etassia/notation/plantuml/label_clearance.rb +67 -0
  307. data/lib/etassia/notation/plantuml/layout.rb +2806 -0
  308. data/lib/etassia/notation/plantuml/member.rb +37 -0
  309. data/lib/etassia/notation/plantuml/member_icon.rb +87 -0
  310. data/lib/etassia/notation/plantuml/member_icon_elements.rb +91 -0
  311. data/lib/etassia/notation/plantuml/member_pitch.rb +76 -0
  312. data/lib/etassia/notation/plantuml/member_rule.rb +48 -0
  313. data/lib/etassia/notation/plantuml/network/grid.rb +170 -0
  314. data/lib/etassia/notation/plantuml/network/layout.rb +158 -0
  315. data/lib/etassia/notation/plantuml/network/model.rb +22 -0
  316. data/lib/etassia/notation/plantuml/network/placement.rb +64 -0
  317. data/lib/etassia/notation/plantuml/network/reader.rb +126 -0
  318. data/lib/etassia/notation/plantuml/network/shapes.rb +109 -0
  319. data/lib/etassia/notation/plantuml/network.rb +37 -0
  320. data/lib/etassia/notation/plantuml/node_graph/archimate.rb +49 -0
  321. data/lib/etassia/notation/plantuml/node_graph/box_reader.rb +86 -0
  322. data/lib/etassia/notation/plantuml/node_graph/cloud.rb +73 -0
  323. data/lib/etassia/notation/plantuml/node_graph/crows_foot.rb +85 -0
  324. data/lib/etassia/notation/plantuml/node_graph/curve.rb +66 -0
  325. data/lib/etassia/notation/plantuml/node_graph/data_reader.rb +77 -0
  326. data/lib/etassia/notation/plantuml/node_graph/data_shape.rb +39 -0
  327. data/lib/etassia/notation/plantuml/node_graph/detour.rb +98 -0
  328. data/lib/etassia/notation/plantuml/node_graph/edge.rb +26 -0
  329. data/lib/etassia/notation/plantuml/node_graph/entity_metrics.rb +49 -0
  330. data/lib/etassia/notation/plantuml/node_graph/entity_reader.rb +65 -0
  331. data/lib/etassia/notation/plantuml/node_graph/entity_shape.rb +111 -0
  332. data/lib/etassia/notation/plantuml/node_graph/figures.rb +92 -0
  333. data/lib/etassia/notation/plantuml/node_graph/frame_ports.rb +73 -0
  334. data/lib/etassia/notation/plantuml/node_graph/graph.rb +40 -0
  335. data/lib/etassia/notation/plantuml/node_graph/heads.rb +89 -0
  336. data/lib/etassia/notation/plantuml/node_graph/label_fan.rb +76 -0
  337. data/lib/etassia/notation/plantuml/node_graph/label_placer.rb +310 -0
  338. data/lib/etassia/notation/plantuml/node_graph/label_waypoint.rb +47 -0
  339. data/lib/etassia/notation/plantuml/node_graph/layout.rb +293 -0
  340. data/lib/etassia/notation/plantuml/node_graph/link.rb +78 -0
  341. data/lib/etassia/notation/plantuml/node_graph/macros.rb +39 -0
  342. data/lib/etassia/notation/plantuml/node_graph/metrics.rb +113 -0
  343. data/lib/etassia/notation/plantuml/node_graph/node.rb +44 -0
  344. data/lib/etassia/notation/plantuml/node_graph/note_tail.rb +58 -0
  345. data/lib/etassia/notation/plantuml/node_graph/packer.rb +126 -0
  346. data/lib/etassia/notation/plantuml/node_graph/parser.rb +309 -0
  347. data/lib/etassia/notation/plantuml/node_graph/paths.rb +48 -0
  348. data/lib/etassia/notation/plantuml/node_graph/ranking.rb +231 -0
  349. data/lib/etassia/notation/plantuml/node_graph/reader.rb +53 -0
  350. data/lib/etassia/notation/plantuml/node_graph/references.rb +38 -0
  351. data/lib/etassia/notation/plantuml/node_graph/renderer.rb +160 -0
  352. data/lib/etassia/notation/plantuml/node_graph/scene.rb +22 -0
  353. data/lib/etassia/notation/plantuml/node_graph/shapes.rb +302 -0
  354. data/lib/etassia/notation/plantuml/node_graph/skipped_ranks.rb +61 -0
  355. data/lib/etassia/notation/plantuml/node_graph/tab.rb +68 -0
  356. data/lib/etassia/notation/plantuml/node_graph/usecase_ellipse.rb +100 -0
  357. data/lib/etassia/notation/plantuml/node_graph.rb +38 -0
  358. data/lib/etassia/notation/plantuml/note.rb +27 -0
  359. data/lib/etassia/notation/plantuml/note_layout.rb +257 -0
  360. data/lib/etassia/notation/plantuml/note_outline.rb +68 -0
  361. data/lib/etassia/notation/plantuml/objects/map_rows.rb +46 -0
  362. data/lib/etassia/notation/plantuml/objects/reader.rb +208 -0
  363. data/lib/etassia/notation/plantuml/objects.rb +39 -0
  364. data/lib/etassia/notation/plantuml/package.rb +45 -0
  365. data/lib/etassia/notation/plantuml/package_color.rb +32 -0
  366. data/lib/etassia/notation/plantuml/package_frames.rb +178 -0
  367. data/lib/etassia/notation/plantuml/parser.rb +622 -0
  368. data/lib/etassia/notation/plantuml/portion_visibility.rb +91 -0
  369. data/lib/etassia/notation/plantuml/qualified_association_layout.rb +332 -0
  370. data/lib/etassia/notation/plantuml/qualifier_box.rb +78 -0
  371. data/lib/etassia/notation/plantuml/rank_chain.rb +527 -0
  372. data/lib/etassia/notation/plantuml/rank_hierarchy.rb +347 -0
  373. data/lib/etassia/notation/plantuml/relation.rb +103 -0
  374. data/lib/etassia/notation/plantuml/relation_label_separator.rb +199 -0
  375. data/lib/etassia/notation/plantuml/relation_path_points.rb +84 -0
  376. data/lib/etassia/notation/plantuml/renderer.rb +490 -0
  377. data/lib/etassia/notation/plantuml/scene.rb +134 -0
  378. data/lib/etassia/notation/plantuml/segment_box.rb +85 -0
  379. data/lib/etassia/notation/plantuml/sequence/activation.rb +28 -0
  380. data/lib/etassia/notation/plantuml/sequence/appearance.rb +71 -0
  381. data/lib/etassia/notation/plantuml/sequence/arrow_end.rb +23 -0
  382. data/lib/etassia/notation/plantuml/sequence/arrow_marks.rb +111 -0
  383. data/lib/etassia/notation/plantuml/sequence/arrow_style.rb +42 -0
  384. data/lib/etassia/notation/plantuml/sequence/arrow_syntax.rb +122 -0
  385. data/lib/etassia/notation/plantuml/sequence/bar_tracker.rb +59 -0
  386. data/lib/etassia/notation/plantuml/sequence/box.rb +23 -0
  387. data/lib/etassia/notation/plantuml/sequence/chrome.rb +60 -0
  388. data/lib/etassia/notation/plantuml/sequence/chrome_rows.rb +184 -0
  389. data/lib/etassia/notation/plantuml/sequence/create.rb +34 -0
  390. data/lib/etassia/notation/plantuml/sequence/delay.rb +19 -0
  391. data/lib/etassia/notation/plantuml/sequence/destroy.rb +20 -0
  392. data/lib/etassia/notation/plantuml/sequence/diagram.rb +86 -0
  393. data/lib/etassia/notation/plantuml/sequence/divider.rb +19 -0
  394. data/lib/etassia/notation/plantuml/sequence/edge.rb +53 -0
  395. data/lib/etassia/notation/plantuml/sequence/embedded.rb +77 -0
  396. data/lib/etassia/notation/plantuml/sequence/fill.rb +56 -0
  397. data/lib/etassia/notation/plantuml/sequence/fragment.rb +28 -0
  398. data/lib/etassia/notation/plantuml/sequence/fragment_shape.rb +138 -0
  399. data/lib/etassia/notation/plantuml/sequence/head_style.rb +43 -0
  400. data/lib/etassia/notation/plantuml/sequence/ir_adapter/blocks.rb +73 -0
  401. data/lib/etassia/notation/plantuml/sequence/ir_adapter/fills.rb +24 -0
  402. data/lib/etassia/notation/plantuml/sequence/ir_adapter/items.rb +48 -0
  403. data/lib/etassia/notation/plantuml/sequence/ir_adapter/messages.rb +62 -0
  404. data/lib/etassia/notation/plantuml/sequence/ir_adapter/participants.rb +42 -0
  405. data/lib/etassia/notation/plantuml/sequence/ir_adapter/settings.rb +54 -0
  406. data/lib/etassia/notation/plantuml/sequence/ir_adapter.rb +38 -0
  407. data/lib/etassia/notation/plantuml/sequence/ir_reader/blocks.rb +77 -0
  408. data/lib/etassia/notation/plantuml/sequence/ir_reader/fills.rb +27 -0
  409. data/lib/etassia/notation/plantuml/sequence/ir_reader/index.rb +36 -0
  410. data/lib/etassia/notation/plantuml/sequence/ir_reader/items.rb +35 -0
  411. data/lib/etassia/notation/plantuml/sequence/ir_reader/messages.rb +70 -0
  412. data/lib/etassia/notation/plantuml/sequence/ir_reader/participants.rb +42 -0
  413. data/lib/etassia/notation/plantuml/sequence/ir_reader/settings.rb +74 -0
  414. data/lib/etassia/notation/plantuml/sequence/ir_reader.rb +38 -0
  415. data/lib/etassia/notation/plantuml/sequence/keyword_defaults.rb +28 -0
  416. data/lib/etassia/notation/plantuml/sequence/layout.rb +1567 -0
  417. data/lib/etassia/notation/plantuml/sequence/message.rb +97 -0
  418. data/lib/etassia/notation/plantuml/sequence/message_wrap.rb +77 -0
  419. data/lib/etassia/notation/plantuml/sequence/note.rb +61 -0
  420. data/lib/etassia/notation/plantuml/sequence/note_fill.rb +35 -0
  421. data/lib/etassia/notation/plantuml/sequence/note_geometry.rb +126 -0
  422. data/lib/etassia/notation/plantuml/sequence/note_shape.rb +51 -0
  423. data/lib/etassia/notation/plantuml/sequence/numbered_label.rb +57 -0
  424. data/lib/etassia/notation/plantuml/sequence/numbering.rb +49 -0
  425. data/lib/etassia/notation/plantuml/sequence/outline.rb +204 -0
  426. data/lib/etassia/notation/plantuml/sequence/page_break.rb +17 -0
  427. data/lib/etassia/notation/plantuml/sequence/paint.rb +79 -0
  428. data/lib/etassia/notation/plantuml/sequence/parallel_message.rb +19 -0
  429. data/lib/etassia/notation/plantuml/sequence/parser.rb +844 -0
  430. data/lib/etassia/notation/plantuml/sequence/participant.rb +27 -0
  431. data/lib/etassia/notation/plantuml/sequence/picture.rb +29 -0
  432. data/lib/etassia/notation/plantuml/sequence/ref.rb +24 -0
  433. data/lib/etassia/notation/plantuml/sequence/ref_shape.rb +74 -0
  434. data/lib/etassia/notation/plantuml/sequence/refusals.rb +47 -0
  435. data/lib/etassia/notation/plantuml/sequence/renderer.rb +501 -0
  436. data/lib/etassia/notation/plantuml/sequence/scene.rb +200 -0
  437. data/lib/etassia/notation/plantuml/sequence/skin.rb +76 -0
  438. data/lib/etassia/notation/plantuml/sequence/spacing.rb +28 -0
  439. data/lib/etassia/notation/plantuml/sequence/style.rb +151 -0
  440. data/lib/etassia/notation/plantuml/sequence/tab_size.rb +42 -0
  441. data/lib/etassia/notation/plantuml/sequence/walker.rb +1190 -0
  442. data/lib/etassia/notation/plantuml/sequence.rb +47 -0
  443. data/lib/etassia/notation/plantuml/source_lines.rb +33 -0
  444. data/lib/etassia/notation/plantuml/state/axis_solver.rb +272 -0
  445. data/lib/etassia/notation/plantuml/state/cluster_padding.rb +75 -0
  446. data/lib/etassia/notation/plantuml/state/curve.rb +73 -0
  447. data/lib/etassia/notation/plantuml/state/dot_axis.rb +248 -0
  448. data/lib/etassia/notation/plantuml/state/extent.rb +42 -0
  449. data/lib/etassia/notation/plantuml/state/hierarchy.rb +355 -0
  450. data/lib/etassia/notation/plantuml/state/label_clearance.rb +148 -0
  451. data/lib/etassia/notation/plantuml/state/label_reach.rb +139 -0
  452. data/lib/etassia/notation/plantuml/state/layout.rb +173 -0
  453. data/lib/etassia/notation/plantuml/state/level_edges.rb +113 -0
  454. data/lib/etassia/notation/plantuml/state/look.rb +36 -0
  455. data/lib/etassia/notation/plantuml/state/note_shape.rb +73 -0
  456. data/lib/etassia/notation/plantuml/state/plane.rb +85 -0
  457. data/lib/etassia/notation/plantuml/state/ranking.rb +181 -0
  458. data/lib/etassia/notation/plantuml/state/reader.rb +310 -0
  459. data/lib/etassia/notation/plantuml/state/renderer.rb +253 -0
  460. data/lib/etassia/notation/plantuml/state/route_detour.rb +115 -0
  461. data/lib/etassia/notation/plantuml/state/route_geometry.rb +186 -0
  462. data/lib/etassia/notation/plantuml/state/routes.rb +196 -0
  463. data/lib/etassia/notation/plantuml/state/row.rb +88 -0
  464. data/lib/etassia/notation/plantuml/state/rows.rb +50 -0
  465. data/lib/etassia/notation/plantuml/state/stretch.rb +71 -0
  466. data/lib/etassia/notation/plantuml/state.rb +62 -0
  467. data/lib/etassia/notation/plantuml/stereotype_head.rb +58 -0
  468. data/lib/etassia/notation/plantuml/style/bound.rb +34 -0
  469. data/lib/etassia/notation/plantuml/style/colour.rb +41 -0
  470. data/lib/etassia/notation/plantuml/style/colour_names.rb +89 -0
  471. data/lib/etassia/notation/plantuml/style/defaults.rb +33 -0
  472. data/lib/etassia/notation/plantuml/style/inline.rb +56 -0
  473. data/lib/etassia/notation/plantuml/style/reader.rb +242 -0
  474. data/lib/etassia/notation/plantuml/style/sheet.rb +103 -0
  475. data/lib/etassia/notation/plantuml/style.rb +32 -0
  476. data/lib/etassia/notation/plantuml/style_reader.rb +160 -0
  477. data/lib/etassia/notation/plantuml/style_sheet.rb +27 -0
  478. data/lib/etassia/notation/plantuml/styled_relation_curve.rb +71 -0
  479. data/lib/etassia/notation/plantuml/timing/arrows.rb +81 -0
  480. data/lib/etassia/notation/plantuml/timing/axis.rb +100 -0
  481. data/lib/etassia/notation/plantuml/timing/layout.rb +172 -0
  482. data/lib/etassia/notation/plantuml/timing/model.rb +34 -0
  483. data/lib/etassia/notation/plantuml/timing/reader.rb +173 -0
  484. data/lib/etassia/notation/plantuml/timing/signals.rb +328 -0
  485. data/lib/etassia/notation/plantuml/timing.rb +38 -0
  486. data/lib/etassia/notation/plantuml/tree/layout.rb +276 -0
  487. data/lib/etassia/notation/plantuml/tree/mindmap_layout.rb +157 -0
  488. data/lib/etassia/notation/plantuml/tree/reader.rb +186 -0
  489. data/lib/etassia/notation/plantuml/tree/renderer.rb +67 -0
  490. data/lib/etassia/notation/plantuml/tree/role.rb +57 -0
  491. data/lib/etassia/notation/plantuml/tree/rules.rb +156 -0
  492. data/lib/etassia/notation/plantuml/tree/wbs_layout.rb +164 -0
  493. data/lib/etassia/notation/plantuml/tree.rb +41 -0
  494. data/lib/etassia/notation/plantuml/unsupported_construct_error.rb +39 -0
  495. data/lib/etassia/notation/plantuml/unsupported_constructs.rb +103 -0
  496. data/lib/etassia/notation/plantuml/usecase.rb +36 -0
  497. data/lib/etassia/notation/plantuml.rb +132 -0
  498. data/lib/etassia/notation/plantuml_data/boxes/frames.rb +101 -0
  499. data/lib/etassia/notation/plantuml_data/boxes/graph_layout.rb +348 -0
  500. data/lib/etassia/notation/plantuml_data/boxes/heads.rb +65 -0
  501. data/lib/etassia/notation/plantuml_data/boxes/highlights.rb +70 -0
  502. data/lib/etassia/notation/plantuml_data/boxes/inline_rows.rb +99 -0
  503. data/lib/etassia/notation/plantuml_data/boxes/json_reader.rb +24 -0
  504. data/lib/etassia/notation/plantuml_data/boxes/link_text.rb +94 -0
  505. data/lib/etassia/notation/plantuml_data/boxes/marks.rb +76 -0
  506. data/lib/etassia/notation/plantuml_data/boxes/ornaments.rb +130 -0
  507. data/lib/etassia/notation/plantuml_data/boxes/renderer.rb +95 -0
  508. data/lib/etassia/notation/plantuml_data/boxes/row_alignment.rb +58 -0
  509. data/lib/etassia/notation/plantuml_data/boxes/row_bend.rb +43 -0
  510. data/lib/etassia/notation/plantuml_data/boxes/scene.rb +27 -0
  511. data/lib/etassia/notation/plantuml_data/boxes/table.rb +56 -0
  512. data/lib/etassia/notation/plantuml_data/boxes/table_box.rb +355 -0
  513. data/lib/etassia/notation/plantuml_data/boxes/text.rb +101 -0
  514. data/lib/etassia/notation/plantuml_data/boxes/tree_builder.rb +82 -0
  515. data/lib/etassia/notation/plantuml_data/boxes/tree_layout.rb +82 -0
  516. data/lib/etassia/notation/plantuml_data/boxes/yaml_reader.rb +105 -0
  517. data/lib/etassia/notation/plantuml_data/openiconic.LICENSE +6 -0
  518. data/lib/etassia/notation/plantuml_data/openiconic.json +1 -0
  519. data/lib/etassia/notation/plantuml_data.rb +84 -0
  520. data/lib/etassia/notation/plantuml_salt/block.rb +80 -0
  521. data/lib/etassia/notation/plantuml_salt/box.rb +30 -0
  522. data/lib/etassia/notation/plantuml_salt/font_metrics.rb +28 -0
  523. data/lib/etassia/notation/plantuml_salt/grid_marks.rb +66 -0
  524. data/lib/etassia/notation/plantuml_salt/group_marks.rb +77 -0
  525. data/lib/etassia/notation/plantuml_salt/layout.rb +206 -0
  526. data/lib/etassia/notation/plantuml_salt/mark.rb +36 -0
  527. data/lib/etassia/notation/plantuml_salt/markup.rb +57 -0
  528. data/lib/etassia/notation/plantuml_salt/parser.rb +239 -0
  529. data/lib/etassia/notation/plantuml_salt/renderer.rb +138 -0
  530. data/lib/etassia/notation/plantuml_salt/row.rb +19 -0
  531. data/lib/etassia/notation/plantuml_salt/run.rb +34 -0
  532. data/lib/etassia/notation/plantuml_salt/scene.rb +23 -0
  533. data/lib/etassia/notation/plantuml_salt/scroll_marks.rb +94 -0
  534. data/lib/etassia/notation/plantuml_salt/tab_marks.rb +74 -0
  535. data/lib/etassia/notation/plantuml_salt/tree_marks.rb +127 -0
  536. data/lib/etassia/notation/plantuml_salt/widget.rb +40 -0
  537. data/lib/etassia/notation/plantuml_salt/widget_marks.rb +319 -0
  538. data/lib/etassia/notation/plantuml_salt.rb +66 -0
  539. data/lib/etassia/notation/plugin_failure.rb +42 -0
  540. data/lib/etassia/notation.rb +289 -0
  541. data/lib/etassia/parser/architecture.rb +25 -0
  542. data/lib/etassia/parser/atoms/delimited_run.rb +82 -0
  543. data/lib/etassia/parser/atoms/greedy_run.rb +116 -0
  544. data/lib/etassia/parser/atoms/joined.rb +47 -0
  545. data/lib/etassia/parser/atoms/trimmed_run.rb +47 -0
  546. data/lib/etassia/parser/base.rb +258 -0
  547. data/lib/etassia/parser/block.rb +32 -0
  548. data/lib/etassia/parser/builders/architecture.rb +135 -0
  549. data/lib/etassia/parser/builders/block.rb +288 -0
  550. data/lib/etassia/parser/builders/c4.rb +275 -0
  551. data/lib/etassia/parser/builders/capture_string.rb +17 -0
  552. data/lib/etassia/parser/builders/class_diagram.rb +539 -0
  553. data/lib/etassia/parser/builders/er_diagram.rb +308 -0
  554. data/lib/etassia/parser/builders/error.rb +51 -0
  555. data/lib/etassia/parser/builders/flowchart.rb +1150 -0
  556. data/lib/etassia/parser/builders/gantt.rb +267 -0
  557. data/lib/etassia/parser/builders/git_graph.rb +301 -0
  558. data/lib/etassia/parser/builders/info.rb +57 -0
  559. data/lib/etassia/parser/builders/kanban.rb +411 -0
  560. data/lib/etassia/parser/builders/mindmap.rb +271 -0
  561. data/lib/etassia/parser/builders/packet.rb +85 -0
  562. data/lib/etassia/parser/builders/pie.rb +105 -0
  563. data/lib/etassia/parser/builders/quadrant.rb +165 -0
  564. data/lib/etassia/parser/builders/radar.rb +157 -0
  565. data/lib/etassia/parser/builders/requirement.rb +230 -0
  566. data/lib/etassia/parser/builders/sankey.rb +142 -0
  567. data/lib/etassia/parser/builders/sequence.rb +431 -0
  568. data/lib/etassia/parser/builders/sequence_blocks.rb +117 -0
  569. data/lib/etassia/parser/builders/sequence_box_title.rb +57 -0
  570. data/lib/etassia/parser/builders/state_diagram.rb +364 -0
  571. data/lib/etassia/parser/builders/timeline.rb +133 -0
  572. data/lib/etassia/parser/builders/treemap.rb +138 -0
  573. data/lib/etassia/parser/builders/xy_chart.rb +148 -0
  574. data/lib/etassia/parser/c4.rb +37 -0
  575. data/lib/etassia/parser/class_diagram.rb +58 -0
  576. data/lib/etassia/parser/class_namespaces.rb +62 -0
  577. data/lib/etassia/parser/class_notes.rb +54 -0
  578. data/lib/etassia/parser/er_diagram.rb +28 -0
  579. data/lib/etassia/parser/error.rb +30 -0
  580. data/lib/etassia/parser/flowchart.rb +87 -0
  581. data/lib/etassia/parser/gantt.rb +43 -0
  582. data/lib/etassia/parser/git_graph.rb +106 -0
  583. data/lib/etassia/parser/grammars/architecture.rb +165 -0
  584. data/lib/etassia/parser/grammars/block.rb +190 -0
  585. data/lib/etassia/parser/grammars/c4.rb +229 -0
  586. data/lib/etassia/parser/grammars/class_diagram.rb +606 -0
  587. data/lib/etassia/parser/grammars/common.rb +107 -0
  588. data/lib/etassia/parser/grammars/er_diagram.rb +232 -0
  589. data/lib/etassia/parser/grammars/error.rb +32 -0
  590. data/lib/etassia/parser/grammars/flowchart.rb +1722 -0
  591. data/lib/etassia/parser/grammars/gantt.rb +198 -0
  592. data/lib/etassia/parser/grammars/git_graph.rb +234 -0
  593. data/lib/etassia/parser/grammars/info.rb +53 -0
  594. data/lib/etassia/parser/grammars/kanban.rb +417 -0
  595. data/lib/etassia/parser/grammars/mermaid_unicode_text.rb +108 -0
  596. data/lib/etassia/parser/grammars/mindmap.rb +220 -0
  597. data/lib/etassia/parser/grammars/packet.rb +71 -0
  598. data/lib/etassia/parser/grammars/pie.rb +128 -0
  599. data/lib/etassia/parser/grammars/quadrant.rb +202 -0
  600. data/lib/etassia/parser/grammars/radar.rb +158 -0
  601. data/lib/etassia/parser/grammars/requirement.rb +258 -0
  602. data/lib/etassia/parser/grammars/sankey.rb +104 -0
  603. data/lib/etassia/parser/grammars/sequence.rb +940 -0
  604. data/lib/etassia/parser/grammars/state_diagram.rb +382 -0
  605. data/lib/etassia/parser/grammars/timeline.rb +153 -0
  606. data/lib/etassia/parser/grammars/treemap.rb +122 -0
  607. data/lib/etassia/parser/grammars/xy_chart.rb +119 -0
  608. data/lib/etassia/parser/info.rb +32 -0
  609. data/lib/etassia/parser/kanban.rb +131 -0
  610. data/lib/etassia/parser/mermaid_shapes.rb +106 -0
  611. data/lib/etassia/parser/metadata_yaml.rb +452 -0
  612. data/lib/etassia/parser/mindmap.rb +92 -0
  613. data/lib/etassia/parser/native/builders/architecture.rb +103 -0
  614. data/lib/etassia/parser/native/builders/block.rb +68 -0
  615. data/lib/etassia/parser/native/builders/c4.rb +16 -0
  616. data/lib/etassia/parser/native/builders/class_diagram.rb +63 -0
  617. data/lib/etassia/parser/native/builders/er_diagram.rb +47 -0
  618. data/lib/etassia/parser/native/builders/flowchart.rb +58 -0
  619. data/lib/etassia/parser/native/builders/gantt.rb +35 -0
  620. data/lib/etassia/parser/native/builders/gitgraph.rb +35 -0
  621. data/lib/etassia/parser/native/builders/info.rb +30 -0
  622. data/lib/etassia/parser/native/builders/kanban.rb +37 -0
  623. data/lib/etassia/parser/native/builders/mindmap.rb +26 -0
  624. data/lib/etassia/parser/native/builders/packet.rb +63 -0
  625. data/lib/etassia/parser/native/builders/pie.rb +82 -0
  626. data/lib/etassia/parser/native/builders/quadrant.rb +27 -0
  627. data/lib/etassia/parser/native/builders/radar.rb +84 -0
  628. data/lib/etassia/parser/native/builders/requirement.rb +25 -0
  629. data/lib/etassia/parser/native/builders/sankey.rb +18 -0
  630. data/lib/etassia/parser/native/builders/sequence.rb +48 -0
  631. data/lib/etassia/parser/native/builders/state_diagram.rb +61 -0
  632. data/lib/etassia/parser/native/builders/timeline.rb +46 -0
  633. data/lib/etassia/parser/native/builders/treemap.rb +60 -0
  634. data/lib/etassia/parser/native/builders/user_journey.rb +83 -0
  635. data/lib/etassia/parser/native/builders/xy_chart.rb +94 -0
  636. data/lib/etassia/parser/native/declined.rb +12 -0
  637. data/lib/etassia/parser/native/mermaid.json +1 -0
  638. data/lib/etassia/parser/native/text.rb +31 -0
  639. data/lib/etassia/parser/native.rb +99 -0
  640. data/lib/etassia/parser/packet.rb +56 -0
  641. data/lib/etassia/parser/pie.rb +32 -0
  642. data/lib/etassia/parser/quadrant.rb +77 -0
  643. data/lib/etassia/parser/radar.rb +99 -0
  644. data/lib/etassia/parser/requirement.rb +32 -0
  645. data/lib/etassia/parser/sankey.rb +47 -0
  646. data/lib/etassia/parser/sequence.rb +34 -0
  647. data/lib/etassia/parser/state_diagram.rb +29 -0
  648. data/lib/etassia/parser/timeline.rb +40 -0
  649. data/lib/etassia/parser/treemap.rb +25 -0
  650. data/lib/etassia/parser/user_journey.rb +253 -0
  651. data/lib/etassia/parser/xy_chart.rb +98 -0
  652. data/lib/etassia/parser.rb +18 -0
  653. data/lib/etassia/renderer/architecture.rb +232 -0
  654. data/lib/etassia/renderer/architecture_edge_router.rb +379 -0
  655. data/lib/etassia/renderer/base.rb +295 -0
  656. data/lib/etassia/renderer/block.rb +157 -0
  657. data/lib/etassia/renderer/c4.rb +294 -0
  658. data/lib/etassia/renderer/class_diagram.rb +671 -0
  659. data/lib/etassia/renderer/er_diagram.rb +842 -0
  660. data/lib/etassia/renderer/error.rb +131 -0
  661. data/lib/etassia/renderer/flowchart.rb +315 -0
  662. data/lib/etassia/renderer/gantt.rb +177 -0
  663. data/lib/etassia/renderer/git_graph.rb +224 -0
  664. data/lib/etassia/renderer/info.rb +59 -0
  665. data/lib/etassia/renderer/kanban.rb +157 -0
  666. data/lib/etassia/renderer/line_break_text.rb +56 -0
  667. data/lib/etassia/renderer/markdown_text.rb +106 -0
  668. data/lib/etassia/renderer/mindmap.rb +290 -0
  669. data/lib/etassia/renderer/packet.rb +87 -0
  670. data/lib/etassia/renderer/pie.rb +113 -0
  671. data/lib/etassia/renderer/quadrant.rb +189 -0
  672. data/lib/etassia/renderer/radar.rb +191 -0
  673. data/lib/etassia/renderer/requirement.rb +209 -0
  674. data/lib/etassia/renderer/sankey.rb +114 -0
  675. data/lib/etassia/renderer/sequence/frame_drawing.rb +169 -0
  676. data/lib/etassia/renderer/sequence/note_drawing.rb +69 -0
  677. data/lib/etassia/renderer/sequence.rb +490 -0
  678. data/lib/etassia/renderer/state_diagram.rb +202 -0
  679. data/lib/etassia/renderer/timeline.rb +134 -0
  680. data/lib/etassia/renderer/timeline_palette.rb +18 -0
  681. data/lib/etassia/renderer/treemap/section_total.rb +33 -0
  682. data/lib/etassia/renderer/treemap.rb +128 -0
  683. data/lib/etassia/renderer/user_journey.rb +209 -0
  684. data/lib/etassia/renderer/xy_chart.rb +180 -0
  685. data/lib/etassia/renderer.rb +22 -0
  686. data/lib/etassia/runtime.rb +125 -0
  687. data/lib/etassia/source/frontmatter.rb +501 -0
  688. data/lib/etassia/source.rb +224 -0
  689. data/lib/etassia/svg/arrowhead.rb +205 -0
  690. data/lib/etassia/svg/circle.rb +19 -0
  691. data/lib/etassia/svg/document.rb +106 -0
  692. data/lib/etassia/svg/element.rb +209 -0
  693. data/lib/etassia/svg/ellipse.rb +23 -0
  694. data/lib/etassia/svg/escaping.rb +149 -0
  695. data/lib/etassia/svg/group.rb +64 -0
  696. data/lib/etassia/svg/line.rb +21 -0
  697. data/lib/etassia/svg/numbers.rb +53 -0
  698. data/lib/etassia/svg/path.rb +67 -0
  699. data/lib/etassia/svg/path_geometry.rb +378 -0
  700. data/lib/etassia/svg/polygon.rb +25 -0
  701. data/lib/etassia/svg/polyline.rb +28 -0
  702. data/lib/etassia/svg/rect.rb +34 -0
  703. data/lib/etassia/svg/style.rb +50 -0
  704. data/lib/etassia/svg/text.rb +181 -0
  705. data/lib/etassia/svg/tspan.rb +48 -0
  706. data/lib/etassia/svg.rb +38 -0
  707. data/lib/etassia/text_measurement/arial_advances.rb +122 -0
  708. data/lib/etassia/text_measurement/dejavu_advances.rb +111 -0
  709. data/lib/etassia/text_measurement/times_advances.rb +64 -0
  710. data/lib/etassia/text_measurement/trebuchet_advances.rb +53 -0
  711. data/lib/etassia/text_measurement/trebuchet_kerning.rb +29 -0
  712. data/lib/etassia/text_measurement.rb +198 -0
  713. data/lib/etassia/theme/builtin/dark.yml +229 -0
  714. data/lib/etassia/theme/builtin/default.yml +239 -0
  715. data/lib/etassia/theme/builtin/high_contrast.yml +231 -0
  716. data/lib/etassia/theme/builtin/light.yml +229 -0
  717. data/lib/etassia/theme/color_palette.rb +97 -0
  718. data/lib/etassia/theme/completion.rb +50 -0
  719. data/lib/etassia/theme/effect_styles.rb +34 -0
  720. data/lib/etassia/theme/registry.rb +45 -0
  721. data/lib/etassia/theme/shape_styles.rb +34 -0
  722. data/lib/etassia/theme/spacing_config.rb +30 -0
  723. data/lib/etassia/theme/typography.rb +40 -0
  724. data/lib/etassia/theme.rb +73 -0
  725. data/lib/etassia/version.rb +5 -0
  726. data/lib/etassia.rb +40 -0
  727. metadata +865 -0
@@ -0,0 +1,1290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "flowchart/directed_placement"
5
+ require_relative "flowchart/edge_router"
6
+ require_relative "flowchart/label_backdrop_geometry"
7
+ require_relative "elk_placement"
8
+ require_relative "../diagram/flowchart"
9
+ require_relative "../diagram/flowchart_label_text"
10
+ require_relative "../notation/mermaid/ir_adapters/flowchart"
11
+ require_relative "../js_math"
12
+
13
+ module Etassia
14
+ module Layout
15
+ # Computes final canvas geometry for flowcharts.
16
+ class Flowchart < Base
17
+ # mmdc draws every flowchart text, node, cluster title and edge
18
+ # label alike, at 16px; a theme overrides it with flowchart_font_size.
19
+ DEFAULT_FONT_SIZE = 16.0
20
+ NODE_LINE_HEIGHT = 1.5
21
+ MIDDLE_BASELINE_EMS = 0.35
22
+
23
+ # The size every flowchart text is measured and drawn at. A theme
24
+ # value that is not a positive finite number falls back to the
25
+ # default: a NaN raises inside the box arithmetic and a non-positive
26
+ # one produces a negative SVG width.
27
+ def self.font_size(theme)
28
+ value = theme&.typography&.flowchart_font_size
29
+ usable = value.is_a?(Numeric) && value.finite? && value.positive?
30
+ usable ? value : DEFAULT_FONT_SIZE
31
+ end
32
+
33
+ # What mmdc adds to the label, measured on its output, as
34
+ # [width, height]. A shape not listed is sized as a box.
35
+ SHAPE_PADDING = {
36
+ "rounded" => [30.0, 30.0],
37
+ "asymmetric" => [24.75, 15.0],
38
+ "hexagon" => [45.0, 15.0],
39
+ "parallelogram" => [60.0, 15.0],
40
+ "parallelogram_alt" => [60.0, 15.0],
41
+ "trapezoid" => [60.0, 15.0],
42
+ "trapezoid_alt" => [60.0, 15.0],
43
+ }.freeze
44
+ BOX_PADDING = [60.0, 30.0].freeze
45
+ # An inferred node whose id is later reused by an empty subgraph keeps
46
+ # the node in mmdc, but receives its smaller collision-placeholder box.
47
+ COLLISION_NODE_PADDING = [32.0, 16.0].freeze
48
+ # A hexagon grows wider with each line under the first: 45 for one
49
+ # line, 48.15 for two in mmdc's output, so 0.118 a pixel of height.
50
+ HEXAGON_WIDTH_PER_HEIGHT = 0.118
51
+ # Square shapes: a rhombus is label width + label height + 30 on a
52
+ # side; a circle is label width + 15 across (25 for a double one).
53
+ CIRCLE_PADDING = { "circle" => 15.0, "double_circle" => 25.0 }.freeze
54
+ RHOMBUS_PADDING = 30.0
55
+
56
+ CLUSTER_CORNER = FlowchartEdgeRouter::CLUSTER_CORNER
57
+ ROUNDED_CORNER = 5.0
58
+ CLUSTER_TITLE_BASELINE = 20
59
+ EDGE_LABEL_LIFT = 5.0
60
+ # mmdc breaks an edge label wider than this at spaces, and the label
61
+ # box takes this width. One word wider than it stays on its line.
62
+ EDGE_LABEL_WRAP_WIDTH = 200.0
63
+ SUBSTITUTE_FONT_HEADROOM = 1.2
64
+ ICON_ADVANCE_EMS = 1.3
65
+ SOURCE_ELK_PLACEMENT = :mermaid_elk
66
+ SOURCE_ELK_SPACING = 35
67
+ private_constant :SOURCE_ELK_PLACEMENT, :SOURCE_ELK_SPACING
68
+
69
+ SELF_LOOP_DEPTH = 0.45
70
+ SELF_LOOP_HALF_SPAN = 0.175
71
+ SELF_LOOP_HALF_SPAN_LIMITS = (18.0..50.0)
72
+ SELF_LOOP_MAX_DEPTH = 48.0
73
+ SELF_LOOP_SIDES = {
74
+ "DOWN" => [0, 1].freeze,
75
+ "UP" => [0, -1].freeze,
76
+ "RIGHT" => [1, 0].freeze,
77
+ "LEFT" => [-1, 0].freeze,
78
+ }.freeze
79
+
80
+ NODE_OUTLINES = {
81
+ "rounded" => "rounded", "stadium" => "stadium",
82
+ "circle" => "circle", "double_circle" => "circle",
83
+ "rhombus" => "rhombus", "hexagon" => "hexagon"
84
+ }.freeze
85
+
86
+ BOUNDARY_SCALES = {
87
+ "rhombus" => :rhombus_scale, "circle" => :circle_scale,
88
+ "hexagon" => :hexagon_scale, "rounded" => :rounded_scale,
89
+ "stadium" => :stadium_scale
90
+ }.freeze
91
+ private_constant :BOUNDARY_SCALES
92
+
93
+ EDGE_HEADS = {
94
+ "arrow" => "arrow", "cross" => "cross", "circle" => "circle"
95
+ }.freeze
96
+ ARROW_LENGTH = 8.0
97
+ ARROW_HALF_WIDTH = 4.0
98
+ CIRCLE_HEAD_RADIUS = 5.5
99
+ CROSS_HEAD_HALF = 4.5
100
+ CIRCLE_HEAD_REACH = 6.6
101
+ CROSS_HEAD_REACH = 6.5
102
+
103
+ class Point < Lutaml::Model::Serializable
104
+ attribute :x, :float
105
+ attribute :y, :float
106
+ end
107
+
108
+ class TextRun < Lutaml::Model::Serializable
109
+ attribute :content, :string
110
+ attribute :x, :float
111
+ attribute :y, :float
112
+ end
113
+
114
+ class Label < Lutaml::Model::Serializable
115
+ attribute :text, :string
116
+ attribute :width, :float
117
+ attribute :height, :float
118
+ attribute :x, :float
119
+ attribute :y, :float
120
+ attribute :text_y, :float
121
+ attribute :font_size, :float
122
+ attribute :lines, :string, collection: true, default: -> { [] }
123
+ attribute :line_ys, :float, collection: true, default: -> { [] }
124
+ attribute :runs, TextRun, collection: true, default: -> { [] }
125
+
126
+ def backdrop_geometry(font_size:)
127
+ LabelBackdropGeometry.for_label(self, font_size: font_size)
128
+ end
129
+
130
+ alias assign_line_ys line_ys=
131
+ private :assign_line_ys
132
+
133
+ def line_ys=(values)
134
+ assign_line_ys(values)
135
+ return unless defined?(@runs) && runs.any?
136
+
137
+ self.text_y = line_ys.first || y
138
+ runs.each_with_index do |run, row|
139
+ run.y = row.zero? ? nil : line_ys.fetch(row, nil)
140
+ end
141
+ end
142
+ end
143
+
144
+ class Line < Lutaml::Model::Serializable
145
+ attribute :x1, :float
146
+ attribute :y1, :float
147
+ attribute :x2, :float
148
+ attribute :y2, :float
149
+ end
150
+
151
+ class Head < Lutaml::Model::Serializable
152
+ attribute :shape, :string
153
+ attribute :points, :string
154
+ attribute :x, :float
155
+ attribute :y, :float
156
+ attribute :radius, :float
157
+ attribute :lines, Line, collection: true, default: -> { [] }
158
+ end
159
+
160
+ class Section < Lutaml::Model::Serializable
161
+ attribute :start_point, Point
162
+ attribute :end_point, Point
163
+ attribute :bend_points, Point, collection: true, default: -> { [] }
164
+ end
165
+
166
+ class Edge < Lutaml::Model::Serializable
167
+ attribute :id, :string
168
+ attribute :source, :string
169
+ attribute :target, :string
170
+ attribute :sections, Section, collection: true, default: -> { [] }
171
+ attribute :labels, Label, collection: true, default: -> { [] }
172
+ attribute :arrow_type, :string
173
+ attribute :path, :string
174
+ attribute :heads, Head, collection: true, default: -> { [] }
175
+ end
176
+
177
+ class Node < Lutaml::Model::Serializable
178
+ attribute :fill, :string
179
+ attribute :stroke, :string
180
+ attribute :id, :string
181
+ attribute :x, :float
182
+ attribute :y, :float
183
+ attribute :width, :float
184
+ attribute :height, :float
185
+ attribute :labels, Label, collection: true, default: -> { [] }
186
+ attribute :shape, :string
187
+ attribute :shape_kind, :string
188
+ attribute :shape_points, :string
189
+ attribute :shape_x, :float
190
+ attribute :shape_y, :float
191
+ attribute :shape_width, :float
192
+ attribute :shape_height, :float
193
+ attribute :center_x, :float
194
+ attribute :center_y, :float
195
+ attribute :radius, :float
196
+ attribute :corner_radius, :float
197
+ attribute :cluster, :boolean, default: false
198
+ attribute :container, :boolean, default: false
199
+ attribute :children, Node, collection: true, default: -> { [] }
200
+ end
201
+
202
+ class Scene < Layout::Scene
203
+ attribute :id, :string
204
+ attribute :view_box, :string
205
+ attribute :children, Node, collection: true, default: -> { [] }
206
+ attribute :edges, Edge, collection: true, default: -> { [] }
207
+ end
208
+
209
+ # Builds a typed Scene from a hand-positioned ELK-shaped graph. This is
210
+ # useful for geometry specs and for the eventual elkrb adapter: the Hash
211
+ # stays inside Layout and never reaches Renderer.
212
+ def self.from_graph(graph, theme: nil)
213
+ layout = new
214
+ layout.theme = theme if theme
215
+ layout.send(:scene_from_graph, graph)
216
+ end
217
+
218
+ # Engine#choose_placement sets this for layout_engine: :elk. Explicit
219
+ # callers retain the dagre-compatible orchestration below.
220
+ attr_accessor :placement
221
+
222
+ def source_placement=(value)
223
+ self.placement = value == :elk ? SOURCE_ELK_PLACEMENT : value
224
+ end
225
+
226
+ def scene(diagram)
227
+ graph = build_graph(ir_graph(diagram))
228
+ place_graph(graph)
229
+ compact_collision_nodes(graph[:children])
230
+ scene_from_graph(graph)
231
+ end
232
+
233
+ private
234
+
235
+ def place_graph(graph)
236
+ unless placement == SOURCE_ELK_PLACEMENT
237
+ return DirectedPlacement.apply(graph, center_edge_labels: true)
238
+ end
239
+
240
+ graph[:layoutOptions][ElkOptions::NODE_NODE_SPACING] =
241
+ SOURCE_ELK_SPACING
242
+ graph[:layoutOptions][ElkOptions::LAYER_SPACING] = SOURCE_ELK_SPACING
243
+ ElkPlacement.apply(graph)
244
+ end
245
+
246
+ def ir_graph(diagram)
247
+ return diagram if diagram.is_a?(IR::Graph)
248
+
249
+ Notation::Mermaid::IRAdapters::Flowchart.call(diagram)
250
+ end
251
+
252
+ # Builds the temporary ELK-shaped input used within this layout.
253
+ def build_graph(graph)
254
+ {
255
+ id: diagram_identifier(graph),
256
+ children: transform_children(graph),
257
+ edges: transform_edges(graph),
258
+ layoutOptions: layout_options(graph),
259
+ }
260
+ end
261
+
262
+ def scene_from_graph(graph)
263
+ flat = flatten(graph)
264
+ flat[:label_endpoints] = label_endpoints(graph, flat[:clusters])
265
+ dx, dy = self_loop_overflow(flat)
266
+ page = shift_page(flat, dx, dy)
267
+ edges = typed_edges(page)
268
+
269
+ Scene.new(id: graph[:id] || "flowchart", **canvas_geometry(page, edges),
270
+ children: typed_children(
271
+ graph[:children] || [], 0, 0, dx, dy
272
+ ),
273
+ edges: edges)
274
+ end
275
+
276
+ def canvas_geometry(page, edges)
277
+ width, height = canvas_size(page, edges)
278
+ { width: width, height: height, view_box: "0 0 #{width} #{height}" }
279
+ end
280
+
281
+ # [width, height] of the boxes and of every routed edge, so a route
282
+ # that detours round a box stays on the page.
283
+ def canvas_size(page, edges)
284
+ [[calculate_width(page), route_reach(edges, :x)].max,
285
+ [calculate_height(page), route_reach(edges, :y)].max]
286
+ end
287
+
288
+ # How far a routed edge reaches along :x or :y, plus the page margin.
289
+ def route_reach(edges, axis)
290
+ points = edges.flat_map do |edge|
291
+ edge.sections.flat_map do |section|
292
+ [section.start_point, *section.bend_points, section.end_point]
293
+ end
294
+ end
295
+ (points.map { |point| point.public_send(axis) }.max || 0) +
296
+ DirectedPlacement::PAGE_MARGIN
297
+ end
298
+
299
+ def flatten(graph)
300
+ clusters = []
301
+ nodes = []
302
+ collect(graph[:children] || [], 0, 0, clusters, nodes)
303
+ graph.merge(children: nodes, clusters: clusters)
304
+ end
305
+
306
+ def collect(children, delta_x, delta_y, clusters, nodes)
307
+ children.each do |child|
308
+ placed = shifted_box(child, delta_x, delta_y)
309
+ unless child[:children]
310
+ nodes << placed
311
+ next
312
+ end
313
+
314
+ clusters << placed.except(:children) if cluster?(child)
315
+ collect(child[:children], placed[:x], placed[:y], clusters, nodes)
316
+ end
317
+ end
318
+
319
+ def shifted_box(box, delta_x, delta_y)
320
+ box.merge(x: (box[:x] || 0) + delta_x,
321
+ y: (box[:y] || 0) + delta_y)
322
+ end
323
+
324
+ def cluster?(child)
325
+ FlowchartEdgeRouter.cluster?(child)
326
+ end
327
+
328
+ def shift_page(graph, delta_x, delta_y)
329
+ return graph if delta_x.zero? && delta_y.zero?
330
+
331
+ graph.merge(children: shift_boxes(graph[:children], delta_x, delta_y),
332
+ clusters: shift_boxes(graph[:clusters], delta_x, delta_y),
333
+ edges: shift_edges(graph[:edges], delta_x, delta_y))
334
+ end
335
+
336
+ def shift_boxes(boxes, delta_x, delta_y)
337
+ (boxes || []).map { |box| shifted_box(box, delta_x, delta_y) }
338
+ end
339
+
340
+ def shift_edges(edges, delta_x, delta_y)
341
+ (edges || []).map { |edge| shifted_edge(edge, delta_x, delta_y) }
342
+ end
343
+
344
+ def shifted_edge(edge, delta_x, delta_y)
345
+ sections = edge[:sections]
346
+ return edge unless sections&.any?
347
+
348
+ edge.merge(sections: sections.map do |section|
349
+ shifted_section(section, delta_x, delta_y)
350
+ end)
351
+ end
352
+
353
+ def shifted_section(section, delta_x, delta_y)
354
+ bends = section[:bendPoints]
355
+ return section unless bends&.any?
356
+
357
+ section.merge(bendPoints: shift_boxes(bends, delta_x, delta_y))
358
+ end
359
+
360
+ def typed_children(children, parent_x, parent_y, delta_x, delta_y)
361
+ children.map do |child|
362
+ x = (child[:x] || 0) + parent_x + delta_x
363
+ y = (child[:y] || 0) + parent_y + delta_y
364
+ typed_node(child, x, y, delta_x, delta_y)
365
+ end
366
+ end
367
+
368
+ def typed_node(node, x_pos, y_pos, delta_x, delta_y)
369
+ bounds = { x: x_pos, y: y_pos, width: node[:width] || 100,
370
+ height: node[:height] || 50 }
371
+ Node.new(**typed_node_attributes(node, bounds),
372
+ children: typed_children(
373
+ node[:children] || [], x_pos - delta_x, y_pos - delta_y,
374
+ delta_x, delta_y
375
+ ))
376
+ end
377
+
378
+ def typed_node_attributes(node, bounds)
379
+ is_cluster = cluster?(node)
380
+ shape = node.dig(:metadata, :shape) || "rect"
381
+ kind = is_cluster ? "cluster" : (NODE_OUTLINES[shape] || "rect")
382
+ { id: node[:id], **bounds, shape: shape, shape_kind: kind,
383
+ fill: node.dig(:metadata, :fill),
384
+ stroke: node.dig(:metadata, :stroke),
385
+ labels: typed_node_labels(node, bounds, is_cluster),
386
+ **shape_geometry(kind, bounds), **painted_box(bounds, is_cluster),
387
+ cluster: is_cluster, container: node.key?(:children) }
388
+ end
389
+
390
+ def shape_geometry(kind, bounds)
391
+ x, y, width, height = bounds.values_at(:x, :y, :width, :height)
392
+ { shape_points: polygon_points(kind, x, y, width, height),
393
+ center_x: x + (width / 2.0), center_y: y + (height / 2.0),
394
+ radius: [width, height].min / 2.0,
395
+ corner_radius: corner_radius(kind, width, height) }
396
+ end
397
+
398
+ def painted_box(bounds, cluster)
399
+ drawn = cluster ? bounds.transform_values(&:to_i) : bounds
400
+ { shape_x: drawn[:x], shape_y: drawn[:y],
401
+ shape_width: drawn[:width], shape_height: drawn[:height] }
402
+ end
403
+
404
+ def typed_node_labels(node, bounds, cluster)
405
+ (node[:labels] || []).first(1).map do |label|
406
+ x, y = node_label_anchor(bounds, cluster)
407
+ y = cluster_title_y(node, bounds, label) if cluster
408
+ Label.new(
409
+ text: label[:text], width: label[:width], height: label[:height],
410
+ x: x, y: y, **label_geometry(label, x, y, with_runs: !cluster)
411
+ )
412
+ end
413
+ end
414
+
415
+ def node_label_anchor(bounds, cluster)
416
+ x, y, width, height = bounds.values_at(:x, :y, :width, :height)
417
+ label_y = y + (cluster ? CLUSTER_TITLE_BASELINE : height / 2.0)
418
+ label_x = cluster ? x + (width.to_i / 2) : x + (width / 2.0)
419
+ [label_x, label_y]
420
+ end
421
+
422
+ # Keep a title inside the reserved header, rather than over its first
423
+ # child. Ordinary headers retain their established label position.
424
+ def cluster_title_y(node, bounds, label)
425
+ header = cluster_header_height(node, bounds)
426
+ half = label[:height].to_f / 2.0
427
+ center = CLUSTER_TITLE_BASELINE.to_f
428
+ center = header / 2.0 if center <= half || center + half >= header
429
+ bounds[:y] + center
430
+ end
431
+
432
+ def cluster_header_height(node, bounds)
433
+ (node[:children] || []).filter_map { |child| child[:y] }.min ||
434
+ bounds[:height]
435
+ end
436
+
437
+ def label_geometry(label, x_pos, y_pos, with_runs: false)
438
+ lines = label[:text].to_s.scrub.split("\n", -1)
439
+ lines = lines.one? ? [] : sentence_lines(lines)
440
+ font_size = layout_font_size
441
+ line_ys = line_baselines(y_pos, label[:height], lines, font_size)
442
+ text_y = line_ys.first || y_pos
443
+ runs = with_runs ? text_runs(lines, line_ys, x_pos) : []
444
+ { font_size: font_size, lines: lines, line_ys: line_ys,
445
+ text_y: text_y, runs: runs }
446
+ end
447
+
448
+ def text_runs(lines, baselines, x_pos)
449
+ lines.each_with_index.map do |line, row|
450
+ TextRun.new(
451
+ content: line,
452
+ x: x_pos,
453
+ y: row.zero? ? nil : baselines.fetch(row),
454
+ )
455
+ end
456
+ end
457
+
458
+ def sentence_lines(lines)
459
+ lines[0...-1].map { |line| "#{line} " } << lines.last
460
+ end
461
+
462
+ def line_baselines(y_pos, height, lines, font_size)
463
+ return [] if lines.empty?
464
+
465
+ pitch = height / lines.size
466
+ first = y_pos - (pitch * (lines.size - 1) / 2.0) +
467
+ (MIDDLE_BASELINE_EMS * font_size)
468
+ lines.each_index.map { |row| first + (row * pitch) }
469
+ end
470
+
471
+ def polygon_points(kind, x_pos, y_pos, width, height)
472
+ case kind
473
+ when "rhombus"
474
+ format_polygon(rhombus_points(x_pos, y_pos, width, height))
475
+ when "hexagon"
476
+ format_polygon(hexagon_points(x_pos, y_pos, width, height))
477
+ end
478
+ end
479
+
480
+ def format_polygon(points)
481
+ points.map { |x, y| "#{x},#{y}" }.join(" ")
482
+ end
483
+
484
+ def rhombus_points(x_pos, y_pos, width, height)
485
+ cx = x_pos + (width / 2.0)
486
+ cy = y_pos + (height / 2.0)
487
+ [[cx, y_pos], [x_pos + width, cy], [cx, y_pos + height], [x_pos, cy]]
488
+ end
489
+
490
+ def hexagon_points(x_pos, y_pos, width, height)
491
+ left, inner_left, inner_right, right = hexagon_xs(x_pos, width)
492
+ middle = y_pos + (height / 2.0)
493
+ bottom = y_pos + height
494
+ [[inner_left, y_pos], [inner_right, y_pos], [right, middle],
495
+ [inner_right, bottom], [inner_left, bottom], [left, middle]]
496
+ end
497
+
498
+ def hexagon_xs(x_pos, width)
499
+ quarter = width / 4.0
500
+ [x_pos, x_pos + quarter, x_pos + width - quarter, x_pos + width]
501
+ end
502
+
503
+ def corner_radius(kind, width, height)
504
+ return CLUSTER_CORNER if kind == "cluster"
505
+ return ROUNDED_CORNER if kind == "rounded"
506
+ return [width, height].min / 2.0 if kind == "stadium"
507
+
508
+ 0.0
509
+ end
510
+
511
+ def typed_edges(graph)
512
+ (graph[:edges] || []).filter_map { |edge| typed_edge(edge, graph) }
513
+ end
514
+
515
+ def typed_edge(edge, graph)
516
+ source = find_endpoint(graph, edge[:sources]&.first)
517
+ target = find_endpoint(graph, edge[:targets]&.first)
518
+ return unless source && target
519
+
520
+ Edge.new(**typed_edge_attributes(edge, graph, source, target))
521
+ end
522
+
523
+ def typed_edge_attributes(edge, graph, source, target)
524
+ route, bends, label_route = edge_route(edge, graph, source, target)
525
+ label_route = external_label_route(edge, graph, label_route)
526
+ type = edge.dig(:metadata, :arrow_type).to_s
527
+ { id: edge[:id], source: source[:id], target: target[:id],
528
+ sections: [typed_section(route, bends)],
529
+ labels: typed_edge_labels(edge, source, target, bends, label_route),
530
+ arrow_type: type, path: edge_path(route, bends),
531
+ heads: typed_heads(route, bends, source, target, type) }
532
+ end
533
+
534
+ # A crossing edge's label belongs between the boxes at its layout
535
+ # level: the enclosing cluster and the outside node, not an interior
536
+ # node's clipped outline. Keep the actual arrow route unchanged.
537
+ def label_endpoints(graph, clusters)
538
+ levels = LevelEdges.new(graph)
539
+ containers = [nil, *clusters.map { |cluster| cluster[:id] }]
540
+ entries = containers.flat_map { |container| levels.at(container) }
541
+ entries.to_h do |edge, from, to|
542
+ [edge[:id], [from, to]]
543
+ end
544
+ end
545
+
546
+ def external_label_route(edge, graph, ordinary)
547
+ ids = crossing_label_ids(edge, graph)
548
+ return ordinary unless ids
549
+
550
+ source, target = ids.map { |id| find_endpoint(graph, id) }
551
+ return ordinary unless source && target
552
+
553
+ edge_router.route(source, target, []).last
554
+ end
555
+
556
+ def crossing_label_ids(edge, graph)
557
+ ids = graph.fetch(:label_endpoints, {})[edge[:id]]
558
+ return unless ids&.all?
559
+ return if ids == [edge[:sources]&.first, edge[:targets]&.first]
560
+
561
+ ids
562
+ end
563
+
564
+ def typed_section(route, bends)
565
+ Section.new(
566
+ start_point: Point.new(x: route[0], y: route[1]),
567
+ end_point: Point.new(x: route[2], y: route[3]),
568
+ bend_points: bends.map do |point|
569
+ Point.new(x: point[:x], y: point[:y])
570
+ end,
571
+ )
572
+ end
573
+
574
+ def typed_edge_labels(edge, source, target, bends, route)
575
+ (edge[:labels] || []).first(1).map do |label|
576
+ x, y = edge_label_anchor(source, target, bends, route)
577
+ Label.new(
578
+ text: label[:text], width: label[:width], height: label[:height],
579
+ x: x, y: y, **label_geometry(label, x, y)
580
+ )
581
+ end
582
+ end
583
+
584
+ def edge_path(route, bends)
585
+ sx, sy, tx, ty = route.map { |coordinate| coordinate.round(2) }
586
+ return "M #{sx} #{sy} L #{tx} #{ty}" if bends.empty?
587
+
588
+ (["M #{sx} #{sy}"] + bends.map do |point|
589
+ "L #{point[:x].round(1)} #{point[:y].round(1)}"
590
+ end + ["L #{tx} #{ty}"]).join(" ")
591
+ end
592
+
593
+ def typed_heads(route, bends, source, target, type)
594
+ head_type = type.sub(/\A(?:thick|dotted)_/, "").delete_suffix("_both")
595
+ shape = EDGE_HEADS[head_type]
596
+ return [] unless shape
597
+
598
+ ends = type.end_with?("_both") ? %i[source target] : [:target]
599
+ ends.map do |which|
600
+ geometry = head_tip_geometry(route, bends, source, target, which)
601
+ typed_head(shape, geometry)
602
+ end
603
+ end
604
+
605
+ def typed_head(shape, geometry)
606
+ case shape
607
+ when "cross" then cross_head(geometry)
608
+ when "circle" then circle_head(geometry)
609
+ else arrow_head(geometry)
610
+ end
611
+ end
612
+
613
+ def arrow_head(geometry)
614
+ Head.new(shape: "arrow", points: arrow_head_points(geometry))
615
+ end
616
+
617
+ def arrow_head_points(geometry)
618
+ tip_x, tip_y, along_x, along_y, back_x, back_y = arrow_back(geometry)
619
+ points = [[tip_x, tip_y],
620
+ [back_x - (along_y * ARROW_HALF_WIDTH),
621
+ back_y + (along_x * ARROW_HALF_WIDTH)],
622
+ [back_x + (along_y * ARROW_HALF_WIDTH),
623
+ back_y - (along_x * ARROW_HALF_WIDTH)]]
624
+ format_head_points(points)
625
+ end
626
+
627
+ def format_head_points(points)
628
+ points.map { |x, y| "#{x.round(1)},#{y.round(1)}" }.join(" ")
629
+ end
630
+
631
+ def arrow_back(geometry)
632
+ tip_x, tip_y, from_x, from_y =
633
+ geometry.values_at(:tip_x, :tip_y, :from_x, :from_y)
634
+ along_x, along_y = unit_towards(tip_x, tip_y, from_x, from_y)
635
+ back_x = tip_x + (along_x * ARROW_LENGTH)
636
+ back_y = tip_y + (along_y * ARROW_LENGTH)
637
+ [tip_x, tip_y, along_x, along_y, back_x, back_y]
638
+ end
639
+
640
+ def circle_head(geometry)
641
+ x, y = backed_off(geometry, CIRCLE_HEAD_REACH)
642
+ Head.new(shape: "circle", x: x.round(1), y: y.round(1),
643
+ radius: CIRCLE_HEAD_RADIUS)
644
+ end
645
+
646
+ def cross_head(geometry)
647
+ tip_x, tip_y, arm_x, arm_y = cross_arms(geometry)
648
+ lines = [[arm_x, arm_y], [arm_y, -arm_x]].map do |line_x, line_y|
649
+ cross_line(tip_x, tip_y, line_x, line_y)
650
+ end
651
+ Head.new(shape: "cross", lines: lines)
652
+ end
653
+
654
+ def cross_arms(geometry)
655
+ tip_x, tip_y = backed_off(geometry, CROSS_HEAD_REACH)
656
+ along_x, along_y = cross_direction(geometry, tip_x, tip_y)
657
+ arm_x = (along_x - along_y) * CROSS_HEAD_HALF
658
+ arm_y = (along_y + along_x) * CROSS_HEAD_HALF
659
+ [tip_x, tip_y, arm_x, arm_y]
660
+ end
661
+
662
+ def cross_direction(geometry, tip_x, tip_y)
663
+ from_x, from_y = geometry.values_at(:from_x, :from_y)
664
+ x, y = unit_towards(tip_x, tip_y, from_x, from_y)
665
+ x = 1.0 if x.zero? && y.zero?
666
+ [x, y]
667
+ end
668
+
669
+ def cross_line(tip_x, tip_y, line_x, line_y)
670
+ Line.new(x1: (tip_x - line_x).round(1), y1: (tip_y - line_y).round(1),
671
+ x2: (tip_x + line_x).round(1), y2: (tip_y + line_y).round(1))
672
+ end
673
+
674
+ def backed_off(geometry, reach)
675
+ tip_x, tip_y, from_x, from_y =
676
+ geometry.values_at(:tip_x, :tip_y, :from_x, :from_y)
677
+ along_x, along_y = unit_towards(tip_x, tip_y, from_x, from_y)
678
+ [tip_x + (along_x * reach), tip_y + (along_y * reach)]
679
+ end
680
+
681
+ def unit_towards(tip_x, tip_y, from_x, from_y)
682
+ span = JsMath.hypot(from_x - tip_x, from_y - tip_y)
683
+ return [0.0, 0.0] if span.zero?
684
+
685
+ [(from_x - tip_x) / span, (from_y - tip_y) / span]
686
+ end
687
+
688
+ def calculate_width(graph)
689
+ boxes = drawn(graph)
690
+ return DirectedPlacement::PAGE_MARGIN * 2 if boxes.empty?
691
+
692
+ max_x = boxes.map { |node| (node[:x] || 0) + (node[:width] || 100) }.max
693
+ [max_x, self_loop_reach(graph)[0]].max + DirectedPlacement::PAGE_MARGIN
694
+ end
695
+
696
+ def calculate_height(graph)
697
+ boxes = drawn(graph)
698
+ return DirectedPlacement::PAGE_MARGIN * 2 if boxes.empty?
699
+
700
+ max_y = boxes.map { |node| (node[:y] || 0) + (node[:height] || 50) }.max
701
+ [max_y, self_loop_reach(graph)[1]].max + DirectedPlacement::PAGE_MARGIN
702
+ end
703
+
704
+ def drawn(graph)
705
+ (graph[:children] || []) + (graph[:clusters] || [])
706
+ end
707
+
708
+ def self_loop_bounds(graph)
709
+ side = self_loop_side(graph)
710
+ (graph[:edges] || []).reduce(nil) do |bounds, edge|
711
+ node = self_loop_node(graph, edge)
712
+ next bounds unless node
713
+
714
+ extent = loop_edge_bounds(edge, node, side)
715
+ merge_bounds(
716
+ bounds, *extent.values_at(:min_x, :max_x, :min_y, :max_y)
717
+ )
718
+ end
719
+ end
720
+
721
+ def loop_edge_bounds(edge, node, side)
722
+ bends = edge_bends(edge, node, node, side)
723
+ bounds = loop_label_bounds(edge, node, bends)
724
+ min_x, max_x = loop_axis_extent(bends, bounds, :x)
725
+ min_y, max_y = loop_axis_extent(bends, bounds, :y)
726
+ { min_x: min_x, max_x: max_x, min_y: min_y, max_y: max_y }
727
+ end
728
+
729
+ def loop_axis_extent(bends, bounds, axis)
730
+ values = bends.map { |point| point[axis] } +
731
+ bounds.values_at(:"min_#{axis}", :"max_#{axis}")
732
+ values.minmax
733
+ end
734
+
735
+ def loop_label_bounds(edge, node, bends)
736
+ x, y = loop_label_anchor(node, bends)
737
+ half_width, height = loop_label_extent(edge)
738
+ { min_x: x - half_width, max_x: x + half_width,
739
+ min_y: y - height, max_y: y }
740
+ end
741
+
742
+ def merge_bounds(bounds, min_x, max_x, min_y, max_y)
743
+ return { min_x:, max_x:, min_y:, max_y: } unless bounds
744
+
745
+ { min_x: [bounds[:min_x], min_x].min,
746
+ max_x: [bounds[:max_x], max_x].max,
747
+ min_y: [bounds[:min_y], min_y].min,
748
+ max_y: [bounds[:max_y], max_y].max }
749
+ end
750
+
751
+ def self_loop_overflow(graph)
752
+ bounds = self_loop_bounds(graph)
753
+ return [0.0, 0.0] unless bounds
754
+
755
+ [bounds[:min_x].negative? ? -bounds[:min_x] : 0.0,
756
+ bounds[:min_y].negative? ? -bounds[:min_y] : 0.0]
757
+ end
758
+
759
+ def self_loop_reach(graph)
760
+ bounds = self_loop_bounds(graph)
761
+ bounds ? [bounds[:max_x], bounds[:max_y]] : [0.0, 0.0]
762
+ end
763
+
764
+ def loop_label_extent(edge)
765
+ label = edge[:labels]&.first
766
+ return [0.0, 0.0] unless label
767
+
768
+ width = measure_text(
769
+ label[:text].to_s, font_size: layout_font_size
770
+ )[:width] *
771
+ SUBSTITUTE_FONT_HEADROOM
772
+ height = layout_font_size * TextMeasurement::HEIGHT_RATIO
773
+ [width / 2.0, height]
774
+ end
775
+
776
+ def self_loop_node(graph, edge)
777
+ source_id = edge[:sources]&.first
778
+ return unless source_id && source_id == edge[:targets]&.first
779
+
780
+ find_endpoint(graph, source_id)
781
+ end
782
+
783
+ def find_endpoint(graph, node_id)
784
+ return unless node_id
785
+
786
+ find_node(graph[:children], node_id) ||
787
+ find_node(graph[:clusters], node_id)
788
+ end
789
+
790
+ def find_node(nodes, node_id)
791
+ (nodes || []).find { |node| node[:id] == node_id }
792
+ end
793
+
794
+ def edge_route(edge, graph, source, target)
795
+ if source[:id] == target[:id]
796
+ return self_edge_route(edge, graph, source, target)
797
+ end
798
+
799
+ routed_edge(edge, source, target)
800
+ end
801
+
802
+ def self_edge_route(edge, graph, source, target)
803
+ bends = edge_bends(edge, source, target, self_loop_side(graph))
804
+ ends = clipped_ends(bends, source, target)
805
+ [ends, bends, ends]
806
+ end
807
+
808
+ def routed_edge(edge, source, target)
809
+ elk_bends = edge.dig(:sections, 0, :bendPoints)
810
+ points, label_route = edge_router.route(source, target, elk_bends)
811
+ bends = points.length > 2 ? points[1...-1] : (elk_bends || [])
812
+ if routed_endpoints?(points, source, target)
813
+ return [FlowchartEdgeRouter.ends_of(points), bends, label_route]
814
+ end
815
+
816
+ route = clipped_ends(bends, source, target)
817
+ [route, bends, route]
818
+ end
819
+
820
+ def routed_endpoints?(points, source, target)
821
+ points.length > 2 || cluster?(source) || cluster?(target)
822
+ end
823
+
824
+ def clipped_ends(bends, source, target)
825
+ [*edge_end(bends, source, target, :source),
826
+ *edge_end(bends, source, target, :target)]
827
+ end
828
+
829
+ def edge_router
830
+ @edge_router ||= FlowchartEdgeRouter.new
831
+ end
832
+
833
+ def edge_end(bends, source, target, which)
834
+ boundary_geometry(bends, source, target, which)
835
+ .values_at(:tip_x, :tip_y).map { |number| number.round(1) }
836
+ end
837
+
838
+ def boundary_geometry(bends, source, target, which)
839
+ node, other = which == :target ? [target, source] : [source, target]
840
+ via = which == :target ? bends.last : bends.first
841
+ from_x, from_y = via ? via.values_at(:x, :y) : node_centre(other)
842
+ tip_x, tip_y = node_boundary(node, from_x, from_y)
843
+ { tip_x: tip_x, tip_y: tip_y, from_x: from_x, from_y: from_y }
844
+ end
845
+
846
+ def self_loop_side(graph)
847
+ SELF_LOOP_SIDES[graph.dig(:layoutOptions, "elk.direction")] ||
848
+ SELF_LOOP_SIDES["DOWN"]
849
+ end
850
+
851
+ def edge_bends(edge, source, target, side)
852
+ bends = edge.dig(:sections, 0, :bendPoints) || []
853
+ return bends unless source[:id] == target[:id] && bends.empty?
854
+
855
+ self_loop_bends(source, side)
856
+ end
857
+
858
+ def self_loop_bends(node, side)
859
+ out_x, out_y = side
860
+ half_span = self_loop_half_span(node, out_y)
861
+ far_x, far_y = self_loop_outer_point(node, side)
862
+ [{ x: far_x - (out_y.abs * half_span),
863
+ y: far_y - (out_x.abs * half_span) },
864
+ { x: far_x + (out_y.abs * half_span),
865
+ y: far_y + (out_x.abs * half_span) }]
866
+ end
867
+
868
+ def self_loop_half_span(node, out_y)
869
+ span_side = out_y.zero? ? node[:height] || 50 : node[:width] || 100
870
+ half_span = (span_side * SELF_LOOP_HALF_SPAN)
871
+ .clamp(SELF_LOOP_HALF_SPAN_LIMITS)
872
+ [half_span, span_side / 2.0].min
873
+ end
874
+
875
+ def self_loop_outer_point(node, side)
876
+ cx, cy = node_centre(node)
877
+ out_x, out_y = side
878
+ edge_x, edge_y = node_boundary(node, cx + out_x, cy + out_y)
879
+ depth = self_loop_depth(node)
880
+ [edge_x + (out_x * depth), edge_y + (out_y * depth)]
881
+ end
882
+
883
+ def self_loop_depth(node)
884
+ width = node[:width] || 100
885
+ height = node[:height] || 50
886
+ [[width, height].min * SELF_LOOP_DEPTH, SELF_LOOP_MAX_DEPTH].min
887
+ end
888
+
889
+ def head_tip_geometry(route, bends, source, target, which)
890
+ node = which == :target ? target : source
891
+ other = which == :target ? :source : :target
892
+ via = which == :target ? bends.last : bends.first
893
+ from_x, from_y = via ? via.values_at(:x, :y) : route_end(route, other)
894
+ tip_x, tip_y = if cluster?(node)
895
+ route_end(route, which)
896
+ else
897
+ node_boundary(node, from_x, from_y)
898
+ end
899
+ { tip_x: tip_x, tip_y: tip_y, from_x: from_x, from_y: from_y }
900
+ end
901
+
902
+ def route_end(route, which)
903
+ which == :target ? route.last(2) : route.first(2)
904
+ end
905
+
906
+ def node_centre(node)
907
+ FlowchartEdgeRouter.centre(node).values_at(:x, :y)
908
+ end
909
+
910
+ def node_boundary(node, from_x, from_y)
911
+ cx, cy = node_centre(node)
912
+ dx = from_x - cx
913
+ dy = from_y - cy
914
+ return [cx, cy] if dx.zero? && dy.zero?
915
+
916
+ scale = boundary_scale(node, dx, dy)
917
+ [cx + (dx * scale), cy + (dy * scale)]
918
+ end
919
+
920
+ def boundary_scale(node, delta_x, delta_y)
921
+ half_w = (node[:width] || 100) / 2.0
922
+ half_h = (node[:height] || 50) / 2.0
923
+ shape = NODE_OUTLINES[node.dig(:metadata, :shape)]
924
+ scale_method = BOUNDARY_SCALES.fetch(shape, :box_scale)
925
+ send(scale_method, half_w, half_h, delta_x, delta_y)
926
+ end
927
+
928
+ def box_scale(half_w, half_h, delta_x, delta_y)
929
+ [delta_x.zero? ? Float::INFINITY : half_w / delta_x.abs,
930
+ delta_y.zero? ? Float::INFINITY : half_h / delta_y.abs].min
931
+ end
932
+
933
+ def rhombus_scale(half_w, half_h, delta_x, delta_y)
934
+ 1 / (axis_ratio(delta_x, half_w) + axis_ratio(delta_y, half_h))
935
+ end
936
+
937
+ def circle_scale(half_w, half_h, delta_x, delta_y)
938
+ [half_w, half_h].min / JsMath.hypot(delta_x, delta_y)
939
+ end
940
+
941
+ def hexagon_scale(half_w, half_h, delta_x, delta_y)
942
+ slope = 1 / (
943
+ axis_ratio(delta_x, half_w) + axis_ratio(delta_y, 2 * half_h)
944
+ )
945
+ [slope, delta_y.zero? ? Float::INFINITY : half_h / delta_y.abs].min
946
+ end
947
+
948
+ def rounded_scale(half_w, half_h, x_diff, y_diff)
949
+ scale = box_scale(half_w, half_h, x_diff, y_diff)
950
+ inner_x = half_w - ROUNDED_CORNER
951
+ inner_y = half_h - ROUNDED_CORNER
952
+ along_x = (x_diff * scale).abs <= inner_x
953
+ along_y = (y_diff * scale).abs <= inner_y
954
+ return scale if along_x || along_y
955
+
956
+ cap_intersection(inner_x * (x_diff.negative? ? -1 : 1), ROUNDED_CORNER,
957
+ x_diff, y_diff, inner_y * (y_diff.negative? ? -1 : 1))
958
+ end
959
+
960
+ def stadium_scale(half_w, half_h, x_diff, y_diff)
961
+ return ellipse_scale(half_w, half_h, x_diff, y_diff) if half_w == half_h
962
+ # A tall stadium is a wide one turned on its side.
963
+ return stadium_scale(half_h, half_w, y_diff, x_diff) if half_w < half_h
964
+
965
+ flat = y_diff.zero? ? Float::INFINITY : half_h / y_diff.abs
966
+ straight = half_w - half_h
967
+ return flat if (flat * x_diff).abs <= straight
968
+
969
+ cap_intersection(straight * (x_diff.negative? ? -1 : 1), half_h,
970
+ x_diff, y_diff)
971
+ end
972
+
973
+ def ellipse_scale(half_w, half_h, delta_x, delta_y)
974
+ x_ratio = axis_ratio(delta_x, half_w)
975
+ y_ratio = axis_ratio(delta_y, half_h)
976
+ 1 / Math.sqrt((x_ratio**2) + (y_ratio**2))
977
+ end
978
+
979
+ def axis_ratio(delta, half_extent)
980
+ delta.zero? ? 0.0 : delta.abs / half_extent
981
+ end
982
+
983
+ def cap_intersection(centre_x, radius, delta_x, delta_y, centre_y = 0)
984
+ a = dot(delta_x, delta_y, delta_x, delta_y)
985
+ b = dot(delta_x, delta_y, centre_x, centre_y)
986
+ c = dot(centre_x, centre_y, centre_x, centre_y) - (radius * radius)
987
+ (b + Math.sqrt([(b * b) - (a * c), 0].max)) / a
988
+ end
989
+
990
+ def dot(x_one, y_one, x_two, y_two)
991
+ (x_one * x_two) + (y_one * y_two)
992
+ end
993
+
994
+ def edge_label_anchor(source, target, bends, route)
995
+ return loop_label_anchor(source, bends) if source[:id] == target[:id]
996
+
997
+ sx, sy, tx, ty = route
998
+ [(sx + tx) / 2.0, ((sy + ty) / 2.0) - EDGE_LABEL_LIFT]
999
+ end
1000
+
1001
+ def loop_label_anchor(node, bends)
1002
+ mid_x, mid_y = loop_label_midpoint(bends)
1003
+ cx, cy = node_centre(node)
1004
+ dx = mid_x - cx
1005
+ dy = mid_y - cy
1006
+ span = JsMath.hypot(dx, dy)
1007
+ return [mid_x, mid_y] if span.zero?
1008
+
1009
+ [mid_x + ((dx / span) * EDGE_LABEL_LIFT),
1010
+ mid_y + ((dy / span) * EDGE_LABEL_LIFT)]
1011
+ end
1012
+
1013
+ def loop_label_midpoint(bends)
1014
+ [(bends.first[:x] + bends.last[:x]) / 2.0,
1015
+ (bends.first[:y] + bends.last[:y]) / 2.0]
1016
+ end
1017
+
1018
+ # Subgraphs become compound children holding their members, which is
1019
+ # what elkrb reads and what lets the layout size a cluster to fit.
1020
+ # A node in no subgraph stays at the top level.
1021
+ def transform_children(graph)
1022
+ semantic_children = graph.nodes.group_by(&:parent_id)
1023
+ boxes = graph.nodes.select { |node| node.role == "group" }
1024
+ placed = graph.nodes.select { |node| node.role == "flow_node" }
1025
+ assemble(boxes, placed, semantic_children)
1026
+ end
1027
+
1028
+ # Model order is kept for emitted clusters. Nested clusters are attached
1029
+ # before member nodes, so every cluster lists boxes before nodes. Parent
1030
+ # lookup is independent of declaration order, so a later cluster can
1031
+ # hold an earlier one.
1032
+ #
1033
+ def assemble(boxes, placed, semantic_children)
1034
+ entries = boxes.map do |box|
1035
+ [box, transform_subgraph(box, semantic_children[box.id])]
1036
+ end
1037
+ clusters_by_id = entries.to_h { |box, cluster| [box.id, cluster] }
1038
+ attach_clusters(entries, clusters_by_id)
1039
+ loose = unclustered_nodes(placed, semantic_children, clusters_by_id)
1040
+ loose + root_clusters(entries, clusters_by_id)
1041
+ end
1042
+
1043
+ def root_clusters(entries, clusters_by_id)
1044
+ entries.reject { |box, _| clusters_by_id[box.parent_id] }.map(&:last)
1045
+ end
1046
+
1047
+ def attach_clusters(entries, clusters_by_id)
1048
+ entries.each do |box, cluster|
1049
+ clusters_by_id[box.parent_id]&.fetch(:children)&.push(cluster)
1050
+ end
1051
+ end
1052
+
1053
+ def unclustered_nodes(placed, semantic_children, clusters_by_id)
1054
+ placed.each_with_object([]) do |node, top_level|
1055
+ transformed = transform_node(node, semantic_children[node.id])
1056
+ holder = clusters_by_id[node.parent_id]
1057
+ holder ? holder[:children] << transformed : top_level << transformed
1058
+ end
1059
+ end
1060
+
1061
+ def transform_subgraph(box, semantics)
1062
+ { id: semantic_value(semantics, "source_identifier") || box.id,
1063
+ width: 0, height: 0, children: [], labels: subgraph_labels(box),
1064
+ metadata: { cluster: true, direction: own_direction(semantics) } }
1065
+ end
1066
+
1067
+ def subgraph_labels(box)
1068
+ title = display_text(box.label)
1069
+ label = measure_text(title, font_size: layout_font_size)
1070
+ [{ text: title, width: label[:width], height: label[:height] }]
1071
+ end
1072
+
1073
+ # The ELK direction a subgraph's own `direction` asks for, nil if it
1074
+ # has none.
1075
+ def own_direction(semantics)
1076
+ written = semantic_value(semantics, "layout_direction")
1077
+ direction_to_layout(written) if written
1078
+ end
1079
+
1080
+ def transform_node(node, semantics)
1081
+ shape = semantic_value(semantics, "shape") || "rect"
1082
+ dims = calculate_dimensions(node.label, shape)
1083
+ collision = semantic_value(semantics, "subgraph_collision") == "true"
1084
+ { id: semantic_value(semantics, "source_identifier") || node.id,
1085
+ width: dims[:width], height: dims[:height],
1086
+ labels: [transformed_node_label(node, dims)],
1087
+ metadata: { shape: shape,
1088
+ classes: semantic_value(semantics, "style_reference"),
1089
+ subgraph_collision: collision,
1090
+ **node_paints(semantics) } }
1091
+ end
1092
+
1093
+ def node_paints(semantics)
1094
+ %w[fill stroke].to_h do |role|
1095
+ [role.to_sym, semantic_value(semantics, role)]
1096
+ end
1097
+ end
1098
+
1099
+ def transformed_node_label(node, dims)
1100
+ { text: Diagram::FlowchartLabelText.lines(node.label).join("\n"),
1101
+ width: dims[:label_width], height: dims[:label_height] }
1102
+ end
1103
+
1104
+ def compact_collision_nodes(children)
1105
+ children.each do |child|
1106
+ compact_collision_nodes(child[:children]) if child[:children]
1107
+ compact_collision_node(child) if
1108
+ child.dig(:metadata, :subgraph_collision)
1109
+ end
1110
+ end
1111
+
1112
+ def compact_collision_node(child)
1113
+ width, height = compact_collision_dimensions(child[:labels].first)
1114
+ center_compact_collision(child, width, height)
1115
+ end
1116
+
1117
+ def center_compact_collision(child, width, height)
1118
+ child[:x] += (child[:width] - width) / 2.0
1119
+ child[:y] += (child[:height] - height) / 2.0
1120
+ child[:width] = width
1121
+ child[:height] = height
1122
+ end
1123
+
1124
+ def compact_collision_dimensions(label)
1125
+ extra_width, extra_height = COLLISION_NODE_PADDING
1126
+ [label[:width] + extra_width, label[:height] + extra_height]
1127
+ end
1128
+
1129
+ def transform_edges(graph)
1130
+ source_ids = source_identifiers(graph)
1131
+ graph.edges.map { |edge| transform_edge(edge, source_ids) }
1132
+ end
1133
+
1134
+ def source_identifiers(graph)
1135
+ children = graph.nodes.group_by(&:parent_id)
1136
+ graph.nodes.to_h do |node|
1137
+ source_id = semantic_value(children[node.id], "source_identifier")
1138
+ [node.id, source_id || node.id]
1139
+ end
1140
+ end
1141
+
1142
+ def transform_edge(edge, source_ids)
1143
+ source = source_ids.fetch(edge.source_id)
1144
+ target = source_ids.fetch(edge.target_id)
1145
+ {
1146
+ id: "#{source}_to_#{target}", sources: [source], targets: [target],
1147
+ labels: edge_labels(edge),
1148
+ metadata: {
1149
+ arrow_type: edge.role,
1150
+ minimum_rank_span: edge.minimum_rank_span,
1151
+ }
1152
+ }
1153
+ end
1154
+
1155
+ def display_text(text)
1156
+ Diagram::FlowchartLabelText.display(text)
1157
+ end
1158
+
1159
+ def edge_labels(edge)
1160
+ return [] if edge.label.nil? || edge.label.empty?
1161
+
1162
+ text = display_text(edge.label)
1163
+ line = measure_text(text, font_size: layout_font_size)
1164
+ lines = wrapped_lines(text)
1165
+ width = lines > 1 ? EDGE_LABEL_WRAP_WIDTH : line[:width]
1166
+
1167
+ [{ text: text, width: width, height: line[:height] * lines }]
1168
+ end
1169
+
1170
+ # How many lines mmdc breaks an edge label into: each takes as many
1171
+ # words as fit in the wrap width, counting the space after a word,
1172
+ # which mmdc keeps on its line.
1173
+ def wrapped_lines(text)
1174
+ lines = [[]]
1175
+ text.split.join(" ").scan(/\S+ ?/).each do |word|
1176
+ lines << [] unless fits_edge_label?(lines.last, word)
1177
+ lines.last << word
1178
+ end
1179
+ lines.size
1180
+ end
1181
+
1182
+ def fits_edge_label?(line_words, word)
1183
+ return true if line_words.empty?
1184
+
1185
+ line = "#{line_words.join}#{word}"
1186
+ measure_text(line, font_size: layout_font_size)[:width] <=
1187
+ EDGE_LABEL_WRAP_WIDTH
1188
+ end
1189
+
1190
+ def calculate_dimensions(label, shape)
1191
+ lines = Diagram::FlowchartLabelText.lines(label)
1192
+ widest = line_widths(label, lines).max
1193
+ line_height = layout_font_size * NODE_LINE_HEIGHT * lines.size
1194
+
1195
+ node_box(widest, line_height, shape)
1196
+ .merge(label_width: widest, label_height: line_height)
1197
+ end
1198
+
1199
+ def line_widths(label, lines)
1200
+ source_lines = label.to_s.split(Diagram::FlowchartLabelText::LINE_BREAK,
1201
+ -1)
1202
+ lines.zip(source_lines).map do |line, source|
1203
+ line_width(line) + icon_advance(source)
1204
+ end
1205
+ end
1206
+
1207
+ def icon_advance(source)
1208
+ count = source.to_s.scan(Diagram::FlowchartLabelText::ICON).length
1209
+ count * layout_font_size * ICON_ADVANCE_EMS
1210
+ end
1211
+
1212
+ def line_width(line)
1213
+ measure_text(line, font_size: layout_font_size)[:width]
1214
+ end
1215
+
1216
+ def node_box(label_width, label_height, shape)
1217
+ if shape == "rhombus"
1218
+ return square(label_width + label_height + RHOMBUS_PADDING)
1219
+ end
1220
+ if CIRCLE_PADDING.key?(shape)
1221
+ return square(label_width + CIRCLE_PADDING[shape])
1222
+ end
1223
+
1224
+ padded_box(label_width, label_height, shape)
1225
+ end
1226
+
1227
+ def square(side)
1228
+ { width: side, height: side }
1229
+ end
1230
+
1231
+ def padded_box(label_width, label_height, shape)
1232
+ extra_width, extra_height = SHAPE_PADDING.fetch(shape, BOX_PADDING)
1233
+ extra_width += hexagon_widening(label_height) if shape == "hexagon"
1234
+ { width: label_width + extra_width,
1235
+ height: label_height + extra_height }
1236
+ end
1237
+
1238
+ # mmdc measures a flowchart label in Trebuchet MS, the first face of
1239
+ # its stylesheet.
1240
+ def measure_text(text, font_size:)
1241
+ TextMeasurement.measure_in(:trebuchet, text, font_size: font_size)
1242
+ end
1243
+
1244
+ def hexagon_widening(label_height)
1245
+ extra = label_height - (layout_font_size * NODE_LINE_HEIGHT)
1246
+ HEXAGON_WIDTH_PER_HEIGHT * extra
1247
+ end
1248
+
1249
+ def layout_font_size
1250
+ self.class.font_size(theme)
1251
+ end
1252
+
1253
+ def layout_options(graph)
1254
+ settings = graph.nodes.find { |node| node.role == "diagram_settings" }
1255
+ children = graph.nodes.select { |node| node.parent_id == settings&.id }
1256
+ direction = direction_to_layout(
1257
+ semantic_value(children, "layout_direction"),
1258
+ )
1259
+ build_elk_options(algorithm: ALGORITHM_LAYERED, direction: direction,
1260
+ **flowchart_spacing_options)
1261
+ end
1262
+
1263
+ def flowchart_spacing_options
1264
+ { ElkOptions::NODE_NODE_SPACING => 50, ElkOptions::LAYER_SPACING => 50,
1265
+ ElkOptions::EDGE_NODE_SPACING => 30,
1266
+ ElkOptions::EDGE_EDGE_SPACING => 20,
1267
+ ElkOptions::NODE_PLACEMENT => "SIMPLE" }
1268
+ end
1269
+
1270
+ def diagram_identifier(graph)
1271
+ settings = graph.nodes.find { |node| node.role == "diagram_settings" }
1272
+ children = graph.nodes.select { |node| node.parent_id == settings&.id }
1273
+ semantic_value(children, "diagram_identifier") || graph.id
1274
+ end
1275
+
1276
+ def semantic_value(nodes, role)
1277
+ (nodes || []).find { |node| node.role == role }&.label
1278
+ end
1279
+
1280
+ def direction_to_layout(direction)
1281
+ case direction
1282
+ when "LR" then DIRECTION_RIGHT
1283
+ when "RL" then DIRECTION_LEFT
1284
+ when "BT" then DIRECTION_UP
1285
+ else DIRECTION_DOWN
1286
+ end
1287
+ end
1288
+ end
1289
+ end
1290
+ end