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,1567 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../layout/base"
4
+ require_relative "chrome_rows"
5
+ require_relative "create"
6
+ require_relative "edge"
7
+ require_relative "ir_adapter"
8
+ require_relative "ir_reader"
9
+ require_relative "message_wrap"
10
+ require_relative "note"
11
+ require_relative "note_geometry"
12
+ require_relative "numbered_label"
13
+ require_relative "parser"
14
+ require_relative "picture"
15
+ require_relative "ref"
16
+ require_relative "ref_shape"
17
+ require_relative "scene"
18
+ require_relative "../drawing"
19
+ require_relative "walker"
20
+
21
+ module Etassia
22
+ module Notation
23
+ module PlantUML
24
+ module Sequence
25
+ # Places participant heads in one row, the items down the page in
26
+ # source order, and the same heads again at the foot of the lifelines.
27
+ class Layout < Etassia::Layout::Base
28
+ MARGIN = 20.0
29
+ DELAY_DASH = "1,4"
30
+ private_constant :DELAY_DASH
31
+ HEAD_PADDING = 12.0
32
+ MIN_HEAD_WIDTH = 80.0
33
+ INTRINSIC_HEAD_PADDING = 7.0
34
+ INTRINSIC_HEAD_HEIGHT = 30.4883
35
+ MIN_GAP = 10.0
36
+ BOX_PADDING = 4.0
37
+ BOX_TITLE_SIZE = 13.0
38
+ BOLD_CHROME_SCALE = 0.9158
39
+ SELF_MIN_CLEARANCE = 50.0
40
+ BOX_TITLE_PADDING = 3.0
41
+ BOX_TITLE_HEIGHT = 26.0
42
+ MIN_WIDTH_PADDING = 14.0
43
+ HEAD_LEADING = 1.1776
44
+ EDGE_GAP = 5.0
45
+ EDGE_MARGIN = 10.0
46
+ PAGE_BREAK_INSET = 6.0
47
+ BANNER_LEFT = 3.0
48
+ BANNER_TOP = 8.0
49
+ BANNER_HEIGHT = 16.64
50
+ BANNER_STEP = 21.64
51
+ BANNER_PADDING = 7.0
52
+ MONOSPACE_ADVANCE = 6.02
53
+ GLYPH_HEAD_HEIGHT = 76.4883
54
+ GLYPH_BOX_HEIGHT = 30.4883
55
+ COMPACT_MARGIN = 10.0
56
+ COMPACT_HEAD_PADDING = 7.0
57
+ COMPACT_MIN_GAP = 20.0
58
+ COMPACT_TIMELINE_GAP = 10.0
59
+ COMPACT_HEAD_HEIGHT = 30.4883
60
+ COMPACT_HEAD_LABEL_INSET = 9.9531
61
+ COMPACT_FIRST_ROW_GAP = 30.1328
62
+ COMPACT_GROUP_FIRST_ROW_GAP = 33.0
63
+ COMPACT_ROW = 29.1328
64
+ COMPACT_UNLABELLED_ROW = 14.0
65
+ COMPACT_GROUP_ROW = 13.3105
66
+ COMPACT_GROUP_CLOSE = 32.0
67
+ COMPACT_BRANCH_GAP = 28.0
68
+ TEOZ_FRAGMENT_BRANCH_GAP = 30.9551
69
+ TEOZ_FIRST_MESSAGE_GAP = 30.3105
70
+ TEOZ_FIRST_UNLABELLED_GAP = 15.0
71
+ private_constant :TEOZ_FRAGMENT_BRANCH_GAP, :TEOZ_FIRST_MESSAGE_GAP,
72
+ :TEOZ_FIRST_UNLABELLED_GAP
73
+ COMPACT_FRAGMENT_CLOSE = 10.0
74
+ COMPACT_SELF_ROW = 42.1328
75
+ COMPACT_SELF_CLEARANCE = 53.0
76
+ CLASSIC_FIRST_ROW_GAP = 32.3105
77
+ CLASSIC_UNLABELLED_FIRST_ROW_GAP = 17.0
78
+ CLASSIC_FOLLOWING_UNLABELLED_ROW = 14.0
79
+ CLASSIC_ROW = 29.3105
80
+ CLASSIC_SELF_ROW = 42.3105
81
+ CLASSIC_SELF_CLEARANCE = 50.0
82
+ CLASSIC_LABEL_INSET = Walker::CLASSIC_LABEL_INSET
83
+ CLASSIC_FRAGMENT_ROW = 24.3105
84
+ CLASSIC_BRANCH_GAP = 2.0
85
+ CLASSIC_FRAME_TOP = 14.3105
86
+ CLASSIC_FRAME_BOTTOM = 21.3105
87
+ CLASSIC_BRANCH_LINE = 20.3105
88
+ CLASSIC_FRAGMENT_CLOSE = 7.0
89
+ CLASSIC_DIVIDER_ROW = 42.3105
90
+ CLASSIC_DEACTIVATION_GAP = 31.0
91
+ CLASSIC_BARE_DEACTIVATION_GAP = 1.0
92
+ MIN_WIDTH_GLYPH_MARGIN = 5.0
93
+ MIN_WIDTH_GLYPH_GAP = 10.0
94
+ MIN_WIDTH_GLYPH_FIRST_ROW_GAP = 31.3105
95
+ ACTOR_INTRINSIC_WIDTH = 26.0
96
+ STEREOTYPE_BOX_HEIGHT = 46.9766
97
+ GLYPH_BOX_OFFSET = 4.0
98
+ QUEUE_HEIGHT = 26.4883
99
+ ICON_LABEL_INSET = 2.9531
100
+ TAIL_ICON_LABEL = 13.5352
101
+ BOX_LABEL_INSET = 10.9531
102
+ TAIL_BOX_LABEL = 20.5352
103
+ QUEUE_LABEL_INSET = 7.9531
104
+ TAIL_QUEUE_LABEL = 18.5352
105
+ ICON_RADIUS = 12.0
106
+ # A created head sits this far above the row of its message, and
107
+ # its lifeline starts this far below the head.
108
+ CREATE_HEAD_RISE = 21.31
109
+ CREATE_LIFELINE_LAG = 0.25
110
+ GLYPH_BUILDERS = {
111
+ actor: :actor_geometry,
112
+ boundary: :boundary_geometry,
113
+ collections: :collections_geometry,
114
+ control: :control_geometry,
115
+ database: :database_geometry,
116
+ entity: :entity_geometry,
117
+ queue: :queue_geometry,
118
+ }.freeze
119
+ SELF_WIDTH = Walker::SELF_WIDTH
120
+ private_constant :MARGIN, :HEAD_PADDING, :MIN_HEAD_WIDTH, :MIN_GAP,
121
+ :BOX_TITLE_SIZE, :BOLD_CHROME_SCALE,
122
+ :SELF_MIN_CLEARANCE,
123
+ :INTRINSIC_HEAD_PADDING, :INTRINSIC_HEAD_HEIGHT,
124
+ :SELF_WIDTH, :CLASSIC_LABEL_INSET, :BOX_PADDING,
125
+ :BOX_TITLE_PADDING,
126
+ :BOX_TITLE_HEIGHT,
127
+ :MIN_WIDTH_PADDING, :HEAD_LEADING, :EDGE_GAP,
128
+ :EDGE_MARGIN,
129
+ :PAGE_BREAK_INSET, :BANNER_LEFT, :BANNER_TOP,
130
+ :BANNER_HEIGHT, :BANNER_STEP, :BANNER_PADDING,
131
+ :MONOSPACE_ADVANCE, :GLYPH_HEAD_HEIGHT,
132
+ :GLYPH_BOX_HEIGHT, :COMPACT_MARGIN,
133
+ :COMPACT_HEAD_PADDING, :COMPACT_MIN_GAP,
134
+ :COMPACT_TIMELINE_GAP,
135
+ :COMPACT_HEAD_HEIGHT, :COMPACT_HEAD_LABEL_INSET,
136
+ :COMPACT_FIRST_ROW_GAP,
137
+ :COMPACT_GROUP_FIRST_ROW_GAP, :COMPACT_ROW,
138
+ :COMPACT_UNLABELLED_ROW, :COMPACT_GROUP_ROW,
139
+ :COMPACT_GROUP_CLOSE, :COMPACT_BRANCH_GAP,
140
+ :COMPACT_FRAGMENT_CLOSE, :COMPACT_SELF_ROW,
141
+ :COMPACT_SELF_CLEARANCE,
142
+ :CLASSIC_FIRST_ROW_GAP,
143
+ :CLASSIC_UNLABELLED_FIRST_ROW_GAP,
144
+ :CLASSIC_FOLLOWING_UNLABELLED_ROW, :CLASSIC_ROW,
145
+ :CLASSIC_SELF_ROW, :CLASSIC_SELF_CLEARANCE,
146
+ :CLASSIC_FRAGMENT_ROW, :CLASSIC_BRANCH_GAP,
147
+ :CLASSIC_FRAME_TOP, :CLASSIC_FRAME_BOTTOM,
148
+ :CLASSIC_BRANCH_LINE,
149
+ :CLASSIC_FRAGMENT_CLOSE,
150
+ :CLASSIC_DIVIDER_ROW, :CLASSIC_DEACTIVATION_GAP,
151
+ :CLASSIC_BARE_DEACTIVATION_GAP,
152
+ :MIN_WIDTH_GLYPH_MARGIN, :MIN_WIDTH_GLYPH_GAP,
153
+ :MIN_WIDTH_GLYPH_FIRST_ROW_GAP,
154
+ :ACTOR_INTRINSIC_WIDTH, :STEREOTYPE_BOX_HEIGHT,
155
+ :GLYPH_BOX_OFFSET, :QUEUE_HEIGHT,
156
+ :ICON_LABEL_INSET, :TAIL_ICON_LABEL,
157
+ :BOX_LABEL_INSET, :TAIL_BOX_LABEL,
158
+ :QUEUE_LABEL_INSET, :TAIL_QUEUE_LABEL,
159
+ :ICON_RADIUS, :GLYPH_BUILDERS, :CREATE_HEAD_RISE,
160
+ :CREATE_LIFELINE_LAG
161
+
162
+ # @param graph [IR::Graph] from {IRAdapter}
163
+ def scene(graph)
164
+ @diagram = order_boxed_member(pictured(IRReader.call(graph)))
165
+ measure_heads
166
+ place_origin
167
+ @flow = walk
168
+ recentre if @flow.left < margin
169
+ build(@widths)
170
+ end
171
+
172
+ private
173
+
174
+ def place_origin
175
+ @origin = margin + banner_room + chrome_rows.top_room
176
+ @top = @origin + (@diagram.boxes.empty? ? 0 : BOX_TITLE_HEIGHT)
177
+ end
178
+
179
+ # Lays out each note's embedded diagram, so the notes know their
180
+ # size before the participants are spaced.
181
+ def pictured(diagram)
182
+ return diagram unless diagram.items.any? { |i| embedding?(i) }
183
+
184
+ diagram.with_items(diagram.items.map { |item| picture(item) })
185
+ end
186
+
187
+ def embedding?(item)
188
+ item.is_a?(Note) && !item.embedded.nil?
189
+ end
190
+
191
+ def picture(item)
192
+ return item unless embedding?(item)
193
+
194
+ embedded = item.embedded
195
+ item.with_picture(Picture.new(inner_scene(embedded),
196
+ embedded.scale))
197
+ end
198
+
199
+ def inner_scene(embedded)
200
+ inner = self.class.new
201
+ inner.theme = theme
202
+ parsed = Parser.new.parse(embedded.document)
203
+ inner.scene(IRAdapter.call(parsed))
204
+ end
205
+
206
+ def measure_heads
207
+ @creations = Create.messages(@diagram.items)
208
+ @activation_depths = activation_depths
209
+ @widths = @diagram.participants.map { |p| head_width(p) }
210
+ @centers = centers(@widths)
211
+ @head_height = head_height
212
+ end
213
+
214
+ # An explicit box declaration moves a participant that an earlier
215
+ # message created implicitly behind the unboxed peer created with it.
216
+ def order_boxed_member(diagram)
217
+ from, to = boxed_member_move(diagram)
218
+ return diagram unless from
219
+
220
+ moved = diagram.participants.dup
221
+ moved.insert(to, moved.delete_at(from))
222
+ diagram_with_participants(diagram, moved.freeze)
223
+ end
224
+
225
+ # Indexes of the sole boxed member and of the unboxed peer it
226
+ # follows, when the member was declared before that peer.
227
+ def boxed_member_move(diagram)
228
+ member = sole_boxed_member(diagram)
229
+ peer = member && outside_peer(diagram, member)
230
+ return unless peer
231
+
232
+ from = participant_index(diagram, member)
233
+ to = participant_index(diagram, peer)
234
+ [from, to] if from && to && from < to
235
+ end
236
+
237
+ def sole_boxed_member(diagram)
238
+ box = diagram.boxes.first if diagram.boxes.one?
239
+ box.members.first if box&.members&.one?
240
+ end
241
+
242
+ def outside_peer(diagram, member)
243
+ peer = first_message_peer(diagram, member)
244
+ peer unless peer == member
245
+ end
246
+
247
+ def participant_index(diagram, id)
248
+ diagram.participants.index { |participant| participant.id == id }
249
+ end
250
+
251
+ def first_message_peer(diagram, member)
252
+ message = diagram.messages.find do |item|
253
+ item.from == member || item.to == member
254
+ end
255
+ return unless message
256
+
257
+ message.from == member ? message.to : message.from
258
+ end
259
+
260
+ def diagram_with_participants(diagram, participants)
261
+ Diagram.new(
262
+ participants: participants, items: diagram.items,
263
+ boxes: diagram.boxes, appearance: diagram.appearance,
264
+ footbox: diagram.footbox?, warnings: diagram.warnings,
265
+ title: diagram.title, chrome: diagram.chrome, teoz: diagram.teoz?
266
+ )
267
+ end
268
+
269
+ def walk
270
+ @edge_right = right_edge
271
+ build_walker.run(@diagram.items, edge_right: @edge_right,
272
+ appearance: @diagram.appearance)
273
+ end
274
+
275
+ def build_walker
276
+ Walker.new(lifelines: ids.zip(@centers).to_h,
277
+ measure: ->(text) { text_width(text) },
278
+ font_size: font_size, config: walker_config,
279
+ start_y: @top + @head_height + first_row_gap)
280
+ end
281
+
282
+ def walker_config
283
+ Walker::Config.new(
284
+ message_measure: ->(text) { label_text_width(text) },
285
+ message_font_size: label_font_size,
286
+ message_family: ("monospace" if compact?),
287
+ label_inset: (7 if compact?), bounds: head_bounds,
288
+ activation_edges: activation_edges?, head_halves: head_halves,
289
+ note_font_size: note_font_size,
290
+ teoz_fragment_cadence: teoz_fragment_cadence?,
291
+ **walker_cadence
292
+ )
293
+ end
294
+
295
+ def head_halves
296
+ ids.zip(@widths.map { |width| width / 2 }).to_h
297
+ end
298
+
299
+ def activation_edges?
300
+ compact_timeline? || two_party_activation_timeline?
301
+ end
302
+
303
+ def walker_cadence
304
+ row_cadence.merge(gap_cadence)
305
+ end
306
+
307
+ def row_cadence
308
+ { row: row_height, self_row: self_row_height,
309
+ unlabelled_row: timeline_value(COMPACT_UNLABELLED_ROW),
310
+ following_unlabelled_row: classic_following_unlabelled_row,
311
+ fragment_row: fragment_row_height,
312
+ group_fragment_row: timeline_value(COMPACT_GROUP_ROW) }
313
+ end
314
+
315
+ def gap_cadence
316
+ { branch_gap: branch_gap,
317
+ fragment_close_gap: fragment_close_gap,
318
+ group_close_gap: group_close_gap,
319
+ divider_row: divider_row_height,
320
+ **frame_cadence,
321
+ deactivation_gap: deactivation_gap,
322
+ bare_deactivation_gap: bare_deactivation_gap }
323
+ end
324
+
325
+ def frame_cadence
326
+ return {} if compact?
327
+
328
+ { frame_top_offset: CLASSIC_FRAME_TOP, frame_bottom_offset:
329
+ CLASSIC_FRAME_BOTTOM, branch_offset: CLASSIC_BRANCH_LINE }
330
+ end
331
+
332
+ def classic_following_unlabelled_row
333
+ CLASSIC_FOLLOWING_UNLABELLED_ROW unless compact?
334
+ end
335
+
336
+ def branch_gap
337
+ return TEOZ_FRAGMENT_BRANCH_GAP if teoz_fragment_cadence?
338
+
339
+ compact? ? COMPACT_BRANCH_GAP : CLASSIC_BRANCH_GAP
340
+ end
341
+
342
+ def fragment_close_gap
343
+ compact? ? COMPACT_FRAGMENT_CLOSE : CLASSIC_FRAGMENT_CLOSE
344
+ end
345
+
346
+ def group_close_gap
347
+ compact? ? COMPACT_GROUP_CLOSE : CLASSIC_FRAGMENT_CLOSE
348
+ end
349
+
350
+ # Where `->]` messages end: past every head, and far enough from
351
+ # each sender to fit its label. Nil when there are none.
352
+ def right_edge
353
+ ends = edge_messages(:right).map do |m|
354
+ @centers[ids.index(m.from)] + edge_room(m)
355
+ end
356
+ return if ends.empty?
357
+
358
+ [head_bounds.last + EDGE_GAP, *ends].max
359
+ end
360
+
361
+ def edge_messages(side)
362
+ @diagram.messages.select do |m|
363
+ m.edge&.global? && m.edge.side == side
364
+ end
365
+ end
366
+
367
+ # Label plus the run either side of it, and the ring if one is drawn.
368
+ def edge_room(message)
369
+ ring = message.edge_end.circle ? Edge::RING_INSET : 0.0
370
+ label_width(message) + Edge::RUN_PADDING + ring
371
+ end
372
+
373
+ def head_bounds
374
+ [@centers.first - (@widths.first / 2),
375
+ @centers.last + (@widths.last / 2)]
376
+ end
377
+
378
+ # Items such as a note left of the first participant stick out
379
+ # past the margin; move everything right instead of clipping.
380
+ def recentre
381
+ shift = margin - @flow.left
382
+ @centers = @centers.map { |centre| centre + shift }
383
+ @flow = walk
384
+ end
385
+
386
+ def build(widths)
387
+ Scene.new(width: canvas_width(widths),
388
+ height: page_height,
389
+ banners: banners, title: title_scene(widths),
390
+ paint: scene_paint,
391
+ text_blocks: text_blocks(widths),
392
+ legend: legend_scene(widths),
393
+ frames: frames(widths),
394
+ heads: heads(widths),
395
+ lifelines: lifelines, page_breaks: page_breaks(widths),
396
+ **flow_items)
397
+ end
398
+
399
+ def scene_paint
400
+ appearance = @diagram.appearance
401
+ width = appearance.arrow_width
402
+ pairs = appearance.paint.map { |role, colour| "#{role}=#{colour}" }
403
+ width ? pairs + ["arrow_width=#{width}"] : pairs
404
+ end
405
+
406
+ # A dotted line across the page where the first page ends.
407
+ def page_breaks(widths)
408
+ right = canvas_width(widths) - PAGE_BREAK_INSET
409
+ @flow.page_breaks.map do |y|
410
+ PlantUML::Scene::Segment.new(x1: 0.0, y1: y, x2: right, y2: y)
411
+ end
412
+ end
413
+
414
+ def heads(widths)
415
+ unless @diagram.footbox?
416
+ return place_heads(widths, @top, tail: false)
417
+ end
418
+
419
+ place_heads(widths, @top, tail: false) +
420
+ place_heads(widths, @flow.y, tail: true)
421
+ end
422
+
423
+ # The lifelines end where the foot heads would begin.
424
+ def foot_height
425
+ @diagram.footbox? ? @head_height : 0.0
426
+ end
427
+
428
+ def flow_items
429
+ { arrows: @flow.arrows, fragments: @flow.fragments,
430
+ notes: @flow.notes, dividers: @flow.dividers,
431
+ delays: @flow.delays,
432
+ bars: @flow.bars, crosses: @flow.crosses }
433
+ end
434
+
435
+ def head_height
436
+ return GLYPH_HEAD_HEIGHT + head_growth if glyph_participants?
437
+
438
+ height = if intrinsic_plain_heads?
439
+ INTRINSIC_HEAD_HEIGHT
440
+ elsif plain_participants?
441
+ plain_head_height
442
+ else
443
+ 56.0
444
+ end
445
+ height + head_growth
446
+ end
447
+
448
+ def glyph_participants?
449
+ @diagram.participants.any? { |participant| glyph?(participant) }
450
+ end
451
+
452
+ def glyph?(participant)
453
+ participant.kind != :participant
454
+ end
455
+
456
+ def head_style
457
+ @diagram.appearance.head_style
458
+ end
459
+
460
+ def head_font_size
461
+ head_style.size || font_size
462
+ end
463
+
464
+ # PlantUML grows a head by 1.1776 for each point its text grows.
465
+ def head_growth
466
+ (head_font_size - font_size) * HEAD_LEADING
467
+ end
468
+
469
+ def head_width(participant)
470
+ if minimum_width_mixed_glyphs? && participant.kind == :actor
471
+ return ACTOR_INTRINSIC_WIDTH
472
+ end
473
+
474
+ minimum, padding = head_box_dimensions
475
+ widths = [minimum, padded_label_width(participant, padding),
476
+ requested_width]
477
+ widths << boxed_title_width(participant) unless boxed_plain_heads?
478
+ widths.max
479
+ end
480
+
481
+ def padded_label_width(participant, padding)
482
+ participant_label_width(participant) + (2 * padding)
483
+ end
484
+
485
+ def participant_label_width(participant)
486
+ NoteGeometry.widest_line(participant.label, head_font_size,
487
+ bold: bold_heads?)
488
+ end
489
+
490
+ # PlantUML draws a head label bold from weight 700 up.
491
+ def bold_heads?
492
+ weight = head_style.weight.to_s
493
+ weight == "bold" || weight.to_i >= 700 ||
494
+ head_style.style.to_s.casecmp?("bold")
495
+ end
496
+
497
+ def head_box_dimensions
498
+ return [0.0, INTRINSIC_HEAD_PADDING] if intrinsic_head_width?
499
+
500
+ padding = compact? ? head_padding : INTRINSIC_HEAD_PADDING
501
+ [minimum_head_width, padding]
502
+ end
503
+
504
+ # Without a requested minimum, a classic head is as wide as its
505
+ # label whatever other kinds of participant share the row.
506
+ def intrinsic_head_width?
507
+ !compact? && @diagram.min_head_width.nil?
508
+ end
509
+
510
+ def intrinsic_plain_heads?
511
+ intrinsic_group_heads? || boxed_plain_heads? ||
512
+ ordinary_plain_heads?
513
+ end
514
+
515
+ def ordinary_plain_heads?
516
+ return false if compact?
517
+
518
+ @diagram.min_head_width.nil? && @diagram.boxes.empty? &&
519
+ plain_participants? && !group_fragment?
520
+ end
521
+
522
+ def intrinsic_group_heads?
523
+ return false if compact?
524
+
525
+ @diagram.min_head_width.nil? && @diagram.boxes.empty? &&
526
+ plain_participants? && group_fragment?
527
+ end
528
+
529
+ def boxed_plain_heads?
530
+ return false if compact?
531
+
532
+ @diagram.min_head_width.nil? && @diagram.boxes.any? &&
533
+ plain_participants?
534
+ end
535
+
536
+ def group_fragment?
537
+ @diagram.items.any? do |item|
538
+ item.is_a?(Fragment) && item.phase == :open &&
539
+ item.keyword == "group"
540
+ end
541
+ end
542
+
543
+ def plain_participants?
544
+ @diagram.participants.all? do |participant|
545
+ participant.kind == :participant && participant.stereotype.nil?
546
+ end
547
+ end
548
+
549
+ # PlantUML draws a head 14 wider than the minimum it is given.
550
+ def requested_width
551
+ return 0.0 unless @diagram.min_head_width
552
+
553
+ @diagram.min_head_width + MIN_WIDTH_PADDING
554
+ end
555
+
556
+ # A box round one participant is as wide as that head, so the head
557
+ # must be wide enough for the title.
558
+ def boxed_title_width(participant)
559
+ box = @diagram.boxes.find { |b| b.members == [participant.id] }
560
+ box ? title_width(box) : 0.0
561
+ end
562
+
563
+ def title_width(box)
564
+ return boxed_title_advance(box) if boxed_plain_heads?
565
+
566
+ measure_text(box.title, font_size: font_size)[:width]
567
+ end
568
+
569
+ def boxed_title_advance(box)
570
+ Drawing::Face.widest_line(box.title, size: BOX_TITLE_SIZE,
571
+ bold: true)
572
+ end
573
+
574
+ def centers(widths)
575
+ gaps = required_gaps(widths)
576
+ x = margin + (widths.first / 2) + left_edge_room(widths, gaps)
577
+ widths.each_index.map do |index|
578
+ x += gaps[index - 1] if index.positive?
579
+ x
580
+ end
581
+ end
582
+
583
+ # How far to push every head right so the label of a `[->` message
584
+ # fits between the left edge and the head it points at.
585
+ def left_edge_room(widths, gaps)
586
+ start = margin + (widths.first / 2)
587
+ short = edge_messages(:left).map { |m| shortfall(m, start, gaps) }
588
+ [0.0, *short, *leading_box_overhang(widths, gaps)].max
589
+ end
590
+
591
+ def leading_box_overhang(widths, gaps)
592
+ return [] unless boxed_plain_heads?
593
+
594
+ @diagram.boxes.filter_map do |box|
595
+ next unless member_indexes(box).min.zero?
596
+
597
+ box_overhang(box, widths, gaps)
598
+ end
599
+ end
600
+
601
+ def shortfall(message, start, gaps)
602
+ edge_room(message) - start - gaps.first(ids.index(message.to)).sum
603
+ end
604
+
605
+ # Distance between neighbouring centres: heads must not overlap and
606
+ # every label must fit on the span it crosses.
607
+ def required_gaps(widths)
608
+ gaps = widths.each_cons(2).with_index.map do |(a, b), index|
609
+ ((a + b) / 2) + participant_gap(index)
610
+ end
611
+ widen_for_items(gaps)
612
+ clear_box_titles(widths, gaps) if boxed_plain_heads?
613
+ gaps
614
+ end
615
+
616
+ def participant_gap(_index)
617
+ minimum_gap
618
+ end
619
+
620
+ # A title wider than its members is centred on them and pushes the
621
+ # neighbouring heads away by the overhang on that side.
622
+ def clear_box_titles(widths, gaps)
623
+ @diagram.boxes.each do |box|
624
+ first, last = member_indexes(box).minmax
625
+ extra = box_overhang(box, widths, gaps)
626
+ [first - 1, last].each do |index|
627
+ gaps[index] += extra if index.between?(0, gaps.size - 1)
628
+ end
629
+ end
630
+ end
631
+
632
+ def box_overhang(box, widths, gaps)
633
+ first, last = member_indexes(box).minmax
634
+ inner = gaps[first...last].sum + box_head_half(widths, first, last)
635
+ need = title_width(box) + (2 * BOX_TITLE_PADDING)
636
+ [(need - inner - (2 * BOX_PADDING)) / 2, 0.0].max
637
+ end
638
+
639
+ def box_head_half(widths, first, last)
640
+ (widths[first] + widths[last]) / 2
641
+ end
642
+
643
+ def widen_for_items(gaps)
644
+ @diagram.messages.each { |m| widen(gaps, m) }
645
+ @diagram.boxes.each { |box| widen_box(gaps, box) }
646
+ each_note { |note, span| widen_note(gaps, note, span) }
647
+ @diagram.items.grep(Ref).each { |ref| widen_ref(gaps, ref) }
648
+ end
649
+
650
+ def each_note
651
+ previous = nil
652
+ @diagram.items.each do |item|
653
+ if free_note?(item, previous)
654
+ yield item, NoteGeometry.span(item, previous, ids)
655
+ end
656
+ previous = item unless item.is_a?(Activation)
657
+ end
658
+ end
659
+
660
+ # A note that is not attached to the fragment before it.
661
+ def free_note?(item, previous)
662
+ return false unless item.is_a?(Note)
663
+ return true unless item.attached?
664
+
665
+ !compact? && !outside_gap_anchor?(previous)
666
+ end
667
+
668
+ def outside_gap_anchor?(previous)
669
+ previous.is_a?(Fragment) ||
670
+ (previous.is_a?(Message) && previous.self_message?)
671
+ end
672
+
673
+ # PlantUML 1.2026.6 classic never moves lifelines for a note: a
674
+ # wide note overlaps its neighbours and only grows the canvas.
675
+ # Teoz keeps a note over several lifelines clear of them.
676
+ def widen_note(gaps, note, span)
677
+ return unless compact?
678
+ return if %i[left right].include?(note.side)
679
+
680
+ need = NoteGeometry.natural_width(note, note_font_size)
681
+ low, high = span
682
+ case note.side
683
+ when :left then raise_gap(gaps, low - 1, need + 12)
684
+ when :right then raise_gap(gaps, high, need + 12)
685
+ else
686
+ widen_over(gaps, (low...high).to_a, low, need) unless low == high
687
+ end
688
+ end
689
+
690
+ # A ref wider than the lifelines it names pushes them apart.
691
+ def widen_ref(gaps, ref)
692
+ low, high = ref.targets.map { |id| ids.index(id) }.minmax
693
+ between = (low...high).to_a
694
+ need = RefShape.width_for(ref.label)
695
+ return widen_over(gaps, between, low, need) if between.empty?
696
+
697
+ spread_ref(gaps, ref, between, need)
698
+ end
699
+
700
+ def spread_ref(gaps, ref, between, need)
701
+ share = (need - (2 * ref_side_pad(ref))) / between.size
702
+ between.each { |index| raise_gap(gaps, index, share) }
703
+ end
704
+
705
+ def ref_side_pad(ref)
706
+ nested_in_fragment?(ref) ? FragmentShape::BASE_PAD : RefShape::SIDE_PAD
707
+ end
708
+
709
+ def widen_over(gaps, between, index, need)
710
+ if between.empty?
711
+ share = (need / 2) + 12
712
+ [index - 1, index].each { |i| raise_gap(gaps, i, share) }
713
+ else
714
+ share = (need - 20) / between.size
715
+ between.each { |i| raise_gap(gaps, i, share) }
716
+ end
717
+ end
718
+
719
+ def raise_gap(gaps, index, need)
720
+ gaps[index] = [gaps[index], need].max if index >= 0 && gaps[index]
721
+ end
722
+
723
+ def widen_box(gaps, box)
724
+ return if boxed_plain_heads?
725
+
726
+ span = member_span(box)
727
+ need = title_width(box) / span.size
728
+ span.each { |i| raise_gap(gaps, i, need) }
729
+ end
730
+
731
+ def member_indexes(box)
732
+ box.members.map { |id| ids.index(id) }
733
+ end
734
+
735
+ def member_span(box)
736
+ places = member_indexes(box).sort
737
+ (places.first...places.last).to_a
738
+ end
739
+
740
+ def ids
741
+ @diagram.participants.map(&:id)
742
+ end
743
+
744
+ def widen(gaps, message)
745
+ return if message.edge
746
+
747
+ low, high = indexes(message).sort
748
+ span = gap_span(gaps, message, low, high)
749
+ need = message_room(message, low == high) + created_room(message)
750
+ span.each { |i| raise_gap(gaps, i, need / span.size) }
751
+ end
752
+
753
+ # The gaps a message crosses; a leftward loop uses the one on the
754
+ # left of its participant.
755
+ def gap_span(gaps, message, low, high)
756
+ return [low - 1] if message.self_message? && message.leftward?
757
+
758
+ (low...[high, low + 1].max).select { |i| i < gaps.size }
759
+ end
760
+
761
+ # The arrow ends at the edge of a created head, not at its centre.
762
+ def created_room(message)
763
+ return 0.0 unless @creations.key?(message)
764
+
765
+ @widths.fetch(ids.index(message.to)) / 2
766
+ end
767
+
768
+ def message_room(message, self_message)
769
+ room = label_width(message) + (2 * head_padding)
770
+ unless compact?
771
+ return classic_message_room(message, room, self_message)
772
+ end
773
+
774
+ compact_message_room(message, room, self_message)
775
+ end
776
+
777
+ def compact_message_room(message, room, self_message)
778
+ unless self_message
779
+ clearance = activation_bar_clearance(message)
780
+ return room + ArrowMarks::LENGTH + clearance
781
+ end
782
+
783
+ label_room = [SELF_WIDTH, 7 + sans_label_width(message)].max
784
+ [COMPACT_SELF_CLEARANCE, label_room + head_padding].max
785
+ end
786
+
787
+ def activation_bar_clearance(message)
788
+ depths = contact_activation_depths(message)
789
+ from, to = indexes(message)
790
+ target = depths[message.to]
791
+ return active_bar_half(target) unless from > to
792
+
793
+ (target * BarTracker::WIDTH / 2) +
794
+ active_bar_half(depths[message.from])
795
+ end
796
+
797
+ def active_bar_half(depth)
798
+ depth.positive? ? BarTracker::WIDTH / 2 : 0.0
799
+ end
800
+
801
+ def contact_activation_depths(message)
802
+ depths = @activation_depths.fetch(message).dup
803
+ index = @diagram.items.index { |item| item.equal?(message) }
804
+ following = @diagram.items.drop(index + 1)
805
+ Walker.following_activations(following, parallel_notes: true)
806
+ .select(&:on?).each { |item| depths[item.participant] += 1 }
807
+ depths
808
+ end
809
+
810
+ def classic_message_room(message, ordinary, self_message)
811
+ return ordinary unless self_message
812
+
813
+ room = [label_width(message) + (2 * INTRINSIC_HEAD_PADDING),
814
+ SELF_MIN_CLEARANCE].max
815
+ return room unless nested_in_fragment?(message)
816
+
817
+ [room, classic_self_clearance(message)].max
818
+ end
819
+
820
+ def classic_self_clearance(message)
821
+ depth = @activation_depths.fetch(message)[message.from]
822
+ extra = depth.positive? ? (depth + 1) * BarTracker::WIDTH : 0
823
+ CLASSIC_SELF_CLEARANCE + extra
824
+ end
825
+
826
+ def activation_depths
827
+ active = Hash.new(0)
828
+ depths = Hash.new.compare_by_identity
829
+ @diagram.items.each_with_object(depths) do |item, measured|
830
+ if item.is_a?(Message)
831
+ measured[item] = active.dup
832
+ else
833
+ update_activation_depth(active, item)
834
+ end
835
+ end
836
+ end
837
+
838
+ def update_activation_depth(active, item)
839
+ case item
840
+ when Activation
841
+ delta = item.on? ? 1 : -1
842
+ depth = active[item.participant] + delta
843
+ active[item.participant] = [depth, 0].max
844
+ when Destroy then active[item.participant] = 0
845
+ end
846
+ end
847
+
848
+ def nested_in_fragment?(item)
849
+ depth = 0
850
+ @diagram.items.each do |candidate|
851
+ return depth.positive? if candidate.equal?(item)
852
+ next unless candidate.is_a?(Fragment)
853
+
854
+ depth += 1 if candidate.phase == :open
855
+ depth -= 1 if candidate.phase == :close
856
+ end
857
+ false
858
+ end
859
+
860
+ def indexes(message)
861
+ [ids.index(message.from), ids.index(message.to)]
862
+ end
863
+
864
+ def sans_label_width(message)
865
+ return 0.0 unless message.label
866
+
867
+ wrap = MessageWrap.new(@diagram.appearance.max_message,
868
+ method(:sans_width))
869
+ wrap.width(message.label) + numbering.extra(message)
870
+ end
871
+
872
+ def sans_width(text)
873
+ NoteGeometry.widest_line(text, message_font_size)
874
+ end
875
+
876
+ def label_width(message)
877
+ return 0.0 unless message.label
878
+
879
+ measured = message_wrap(message).width(message.label)
880
+ measured + numbering.extra(message)
881
+ end
882
+
883
+ def message_wrap(message)
884
+ MessageWrap.new(@diagram.appearance.max_message,
885
+ label_measure(message))
886
+ end
887
+
888
+ def label_measure(message)
889
+ if message.label_family == "monospace" || !compact?
890
+ return method(:label_text_width)
891
+ end
892
+
893
+ method(:sans_width)
894
+ end
895
+
896
+ def wrap
897
+ MessageWrap.new(@diagram.appearance.max_message,
898
+ method(:label_text_width))
899
+ end
900
+
901
+ def numbering
902
+ NumberedLabel.new(method(:label_text_width))
903
+ end
904
+
905
+ def canvas_width(widths)
906
+ padded = [head_extent(widths), @flow.right].max + margin
907
+ [padded, edge_extent, banner_extent, title_extent].max
908
+ end
909
+
910
+ def title_extent
911
+ chrome_rows.width
912
+ end
913
+
914
+ def chrome_rows
915
+ @chrome_rows ||= ChromeRows.new(@diagram, method(:chrome_width))
916
+ end
917
+
918
+ def chrome_width(text, size, bold: false)
919
+ size ||= font_size
920
+ return bold_width(text, size) if bold
921
+
922
+ measure_text(text, font_size: size)[:width]
923
+ end
924
+
925
+ # The JAR measures a bold chrome text at 0.916 of DejaVu Bold: its
926
+ # textLength over the table's width is 0.9153, 0.9160 and 0.9159
927
+ # for three titles of 17 to 36 characters.
928
+ def bold_width(text, size)
929
+ Drawing::Face.width(text, size: size, bold: true) *
930
+ BOLD_CHROME_SCALE
931
+ end
932
+
933
+ def page_height
934
+ @flow.y + foot_height + margin + chrome_rows.bottom_room
935
+ end
936
+
937
+ def chrome_top
938
+ margin + banner_room
939
+ end
940
+
941
+ def title_scene(widths)
942
+ chrome_rows.title(chrome_top, canvas_width(widths))
943
+ end
944
+
945
+ def text_blocks(widths)
946
+ chrome_rows.blocks(chrome_top, @flow.y + foot_height,
947
+ canvas_width(widths))
948
+ end
949
+
950
+ def legend_scene(widths)
951
+ chrome_rows.legend(chrome_top, @flow.y + foot_height,
952
+ canvas_width(widths))
953
+ end
954
+
955
+ def banner_room
956
+ @diagram.warnings.size * BANNER_STEP
957
+ end
958
+
959
+ def banner_extent
960
+ widest = @diagram.warnings.map { |line| banner_width(line) }.max
961
+ widest ? BANNER_LEFT + widest + BANNER_LEFT : 0.0
962
+ end
963
+
964
+ def banner_width(line)
965
+ text = line.length * MONOSPACE_ADVANCE
966
+ (text + (2 * BANNER_PADDING)).round(2)
967
+ end
968
+
969
+ def banners
970
+ @diagram.warnings.each_with_index.map do |line, index|
971
+ banner(line, BANNER_TOP + (index * BANNER_STEP))
972
+ end
973
+ end
974
+
975
+ def banner(line, top)
976
+ width = banner_width(line)
977
+ Scene::Banner.new(
978
+ x: BANNER_LEFT, y: top, width: width, height: BANNER_HEIGHT,
979
+ texts: [PlantUML::Scene::Text.new(
980
+ content: line, x: BANNER_LEFT + BANNER_PADDING,
981
+ y: top + BANNER_HEIGHT - 6, role: "warning", anchor: "start"
982
+ )]
983
+ )
984
+ end
985
+
986
+ def head_extent(widths)
987
+ right = @centers.last + (widths.last / 2)
988
+ self_room = self_reach(@diagram.participants.size - 1)
989
+ [right, @centers.last + self_room].max
990
+ end
991
+
992
+ def edge_extent
993
+ @edge_right ? @edge_right + EDGE_MARGIN : 0.0
994
+ end
995
+
996
+ def self_reach(index)
997
+ labels = @diagram.messages.select do |m|
998
+ m.self_message? && !m.leftward? && indexes(m).first == index
999
+ end
1000
+ return 0.0 if labels.empty?
1001
+
1002
+ reach_for(labels.map { |m| label_width(m) }.max)
1003
+ end
1004
+
1005
+ def reach_for(widest)
1006
+ return SELF_WIDTH + 6 + widest if compact?
1007
+
1008
+ [SELF_WIDTH, CLASSIC_LABEL_INSET + widest].max
1009
+ end
1010
+
1011
+ def place_heads(widths, top, tail:)
1012
+ @diagram.participants.each_with_index.map do |participant, index|
1013
+ at = tail ? top : created_top(participant.id, top)
1014
+ head(participant, @centers[index], widths[index], at, tail)
1015
+ end
1016
+ end
1017
+
1018
+ def head(participant, centre, width, top, tail)
1019
+ geometry = head_geometry(participant, centre, width, top, tail)
1020
+ Scene::Head.new(
1021
+ id: participant.id, kind: participant.kind.to_s,
1022
+ x: centre - (width / 2), y: top, width: width,
1023
+ height: @head_height, fill: participant.fill&.colour,
1024
+ fill_opacity: participant.fill&.opacity,
1025
+ stroke: head_style.line,
1026
+ texts: head_texts(participant, centre, top, tail), **geometry
1027
+ )
1028
+ end
1029
+
1030
+ def head_texts(participant, centre, top, tail)
1031
+ unless glyph?(participant)
1032
+ return participant_texts(participant, centre, top)
1033
+ end
1034
+
1035
+ glyph_texts(participant, centre, top, tail)
1036
+ end
1037
+
1038
+ def participant_texts(participant, centre, top)
1039
+ label = head_label(participant, centre, top)
1040
+ name = above_label(participant)
1041
+ return [label] unless name
1042
+
1043
+ lines = [text(name, centre, top + 14, "kind"), label]
1044
+ participant.stereotype ? align(lines, centre) : lines
1045
+ end
1046
+
1047
+ def glyph_texts(participant, centre, top, tail)
1048
+ baseline = glyph_label_y(participant.kind, top, tail)
1049
+ label = restyle(text(participant.label, centre, baseline,
1050
+ "participant"), head_style)
1051
+ return [label] unless participant.stereotype
1052
+
1053
+ stereotype = text("«#{participant.stereotype}»", centre,
1054
+ baseline - 18, "kind")
1055
+ align([stereotype, label], centre)
1056
+ end
1057
+
1058
+ def glyph_label_y(kind, top, tail)
1059
+ top + glyph_label_offset(kind, tail)
1060
+ end
1061
+
1062
+ def glyph_label_offset(kind, tail)
1063
+ if boxed_glyph?(kind)
1064
+ return label_offset(tail, TAIL_BOX_LABEL, BOX_LABEL_INSET)
1065
+ end
1066
+ if kind == :queue
1067
+ return label_offset(tail, TAIL_QUEUE_LABEL, QUEUE_LABEL_INSET)
1068
+ end
1069
+
1070
+ label_offset(tail, TAIL_ICON_LABEL, ICON_LABEL_INSET)
1071
+ end
1072
+
1073
+ def label_offset(tail, tail_label, inset)
1074
+ tail ? tail_label : @head_height - inset
1075
+ end
1076
+
1077
+ def head_label(participant, centre, top)
1078
+ baseline = top + @head_height - head_label_inset
1079
+ label = text(participant.label, centre, baseline, "participant")
1080
+ restyle(label, head_style)
1081
+ end
1082
+
1083
+ def restyle(label, style)
1084
+ label.colour = style.colour
1085
+ label.size = style.size
1086
+ label.family = style.family
1087
+ label.font_style = style.style
1088
+ label.weight = style.weight
1089
+ label
1090
+ end
1091
+
1092
+ # Lines of a stereotyped head sit against the edge of the block
1093
+ # they form, which is as wide as its widest line.
1094
+ def align(lines, centre)
1095
+ alignment = @diagram.appearance.alignment
1096
+ return lines if alignment == :center
1097
+
1098
+ half = lines.map { |line| line_width(line) }.max / 2
1099
+ lines.each do |line|
1100
+ line.anchor = alignment == :left ? "start" : "end"
1101
+ line.x = alignment == :left ? centre - half : centre + half
1102
+ end
1103
+ end
1104
+
1105
+ # The renderer draws a "kind" line at 0.85 of the normal size.
1106
+ def line_width(line)
1107
+ scale = line.role == "kind" ? 0.85 : 1.0
1108
+ measure_text(line.content, font_size: font_size * scale)[:width]
1109
+ end
1110
+
1111
+ def above_label(participant)
1112
+ "«#{participant.stereotype}»" if participant.stereotype
1113
+ end
1114
+
1115
+ def head_geometry(participant, centre, width, top, tail)
1116
+ builder = GLYPH_BUILDERS[participant.kind]
1117
+ unless builder
1118
+ return rectangle_geometry(participant, centre, width, top, tail)
1119
+ end
1120
+
1121
+ send(builder, centre, width, top, tail)
1122
+ end
1123
+
1124
+ def rectangle_geometry(participant, centre, width, top, tail)
1125
+ left = centre - (width / 2)
1126
+ height = if glyph_participants?
1127
+ participant_box_height(participant)
1128
+ else
1129
+ @head_height
1130
+ end
1131
+ y = rectangle_top(top, height, tail)
1132
+ radius = glyph_participants? ? 2.5 : 3
1133
+ rectangle = glyph_rectangle(left, y, width, height, radius: radius)
1134
+ { rectangles: [rectangle] }
1135
+ end
1136
+
1137
+ def collections_geometry(centre, width, top, tail)
1138
+ left = centre - (width / 2)
1139
+ y = glyph_box_top(top, tail)
1140
+ glyph_width = width - GLYPH_BOX_OFFSET
1141
+ back = glyph_rectangle(left + GLYPH_BOX_OFFSET,
1142
+ y - GLYPH_BOX_OFFSET, glyph_width)
1143
+ front = glyph_rectangle(left, y, glyph_width)
1144
+ { rectangles: [back, front] }
1145
+ end
1146
+
1147
+ def rectangle_top(top, height, tail)
1148
+ return top unless glyph_participants?
1149
+
1150
+ return top if tail
1151
+
1152
+ top + @head_height - height - 1
1153
+ end
1154
+
1155
+ def glyph_rectangle(left, top, width, height = GLYPH_BOX_HEIGHT,
1156
+ radius: nil)
1157
+ Scene::Head::Rectangle.new(
1158
+ x: left, y: top, width: width, height: height,
1159
+ rx: radius, ry: radius
1160
+ )
1161
+ end
1162
+
1163
+ def actor_geometry(centre, _width, top, tail)
1164
+ centre_y = tail ? top + @head_height - 51.5 : top + 8.5
1165
+ ellipse = Scene::Head::Ellipse.new(
1166
+ cx: centre, cy: centre_y, rx: 8, ry: 8,
1167
+ )
1168
+ path = Scene::Head::Path.new(
1169
+ data: actor_path(centre, centre_y), filled: false,
1170
+ )
1171
+ { ellipses: [ellipse], paths: [path] }
1172
+ end
1173
+
1174
+ def actor_path(centre, centre_y)
1175
+ body_top = centre_y + 8
1176
+ body_bottom = centre_y + 35
1177
+ foot = centre_y + 50
1178
+ ["M #{centre} #{body_top} L #{centre} #{body_bottom}",
1179
+ "M #{centre - 13} #{body_top + 8} " \
1180
+ "L #{centre + 13} #{body_top + 8}",
1181
+ "M #{centre} #{body_bottom} L #{centre - 13} #{foot}",
1182
+ "M #{centre} #{body_bottom} L #{centre + 13} #{foot}"].join(" ")
1183
+ end
1184
+
1185
+ def boundary_geometry(centre, _width, top, tail)
1186
+ centre_y = icon_centre_y(top, tail)
1187
+ stem_x = centre - 29
1188
+ stem = Scene::Head::Path.new(
1189
+ data: "M #{stem_x} #{centre_y - 12} " \
1190
+ "L #{stem_x} #{centre_y + 12} " \
1191
+ "M #{stem_x} #{centre_y} L #{centre - 12} #{centre_y}",
1192
+ filled: false,
1193
+ )
1194
+ { ellipses: [icon_ellipse(centre, centre_y)], paths: [stem] }
1195
+ end
1196
+
1197
+ def control_geometry(centre, _width, top, tail)
1198
+ centre_y = icon_centre_y(top, tail)
1199
+ points = [[centre - 4, centre_y - 12],
1200
+ [centre + 2, centre_y - 17],
1201
+ [centre, centre_y - 12],
1202
+ [centre + 2, centre_y - 7]]
1203
+ pointer = Scene::Head::Polygon.new(
1204
+ points: polygon_points(points << points.first),
1205
+ )
1206
+ { ellipses: [icon_ellipse(centre, centre_y)], polygons: [pointer] }
1207
+ end
1208
+
1209
+ def entity_geometry(centre, _width, top, tail)
1210
+ centre_y = icon_centre_y(top, tail)
1211
+ base = Scene::Head::Line.new(
1212
+ x1: centre - ICON_RADIUS, y1: centre_y + 14,
1213
+ x2: centre + ICON_RADIUS, y2: centre_y + 14
1214
+ )
1215
+ { ellipses: [icon_ellipse(centre, centre_y)], lines: [base] }
1216
+ end
1217
+
1218
+ def icon_ellipse(centre, centre_y)
1219
+ Scene::Head::Ellipse.new(
1220
+ cx: centre, cy: centre_y, rx: ICON_RADIUS, ry: ICON_RADIUS,
1221
+ )
1222
+ end
1223
+
1224
+ def icon_centre_y(top, tail)
1225
+ tail ? top + @head_height - 44 : top + 44
1226
+ end
1227
+
1228
+ def database_geometry(centre, _width, top, tail)
1229
+ y = tail ? top + @head_height - 60 : top + 14
1230
+ left = centre - 18
1231
+ right = centre + 18
1232
+ outline = Scene::Head::Path.new(
1233
+ data: database_outline(left, centre, right, y), filled: true,
1234
+ )
1235
+ rim = Scene::Head::Path.new(
1236
+ data: database_rim(left, centre, right, y), filled: false,
1237
+ )
1238
+ { paths: [outline, rim] }
1239
+ end
1240
+
1241
+ def database_outline(left, centre, right, top)
1242
+ ["M #{left} #{top + 10}",
1243
+ "C #{left} #{top} #{centre} #{top} #{centre} #{top}",
1244
+ "C #{centre} #{top} #{right} #{top} #{right} #{top + 10}",
1245
+ "L #{right} #{top + 36}",
1246
+ "C #{right} #{top + 46} #{centre} #{top + 46} " \
1247
+ "#{centre} #{top + 46}",
1248
+ "C #{centre} #{top + 46} #{left} #{top + 46} " \
1249
+ "#{left} #{top + 36}",
1250
+ "L #{left} #{top + 10}"].join(" ")
1251
+ end
1252
+
1253
+ def database_rim(left, centre, right, top)
1254
+ ["M #{left} #{top + 10}",
1255
+ "C #{left} #{top + 20} #{centre} #{top + 20} " \
1256
+ "#{centre} #{top + 20}",
1257
+ "C #{centre} #{top + 20} #{right} #{top + 20} " \
1258
+ "#{right} #{top + 10}"].join(" ")
1259
+ end
1260
+
1261
+ def queue_geometry(centre, width, top, tail)
1262
+ y = tail ? top : top + @head_height - QUEUE_HEIGHT
1263
+ left = centre - (width / 2)
1264
+ right = left + width
1265
+ middle = y + (QUEUE_HEIGHT / 2)
1266
+ outline = glyph_path(queue_outline(left, right, y, middle), true)
1267
+ rim = glyph_path(queue_rim(right, y, middle), false)
1268
+ { paths: [outline, rim] }
1269
+ end
1270
+
1271
+ def queue_outline(left, right, top, middle)
1272
+ ["M #{left + 5} #{top} L #{right - 5} #{top}",
1273
+ "C #{right} #{top} #{right} #{middle} #{right} #{middle}",
1274
+ "C #{right} #{middle} #{right} #{top + QUEUE_HEIGHT} " \
1275
+ "#{right - 5} #{top + QUEUE_HEIGHT}",
1276
+ "L #{left + 5} #{top + QUEUE_HEIGHT}",
1277
+ "C #{left} #{top + QUEUE_HEIGHT} #{left} #{middle} " \
1278
+ "#{left} #{middle}",
1279
+ "C #{left} #{middle} #{left} #{top} #{left + 5} #{top}"].join(" ")
1280
+ end
1281
+
1282
+ def queue_rim(right, top, middle)
1283
+ ["M #{right - 5} #{top}",
1284
+ "C #{right - 10} #{top} #{right - 10} #{middle} " \
1285
+ "#{right - 10} #{middle}",
1286
+ "C #{right - 10} #{top + QUEUE_HEIGHT} " \
1287
+ "#{right - 5} #{top + QUEUE_HEIGHT} " \
1288
+ "#{right - 5} #{top + QUEUE_HEIGHT}"]
1289
+ .join(" ")
1290
+ end
1291
+
1292
+ def polygon_points(points)
1293
+ points.map { |point_x, point_y| "#{point_x},#{point_y}" }.join(" ")
1294
+ end
1295
+
1296
+ def glyph_path(data, filled)
1297
+ Scene::Head::Path.new(data: data, filled: filled)
1298
+ end
1299
+
1300
+ def glyph_box_top(top, tail)
1301
+ tail ? top : top + @head_height - GLYPH_BOX_HEIGHT - 1
1302
+ end
1303
+
1304
+ def participant_box_height(participant)
1305
+ if minimum_width_mixed_glyphs?
1306
+ return STEREOTYPE_BOX_HEIGHT if participant.stereotype
1307
+
1308
+ return GLYPH_BOX_HEIGHT
1309
+ end
1310
+
1311
+ @diagram.participants.any?(&:stereotype) ? 56.0 : GLYPH_BOX_HEIGHT
1312
+ end
1313
+
1314
+ def boxed_glyph?(kind)
1315
+ %i[collections participant].include?(kind)
1316
+ end
1317
+
1318
+ def frames(widths)
1319
+ @diagram.boxes.map { |box| frame(box, widths) }
1320
+ end
1321
+
1322
+ def frame(box, widths)
1323
+ left, right = frame_edges(box, widths)
1324
+ Scene::Frame.new(
1325
+ x: left, y: @origin, width: right - left,
1326
+ height: @flow.y + foot_height + BOX_PADDING - @origin,
1327
+ fill: box.fill&.colour, texts: [box_title(box, left, right)]
1328
+ )
1329
+ end
1330
+
1331
+ def box_title(box, left, right)
1332
+ text(box.title, (left + right) / 2, @origin + 17, "box")
1333
+ end
1334
+
1335
+ def frame_edges(box, widths)
1336
+ left, right = member_edges(box, widths)
1337
+ return [left, right] unless boxed_plain_heads?
1338
+
1339
+ widen_to_title(box, left, right)
1340
+ end
1341
+
1342
+ def member_edges(box, widths)
1343
+ first, last = member_indexes(box).minmax
1344
+ [@centers[first] - (widths[first] / 2) - BOX_PADDING,
1345
+ @centers[last] + (widths[last] / 2) + BOX_PADDING]
1346
+ end
1347
+
1348
+ def widen_to_title(box, left, right)
1349
+ shortfall = title_width(box) + (2 * BOX_TITLE_PADDING) -
1350
+ (right - left)
1351
+ return [left, right] unless shortfall.positive?
1352
+
1353
+ [left - (shortfall / 2), right + (shortfall / 2)]
1354
+ end
1355
+
1356
+ def lifelines
1357
+ ids.zip(@centers).flat_map do |id, centre|
1358
+ lifeline_pieces(centre, lifeline_top(id), @flow.y)
1359
+ end
1360
+ end
1361
+
1362
+ # The lifeline from `top` to `bottom`, dotted across each gap.
1363
+ def lifeline_pieces(centre, top, bottom)
1364
+ at = top
1365
+ pieces = gaps.flat_map do |gap|
1366
+ next [] if gap.y1 < at || gap.y2 > bottom
1367
+
1368
+ piece = [lifeline_segment(centre, at, gap.y1),
1369
+ lifeline_segment(centre, gap.y1, gap.y2, DELAY_DASH)]
1370
+ at = gap.y2
1371
+ piece
1372
+ end
1373
+ pieces << lifeline_segment(centre, at, bottom)
1374
+ end
1375
+
1376
+ def gaps
1377
+ @flow.delays.select { |delay| delay.y2 > delay.y1 }
1378
+ end
1379
+
1380
+ def lifeline_segment(centre, from, to, dash = nil)
1381
+ PlantUML::Scene::Segment.new(x1: centre, y1: from, x2: centre,
1382
+ y2: to, dash: dash)
1383
+ end
1384
+
1385
+ def created_top(id, top)
1386
+ row = @flow.created[id]
1387
+ row ? row - CREATE_HEAD_RISE : top
1388
+ end
1389
+
1390
+ def lifeline_top(id)
1391
+ return @top + @head_height unless @flow.created.key?(id)
1392
+
1393
+ created_top(id, @top) + @head_height + CREATE_LIFELINE_LAG
1394
+ end
1395
+
1396
+ def text(content, at_x, at_y, role, anchor = "middle")
1397
+ PlantUML::Scene::Text.new(content: content, x: at_x, y: at_y,
1398
+ role: role, anchor: anchor)
1399
+ end
1400
+
1401
+ def font_size
1402
+ theme.typography.font_size_normal.to_f
1403
+ end
1404
+
1405
+ # The default skin draws a note in 13px, a theme in its own size.
1406
+ def note_font_size
1407
+ return font_size unless [nil, "default"].include?(theme.name)
1408
+
1409
+ NoteGeometry::SKIN_FONT_SIZE
1410
+ end
1411
+
1412
+ def text_width(string)
1413
+ measure_text(string, font_size: font_size)[:width]
1414
+ end
1415
+
1416
+ def message_font_size
1417
+ font_size - 1
1418
+ end
1419
+
1420
+ def message_text_width(string)
1421
+ measure_text(string, font_size: message_font_size,
1422
+ monospace: true)[:width]
1423
+ end
1424
+
1425
+ def compact?
1426
+ @diagram.teoz?
1427
+ end
1428
+
1429
+ def minimum_width_mixed_glyphs?
1430
+ return false if compact? || !@diagram.min_head_width
1431
+
1432
+ @diagram.participants.any? { |participant| glyph?(participant) } &&
1433
+ @diagram.participants.any? { |participant| !glyph?(participant) }
1434
+ end
1435
+
1436
+ def margin
1437
+ return MIN_WIDTH_GLYPH_MARGIN if minimum_width_mixed_glyphs?
1438
+
1439
+ compact? ? COMPACT_MARGIN : MARGIN
1440
+ end
1441
+
1442
+ def head_padding
1443
+ compact? ? COMPACT_HEAD_PADDING : HEAD_PADDING
1444
+ end
1445
+
1446
+ def minimum_head_width
1447
+ compact? ? 0.0 : MIN_HEAD_WIDTH
1448
+ end
1449
+
1450
+ def minimum_gap
1451
+ return MIN_WIDTH_GLYPH_GAP if minimum_width_mixed_glyphs?
1452
+ return COMPACT_TIMELINE_GAP if compact_timeline?
1453
+
1454
+ compact? ? COMPACT_TIMELINE_GAP : MIN_GAP
1455
+ end
1456
+
1457
+ def plain_head_height
1458
+ compact? ? COMPACT_HEAD_HEIGHT : 36.0
1459
+ end
1460
+
1461
+ def head_label_inset
1462
+ compact? ? COMPACT_HEAD_LABEL_INSET : 12
1463
+ end
1464
+
1465
+ def first_row_gap
1466
+ return MIN_WIDTH_GLYPH_FIRST_ROW_GAP if minimum_width_mixed_glyphs?
1467
+ return COMPACT_GROUP_FIRST_ROW_GAP if compact_timeline?
1468
+ return teoz_first_row_gap if teoz_fragment_cadence?
1469
+ if classic_unlabelled_first_message?
1470
+ return CLASSIC_UNLABELLED_FIRST_ROW_GAP
1471
+ end
1472
+
1473
+ compact? ? compact_first_row_gap : CLASSIC_FIRST_ROW_GAP
1474
+ end
1475
+
1476
+ def compact_first_row_gap
1477
+ first = @diagram.messages.first
1478
+ return COMPACT_FIRST_ROW_GAP if first&.label_family == "monospace"
1479
+
1480
+ TEOZ_FIRST_MESSAGE_GAP
1481
+ end
1482
+
1483
+ def teoz_first_row_gap
1484
+ first = @diagram.items.find { |item| !item.is_a?(Activation) }
1485
+ return COMPACT_GROUP_FIRST_ROW_GAP if first.is_a?(Fragment)
1486
+ if first.is_a?(Message) && first.label.to_s.empty?
1487
+ return TEOZ_FIRST_UNLABELLED_GAP
1488
+ end
1489
+
1490
+ TEOZ_FIRST_MESSAGE_GAP
1491
+ end
1492
+
1493
+ def teoz_fragment_cadence?
1494
+ compact? && @diagram.items.any?(Fragment) && !compact_timeline?
1495
+ end
1496
+
1497
+ def classic_unlabelled_first_message?
1498
+ first = @diagram.items.first
1499
+ !compact? && first.is_a?(Message) && first.label.to_s.empty? &&
1500
+ !first_message_carries_picture?
1501
+ end
1502
+
1503
+ def first_message_carries_picture?
1504
+ following = @diagram.items.drop(1).find do |item|
1505
+ !item.is_a?(Activation)
1506
+ end
1507
+ following.is_a?(Note) && following.attached? &&
1508
+ !following.picture.nil?
1509
+ end
1510
+
1511
+ def row_height
1512
+ return CLASSIC_ROW if teoz_fragment_cadence?
1513
+
1514
+ compact? ? COMPACT_ROW : CLASSIC_ROW
1515
+ end
1516
+
1517
+ def self_row_height
1518
+ return CLASSIC_SELF_ROW if teoz_fragment_cadence?
1519
+
1520
+ compact? ? COMPACT_SELF_ROW : CLASSIC_SELF_ROW
1521
+ end
1522
+
1523
+ def fragment_row_height
1524
+ compact? ? Walker::TAB_HEIGHT : CLASSIC_FRAGMENT_ROW
1525
+ end
1526
+
1527
+ def timeline_value(value)
1528
+ value if compact_timeline?
1529
+ end
1530
+
1531
+ def compact_timeline?
1532
+ compact? && group_fragment? && @diagram.messages.all? do |message|
1533
+ message.label.to_s.empty?
1534
+ end
1535
+ end
1536
+
1537
+ def two_party_activation_timeline?
1538
+ compact? && @diagram.participants.size == 2 &&
1539
+ @diagram.items.none?(Fragment) && @diagram.items.any?(Activation)
1540
+ end
1541
+
1542
+ def divider_row_height
1543
+ compact? ? Walker::ROW : CLASSIC_DIVIDER_ROW
1544
+ end
1545
+
1546
+ def deactivation_gap
1547
+ compact? ? 0.0 : CLASSIC_DEACTIVATION_GAP
1548
+ end
1549
+
1550
+ def bare_deactivation_gap
1551
+ compact? ? 0.0 : CLASSIC_BARE_DEACTIVATION_GAP
1552
+ end
1553
+
1554
+ def label_font_size
1555
+ message_font_size
1556
+ end
1557
+
1558
+ def label_text_width(string)
1559
+ return message_text_width(string) if compact?
1560
+
1561
+ NoteGeometry.widest_line(string, message_font_size)
1562
+ end
1563
+ end
1564
+ end
1565
+ end
1566
+ end
1567
+ end