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,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../svg/document"
|
|
4
|
+
require_relative "../svg/rect"
|
|
5
|
+
require_relative "../svg/text"
|
|
6
|
+
require_relative "../svg/group"
|
|
7
|
+
require_relative "../svg/line"
|
|
8
|
+
|
|
9
|
+
module Sirena
|
|
10
|
+
module Renderer
|
|
11
|
+
# Renders a Kanban board layout to SVG.
|
|
12
|
+
#
|
|
13
|
+
# The renderer converts the positioned layout structure from
|
|
14
|
+
# Transform::Kanban into an SVG visualization showing:
|
|
15
|
+
# - Columns with headers
|
|
16
|
+
# - Cards stacked vertically within columns
|
|
17
|
+
# - Card metadata (assigned, ticket, priority, etc.)
|
|
18
|
+
# - Professional kanban board styling
|
|
19
|
+
#
|
|
20
|
+
# @example Render a kanban board
|
|
21
|
+
# renderer = Renderer::Kanban.new(theme: my_theme)
|
|
22
|
+
# svg = renderer.render(layout)
|
|
23
|
+
class Kanban < Base
|
|
24
|
+
# Renders the layout structure to SVG.
|
|
25
|
+
#
|
|
26
|
+
# @param layout [Hash] layout data from Transform::Kanban
|
|
27
|
+
# @return [Svg::Document] rendered SVG document
|
|
28
|
+
def render(layout)
|
|
29
|
+
svg = create_document_from_layout(layout)
|
|
30
|
+
|
|
31
|
+
# Render columns then cards
|
|
32
|
+
render_columns(layout, svg)
|
|
33
|
+
render_cards(layout, svg)
|
|
34
|
+
|
|
35
|
+
svg
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
# Creates an SVG document with dimensions from layout.
|
|
41
|
+
#
|
|
42
|
+
# @param layout [Hash] layout data
|
|
43
|
+
# @return [Svg::Document] new SVG document
|
|
44
|
+
def create_document_from_layout(layout)
|
|
45
|
+
padding = 40
|
|
46
|
+
|
|
47
|
+
Svg::Document.new.tap do |doc|
|
|
48
|
+
doc.width = layout[:width] + (padding * 2)
|
|
49
|
+
doc.height = layout[:height] + (padding * 2)
|
|
50
|
+
doc.view_box = "0 0 #{doc.width} #{doc.height}"
|
|
51
|
+
|
|
52
|
+
# Add offset for padding
|
|
53
|
+
@offset_x = padding
|
|
54
|
+
@offset_y = padding
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Renders all columns
|
|
59
|
+
#
|
|
60
|
+
# @param layout [Hash] layout data
|
|
61
|
+
# @param svg [Svg::Document] SVG document
|
|
62
|
+
# @return [void]
|
|
63
|
+
def render_columns(layout, svg)
|
|
64
|
+
layout[:columns].each do |column|
|
|
65
|
+
render_column(column, svg)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Renders a single column with header
|
|
70
|
+
#
|
|
71
|
+
# @param column [Hash] column data
|
|
72
|
+
# @param svg [Svg::Document] SVG document
|
|
73
|
+
# @return [void]
|
|
74
|
+
def render_column(column, svg)
|
|
75
|
+
x = column[:x] + @offset_x
|
|
76
|
+
y = column[:y] + @offset_y
|
|
77
|
+
|
|
78
|
+
# Column background
|
|
79
|
+
column_bg = Svg::Rect.new.tap do |r|
|
|
80
|
+
r.x = x
|
|
81
|
+
r.y = y
|
|
82
|
+
r.width = column[:width]
|
|
83
|
+
r.height = column[:height]
|
|
84
|
+
r.rx = 8
|
|
85
|
+
r.ry = 8
|
|
86
|
+
r.fill = theme_color(:background) || "#f3f4f6"
|
|
87
|
+
r.stroke = theme_color(:border) || "#d1d5db"
|
|
88
|
+
r.stroke_width = "1"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
svg.add_element(column_bg)
|
|
92
|
+
|
|
93
|
+
# Column header
|
|
94
|
+
render_column_header(column, x, y, svg)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Renders column header
|
|
98
|
+
#
|
|
99
|
+
# @param column [Hash] column data
|
|
100
|
+
# @param x [Numeric] X position
|
|
101
|
+
# @param y [Numeric] Y position
|
|
102
|
+
# @param svg [Svg::Document] SVG document
|
|
103
|
+
# @return [void]
|
|
104
|
+
def render_column_header(column, x, y, svg)
|
|
105
|
+
header_height = 50
|
|
106
|
+
|
|
107
|
+
# Header background
|
|
108
|
+
header_bg = Svg::Rect.new.tap do |r|
|
|
109
|
+
r.x = x
|
|
110
|
+
r.y = y
|
|
111
|
+
r.width = column[:width]
|
|
112
|
+
r.height = header_height
|
|
113
|
+
r.rx = 8
|
|
114
|
+
r.ry = 8
|
|
115
|
+
r.fill = theme_color(:primary) || "#3b82f6"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
svg.add_element(header_bg)
|
|
119
|
+
|
|
120
|
+
# Header text
|
|
121
|
+
header_text = Svg::Text.new.tap do |t|
|
|
122
|
+
t.x = x + column[:width] / 2
|
|
123
|
+
t.y = y + header_height / 2 + 5
|
|
124
|
+
t.text_anchor = "middle"
|
|
125
|
+
t.fill = "#ffffff"
|
|
126
|
+
t.font_size = (theme_typography(:font_size) || 14).to_s
|
|
127
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
128
|
+
t.font_weight = "bold"
|
|
129
|
+
t.content = column[:title]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
svg.add_element(header_text)
|
|
133
|
+
|
|
134
|
+
# Card count badge (optional)
|
|
135
|
+
if column[:card_count] > 0
|
|
136
|
+
badge_x = x + column[:width] - 25
|
|
137
|
+
badge_y = y + 15
|
|
138
|
+
|
|
139
|
+
badge_circle = Svg::Rect.new.tap do |r|
|
|
140
|
+
r.x = badge_x
|
|
141
|
+
r.y = badge_y
|
|
142
|
+
r.width = 20
|
|
143
|
+
r.height = 20
|
|
144
|
+
r.rx = 10
|
|
145
|
+
r.ry = 10
|
|
146
|
+
r.fill = "#ffffff"
|
|
147
|
+
r.opacity = "0.3"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
svg.add_element(badge_circle)
|
|
151
|
+
|
|
152
|
+
badge_text = Svg::Text.new.tap do |t|
|
|
153
|
+
t.x = badge_x + 10
|
|
154
|
+
t.y = badge_y + 14
|
|
155
|
+
t.text_anchor = "middle"
|
|
156
|
+
t.fill = "#ffffff"
|
|
157
|
+
t.font_size = "11"
|
|
158
|
+
t.font_weight = "bold"
|
|
159
|
+
t.content = column[:card_count].to_s
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
svg.add_element(badge_text)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Renders all cards
|
|
167
|
+
#
|
|
168
|
+
# @param layout [Hash] layout data
|
|
169
|
+
# @param svg [Svg::Document] SVG document
|
|
170
|
+
# @return [void]
|
|
171
|
+
def render_cards(layout, svg)
|
|
172
|
+
layout[:cards].each do |card|
|
|
173
|
+
render_card(card, svg)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Renders a single card
|
|
178
|
+
#
|
|
179
|
+
# @param card [Hash] card data
|
|
180
|
+
# @param svg [Svg::Document] SVG document
|
|
181
|
+
# @return [void]
|
|
182
|
+
def render_card(card, svg)
|
|
183
|
+
x = card[:x] + @offset_x
|
|
184
|
+
y = card[:y] + @offset_y
|
|
185
|
+
|
|
186
|
+
# Card background
|
|
187
|
+
card_bg = Svg::Rect.new.tap do |r|
|
|
188
|
+
r.x = x
|
|
189
|
+
r.y = y
|
|
190
|
+
r.width = card[:width]
|
|
191
|
+
r.height = card[:height]
|
|
192
|
+
r.rx = 6
|
|
193
|
+
r.ry = 6
|
|
194
|
+
r.fill = "#ffffff"
|
|
195
|
+
r.stroke = theme_color(:border) || "#d1d5db"
|
|
196
|
+
r.stroke_width = "1"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
svg.add_element(card_bg)
|
|
200
|
+
|
|
201
|
+
# Card text
|
|
202
|
+
render_card_text(card, x, y, svg)
|
|
203
|
+
|
|
204
|
+
# Metadata if present
|
|
205
|
+
if card[:has_metadata]
|
|
206
|
+
render_card_metadata(card, x, y, svg)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Renders card text
|
|
211
|
+
#
|
|
212
|
+
# @param card [Hash] card data
|
|
213
|
+
# @param x [Numeric] X position
|
|
214
|
+
# @param y [Numeric] Y position
|
|
215
|
+
# @param svg [Svg::Document] SVG document
|
|
216
|
+
# @return [void]
|
|
217
|
+
def render_card_text(card, x, y, svg)
|
|
218
|
+
text_y = y + 25
|
|
219
|
+
|
|
220
|
+
text = Svg::Text.new.tap do |t|
|
|
221
|
+
t.x = x + 10
|
|
222
|
+
t.y = text_y
|
|
223
|
+
t.fill = theme_color(:text) || "#1f2937"
|
|
224
|
+
t.font_size = (theme_typography(:font_size) || 13).to_s
|
|
225
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
226
|
+
t.content = truncate_text(card[:text], 25)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
svg.add_element(text)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Renders card metadata
|
|
233
|
+
#
|
|
234
|
+
# @param card [Hash] card data
|
|
235
|
+
# @param x [Numeric] X position
|
|
236
|
+
# @param y [Numeric] Y position
|
|
237
|
+
# @param svg [Svg::Document] SVG document
|
|
238
|
+
# @return [void]
|
|
239
|
+
def render_card_metadata(card, x, y, svg)
|
|
240
|
+
metadata_y = y + 50
|
|
241
|
+
line_height = 18
|
|
242
|
+
|
|
243
|
+
card[:metadata].each_with_index do |(key, value), index|
|
|
244
|
+
next if value.nil? || value.to_s.empty?
|
|
245
|
+
|
|
246
|
+
current_y = metadata_y + (index * line_height)
|
|
247
|
+
|
|
248
|
+
# Metadata label
|
|
249
|
+
label = Svg::Text.new.tap do |t|
|
|
250
|
+
t.x = x + 10
|
|
251
|
+
t.y = current_y
|
|
252
|
+
t.fill = theme_color(:secondary) || "#6b7280"
|
|
253
|
+
t.font_size = "10"
|
|
254
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
255
|
+
t.content = "#{format_metadata_key(key)}:"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
svg.add_element(label)
|
|
259
|
+
|
|
260
|
+
# Metadata value
|
|
261
|
+
value_text = Svg::Text.new.tap do |t|
|
|
262
|
+
t.x = x + 70
|
|
263
|
+
t.y = current_y
|
|
264
|
+
t.fill = theme_color(:text) || "#1f2937"
|
|
265
|
+
t.font_size = "10"
|
|
266
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
267
|
+
t.font_weight = "bold"
|
|
268
|
+
t.content = value.to_s
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
svg.add_element(value_text)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Formats metadata key for display
|
|
276
|
+
#
|
|
277
|
+
# @param key [Symbol, String] metadata key
|
|
278
|
+
# @return [String] formatted key
|
|
279
|
+
def format_metadata_key(key)
|
|
280
|
+
key.to_s.capitalize
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Truncates text to a maximum length
|
|
284
|
+
#
|
|
285
|
+
# @param text [String] text to truncate
|
|
286
|
+
# @param max_length [Integer] maximum length
|
|
287
|
+
# @return [String] truncated text
|
|
288
|
+
def truncate_text(text, max_length)
|
|
289
|
+
return text if text.length <= max_length
|
|
290
|
+
|
|
291
|
+
"#{text[0...max_length - 3]}..."
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../svg/document"
|
|
4
|
+
require_relative "../svg/circle"
|
|
5
|
+
require_relative "../svg/rect"
|
|
6
|
+
require_relative "../svg/path"
|
|
7
|
+
require_relative "../svg/line"
|
|
8
|
+
require_relative "../svg/text"
|
|
9
|
+
require_relative "../svg/group"
|
|
10
|
+
require_relative "../svg/polygon"
|
|
11
|
+
|
|
12
|
+
module Sirena
|
|
13
|
+
module Renderer
|
|
14
|
+
# Renders a Mindmap layout to SVG.
|
|
15
|
+
#
|
|
16
|
+
# The renderer converts the positioned layout structure from
|
|
17
|
+
# Transform::Mindmap into an SVG visualization showing:
|
|
18
|
+
# - Nodes with different shapes (circle, cloud, hexagon, etc.)
|
|
19
|
+
# - Connections between parent and child nodes
|
|
20
|
+
# - Icons and custom classes
|
|
21
|
+
# - Level-based or branch-based coloring
|
|
22
|
+
#
|
|
23
|
+
# @example Render a mindmap
|
|
24
|
+
# renderer = Renderer::Mindmap.new(theme: my_theme)
|
|
25
|
+
# svg = renderer.render(layout)
|
|
26
|
+
class Mindmap < Base
|
|
27
|
+
# Renders the layout structure to SVG.
|
|
28
|
+
#
|
|
29
|
+
# @param layout [Hash] layout data from Transform::Mindmap
|
|
30
|
+
# @return [Svg::Document] rendered SVG document
|
|
31
|
+
def render(layout)
|
|
32
|
+
svg = create_document_from_layout(layout)
|
|
33
|
+
|
|
34
|
+
# Render in order: connections, then nodes, then labels
|
|
35
|
+
render_connections(layout, svg)
|
|
36
|
+
render_nodes(layout, svg)
|
|
37
|
+
|
|
38
|
+
svg
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
# Creates an SVG document with dimensions from layout.
|
|
44
|
+
#
|
|
45
|
+
# @param layout [Hash] layout data
|
|
46
|
+
# @return [Svg::Document] new SVG document
|
|
47
|
+
def create_document_from_layout(layout)
|
|
48
|
+
padding = 40
|
|
49
|
+
|
|
50
|
+
Svg::Document.new.tap do |doc|
|
|
51
|
+
doc.width = layout[:width] + (padding * 2)
|
|
52
|
+
doc.height = layout[:height] + (padding * 2)
|
|
53
|
+
doc.view_box = "0 0 #{doc.width} #{doc.height}"
|
|
54
|
+
|
|
55
|
+
# Add a group with padding offset
|
|
56
|
+
@offset_x = padding
|
|
57
|
+
@offset_y = padding
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Renders all connections between nodes.
|
|
62
|
+
#
|
|
63
|
+
# @param layout [Hash] layout data
|
|
64
|
+
# @param svg [Svg::Document] SVG document
|
|
65
|
+
# @return [void]
|
|
66
|
+
def render_connections(layout, svg)
|
|
67
|
+
layout[:connections].each do |connection|
|
|
68
|
+
render_connection(connection, layout, svg)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Renders a single connection line.
|
|
73
|
+
#
|
|
74
|
+
# @param connection [Hash] connection data
|
|
75
|
+
# @param layout [Hash] layout data
|
|
76
|
+
# @param svg [Svg::Document] SVG document
|
|
77
|
+
# @return [void]
|
|
78
|
+
def render_connection(connection, layout, svg)
|
|
79
|
+
from_node = layout[:nodes].find { |n| n[:id] == connection[:from] }
|
|
80
|
+
to_node = layout[:nodes].find { |n| n[:id] == connection[:to] }
|
|
81
|
+
|
|
82
|
+
return unless from_node && to_node
|
|
83
|
+
|
|
84
|
+
from_x = from_node[:x] + @offset_x
|
|
85
|
+
from_y = from_node[:y] + @offset_y + (from_node[:height] / 2)
|
|
86
|
+
to_x = to_node[:x] + @offset_x
|
|
87
|
+
to_y = to_node[:y] + @offset_y
|
|
88
|
+
|
|
89
|
+
# Use bezier curve for connections
|
|
90
|
+
control_y = from_y + (to_y - from_y) / 2
|
|
91
|
+
|
|
92
|
+
path_data = "M #{from_x} #{from_y} " \
|
|
93
|
+
"C #{from_x} #{control_y}, " \
|
|
94
|
+
"#{to_x} #{control_y}, " \
|
|
95
|
+
"#{to_x} #{to_y}"
|
|
96
|
+
|
|
97
|
+
color = get_node_color(from_node)
|
|
98
|
+
|
|
99
|
+
path = Svg::Path.new.tap do |p|
|
|
100
|
+
p.d = path_data
|
|
101
|
+
p.stroke = color
|
|
102
|
+
p.stroke_width = "2"
|
|
103
|
+
p.fill = "none"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
svg.add_element(path)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Renders all nodes with their shapes.
|
|
110
|
+
#
|
|
111
|
+
# @param layout [Hash] layout data
|
|
112
|
+
# @param svg [Svg::Document] SVG document
|
|
113
|
+
# @return [void]
|
|
114
|
+
def render_nodes(layout, svg)
|
|
115
|
+
layout[:nodes].each do |node|
|
|
116
|
+
render_node(node, svg)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Renders a single node with appropriate shape.
|
|
121
|
+
#
|
|
122
|
+
# @param node [Hash] node data
|
|
123
|
+
# @param svg [Svg::Document] SVG document
|
|
124
|
+
# @return [void]
|
|
125
|
+
def render_node(node, svg)
|
|
126
|
+
x = node[:x] + @offset_x
|
|
127
|
+
y = node[:y] + @offset_y
|
|
128
|
+
|
|
129
|
+
case node[:shape]
|
|
130
|
+
when "circle"
|
|
131
|
+
render_circle_node(node, x, y, svg)
|
|
132
|
+
when "cloud"
|
|
133
|
+
render_cloud_node(node, x, y, svg)
|
|
134
|
+
when "bang"
|
|
135
|
+
render_bang_node(node, x, y, svg)
|
|
136
|
+
when "hexagon"
|
|
137
|
+
render_hexagon_node(node, x, y, svg)
|
|
138
|
+
when "square"
|
|
139
|
+
render_square_node(node, x, y, svg)
|
|
140
|
+
else
|
|
141
|
+
render_default_node(node, x, y, svg)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Renders a default rounded rectangle node.
|
|
146
|
+
#
|
|
147
|
+
# @param node [Hash] node data
|
|
148
|
+
# @param x [Numeric] X position
|
|
149
|
+
# @param y [Numeric] Y position
|
|
150
|
+
# @param svg [Svg::Document] SVG document
|
|
151
|
+
# @return [void]
|
|
152
|
+
def render_default_node(node, x, y, svg)
|
|
153
|
+
width = node[:width]
|
|
154
|
+
height = node[:height]
|
|
155
|
+
color = get_node_color(node)
|
|
156
|
+
|
|
157
|
+
# Rounded rectangle background
|
|
158
|
+
rect = Svg::Rect.new.tap do |r|
|
|
159
|
+
r.x = x - width / 2
|
|
160
|
+
r.y = y
|
|
161
|
+
r.width = width
|
|
162
|
+
r.height = height
|
|
163
|
+
r.rx = 5
|
|
164
|
+
r.ry = 5
|
|
165
|
+
r.fill = lighten_color(color, 0.9)
|
|
166
|
+
r.stroke = color
|
|
167
|
+
r.stroke_width = "2"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
svg.add_element(rect)
|
|
171
|
+
|
|
172
|
+
# Text
|
|
173
|
+
render_node_text(node, x, y + height / 2, svg)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Renders a circle node.
|
|
177
|
+
#
|
|
178
|
+
# @param node [Hash] node data
|
|
179
|
+
# @param x [Numeric] X position
|
|
180
|
+
# @param y [Numeric] Y position
|
|
181
|
+
# @param svg [Svg::Document] SVG document
|
|
182
|
+
# @return [void]
|
|
183
|
+
def render_circle_node(node, x, y, svg)
|
|
184
|
+
radius = [node[:width], node[:height]].max / 2
|
|
185
|
+
color = get_node_color(node)
|
|
186
|
+
|
|
187
|
+
circle = Svg::Circle.new.tap do |c|
|
|
188
|
+
c.cx = x
|
|
189
|
+
c.cy = y + radius
|
|
190
|
+
c.r = radius
|
|
191
|
+
c.fill = lighten_color(color, 0.9)
|
|
192
|
+
c.stroke = color
|
|
193
|
+
c.stroke_width = "2"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
svg.add_element(circle)
|
|
197
|
+
|
|
198
|
+
# Text
|
|
199
|
+
render_node_text(node, x, y + radius, svg)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Renders a square node.
|
|
203
|
+
#
|
|
204
|
+
# @param node [Hash] node data
|
|
205
|
+
# @param x [Numeric] X position
|
|
206
|
+
# @param y [Numeric] Y position
|
|
207
|
+
# @param svg [Svg::Document] SVG document
|
|
208
|
+
# @return [void]
|
|
209
|
+
def render_square_node(node, x, y, svg)
|
|
210
|
+
width = node[:width]
|
|
211
|
+
height = node[:height]
|
|
212
|
+
color = get_node_color(node)
|
|
213
|
+
|
|
214
|
+
rect = Svg::Rect.new.tap do |r|
|
|
215
|
+
r.x = x - width / 2
|
|
216
|
+
r.y = y
|
|
217
|
+
r.width = width
|
|
218
|
+
r.height = height
|
|
219
|
+
r.fill = lighten_color(color, 0.9)
|
|
220
|
+
r.stroke = color
|
|
221
|
+
r.stroke_width = "2"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
svg.add_element(rect)
|
|
225
|
+
|
|
226
|
+
# Text
|
|
227
|
+
render_node_text(node, x, y + height / 2, svg)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Renders a hexagon node.
|
|
231
|
+
#
|
|
232
|
+
# @param node [Hash] node data
|
|
233
|
+
# @param x [Numeric] X position
|
|
234
|
+
# @param y [Numeric] Y position
|
|
235
|
+
# @param svg [Svg::Document] SVG document
|
|
236
|
+
# @return [void]
|
|
237
|
+
def render_hexagon_node(node, x, y, svg)
|
|
238
|
+
width = node[:width]
|
|
239
|
+
height = node[:height]
|
|
240
|
+
color = get_node_color(node)
|
|
241
|
+
|
|
242
|
+
# Hexagon points
|
|
243
|
+
offset = width * 0.2
|
|
244
|
+
points = [
|
|
245
|
+
[x - width / 2 + offset, y],
|
|
246
|
+
[x + width / 2 - offset, y],
|
|
247
|
+
[x + width / 2, y + height / 2],
|
|
248
|
+
[x + width / 2 - offset, y + height],
|
|
249
|
+
[x - width / 2 + offset, y + height],
|
|
250
|
+
[x - width / 2, y + height / 2]
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
points_str = points.map { |p| p.join(",") }.join(" ")
|
|
254
|
+
|
|
255
|
+
polygon = Svg::Polygon.new.tap do |p|
|
|
256
|
+
p.points = points_str
|
|
257
|
+
p.fill = lighten_color(color, 0.9)
|
|
258
|
+
p.stroke = color
|
|
259
|
+
p.stroke_width = "2"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
svg.add_element(polygon)
|
|
263
|
+
|
|
264
|
+
# Text
|
|
265
|
+
render_node_text(node, x, y + height / 2, svg)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Renders a cloud node.
|
|
269
|
+
#
|
|
270
|
+
# @param node [Hash] node data
|
|
271
|
+
# @param x [Numeric] X position
|
|
272
|
+
# @param y [Numeric] Y position
|
|
273
|
+
# @param svg [Svg::Document] SVG document
|
|
274
|
+
# @return [void]
|
|
275
|
+
def render_cloud_node(node, x, y, svg)
|
|
276
|
+
width = node[:width]
|
|
277
|
+
height = node[:height]
|
|
278
|
+
color = get_node_color(node)
|
|
279
|
+
|
|
280
|
+
# Cloud shape using path
|
|
281
|
+
cloud_path = create_cloud_path(x, y, width, height)
|
|
282
|
+
|
|
283
|
+
path = Svg::Path.new.tap do |p|
|
|
284
|
+
p.d = cloud_path
|
|
285
|
+
p.fill = lighten_color(color, 0.9)
|
|
286
|
+
p.stroke = color
|
|
287
|
+
p.stroke_width = "2"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
svg.add_element(path)
|
|
291
|
+
|
|
292
|
+
# Text
|
|
293
|
+
render_node_text(node, x, y + height / 2, svg)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Renders a bang node (cloud with emphasis).
|
|
297
|
+
#
|
|
298
|
+
# @param node [Hash] node data
|
|
299
|
+
# @param x [Numeric] X position
|
|
300
|
+
# @param y [Numeric] Y position
|
|
301
|
+
# @param svg [Svg::Document] SVG document
|
|
302
|
+
# @return [void]
|
|
303
|
+
def render_bang_node(node, x, y, svg)
|
|
304
|
+
# Similar to cloud but with different styling
|
|
305
|
+
render_cloud_node(node, x, y, svg)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Creates a cloud path.
|
|
309
|
+
#
|
|
310
|
+
# @param x [Numeric] center X
|
|
311
|
+
# @param y [Numeric] top Y
|
|
312
|
+
# @param width [Numeric] width
|
|
313
|
+
# @param height [Numeric] height
|
|
314
|
+
# @return [String] SVG path data
|
|
315
|
+
def create_cloud_path(x, y, width, height)
|
|
316
|
+
# Simplified cloud shape
|
|
317
|
+
w2 = width / 2
|
|
318
|
+
h = height
|
|
319
|
+
|
|
320
|
+
"M #{x - w2} #{y + h * 0.6} " \
|
|
321
|
+
"Q #{x - w2} #{y + h * 0.3}, #{x - w2 * 0.6} #{y + h * 0.2} " \
|
|
322
|
+
"Q #{x - w2 * 0.6} #{y}, #{x - w2 * 0.2} #{y + h * 0.1} " \
|
|
323
|
+
"Q #{x} #{y}, #{x + w2 * 0.2} #{y + h * 0.1} " \
|
|
324
|
+
"Q #{x + w2 * 0.6} #{y}, #{x + w2 * 0.6} #{y + h * 0.2} " \
|
|
325
|
+
"Q #{x + w2} #{y + h * 0.3}, #{x + w2} #{y + h * 0.6} " \
|
|
326
|
+
"Q #{x + w2} #{y + h}, #{x} #{y + h} " \
|
|
327
|
+
"Q #{x - w2} #{y + h}, #{x - w2} #{y + h * 0.6} Z"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Renders text content for a node.
|
|
331
|
+
#
|
|
332
|
+
# @param node [Hash] node data
|
|
333
|
+
# @param x [Numeric] X position
|
|
334
|
+
# @param y [Numeric] Y position
|
|
335
|
+
# @param svg [Svg::Document] SVG document
|
|
336
|
+
# @return [void]
|
|
337
|
+
def render_node_text(node, x, y, svg)
|
|
338
|
+
return unless node[:content]
|
|
339
|
+
|
|
340
|
+
text = Svg::Text.new.tap do |t|
|
|
341
|
+
t.x = x
|
|
342
|
+
t.y = y + 5
|
|
343
|
+
t.text_anchor = "middle"
|
|
344
|
+
t.fill = theme_color(:text) || "#000000"
|
|
345
|
+
t.font_size = (theme_typography(:font_size) || 12).to_s
|
|
346
|
+
t.font_family = theme_typography(:font_family) || "Arial, sans-serif"
|
|
347
|
+
t.content = node[:content]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
svg.add_element(text)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Gets the color for a node based on its level.
|
|
354
|
+
#
|
|
355
|
+
# @param node [Hash] node data
|
|
356
|
+
# @return [String] color value
|
|
357
|
+
def get_node_color(node)
|
|
358
|
+
level = node[:level] || 0
|
|
359
|
+
|
|
360
|
+
colors = [
|
|
361
|
+
theme_color(:primary) || "#2563eb",
|
|
362
|
+
theme_color(:secondary) || "#7c3aed",
|
|
363
|
+
theme_color(:accent) || "#db2777",
|
|
364
|
+
"#ea580c",
|
|
365
|
+
"#16a34a",
|
|
366
|
+
"#0891b2"
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
colors[level % colors.length]
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Lightens a color by a given factor.
|
|
373
|
+
#
|
|
374
|
+
# @param color [String] hex color
|
|
375
|
+
# @param factor [Float] lightening factor (0-1)
|
|
376
|
+
# @return [String] lightened hex color
|
|
377
|
+
def lighten_color(color, factor)
|
|
378
|
+
# Remove # if present
|
|
379
|
+
color = color.sub(/^#/, "")
|
|
380
|
+
|
|
381
|
+
# Parse RGB
|
|
382
|
+
r = color[0..1].to_i(16)
|
|
383
|
+
g = color[2..3].to_i(16)
|
|
384
|
+
b = color[4..5].to_i(16)
|
|
385
|
+
|
|
386
|
+
# Lighten
|
|
387
|
+
r = (r + (255 - r) * factor).round
|
|
388
|
+
g = (g + (255 - g) * factor).round
|
|
389
|
+
b = (b + (255 - b) * factor).round
|
|
390
|
+
|
|
391
|
+
# Return hex
|
|
392
|
+
format("#%02x%02x%02x", r, g, b)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|