sirena 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.
- checksums.yaml +7 -0
- data/.github/workflows/build_deploy.yml +59 -0
- data/.github/workflows/links.yml +85 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +68 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.rubocop_todo.yml +70 -0
- data/ARCHITECTURE.md +744 -0
- data/Gemfile +12 -0
- data/LICENSE +25 -0
- data/README.adoc +357 -0
- data/Rakefile +11 -0
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +182 -0
- data/docs/_diagram_types/architecture-diagram.adoc +314 -0
- data/docs/_diagram_types/block-diagram.adoc +345 -0
- data/docs/_diagram_types/c4-diagram.adoc +559 -0
- data/docs/_diagram_types/class-diagram.adoc +816 -0
- data/docs/_diagram_types/er-diagram.adoc +719 -0
- data/docs/_diagram_types/error-diagram.adoc +114 -0
- data/docs/_diagram_types/examples/flowchart-examples.adoc +29 -0
- data/docs/_diagram_types/flowchart.adoc +488 -0
- data/docs/_diagram_types/gantt-chart.adoc +502 -0
- data/docs/_diagram_types/git-graph.adoc +600 -0
- data/docs/_diagram_types/index.adoc +192 -0
- data/docs/_diagram_types/info-diagram.adoc +103 -0
- data/docs/_diagram_types/kanban-diagram.adoc +262 -0
- data/docs/_diagram_types/mindmap.adoc +603 -0
- data/docs/_diagram_types/packet-diagram.adoc +378 -0
- data/docs/_diagram_types/pie-chart.adoc +335 -0
- data/docs/_diagram_types/quadrant-chart.adoc +406 -0
- data/docs/_diagram_types/radar-chart.adoc +528 -0
- data/docs/_diagram_types/requirement-diagram.adoc +416 -0
- data/docs/_diagram_types/sankey-diagram.adoc +357 -0
- data/docs/_diagram_types/sequence-diagram.adoc +664 -0
- data/docs/_diagram_types/state-diagram.adoc +658 -0
- data/docs/_diagram_types/timeline.adoc +352 -0
- data/docs/_diagram_types/treemap-diagram.adoc +462 -0
- data/docs/_diagram_types/user-journey.adoc +602 -0
- data/docs/_features/index.adoc +129 -0
- data/docs/_guides/cli-reference.adoc +203 -0
- data/docs/_guides/index.adoc +56 -0
- data/docs/_guides/installation.adoc +100 -0
- data/docs/_guides/quick-start.adoc +132 -0
- data/docs/_pages/comparison.adoc +441 -0
- data/docs/_pages/compatibility.adoc +300 -0
- data/docs/_pages/index.adoc +39 -0
- data/docs/_references/index.adoc +103 -0
- data/docs/_tutorials/index.adoc +57 -0
- data/docs/index.adoc +166 -0
- data/docs/lychee.toml +54 -0
- data/examples/.gitignore +10 -0
- data/examples/README.adoc +196 -0
- data/examples/README.md +64 -0
- data/examples/architecture/01-basic-services.mmd +9 -0
- data/examples/architecture/01-basic-services.svg +37 -0
- data/examples/architecture/02-service-groups.mmd +16 -0
- data/examples/architecture/02-service-groups.svg +55 -0
- data/examples/architecture/README.adoc +79 -0
- data/examples/block/01-basic-blocks.mmd +13 -0
- data/examples/block/01-basic-blocks.svg +44 -0
- data/examples/block/02-block-shapes.mmd +13 -0
- data/examples/block/02-block-shapes.svg +47 -0
- data/examples/block/README.adoc +85 -0
- data/examples/c4/01-context-diagram.mmd +10 -0
- data/examples/c4/01-context-diagram.svg +45 -0
- data/examples/c4/02-container-diagram.mmd +24 -0
- data/examples/c4/02-container-diagram.svg +105 -0
- data/examples/c4/README.adoc +92 -0
- data/examples/class_diagram/01-basic-classes.mmd +61 -0
- data/examples/class_diagram/01-basic-classes.svg +117 -0
- data/examples/class_diagram/02-relationships.mmd +61 -0
- data/examples/class_diagram/02-relationships.svg +129 -0
- data/examples/class_diagram/README.adoc +93 -0
- data/examples/er_diagram/01-basic-entities.mmd +64 -0
- data/examples/er_diagram/01-basic-entities.svg +5 -0
- data/examples/er_diagram/02-cardinality.mmd +57 -0
- data/examples/er_diagram/02-cardinality.svg +125 -0
- data/examples/er_diagram/README.adoc +88 -0
- data/examples/error/01-basic-error.mmd +1 -0
- data/examples/error/01-basic-error.svg +13 -0
- data/examples/error/02-error-display.mmd +1 -0
- data/examples/error/02-error-display.svg +13 -0
- data/examples/error/README.adoc +71 -0
- data/examples/error_message_example.svg +13 -0
- data/examples/flowchart/00-original.mmd +13 -0
- data/examples/flowchart/00-original.svg +5 -0
- data/examples/flowchart/01-basic-flow.mmd +7 -0
- data/examples/flowchart/01-basic-flow.svg +52 -0
- data/examples/flowchart/01-basic-flow.yml +13 -0
- data/examples/flowchart/02*.svg +87 -0
- data/examples/flowchart/02-node-shapes.mmd +9 -0
- data/examples/flowchart/02-node-shapes.svg +33 -0
- data/examples/flowchart/03-edge-types.mmd +7 -0
- data/examples/flowchart/03-edge-types.svg +53 -0
- data/examples/flowchart/04-subgraphs.mmd +9 -0
- data/examples/flowchart/04-subgraphs.svg +33 -0
- data/examples/flowchart/05-styling.mmd +9 -0
- data/examples/flowchart/05-styling.svg +33 -0
- data/examples/flowchart/06-complex-flow.mmd +8 -0
- data/examples/flowchart/06-complex-flow.svg +59 -0
- data/examples/flowchart/README.adoc +167 -0
- data/examples/gantt/01-simple-timeline.* +14 -0
- data/examples/gantt/01-simple-timeline.mmd +6 -0
- data/examples/gantt/01-simple-timeline.svg +26 -0
- data/examples/gantt/02-task-dependencies.mmd +6 -0
- data/examples/gantt/02-task-dependencies.svg +26 -0
- data/examples/gantt/README.adoc +86 -0
- data/examples/git_graph/01-linear-history.mmd +12 -0
- data/examples/git_graph/01-linear-history.svg +26 -0
- data/examples/git_graph/02-branching.mmd +12 -0
- data/examples/git_graph/02-branching.svg +26 -0
- data/examples/git_graph/README.adoc +73 -0
- data/examples/info/02-showinfo.mmd +1 -0
- data/examples/info/02-showinfo.svg +10 -0
- data/examples/info/README.adoc +58 -0
- data/examples/info_showinfo_example.svg +10 -0
- data/examples/kanban/01-simple-board.mmd +8 -0
- data/examples/kanban/01-simple-board.svg +43 -0
- data/examples/kanban/02-workflow.mmd +8 -0
- data/examples/kanban/02-workflow.svg +43 -0
- data/examples/kanban/README.adoc +79 -0
- data/examples/mindmap/01-simple-tree.mmd +19 -0
- data/examples/mindmap/01-simple-tree.svg +61 -0
- data/examples/mindmap/02-knowledge-map.mmd +19 -0
- data/examples/mindmap/02-knowledge-map.svg +61 -0
- data/examples/mindmap/README.adoc +77 -0
- data/examples/packet/01-basic-packet.* +17 -0
- data/examples/packet/01-basic-packet.mmd +4 -0
- data/examples/packet/01-basic-packet.svg +82 -0
- data/examples/packet/README.adoc +58 -0
- data/examples/pie/01-simple-chart.mmd +5 -0
- data/examples/pie/01-simple-chart.svg +17 -0
- data/examples/pie/02-labeled-slices.mmd +6 -0
- data/examples/pie/02-labeled-slices.svg +19 -0
- data/examples/pie/README.adoc +75 -0
- data/examples/quadrant/01-basic-quadrant.mmd +13 -0
- data/examples/quadrant/01-basic-quadrant.svg +33 -0
- data/examples/quadrant/02-positioned-items.mmd +14 -0
- data/examples/quadrant/02-positioned-items.svg +35 -0
- data/examples/quadrant/README.adoc +84 -0
- data/examples/radar/01-simple-radar.* +5 -0
- data/examples/radar/01-simple-radar.mmd +3 -0
- data/examples/radar/01-simple-radar.svg +25 -0
- data/examples/radar/02-multiple-curves.mmd +4 -0
- data/examples/radar/02-multiple-curves.svg +43 -0
- data/examples/radar/README.adoc +75 -0
- data/examples/requirement/01-basic-requirements.mmd +23 -0
- data/examples/requirement/01-basic-requirements.svg +49 -0
- data/examples/requirement/02-risk-levels.mmd +23 -0
- data/examples/requirement/02-risk-levels.svg +49 -0
- data/examples/requirement/README.adoc +85 -0
- data/examples/sankey/01-simple-flow.mmd +7 -0
- data/examples/sankey/01-simple-flow.svg +34 -0
- data/examples/sankey/02-multi-stage.mmd +11 -0
- data/examples/sankey/02-multi-stage.svg +44 -0
- data/examples/sankey/README.adoc +74 -0
- data/examples/sequence/01-basic-sequence.mmd +27 -0
- data/examples/sequence/01-basic-sequence.svg +5 -0
- data/examples/sequence/02-activations.mmd +17 -0
- data/examples/sequence/02-activations.svg +78 -0
- data/examples/sequence/README.adoc +86 -0
- data/examples/state_diagram/01-simple-states.mmd +29 -0
- data/examples/state_diagram/01-simple-states.svg +5 -0
- data/examples/state_diagram/02-composite.mmd +19 -0
- data/examples/state_diagram/02-composite.svg +81 -0
- data/examples/state_diagram/README.adoc +90 -0
- data/examples/timeline/01-simple-timeline.mmd +11 -0
- data/examples/timeline/01-simple-timeline.svg +36 -0
- data/examples/timeline/02-periods.mmd +15 -0
- data/examples/timeline/02-periods.svg +47 -0
- data/examples/timeline/README.adoc +78 -0
- data/examples/treemap/01-basic-treemap.mmd +12 -0
- data/examples/treemap/01-basic-treemap.svg +59 -0
- data/examples/treemap/README.adoc +59 -0
- data/examples/user_journey/01-simple-journey.mmd +23 -0
- data/examples/user_journey/01-simple-journey.svg +5 -0
- data/examples/user_journey/02-multi-actor.mmd +18 -0
- data/examples/user_journey/02-multi-actor.svg +129 -0
- data/examples/user_journey/README.adoc +81 -0
- data/examples/xychart/01-line-chart.mmd +5 -0
- data/examples/xychart/01-line-chart.svg +43 -0
- data/examples/xychart/02-bar-chart.mmd +7 -0
- data/examples/xychart/02-bar-chart.svg +48 -0
- data/examples/xychart/README.adoc +80 -0
- data/exe/sirena +7 -0
- data/lib/sirena/cli.rb +138 -0
- data/lib/sirena/commands/batch.rb +117 -0
- data/lib/sirena/commands/render.rb +80 -0
- data/lib/sirena/commands/types.rb +29 -0
- data/lib/sirena/commands/version.rb +24 -0
- data/lib/sirena/diagram/architecture.rb +46 -0
- data/lib/sirena/diagram/base.rb +61 -0
- data/lib/sirena/diagram/block.rb +81 -0
- data/lib/sirena/diagram/c4.rb +328 -0
- data/lib/sirena/diagram/class_diagram.rb +385 -0
- data/lib/sirena/diagram/er_diagram.rb +238 -0
- data/lib/sirena/diagram/error.rb +38 -0
- data/lib/sirena/diagram/flowchart.rb +160 -0
- data/lib/sirena/diagram/gantt.rb +71 -0
- data/lib/sirena/diagram/git_graph.rb +36 -0
- data/lib/sirena/diagram/info.rb +38 -0
- data/lib/sirena/diagram/kanban.rb +178 -0
- data/lib/sirena/diagram/mindmap.rb +54 -0
- data/lib/sirena/diagram/packet.rb +79 -0
- data/lib/sirena/diagram/pie.rb +115 -0
- data/lib/sirena/diagram/quadrant.rb +138 -0
- data/lib/sirena/diagram/radar.rb +52 -0
- data/lib/sirena/diagram/requirement.rb +133 -0
- data/lib/sirena/diagram/sankey.rb +217 -0
- data/lib/sirena/diagram/sequence.rb +242 -0
- data/lib/sirena/diagram/state_diagram.rb +237 -0
- data/lib/sirena/diagram/timeline.rb +171 -0
- data/lib/sirena/diagram/treemap.rb +84 -0
- data/lib/sirena/diagram/user_journey.rb +149 -0
- data/lib/sirena/diagram/xy_chart.rb +76 -0
- data/lib/sirena/diagram.rb +8 -0
- data/lib/sirena/diagram_registry.rb +101 -0
- data/lib/sirena/engine.rb +292 -0
- data/lib/sirena/parser/architecture.rb +41 -0
- data/lib/sirena/parser/base.rb +41 -0
- data/lib/sirena/parser/block.rb +72 -0
- data/lib/sirena/parser/c4.rb +53 -0
- data/lib/sirena/parser/class_diagram.rb +63 -0
- data/lib/sirena/parser/er_diagram.rb +40 -0
- data/lib/sirena/parser/error.rb +49 -0
- data/lib/sirena/parser/flowchart.rb +71 -0
- data/lib/sirena/parser/gantt.rb +60 -0
- data/lib/sirena/parser/git_graph.rb +95 -0
- data/lib/sirena/parser/grammars/architecture.rb +145 -0
- data/lib/sirena/parser/grammars/block.rb +190 -0
- data/lib/sirena/parser/grammars/c4.rb +226 -0
- data/lib/sirena/parser/grammars/class_diagram.rb +284 -0
- data/lib/sirena/parser/grammars/common.rb +84 -0
- data/lib/sirena/parser/grammars/er_diagram.rb +114 -0
- data/lib/sirena/parser/grammars/error.rb +40 -0
- data/lib/sirena/parser/grammars/flowchart.rb +298 -0
- data/lib/sirena/parser/grammars/gantt.rb +252 -0
- data/lib/sirena/parser/grammars/git_graph.rb +167 -0
- data/lib/sirena/parser/grammars/info.rb +58 -0
- data/lib/sirena/parser/grammars/kanban.rb +83 -0
- data/lib/sirena/parser/grammars/mindmap.rb +115 -0
- data/lib/sirena/parser/grammars/packet.rb +73 -0
- data/lib/sirena/parser/grammars/pie.rb +128 -0
- data/lib/sirena/parser/grammars/quadrant.rb +199 -0
- data/lib/sirena/parser/grammars/radar.rb +150 -0
- data/lib/sirena/parser/grammars/requirement.rb +188 -0
- data/lib/sirena/parser/grammars/sankey.rb +104 -0
- data/lib/sirena/parser/grammars/sequence.rb +247 -0
- data/lib/sirena/parser/grammars/state_diagram.rb +172 -0
- data/lib/sirena/parser/grammars/timeline.rb +142 -0
- data/lib/sirena/parser/grammars/treemap.rb +120 -0
- data/lib/sirena/parser/grammars/xy_chart.rb +120 -0
- data/lib/sirena/parser/info.rb +49 -0
- data/lib/sirena/parser/kanban.rb +97 -0
- data/lib/sirena/parser/mindmap.rb +106 -0
- data/lib/sirena/parser/packet.rb +76 -0
- data/lib/sirena/parser/pie.rb +49 -0
- data/lib/sirena/parser/quadrant.rb +57 -0
- data/lib/sirena/parser/radar.rb +104 -0
- data/lib/sirena/parser/requirement.rb +70 -0
- data/lib/sirena/parser/sankey.rb +64 -0
- data/lib/sirena/parser/sequence.rb +51 -0
- data/lib/sirena/parser/state_diagram.rb +69 -0
- data/lib/sirena/parser/timeline.rb +57 -0
- data/lib/sirena/parser/transforms/architecture.rb +97 -0
- data/lib/sirena/parser/transforms/block.rb +254 -0
- data/lib/sirena/parser/transforms/c4.rb +347 -0
- data/lib/sirena/parser/transforms/class_diagram.rb +352 -0
- data/lib/sirena/parser/transforms/er_diagram.rb +169 -0
- data/lib/sirena/parser/transforms/error.rb +58 -0
- data/lib/sirena/parser/transforms/flowchart.rb +293 -0
- data/lib/sirena/parser/transforms/gantt.rb +215 -0
- data/lib/sirena/parser/transforms/git_graph.rb +160 -0
- data/lib/sirena/parser/transforms/info.rb +58 -0
- data/lib/sirena/parser/transforms/kanban.rb +176 -0
- data/lib/sirena/parser/transforms/mindmap.rb +227 -0
- data/lib/sirena/parser/transforms/packet.rb +63 -0
- data/lib/sirena/parser/transforms/pie.rb +143 -0
- data/lib/sirena/parser/transforms/quadrant.rb +177 -0
- data/lib/sirena/parser/transforms/radar.rb +126 -0
- data/lib/sirena/parser/transforms/requirement.rb +272 -0
- data/lib/sirena/parser/transforms/sankey.rb +122 -0
- data/lib/sirena/parser/transforms/sequence.rb +342 -0
- data/lib/sirena/parser/transforms/state_diagram.rb +292 -0
- data/lib/sirena/parser/transforms/timeline.rb +177 -0
- data/lib/sirena/parser/transforms/treemap.rb +81 -0
- data/lib/sirena/parser/transforms/xy_chart.rb +132 -0
- data/lib/sirena/parser/treemap.rb +98 -0
- data/lib/sirena/parser/user_journey.rb +120 -0
- data/lib/sirena/parser/xy_chart.rb +114 -0
- data/lib/sirena/parser.rb +8 -0
- data/lib/sirena/renderer/architecture.rb +251 -0
- data/lib/sirena/renderer/base.rb +251 -0
- data/lib/sirena/renderer/block.rb +286 -0
- data/lib/sirena/renderer/c4.rb +490 -0
- data/lib/sirena/renderer/class_diagram.rb +499 -0
- data/lib/sirena/renderer/er_diagram.rb +417 -0
- data/lib/sirena/renderer/error.rb +131 -0
- data/lib/sirena/renderer/flowchart.rb +301 -0
- data/lib/sirena/renderer/gantt.rb +331 -0
- data/lib/sirena/renderer/git_graph.rb +368 -0
- data/lib/sirena/renderer/info.rb +93 -0
- data/lib/sirena/renderer/kanban.rb +295 -0
- data/lib/sirena/renderer/mindmap.rb +396 -0
- data/lib/sirena/renderer/packet.rb +239 -0
- data/lib/sirena/renderer/pie.rb +235 -0
- data/lib/sirena/renderer/quadrant.rb +292 -0
- data/lib/sirena/renderer/radar.rb +323 -0
- data/lib/sirena/renderer/requirement.rb +371 -0
- data/lib/sirena/renderer/sankey.rb +255 -0
- data/lib/sirena/renderer/sequence.rb +424 -0
- data/lib/sirena/renderer/state_diagram.rb +328 -0
- data/lib/sirena/renderer/timeline.rb +304 -0
- data/lib/sirena/renderer/treemap.rb +152 -0
- data/lib/sirena/renderer/user_journey.rb +331 -0
- data/lib/sirena/renderer/xy_chart.rb +452 -0
- data/lib/sirena/renderer.rb +8 -0
- data/lib/sirena/svg/circle.rb +41 -0
- data/lib/sirena/svg/document.rb +103 -0
- data/lib/sirena/svg/element.rb +65 -0
- data/lib/sirena/svg/ellipse.rb +33 -0
- data/lib/sirena/svg/group.rb +71 -0
- data/lib/sirena/svg/line.rb +49 -0
- data/lib/sirena/svg/path.rb +76 -0
- data/lib/sirena/svg/polygon.rb +43 -0
- data/lib/sirena/svg/polyline.rb +35 -0
- data/lib/sirena/svg/rect.rb +57 -0
- data/lib/sirena/svg/style.rb +44 -0
- data/lib/sirena/svg/text.rb +72 -0
- data/lib/sirena/svg.rb +19 -0
- data/lib/sirena/text_measurement.rb +71 -0
- data/lib/sirena/theme/builtin/dark.yml +70 -0
- data/lib/sirena/theme/builtin/default.yml +80 -0
- data/lib/sirena/theme/builtin/high_contrast.yml +70 -0
- data/lib/sirena/theme/builtin/light.yml +70 -0
- data/lib/sirena/theme/color_palette.rb +48 -0
- data/lib/sirena/theme/effect_styles.rb +28 -0
- data/lib/sirena/theme/registry.rb +41 -0
- data/lib/sirena/theme/shape_styles.rb +28 -0
- data/lib/sirena/theme/spacing_config.rb +24 -0
- data/lib/sirena/theme/typography.rb +30 -0
- data/lib/sirena/theme.rb +69 -0
- data/lib/sirena/transform/architecture.rb +273 -0
- data/lib/sirena/transform/base.rb +199 -0
- data/lib/sirena/transform/block.rb +215 -0
- data/lib/sirena/transform/c4.rb +288 -0
- data/lib/sirena/transform/class_diagram.rb +296 -0
- data/lib/sirena/transform/er_diagram.rb +204 -0
- data/lib/sirena/transform/error.rb +39 -0
- data/lib/sirena/transform/flowchart.rb +161 -0
- data/lib/sirena/transform/gantt.rb +253 -0
- data/lib/sirena/transform/git_graph.rb +283 -0
- data/lib/sirena/transform/info.rb +39 -0
- data/lib/sirena/transform/kanban.rb +180 -0
- data/lib/sirena/transform/mindmap.rb +251 -0
- data/lib/sirena/transform/packet.rb +185 -0
- data/lib/sirena/transform/pie.rb +62 -0
- data/lib/sirena/transform/quadrant.rb +167 -0
- data/lib/sirena/transform/radar.rb +227 -0
- data/lib/sirena/transform/requirement.rb +233 -0
- data/lib/sirena/transform/sankey.rb +212 -0
- data/lib/sirena/transform/sequence.rb +143 -0
- data/lib/sirena/transform/state_diagram.rb +228 -0
- data/lib/sirena/transform/timeline.rb +139 -0
- data/lib/sirena/transform/treemap.rb +120 -0
- data/lib/sirena/transform/user_journey.rb +207 -0
- data/lib/sirena/transform/xy_chart.rb +273 -0
- data/lib/sirena/transform.rb +8 -0
- data/lib/sirena/version.rb +5 -0
- data/lib/sirena.rb +328 -0
- data/lib/tasks/benchmark.rake +532 -0
- data/lib/tasks/examples.rake +468 -0
- data/lib/tasks/generate_mermaid_fixtures.rake +363 -0
- data/lib/tasks/mermaid_fixtures.rake +46 -0
- data/scripts/extract_mermaid_tests.rb +493 -0
- data/scripts/rename_to_sirena.rb +73 -0
- data/sirena.gemspec +47 -0
- metadata +529 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/class_diagram'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Class diagram transformer for converting class models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed class diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles class box sizing
|
|
12
|
+
# based on attributes and methods, relationship mapping, and hierarchical
|
|
13
|
+
# layout configuration.
|
|
14
|
+
#
|
|
15
|
+
# @example Transform a class diagram
|
|
16
|
+
# transform = ClassDiagramTransform.new
|
|
17
|
+
# graph = transform.to_graph(class_diagram)
|
|
18
|
+
class ClassDiagramTransform < Base
|
|
19
|
+
# Default font size for text measurement
|
|
20
|
+
DEFAULT_FONT_SIZE = 14
|
|
21
|
+
|
|
22
|
+
# Minimum width for a class box
|
|
23
|
+
MIN_CLASS_WIDTH = 120
|
|
24
|
+
|
|
25
|
+
# Height per class compartment line
|
|
26
|
+
LINE_HEIGHT = 20
|
|
27
|
+
|
|
28
|
+
# Padding within class box compartments
|
|
29
|
+
COMPARTMENT_PADDING = 10
|
|
30
|
+
|
|
31
|
+
# Spacing between classes
|
|
32
|
+
CLASS_SPACING = 80
|
|
33
|
+
|
|
34
|
+
# Converts a class diagram to a graph structure.
|
|
35
|
+
#
|
|
36
|
+
# @param diagram [Diagram::ClassDiagram] the class diagram to transform
|
|
37
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
38
|
+
# @raise [TransformError] if diagram is invalid
|
|
39
|
+
def to_graph(diagram)
|
|
40
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
id: diagram.id || 'class_diagram',
|
|
44
|
+
children: transform_entities(diagram),
|
|
45
|
+
edges: transform_relationships(diagram),
|
|
46
|
+
layoutOptions: layout_options(diagram)
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def transform_entities(diagram)
|
|
53
|
+
diagram.entities.map do |entity|
|
|
54
|
+
dims = calculate_entity_dimensions(entity)
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
id: entity.id,
|
|
58
|
+
width: dims[:width],
|
|
59
|
+
height: dims[:height],
|
|
60
|
+
labels: entity_labels(entity),
|
|
61
|
+
metadata: {
|
|
62
|
+
name: entity.name,
|
|
63
|
+
stereotype: entity.stereotype,
|
|
64
|
+
attributes: entity.attributes.map { |a| attribute_to_hash(a) },
|
|
65
|
+
methods: entity.class_methods.map { |m| method_to_hash(m) }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def transform_relationships(diagram)
|
|
72
|
+
return [] if diagram.relationships.nil? ||
|
|
73
|
+
diagram.relationships.empty?
|
|
74
|
+
|
|
75
|
+
diagram.relationships.map do |rel|
|
|
76
|
+
{
|
|
77
|
+
id: "#{rel.from_id}_to_#{rel.to_id}",
|
|
78
|
+
sources: [rel.from_id],
|
|
79
|
+
targets: [rel.to_id],
|
|
80
|
+
labels: relationship_labels(rel),
|
|
81
|
+
metadata: {
|
|
82
|
+
relationship_type: rel.relationship_type,
|
|
83
|
+
source_cardinality: rel.source_cardinality,
|
|
84
|
+
target_cardinality: rel.target_cardinality
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def calculate_entity_dimensions(entity)
|
|
91
|
+
# Calculate width based on longest line (name, attributes, methods)
|
|
92
|
+
max_width = MIN_CLASS_WIDTH
|
|
93
|
+
|
|
94
|
+
# Check class name width
|
|
95
|
+
name_text = if entity.stereotype
|
|
96
|
+
"<<#{entity.stereotype}>>\n#{entity.name}"
|
|
97
|
+
else
|
|
98
|
+
entity.name
|
|
99
|
+
end
|
|
100
|
+
name_width = measure_text(
|
|
101
|
+
name_text,
|
|
102
|
+
font_size: DEFAULT_FONT_SIZE
|
|
103
|
+
)[:width]
|
|
104
|
+
max_width = [max_width, name_width].max
|
|
105
|
+
|
|
106
|
+
# Check attribute widths
|
|
107
|
+
entity.attributes.each do |attr|
|
|
108
|
+
attr_text = format_attribute(attr)
|
|
109
|
+
attr_width = measure_text(
|
|
110
|
+
attr_text,
|
|
111
|
+
font_size: DEFAULT_FONT_SIZE
|
|
112
|
+
)[:width]
|
|
113
|
+
max_width = [max_width, attr_width].max
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check method widths
|
|
117
|
+
entity.class_methods.each do |method|
|
|
118
|
+
method_text = format_method(method)
|
|
119
|
+
method_width = measure_text(
|
|
120
|
+
method_text,
|
|
121
|
+
font_size: DEFAULT_FONT_SIZE
|
|
122
|
+
)[:width]
|
|
123
|
+
max_width = [max_width, method_width].max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add padding
|
|
127
|
+
total_width = max_width + (COMPARTMENT_PADDING * 2)
|
|
128
|
+
|
|
129
|
+
# Calculate height based on compartments
|
|
130
|
+
# Name compartment
|
|
131
|
+
name_lines = entity.stereotype ? 2 : 1
|
|
132
|
+
name_height = name_lines * LINE_HEIGHT
|
|
133
|
+
|
|
134
|
+
# Attributes compartment
|
|
135
|
+
attr_height = if entity.attributes.empty?
|
|
136
|
+
0
|
|
137
|
+
else
|
|
138
|
+
(entity.attributes.length * LINE_HEIGHT)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Methods compartment
|
|
142
|
+
method_height = if entity.class_methods.empty?
|
|
143
|
+
0
|
|
144
|
+
else
|
|
145
|
+
(entity.class_methods.length * LINE_HEIGHT)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Total height with compartment separators
|
|
149
|
+
compartment_count = [
|
|
150
|
+
1, # name always present
|
|
151
|
+
entity.attributes.empty? ? 0 : 1,
|
|
152
|
+
entity.class_methods.empty? ? 0 : 1
|
|
153
|
+
].sum
|
|
154
|
+
separator_height = (compartment_count - 1) * 2 # 2px per separator
|
|
155
|
+
|
|
156
|
+
total_height = name_height + attr_height + method_height +
|
|
157
|
+
separator_height + (COMPARTMENT_PADDING * 2)
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
width: total_width,
|
|
161
|
+
height: total_height
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def entity_labels(entity)
|
|
166
|
+
labels = []
|
|
167
|
+
|
|
168
|
+
# Main label with class name
|
|
169
|
+
name_text = if entity.stereotype
|
|
170
|
+
"<<#{entity.stereotype}>>\n#{entity.name}"
|
|
171
|
+
else
|
|
172
|
+
entity.name
|
|
173
|
+
end
|
|
174
|
+
name_dims = measure_text(name_text, font_size: DEFAULT_FONT_SIZE)
|
|
175
|
+
|
|
176
|
+
labels << {
|
|
177
|
+
text: name_text,
|
|
178
|
+
width: name_dims[:width],
|
|
179
|
+
height: name_dims[:height]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
labels
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def relationship_labels(relationship)
|
|
186
|
+
labels = []
|
|
187
|
+
|
|
188
|
+
# Add relationship label if present
|
|
189
|
+
if relationship.label && !relationship.label.empty?
|
|
190
|
+
label_dims = measure_text(
|
|
191
|
+
relationship.label,
|
|
192
|
+
font_size: DEFAULT_FONT_SIZE
|
|
193
|
+
)
|
|
194
|
+
labels << {
|
|
195
|
+
text: relationship.label,
|
|
196
|
+
width: label_dims[:width],
|
|
197
|
+
height: label_dims[:height]
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Add cardinality labels if present
|
|
202
|
+
if relationship.source_cardinality &&
|
|
203
|
+
!relationship.source_cardinality.empty?
|
|
204
|
+
card_dims = measure_text(
|
|
205
|
+
relationship.source_cardinality,
|
|
206
|
+
font_size: DEFAULT_FONT_SIZE - 2
|
|
207
|
+
)
|
|
208
|
+
labels << {
|
|
209
|
+
text: relationship.source_cardinality,
|
|
210
|
+
width: card_dims[:width],
|
|
211
|
+
height: card_dims[:height],
|
|
212
|
+
position: 'source'
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if relationship.target_cardinality &&
|
|
217
|
+
!relationship.target_cardinality.empty?
|
|
218
|
+
card_dims = measure_text(
|
|
219
|
+
relationship.target_cardinality,
|
|
220
|
+
font_size: DEFAULT_FONT_SIZE - 2
|
|
221
|
+
)
|
|
222
|
+
labels << {
|
|
223
|
+
text: relationship.target_cardinality,
|
|
224
|
+
width: card_dims[:width],
|
|
225
|
+
height: card_dims[:height],
|
|
226
|
+
position: 'target'
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
labels
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def format_attribute(attribute)
|
|
234
|
+
parts = [attribute.visibility_symbol, attribute.name]
|
|
235
|
+
parts << ": #{attribute.type}" if attribute.type &&
|
|
236
|
+
!attribute.type.empty?
|
|
237
|
+
parts.join(' ')
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def format_method(method)
|
|
241
|
+
parts = [method.visibility_symbol, method.signature]
|
|
242
|
+
parts.join(' ')
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def attribute_to_hash(attribute)
|
|
246
|
+
{
|
|
247
|
+
name: attribute.name,
|
|
248
|
+
type: attribute.type,
|
|
249
|
+
visibility: attribute.visibility
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def method_to_hash(method)
|
|
254
|
+
{
|
|
255
|
+
name: method.name,
|
|
256
|
+
parameters: method.parameters,
|
|
257
|
+
return_type: method.return_type,
|
|
258
|
+
visibility: method.visibility
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def layout_options(diagram)
|
|
263
|
+
# Class diagrams use layered algorithm for UML hierarchy
|
|
264
|
+
# This optimally handles inheritance relationships and class groupings
|
|
265
|
+
# NETWORK_SIMPLEX node placement balances hierarchy with aesthetics
|
|
266
|
+
build_elk_options(
|
|
267
|
+
algorithm: ALGORITHM_LAYERED,
|
|
268
|
+
direction: direction_to_layout(diagram.direction),
|
|
269
|
+
ElkOptions::NODE_NODE_SPACING => CLASS_SPACING,
|
|
270
|
+
ElkOptions::LAYER_SPACING => CLASS_SPACING,
|
|
271
|
+
ElkOptions::EDGE_NODE_SPACING => 40,
|
|
272
|
+
ElkOptions::EDGE_EDGE_SPACING => 20,
|
|
273
|
+
# NETWORK_SIMPLEX for better UML layout with inheritance
|
|
274
|
+
ElkOptions::NODE_PLACEMENT => 'NETWORK_SIMPLEX',
|
|
275
|
+
ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES',
|
|
276
|
+
ElkOptions::HIERARCHY_HANDLING => 'INCLUDE_CHILDREN'
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def direction_to_layout(direction)
|
|
281
|
+
case direction
|
|
282
|
+
when 'TD', 'TB'
|
|
283
|
+
DIRECTION_DOWN
|
|
284
|
+
when 'LR'
|
|
285
|
+
DIRECTION_RIGHT
|
|
286
|
+
when 'RL'
|
|
287
|
+
DIRECTION_LEFT
|
|
288
|
+
when 'BT'
|
|
289
|
+
DIRECTION_UP
|
|
290
|
+
else
|
|
291
|
+
DIRECTION_DOWN # Default for class diagrams is top-down
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/er_diagram'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# ER diagram transformer for converting ER models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed ER diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles entity box sizing
|
|
12
|
+
# based on entity name and attribute count, relationship mapping with
|
|
13
|
+
# cardinality, and layered layout configuration.
|
|
14
|
+
#
|
|
15
|
+
# @example Transform an ER diagram
|
|
16
|
+
# transform = ErDiagramTransform.new
|
|
17
|
+
# graph = transform.to_graph(er_diagram)
|
|
18
|
+
class ErDiagramTransform < Base
|
|
19
|
+
# Default font size for text measurement
|
|
20
|
+
DEFAULT_FONT_SIZE = 14
|
|
21
|
+
|
|
22
|
+
# Minimum width for an entity box
|
|
23
|
+
MIN_ENTITY_WIDTH = 150
|
|
24
|
+
|
|
25
|
+
# Height per entity line (name + attributes)
|
|
26
|
+
LINE_HEIGHT = 20
|
|
27
|
+
|
|
28
|
+
# Padding within entity box
|
|
29
|
+
ENTITY_PADDING = 10
|
|
30
|
+
|
|
31
|
+
# Spacing between entities
|
|
32
|
+
ENTITY_SPACING = 100
|
|
33
|
+
|
|
34
|
+
# Converts an ER diagram to a graph structure.
|
|
35
|
+
#
|
|
36
|
+
# @param diagram [Diagram::ErDiagram] the ER diagram to transform
|
|
37
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
38
|
+
# @raise [TransformError] if diagram is invalid
|
|
39
|
+
def to_graph(diagram)
|
|
40
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
id: diagram.id || 'er_diagram',
|
|
44
|
+
children: transform_entities(diagram),
|
|
45
|
+
edges: transform_relationships(diagram),
|
|
46
|
+
layoutOptions: layout_options
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def transform_entities(diagram)
|
|
53
|
+
diagram.entities.map do |entity|
|
|
54
|
+
dims = calculate_entity_dimensions(entity)
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
id: entity.id,
|
|
58
|
+
width: dims[:width],
|
|
59
|
+
height: dims[:height],
|
|
60
|
+
labels: entity_labels(entity),
|
|
61
|
+
metadata: {
|
|
62
|
+
name: entity.name,
|
|
63
|
+
attributes: entity.attributes.map { |a| attribute_to_hash(a) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def transform_relationships(diagram)
|
|
70
|
+
return [] if diagram.relationships.nil? ||
|
|
71
|
+
diagram.relationships.empty?
|
|
72
|
+
|
|
73
|
+
diagram.relationships.map do |rel|
|
|
74
|
+
{
|
|
75
|
+
id: "#{rel.from_id}_to_#{rel.to_id}",
|
|
76
|
+
sources: [rel.from_id],
|
|
77
|
+
targets: [rel.to_id],
|
|
78
|
+
labels: relationship_labels(rel),
|
|
79
|
+
metadata: {
|
|
80
|
+
relationship_type: rel.relationship_type,
|
|
81
|
+
cardinality_from: rel.cardinality_from,
|
|
82
|
+
cardinality_to: rel.cardinality_to
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def calculate_entity_dimensions(entity)
|
|
89
|
+
# Calculate width based on entity name and attributes
|
|
90
|
+
max_width = MIN_ENTITY_WIDTH
|
|
91
|
+
|
|
92
|
+
# Check entity name width
|
|
93
|
+
name_width = measure_text(
|
|
94
|
+
entity.name,
|
|
95
|
+
font_size: DEFAULT_FONT_SIZE + 2
|
|
96
|
+
)[:width]
|
|
97
|
+
max_width = [max_width, name_width].max
|
|
98
|
+
|
|
99
|
+
# Check attribute widths
|
|
100
|
+
entity.attributes.each do |attr|
|
|
101
|
+
attr_text = format_attribute(attr)
|
|
102
|
+
attr_width = measure_text(
|
|
103
|
+
attr_text,
|
|
104
|
+
font_size: DEFAULT_FONT_SIZE
|
|
105
|
+
)[:width]
|
|
106
|
+
max_width = [max_width, attr_width].max
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Add padding
|
|
110
|
+
total_width = max_width + (ENTITY_PADDING * 2)
|
|
111
|
+
|
|
112
|
+
# Calculate height: entity name + attributes
|
|
113
|
+
line_count = 1 + entity.attributes.length
|
|
114
|
+
total_height = (line_count * LINE_HEIGHT) + (ENTITY_PADDING * 2)
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
width: total_width,
|
|
118
|
+
height: total_height
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def entity_labels(entity)
|
|
123
|
+
labels = []
|
|
124
|
+
|
|
125
|
+
# Main label with entity name
|
|
126
|
+
name_dims = measure_text(
|
|
127
|
+
entity.name,
|
|
128
|
+
font_size: DEFAULT_FONT_SIZE + 2
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
labels << {
|
|
132
|
+
text: entity.name,
|
|
133
|
+
width: name_dims[:width],
|
|
134
|
+
height: name_dims[:height]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
labels
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def relationship_labels(relationship)
|
|
141
|
+
labels = []
|
|
142
|
+
|
|
143
|
+
# Add relationship label if present
|
|
144
|
+
if relationship.label && !relationship.label.empty?
|
|
145
|
+
label_dims = measure_text(
|
|
146
|
+
relationship.label,
|
|
147
|
+
font_size: DEFAULT_FONT_SIZE
|
|
148
|
+
)
|
|
149
|
+
labels << {
|
|
150
|
+
text: relationship.label,
|
|
151
|
+
width: label_dims[:width],
|
|
152
|
+
height: label_dims[:height]
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
labels
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def format_attribute(attribute)
|
|
160
|
+
parts = []
|
|
161
|
+
|
|
162
|
+
# Add key type marker if present
|
|
163
|
+
parts << attribute.key_type if attribute.key_type &&
|
|
164
|
+
!attribute.key_type.empty?
|
|
165
|
+
|
|
166
|
+
# Add attribute name
|
|
167
|
+
parts << attribute.name
|
|
168
|
+
|
|
169
|
+
# Add type if present
|
|
170
|
+
parts << attribute.attribute_type if attribute.attribute_type &&
|
|
171
|
+
!attribute.attribute_type.empty?
|
|
172
|
+
|
|
173
|
+
parts.join(' ')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def attribute_to_hash(attribute)
|
|
177
|
+
{
|
|
178
|
+
name: attribute.name,
|
|
179
|
+
attribute_type: attribute.attribute_type,
|
|
180
|
+
key_type: attribute.key_type
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def layout_options
|
|
185
|
+
# ER diagrams use layered algorithm for hierarchical entity layout
|
|
186
|
+
# DIRECTION_RIGHT provides left-to-right flow for entity relationships
|
|
187
|
+
# NETWORK_SIMPLEX placement optimizes entity box positioning while
|
|
188
|
+
# respecting relationship cardinality and foreign key constraints
|
|
189
|
+
build_elk_options(
|
|
190
|
+
algorithm: ALGORITHM_LAYERED,
|
|
191
|
+
direction: DIRECTION_RIGHT,
|
|
192
|
+
ElkOptions::NODE_NODE_SPACING => ENTITY_SPACING,
|
|
193
|
+
ElkOptions::LAYER_SPACING => ENTITY_SPACING,
|
|
194
|
+
ElkOptions::EDGE_NODE_SPACING => 50,
|
|
195
|
+
ElkOptions::EDGE_EDGE_SPACING => 30,
|
|
196
|
+
# NETWORK_SIMPLEX for better entity relationship layout
|
|
197
|
+
ElkOptions::NODE_PLACEMENT => 'NETWORK_SIMPLEX',
|
|
198
|
+
ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES',
|
|
199
|
+
ElkOptions::HIERARCHY_HANDLING => 'INCLUDE_CHILDREN'
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/error'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Error diagram transformer for converting error models to renderable structure.
|
|
9
|
+
#
|
|
10
|
+
# Error diagrams have no complex layout requirements - they simply display
|
|
11
|
+
# an error message. This transformer prepares basic data for rendering.
|
|
12
|
+
#
|
|
13
|
+
# @example Transform an error diagram
|
|
14
|
+
# transform = ErrorTransform.new
|
|
15
|
+
# data = transform.to_graph(error_diagram)
|
|
16
|
+
class ErrorTransform < Base
|
|
17
|
+
# Converts an error diagram to a simple data structure.
|
|
18
|
+
#
|
|
19
|
+
# Error diagrams don't need layout computation. This method validates
|
|
20
|
+
# the diagram and returns a structure for the renderer.
|
|
21
|
+
#
|
|
22
|
+
# @param diagram [Diagram::Error] the error diagram to transform
|
|
23
|
+
# @return [Hash] data structure for rendering
|
|
24
|
+
# @raise [TransformError] if diagram is invalid
|
|
25
|
+
def to_graph(diagram)
|
|
26
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
id: diagram.id || 'error',
|
|
30
|
+
title: diagram.title,
|
|
31
|
+
message: diagram.message,
|
|
32
|
+
metadata: {
|
|
33
|
+
diagram_type: :error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/flowchart'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Flowchart transformer for converting flowchart models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed flowchart diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles node dimension
|
|
12
|
+
# calculation, edge mapping, and layout configuration.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a flowchart
|
|
15
|
+
# transform = FlowchartTransform.new
|
|
16
|
+
# graph = transform.to_graph(flowchart_diagram)
|
|
17
|
+
class FlowchartTransform < Base
|
|
18
|
+
# Default font size for text measurement
|
|
19
|
+
DEFAULT_FONT_SIZE = 14
|
|
20
|
+
|
|
21
|
+
# Converts a flowchart diagram to a graph structure.
|
|
22
|
+
#
|
|
23
|
+
# @param diagram [Diagram::Flowchart] the flowchart to transform
|
|
24
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
25
|
+
# @raise [TransformError] if diagram is invalid
|
|
26
|
+
def to_graph(diagram)
|
|
27
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
id: diagram.id || 'flowchart',
|
|
31
|
+
children: transform_nodes(diagram),
|
|
32
|
+
edges: transform_edges(diagram),
|
|
33
|
+
layoutOptions: layout_options(diagram)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def transform_nodes(diagram)
|
|
40
|
+
diagram.nodes.map do |node|
|
|
41
|
+
dims = calculate_dimensions(node)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
id: node.id,
|
|
45
|
+
width: dims[:width],
|
|
46
|
+
height: dims[:height],
|
|
47
|
+
labels: [
|
|
48
|
+
{
|
|
49
|
+
text: node.label,
|
|
50
|
+
width: dims[:label_width],
|
|
51
|
+
height: dims[:label_height]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
metadata: {
|
|
55
|
+
shape: node.shape,
|
|
56
|
+
classes: node.classes
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transform_edges(diagram)
|
|
63
|
+
return [] if diagram.edges.nil? || diagram.edges.empty?
|
|
64
|
+
|
|
65
|
+
diagram.edges.map do |edge|
|
|
66
|
+
{
|
|
67
|
+
id: "#{edge.source_id}_to_#{edge.target_id}",
|
|
68
|
+
sources: [edge.source_id],
|
|
69
|
+
targets: [edge.target_id],
|
|
70
|
+
labels: edge_labels(edge),
|
|
71
|
+
metadata: {
|
|
72
|
+
arrow_type: edge.arrow_type
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def edge_labels(edge)
|
|
79
|
+
return [] if edge.label.nil? || edge.label.empty?
|
|
80
|
+
|
|
81
|
+
label_dims = measure_text(edge.label, font_size: DEFAULT_FONT_SIZE)
|
|
82
|
+
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
text: edge.label,
|
|
86
|
+
width: label_dims[:width],
|
|
87
|
+
height: label_dims[:height]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def calculate_dimensions(node)
|
|
93
|
+
label_dims = measure_text(
|
|
94
|
+
node.label,
|
|
95
|
+
font_size: DEFAULT_FONT_SIZE
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
node_dims = calculate_node_dimensions(
|
|
99
|
+
label_dims[:width],
|
|
100
|
+
label_dims[:height],
|
|
101
|
+
shape_to_type(node.shape)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
width: node_dims[:width],
|
|
106
|
+
height: node_dims[:height],
|
|
107
|
+
label_width: label_dims[:width],
|
|
108
|
+
label_height: label_dims[:height]
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def shape_to_type(shape)
|
|
113
|
+
case shape
|
|
114
|
+
when 'rect', 'subroutine'
|
|
115
|
+
:rect
|
|
116
|
+
when 'circle', 'double_circle'
|
|
117
|
+
:circle
|
|
118
|
+
when 'rhombus', 'hexagon'
|
|
119
|
+
:diamond
|
|
120
|
+
else
|
|
121
|
+
:rect
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def layout_options(diagram)
|
|
126
|
+
direction = direction_to_layout(diagram.direction)
|
|
127
|
+
|
|
128
|
+
# Flowcharts use layered algorithm for hierarchical flow
|
|
129
|
+
# This ensures nodes are placed in distinct layers and edges flow
|
|
130
|
+
# in the specified direction with minimal crossings
|
|
131
|
+
build_elk_options(
|
|
132
|
+
algorithm: ALGORITHM_LAYERED,
|
|
133
|
+
direction: direction,
|
|
134
|
+
# Additional flowchart-specific options
|
|
135
|
+
ElkOptions::NODE_NODE_SPACING => 50,
|
|
136
|
+
ElkOptions::LAYER_SPACING => 50,
|
|
137
|
+
ElkOptions::EDGE_NODE_SPACING => 30,
|
|
138
|
+
ElkOptions::EDGE_EDGE_SPACING => 20,
|
|
139
|
+
# SIMPLE node placement for predictable, straightforward layouts
|
|
140
|
+
# This is ideal for flowcharts where clarity is paramount
|
|
141
|
+
ElkOptions::NODE_PLACEMENT => 'SIMPLE'
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def direction_to_layout(direction)
|
|
146
|
+
case direction
|
|
147
|
+
when 'TD', 'TB'
|
|
148
|
+
DIRECTION_DOWN
|
|
149
|
+
when 'LR'
|
|
150
|
+
DIRECTION_RIGHT
|
|
151
|
+
when 'RL'
|
|
152
|
+
DIRECTION_LEFT
|
|
153
|
+
when 'BT'
|
|
154
|
+
DIRECTION_UP
|
|
155
|
+
else
|
|
156
|
+
DIRECTION_DOWN # Default direction
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|