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,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sirena
|
|
4
|
+
module Transform
|
|
5
|
+
# Abstract base class for diagram transformers.
|
|
6
|
+
#
|
|
7
|
+
# Transformers convert typed diagram models into generic graph structures
|
|
8
|
+
# suitable for layout computation by elkrb. Each diagram type has its own
|
|
9
|
+
# transformer that maps diagram-specific elements to graph nodes and edges.
|
|
10
|
+
#
|
|
11
|
+
# The transformer is also responsible for calculating node dimensions
|
|
12
|
+
# using TextMeasurement and setting appropriate layout options.
|
|
13
|
+
#
|
|
14
|
+
# @example Define a custom transformer
|
|
15
|
+
# class FlowchartTransform < Transform::Base
|
|
16
|
+
# def to_graph(diagram)
|
|
17
|
+
# graph = create_graph
|
|
18
|
+
# # Add nodes and edges based on diagram structure
|
|
19
|
+
# graph
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @abstract Subclass and implement #to_graph
|
|
24
|
+
class Base
|
|
25
|
+
# ELK layout algorithms supported (matching mermaid-js)
|
|
26
|
+
# @see https://www.eclipse.org/elk/reference/algorithms.html
|
|
27
|
+
ALGORITHM_LAYERED = 'layered'
|
|
28
|
+
ALGORITHM_STRESS = 'stress'
|
|
29
|
+
ALGORITHM_FORCE = 'force'
|
|
30
|
+
ALGORITHM_MRTREE = 'mrtree'
|
|
31
|
+
ALGORITHM_SPORE_OVERLAP = 'sporeOverlap'
|
|
32
|
+
|
|
33
|
+
# ELK layout directions
|
|
34
|
+
DIRECTION_DOWN = 'DOWN'
|
|
35
|
+
DIRECTION_UP = 'UP'
|
|
36
|
+
DIRECTION_LEFT = 'LEFT'
|
|
37
|
+
DIRECTION_RIGHT = 'RIGHT'
|
|
38
|
+
|
|
39
|
+
# Default spacing values (in pixels)
|
|
40
|
+
DEFAULT_NODE_SPACING = 50
|
|
41
|
+
DEFAULT_EDGE_SPACING = 30
|
|
42
|
+
DEFAULT_LAYER_SPACING = 50
|
|
43
|
+
|
|
44
|
+
# ELK option keys for consistent configuration
|
|
45
|
+
# @see https://www.eclipse.org/elk/reference/options.html
|
|
46
|
+
module ElkOptions
|
|
47
|
+
ALGORITHM = 'algorithm'
|
|
48
|
+
DIRECTION = 'elk.direction'
|
|
49
|
+
|
|
50
|
+
# Spacing options
|
|
51
|
+
NODE_NODE_SPACING = 'elk.spacing.nodeNode'
|
|
52
|
+
EDGE_NODE_SPACING = 'elk.spacing.edgeNode'
|
|
53
|
+
EDGE_EDGE_SPACING = 'elk.spacing.edgeEdge'
|
|
54
|
+
LAYER_SPACING = 'elk.layered.spacing.nodeNodeBetweenLayers'
|
|
55
|
+
|
|
56
|
+
# Layered algorithm options
|
|
57
|
+
NODE_PLACEMENT = 'elk.layered.nodePlacement.strategy'
|
|
58
|
+
CROSSING_MINIMIZATION =
|
|
59
|
+
'elk.layered.crossingMinimization.strategy'
|
|
60
|
+
MODEL_ORDER = 'elk.layered.considerModelOrder.strategy'
|
|
61
|
+
COMPACTION = 'elk.layered.compaction.postCompaction.strategy'
|
|
62
|
+
|
|
63
|
+
# Hierarchy and grouping
|
|
64
|
+
HIERARCHY_HANDLING = 'elk.hierarchyHandling'
|
|
65
|
+
|
|
66
|
+
# Edge routing
|
|
67
|
+
EDGE_ROUTING = 'elk.edgeRouting'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Converts a diagram model to an elkrb graph structure.
|
|
71
|
+
#
|
|
72
|
+
# This method should be overridden by subclasses to implement
|
|
73
|
+
# diagram-specific graph conversion logic.
|
|
74
|
+
#
|
|
75
|
+
# @param diagram [Diagram::Base] the diagram model to convert
|
|
76
|
+
# @return [Object] elkrb graph object with nodes and edges
|
|
77
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
78
|
+
def to_graph(diagram)
|
|
79
|
+
raise NotImplementedError,
|
|
80
|
+
"#{self.class} must implement #to_graph(diagram)"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
# Measures text dimensions for node sizing.
|
|
86
|
+
#
|
|
87
|
+
# @param text [String] the text to measure
|
|
88
|
+
# @param font_size [Numeric] the font size in points
|
|
89
|
+
# @param width [Numeric, nil] optional width override
|
|
90
|
+
# @param height [Numeric, nil] optional height override
|
|
91
|
+
# @return [Hash] hash with :width and :height keys
|
|
92
|
+
def measure_text(text, font_size:, width: nil, height: nil)
|
|
93
|
+
TextMeasurement.measure(text,
|
|
94
|
+
font_size: font_size,
|
|
95
|
+
width: width,
|
|
96
|
+
height: height)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Creates ELK layout options with proper configuration.
|
|
100
|
+
#
|
|
101
|
+
# This method provides sensible defaults based on mermaid-js patterns
|
|
102
|
+
# and can be overridden by subclasses for diagram-specific needs.
|
|
103
|
+
#
|
|
104
|
+
# @param algorithm [String] ELK algorithm to use
|
|
105
|
+
# @param direction [String] layout direction
|
|
106
|
+
# @param options [Hash] additional ELK options to merge
|
|
107
|
+
# @return [Hash] complete ELK layout options
|
|
108
|
+
def build_elk_options(algorithm: ALGORITHM_LAYERED,
|
|
109
|
+
direction: DIRECTION_DOWN,
|
|
110
|
+
**options)
|
|
111
|
+
base_options = {
|
|
112
|
+
ElkOptions::ALGORITHM => algorithm,
|
|
113
|
+
ElkOptions::DIRECTION => direction
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Add algorithm-specific defaults
|
|
117
|
+
case algorithm
|
|
118
|
+
when ALGORITHM_LAYERED
|
|
119
|
+
base_options.merge!(layered_algorithm_options)
|
|
120
|
+
when ALGORITHM_STRESS, ALGORITHM_FORCE
|
|
121
|
+
base_options.merge!(force_based_algorithm_options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
base_options.merge(options)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Default layout options for layered algorithm.
|
|
128
|
+
#
|
|
129
|
+
# The layered algorithm is optimal for hierarchical diagrams like
|
|
130
|
+
# flowcharts, sequence diagrams, and class diagrams. It minimizes
|
|
131
|
+
# edge crossings and places nodes in distinct layers.
|
|
132
|
+
#
|
|
133
|
+
# @return [Hash] layered algorithm options
|
|
134
|
+
def layered_algorithm_options
|
|
135
|
+
{
|
|
136
|
+
# Node and edge spacing
|
|
137
|
+
ElkOptions::NODE_NODE_SPACING => DEFAULT_NODE_SPACING,
|
|
138
|
+
ElkOptions::EDGE_NODE_SPACING => DEFAULT_EDGE_SPACING,
|
|
139
|
+
ElkOptions::EDGE_EDGE_SPACING => DEFAULT_EDGE_SPACING,
|
|
140
|
+
ElkOptions::LAYER_SPACING => DEFAULT_LAYER_SPACING,
|
|
141
|
+
|
|
142
|
+
# Use SIMPLE node placement for predictable layouts
|
|
143
|
+
ElkOptions::NODE_PLACEMENT => 'SIMPLE',
|
|
144
|
+
|
|
145
|
+
# Consider model order for consistent positioning
|
|
146
|
+
ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES'
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Default layout options for force-based algorithms.
|
|
151
|
+
#
|
|
152
|
+
# Force-based algorithms (stress, force) are optimal for graphs
|
|
153
|
+
# without clear hierarchy, like ER diagrams or network diagrams.
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] force-based algorithm options
|
|
156
|
+
def force_based_algorithm_options
|
|
157
|
+
{
|
|
158
|
+
ElkOptions::NODE_NODE_SPACING => DEFAULT_NODE_SPACING * 1.5,
|
|
159
|
+
ElkOptions::EDGE_NODE_SPACING => DEFAULT_EDGE_SPACING,
|
|
160
|
+
ElkOptions::EDGE_EDGE_SPACING => DEFAULT_EDGE_SPACING
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Calculates node padding based on content type.
|
|
165
|
+
#
|
|
166
|
+
# @param node_type [Symbol] the type of node
|
|
167
|
+
# @return [Hash] padding hash with :top, :bottom, :left, :right
|
|
168
|
+
def node_padding(node_type)
|
|
169
|
+
case node_type
|
|
170
|
+
when :rect
|
|
171
|
+
{ top: 10, bottom: 10, left: 15, right: 15 }
|
|
172
|
+
when :circle
|
|
173
|
+
{ top: 15, bottom: 15, left: 15, right: 15 }
|
|
174
|
+
when :diamond
|
|
175
|
+
{ top: 20, bottom: 20, left: 20, right: 20 }
|
|
176
|
+
else
|
|
177
|
+
{ top: 10, bottom: 10, left: 10, right: 10 }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Calculates total node dimensions including padding.
|
|
182
|
+
#
|
|
183
|
+
# @param content_width [Numeric] width of node content
|
|
184
|
+
# @param content_height [Numeric] height of node content
|
|
185
|
+
# @param node_type [Symbol] the type of node
|
|
186
|
+
# @return [Hash] dimensions hash with :width and :height
|
|
187
|
+
def calculate_node_dimensions(content_width, content_height, node_type)
|
|
188
|
+
padding = node_padding(node_type)
|
|
189
|
+
{
|
|
190
|
+
width: content_width + padding[:left] + padding[:right],
|
|
191
|
+
height: content_height + padding[:top] + padding[:bottom]
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Error raised during transformation.
|
|
197
|
+
class TransformError < StandardError; end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/block'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# Block diagram transformer for converting block models to positioned layouts.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed block diagram model into a column-based layout structure.
|
|
11
|
+
# Handles block dimension calculation, column-based positioning, and
|
|
12
|
+
# connection routing.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a block diagram
|
|
15
|
+
# transform = BlockTransform.new
|
|
16
|
+
# layout = transform.to_layout(block_diagram)
|
|
17
|
+
class BlockTransform < Base
|
|
18
|
+
# Default dimensions
|
|
19
|
+
DEFAULT_BLOCK_WIDTH = 100
|
|
20
|
+
DEFAULT_BLOCK_HEIGHT = 60
|
|
21
|
+
DEFAULT_SPACING = 20
|
|
22
|
+
DEFAULT_COMPOUND_PADDING = 20
|
|
23
|
+
|
|
24
|
+
# Converts a block diagram to a positioned layout structure.
|
|
25
|
+
#
|
|
26
|
+
# @param diagram [Diagram::BlockDiagram] the block diagram to transform
|
|
27
|
+
# @return [Hash] positioned layout hash
|
|
28
|
+
# @raise [TransformError] if diagram is invalid
|
|
29
|
+
def to_graph(diagram)
|
|
30
|
+
raise TransformError, 'Diagram cannot be nil' if diagram.nil?
|
|
31
|
+
|
|
32
|
+
blocks_layout = calculate_column_layout(diagram)
|
|
33
|
+
connections_layout = calculate_connections(diagram, blocks_layout)
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
blocks: blocks_layout,
|
|
37
|
+
connections: connections_layout,
|
|
38
|
+
columns: diagram.columns,
|
|
39
|
+
width: calculate_total_width(blocks_layout, diagram.columns),
|
|
40
|
+
height: calculate_total_height(blocks_layout)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def calculate_column_layout(diagram)
|
|
47
|
+
columns = diagram.columns
|
|
48
|
+
blocks = diagram.blocks
|
|
49
|
+
positioned_blocks = {}
|
|
50
|
+
|
|
51
|
+
current_row = 0
|
|
52
|
+
current_col = 0
|
|
53
|
+
row_heights = []
|
|
54
|
+
col_widths = Array.new(columns, 0)
|
|
55
|
+
|
|
56
|
+
blocks.each do |block|
|
|
57
|
+
# Handle space blocks
|
|
58
|
+
if block.space?
|
|
59
|
+
current_col += 1
|
|
60
|
+
if current_col >= columns
|
|
61
|
+
current_col = 0
|
|
62
|
+
current_row += 1
|
|
63
|
+
end
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Calculate block dimensions
|
|
68
|
+
dims = calculate_block_dimensions(block)
|
|
69
|
+
block_width = block.width || 1
|
|
70
|
+
|
|
71
|
+
# Check if block fits in current row
|
|
72
|
+
if current_col + block_width > columns
|
|
73
|
+
current_col = 0
|
|
74
|
+
current_row += 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Position block
|
|
78
|
+
x = calculate_x_position(current_col, col_widths)
|
|
79
|
+
y = calculate_y_position(current_row, row_heights)
|
|
80
|
+
|
|
81
|
+
positioned_blocks[block.id] = {
|
|
82
|
+
block: block,
|
|
83
|
+
x: x,
|
|
84
|
+
y: y,
|
|
85
|
+
width: dims[:width] * block_width,
|
|
86
|
+
height: dims[:height],
|
|
87
|
+
row: current_row,
|
|
88
|
+
col: current_col,
|
|
89
|
+
col_span: block_width
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Update column widths
|
|
93
|
+
(current_col...current_col + block_width).each do |col|
|
|
94
|
+
col_widths[col] = [col_widths[col], dims[:width]].max if col < columns
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Update row height
|
|
98
|
+
row_heights[current_row] = [row_heights[current_row] || 0, dims[:height]].max
|
|
99
|
+
|
|
100
|
+
# Handle compound blocks
|
|
101
|
+
if block.compound? && !block.children.empty?
|
|
102
|
+
child_layout = layout_compound_children(block, x, y, dims)
|
|
103
|
+
positioned_blocks.merge!(child_layout)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Move to next position
|
|
107
|
+
current_col += block_width
|
|
108
|
+
if current_col >= columns
|
|
109
|
+
current_col = 0
|
|
110
|
+
current_row += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
positioned_blocks
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def layout_compound_children(parent_block, parent_x, parent_y, parent_dims)
|
|
118
|
+
positioned = {}
|
|
119
|
+
child_y = parent_y + DEFAULT_COMPOUND_PADDING
|
|
120
|
+
|
|
121
|
+
parent_block.children.each_with_index do |child, index|
|
|
122
|
+
child_dims = calculate_block_dimensions(child)
|
|
123
|
+
|
|
124
|
+
positioned[child.id] = {
|
|
125
|
+
block: child,
|
|
126
|
+
x: parent_x + DEFAULT_COMPOUND_PADDING,
|
|
127
|
+
y: child_y,
|
|
128
|
+
width: child_dims[:width],
|
|
129
|
+
height: child_dims[:height],
|
|
130
|
+
parent_id: parent_block.id
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
child_y += child_dims[:height] + DEFAULT_SPACING
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
positioned
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def calculate_block_dimensions(block)
|
|
140
|
+
if block.arrow?
|
|
141
|
+
return {
|
|
142
|
+
width: DEFAULT_BLOCK_WIDTH / 2,
|
|
143
|
+
height: DEFAULT_BLOCK_HEIGHT / 2
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
label = block.label || block.id
|
|
148
|
+
label_dims = measure_text(label, font_size: 14)
|
|
149
|
+
|
|
150
|
+
# Add padding
|
|
151
|
+
width = [label_dims[:width] + 40, DEFAULT_BLOCK_WIDTH].max
|
|
152
|
+
height = [label_dims[:height] + 30, DEFAULT_BLOCK_HEIGHT].max
|
|
153
|
+
|
|
154
|
+
if block.compound?
|
|
155
|
+
# Compound blocks need more space
|
|
156
|
+
child_height = block.children.reduce(0) do |sum, child|
|
|
157
|
+
child_dims = calculate_block_dimensions(child)
|
|
158
|
+
sum + child_dims[:height] + DEFAULT_SPACING
|
|
159
|
+
end
|
|
160
|
+
height = [height, child_height + DEFAULT_COMPOUND_PADDING * 2].max
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
width: width,
|
|
165
|
+
height: height
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def calculate_x_position(col, col_widths)
|
|
170
|
+
return DEFAULT_SPACING if col == 0
|
|
171
|
+
|
|
172
|
+
col_widths[0...col].sum + (DEFAULT_SPACING * (col + 1))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def calculate_y_position(row, row_heights)
|
|
176
|
+
return DEFAULT_SPACING if row == 0
|
|
177
|
+
|
|
178
|
+
row_heights[0...row].sum + (DEFAULT_SPACING * (row + 1))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def calculate_connections(diagram, blocks_layout)
|
|
182
|
+
diagram.connections.map do |conn|
|
|
183
|
+
from_block = blocks_layout[conn.from]
|
|
184
|
+
to_block = blocks_layout[conn.to]
|
|
185
|
+
|
|
186
|
+
next unless from_block && to_block
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
from: conn.from,
|
|
190
|
+
to: conn.to,
|
|
191
|
+
from_x: from_block[:x] + from_block[:width] / 2,
|
|
192
|
+
from_y: from_block[:y] + from_block[:height],
|
|
193
|
+
to_x: to_block[:x] + to_block[:width] / 2,
|
|
194
|
+
to_y: to_block[:y],
|
|
195
|
+
connection_type: conn.connection_type
|
|
196
|
+
}
|
|
197
|
+
end.compact
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def calculate_total_width(blocks_layout, columns)
|
|
201
|
+
return DEFAULT_SPACING * 2 if blocks_layout.empty?
|
|
202
|
+
|
|
203
|
+
max_x = blocks_layout.values.map { |b| b[:x] + b[:width] }.max || 0
|
|
204
|
+
max_x + DEFAULT_SPACING
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def calculate_total_height(blocks_layout)
|
|
208
|
+
return DEFAULT_SPACING * 2 if blocks_layout.empty?
|
|
209
|
+
|
|
210
|
+
max_y = blocks_layout.values.map { |b| b[:y] + b[:height] }.max || 0
|
|
211
|
+
max_y + DEFAULT_SPACING
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../diagram/c4'
|
|
5
|
+
|
|
6
|
+
module Sirena
|
|
7
|
+
module Transform
|
|
8
|
+
# C4 transformer for converting C4 models to graphs.
|
|
9
|
+
#
|
|
10
|
+
# Converts a typed C4 diagram model into a generic graph structure
|
|
11
|
+
# suitable for layout computation by elkrb. Handles element positioning,
|
|
12
|
+
# boundary grouping, and relationship routing.
|
|
13
|
+
#
|
|
14
|
+
# @example Transform a C4 diagram
|
|
15
|
+
# transform = C4Transform.new
|
|
16
|
+
# graph = transform.to_graph(c4_diagram)
|
|
17
|
+
class C4Transform < Base
|
|
18
|
+
# Default font size for text measurement
|
|
19
|
+
DEFAULT_FONT_SIZE = 14
|
|
20
|
+
|
|
21
|
+
# Element dimensions based on type
|
|
22
|
+
PERSON_WIDTH = 140
|
|
23
|
+
PERSON_HEIGHT = 180
|
|
24
|
+
SYSTEM_WIDTH = 160
|
|
25
|
+
SYSTEM_HEIGHT = 120
|
|
26
|
+
CONTAINER_WIDTH = 160
|
|
27
|
+
CONTAINER_HEIGHT = 120
|
|
28
|
+
COMPONENT_WIDTH = 160
|
|
29
|
+
COMPONENT_HEIGHT = 100
|
|
30
|
+
|
|
31
|
+
# Spacing
|
|
32
|
+
ELEMENT_SPACING = 60
|
|
33
|
+
BOUNDARY_PADDING = 40
|
|
34
|
+
LEVEL_SPACING = 80
|
|
35
|
+
|
|
36
|
+
# Converts a C4 diagram to a graph structure.
|
|
37
|
+
#
|
|
38
|
+
# @param diagram [Diagram::C4] the C4 diagram to transform
|
|
39
|
+
# @return [Hash] elkrb-compatible graph hash
|
|
40
|
+
# @raise [TransformError] if diagram is invalid
|
|
41
|
+
def to_graph(diagram)
|
|
42
|
+
raise TransformError, 'Invalid diagram' unless diagram.valid?
|
|
43
|
+
|
|
44
|
+
# Build hierarchy with boundaries as containers
|
|
45
|
+
root_elements = diagram.elements.select { |e| e.boundary_id.nil? }
|
|
46
|
+
root_boundaries = diagram.boundaries.select { |b| b.parent_id.nil? }
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
id: diagram.id || 'c4',
|
|
50
|
+
children: transform_root_nodes(diagram, root_elements,
|
|
51
|
+
root_boundaries),
|
|
52
|
+
edges: transform_relationships(diagram),
|
|
53
|
+
layoutOptions: layout_options(diagram),
|
|
54
|
+
metadata: {
|
|
55
|
+
level: diagram.level,
|
|
56
|
+
title: diagram.title,
|
|
57
|
+
element_count: diagram.elements.length,
|
|
58
|
+
relationship_count: diagram.relationships.length
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def transform_root_nodes(diagram, elements, boundaries)
|
|
66
|
+
nodes = []
|
|
67
|
+
|
|
68
|
+
# Add root-level boundaries (which contain their own elements)
|
|
69
|
+
boundaries.each do |boundary|
|
|
70
|
+
nodes << transform_boundary(diagram, boundary)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Add root-level elements (not in any boundary)
|
|
74
|
+
elements.each do |element|
|
|
75
|
+
nodes << transform_element(element)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
nodes
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def transform_boundary(diagram, boundary)
|
|
82
|
+
# Get elements in this boundary
|
|
83
|
+
elements = diagram.elements_in_boundary(boundary.id)
|
|
84
|
+
child_boundaries = diagram.boundaries_in_boundary(boundary.id)
|
|
85
|
+
|
|
86
|
+
children = []
|
|
87
|
+
|
|
88
|
+
# Add child boundaries first
|
|
89
|
+
child_boundaries.each do |child_boundary|
|
|
90
|
+
children << transform_boundary(diagram, child_boundary)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Add elements in this boundary
|
|
94
|
+
elements.each do |element|
|
|
95
|
+
children << transform_element(element)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Calculate boundary dimensions based on contents
|
|
99
|
+
dims = calculate_boundary_dimensions(children)
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
id: boundary.id,
|
|
103
|
+
width: dims[:width],
|
|
104
|
+
height: dims[:height],
|
|
105
|
+
labels: [
|
|
106
|
+
{
|
|
107
|
+
text: boundary.label,
|
|
108
|
+
width: measure_text(boundary.label,
|
|
109
|
+
font_size: DEFAULT_FONT_SIZE + 2)[:width],
|
|
110
|
+
height: measure_text(boundary.label,
|
|
111
|
+
font_size: DEFAULT_FONT_SIZE + 2)[:height]
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
children: children,
|
|
115
|
+
layoutOptions: boundary_layout_options,
|
|
116
|
+
metadata: {
|
|
117
|
+
boundary_type: boundary.boundary_type,
|
|
118
|
+
type_param: boundary.type_param,
|
|
119
|
+
link: boundary.link,
|
|
120
|
+
tags: boundary.tags
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def transform_element(element)
|
|
126
|
+
dims = element_dimensions(element)
|
|
127
|
+
|
|
128
|
+
labels = []
|
|
129
|
+
|
|
130
|
+
# Main label
|
|
131
|
+
label_dims = measure_text(element.label,
|
|
132
|
+
font_size: DEFAULT_FONT_SIZE + 2)
|
|
133
|
+
labels << {
|
|
134
|
+
text: element.label,
|
|
135
|
+
width: label_dims[:width],
|
|
136
|
+
height: label_dims[:height]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Description (if present)
|
|
140
|
+
if element.description && !element.description.empty?
|
|
141
|
+
desc_dims = measure_text(element.description,
|
|
142
|
+
font_size: DEFAULT_FONT_SIZE - 2)
|
|
143
|
+
labels << {
|
|
144
|
+
text: element.description,
|
|
145
|
+
width: desc_dims[:width],
|
|
146
|
+
height: desc_dims[:height]
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Technology (if present)
|
|
151
|
+
if element.technology && !element.technology.empty?
|
|
152
|
+
tech_dims = measure_text(element.technology,
|
|
153
|
+
font_size: DEFAULT_FONT_SIZE - 2)
|
|
154
|
+
labels << {
|
|
155
|
+
text: "[#{element.technology}]",
|
|
156
|
+
width: tech_dims[:width],
|
|
157
|
+
height: tech_dims[:height]
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
id: element.id,
|
|
163
|
+
width: dims[:width],
|
|
164
|
+
height: dims[:height],
|
|
165
|
+
labels: labels,
|
|
166
|
+
metadata: {
|
|
167
|
+
element_type: element.element_type,
|
|
168
|
+
base_type: element.base_type,
|
|
169
|
+
external: element.external,
|
|
170
|
+
sprite: element.sprite,
|
|
171
|
+
link: element.link,
|
|
172
|
+
tags: element.tags,
|
|
173
|
+
person: element.person?,
|
|
174
|
+
system: element.system?,
|
|
175
|
+
container: element.container?,
|
|
176
|
+
component: element.component?
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def transform_relationships(diagram)
|
|
182
|
+
return [] if diagram.relationships.nil? || diagram.relationships.empty?
|
|
183
|
+
|
|
184
|
+
diagram.relationships.map.with_index do |rel, index|
|
|
185
|
+
labels = []
|
|
186
|
+
|
|
187
|
+
if rel.label && !rel.label.empty?
|
|
188
|
+
label_dims = measure_text(rel.label, font_size: DEFAULT_FONT_SIZE)
|
|
189
|
+
labels << {
|
|
190
|
+
text: rel.label,
|
|
191
|
+
width: label_dims[:width],
|
|
192
|
+
height: label_dims[:height]
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if rel.technology && !rel.technology.empty?
|
|
197
|
+
tech_dims = measure_text("[#{rel.technology}]",
|
|
198
|
+
font_size: DEFAULT_FONT_SIZE - 2)
|
|
199
|
+
labels << {
|
|
200
|
+
text: "[#{rel.technology}]",
|
|
201
|
+
width: tech_dims[:width],
|
|
202
|
+
height: tech_dims[:height]
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
id: "rel_#{index}",
|
|
208
|
+
sources: [rel.from_id],
|
|
209
|
+
targets: [rel.to_id],
|
|
210
|
+
labels: labels,
|
|
211
|
+
metadata: {
|
|
212
|
+
rel_type: rel.rel_type,
|
|
213
|
+
bidirectional: rel.bidirectional?
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def element_dimensions(element)
|
|
220
|
+
# Base dimensions on element type
|
|
221
|
+
if element.person?
|
|
222
|
+
{ width: PERSON_WIDTH, height: PERSON_HEIGHT }
|
|
223
|
+
elsif element.system?
|
|
224
|
+
{ width: SYSTEM_WIDTH, height: SYSTEM_HEIGHT }
|
|
225
|
+
elsif element.container?
|
|
226
|
+
{ width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }
|
|
227
|
+
elsif element.component?
|
|
228
|
+
{ width: COMPONENT_WIDTH, height: COMPONENT_HEIGHT }
|
|
229
|
+
else
|
|
230
|
+
{ width: SYSTEM_WIDTH, height: SYSTEM_HEIGHT }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def calculate_boundary_dimensions(children)
|
|
235
|
+
return { width: 300, height: 200 } if children.empty?
|
|
236
|
+
|
|
237
|
+
# Calculate based on child count and type
|
|
238
|
+
# Simple heuristic: arrange in grid
|
|
239
|
+
count = children.length
|
|
240
|
+
cols = Math.sqrt(count).ceil
|
|
241
|
+
rows = (count.to_f / cols).ceil
|
|
242
|
+
|
|
243
|
+
max_width = children.map { |c| c[:width] || 160 }.max
|
|
244
|
+
max_height = children.map { |c| c[:height] || 120 }.max
|
|
245
|
+
|
|
246
|
+
width = (cols * max_width) + ((cols + 1) * ELEMENT_SPACING) +
|
|
247
|
+
(2 * BOUNDARY_PADDING)
|
|
248
|
+
height = (rows * max_height) + ((rows + 1) * ELEMENT_SPACING) +
|
|
249
|
+
(2 * BOUNDARY_PADDING) + 30 # Extra for title
|
|
250
|
+
|
|
251
|
+
{ width: [width, 300].max, height: [height, 200].max }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def layout_options(diagram)
|
|
255
|
+
# C4 diagrams use hierarchical layout
|
|
256
|
+
# Top-down for Context/Container, can be left-right for Component
|
|
257
|
+
direction = case diagram.level
|
|
258
|
+
when 'Component', 'Code'
|
|
259
|
+
DIRECTION_RIGHT
|
|
260
|
+
else
|
|
261
|
+
DIRECTION_DOWN
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
build_elk_options(
|
|
265
|
+
algorithm: ALGORITHM_LAYERED,
|
|
266
|
+
direction: direction,
|
|
267
|
+
ElkOptions::NODE_NODE_SPACING => ELEMENT_SPACING,
|
|
268
|
+
ElkOptions::LAYER_SPACING => LEVEL_SPACING,
|
|
269
|
+
ElkOptions::EDGE_NODE_SPACING => 25,
|
|
270
|
+
ElkOptions::EDGE_EDGE_SPACING => 20,
|
|
271
|
+
ElkOptions::HIERARCHY_HANDLING => 'INCLUDE_CHILDREN',
|
|
272
|
+
ElkOptions::NODE_PLACEMENT => 'NETWORK_SIMPLEX'
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def boundary_layout_options
|
|
277
|
+
# Boundaries use box packing for internal layout
|
|
278
|
+
{
|
|
279
|
+
'elk.algorithm' => 'box',
|
|
280
|
+
'elk.box.packingMode' => 'GROUP_MIXED',
|
|
281
|
+
'elk.padding' => "[top=#{BOUNDARY_PADDING},left=#{BOUNDARY_PADDING}," \
|
|
282
|
+
"bottom=#{BOUNDARY_PADDING},right=#{BOUNDARY_PADDING}]",
|
|
283
|
+
'elk.spacing.nodeNode' => ELEMENT_SPACING.to_s
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|